Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature] Integrate d2-logger (coding dojo) #23

Merged
merged 20 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e47d3cc
feat: setup d2-logger config
deeonwuli Sep 24, 2024
1122a06
feat: reduce indentation
deeonwuli Sep 25, 2024
c946d9e
refactor: move script logic to a use case
deeonwuli Oct 9, 2024
c52aafe
refactor: use smaller functions on alert data d2 repo
deeonwuli Oct 9, 2024
510a53c
feat: move dhis2 ids to data layer
deeonwuli Oct 9, 2024
16efe26
Merge branch 'development' of https://github.com/EyeSeeTea/zebra-dev …
deeonwuli Oct 14, 2024
34f5483
fix: failing test
deeonwuli Oct 14, 2024
abf700b
fix: import AlertSyncData type correctly
deeonwuli Oct 14, 2024
649fa13
feat: use options object for repositories
deeonwuli Oct 17, 2024
df2d1ea
refactor: use ts record instead of ternary, clean archi fixes
deeonwuli Oct 18, 2024
f564237
Merge branch 'development' of https://github.com/EyeSeeTea/zebra-dev …
deeonwuli Oct 18, 2024
8ac3279
refactor: move static outbreak mappings
deeonwuli Nov 5, 2024
fb29557
fix: error on executing script
deeonwuli Nov 6, 2024
1ad92f5
feat: use verification status in notification (not code)
deeonwuli Dec 17, 2024
ef648d3
feat: include outbreak and ems IDs and district name in notification
deeonwuli Dec 17, 2024
97a53cd
feat: send notification for only districts/outbreaks that haven't bee…
deeonwuli Dec 17, 2024
ccedbda
refactor: rename parameters in buildNotificationText function
deeonwuli Dec 20, 2024
6d6c101
Merge pull request #47 from EyeSeeTea/feat/changes-to-script-notifica…
bhavananarayanan Jan 10, 2025
e8d07a4
Merge branch 'development' of https://github.com/EyeSeeTea/zebra-dev …
deeonwuli Jan 15, 2025
5d1479e
Merge branch 'development' of https://github.com/EyeSeeTea/zebra-dev …
deeonwuli Jan 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@emotion/react": "11.11.4",
"@emotion/styled": "11.11.5",
"@eyeseetea/d2-api": "1.16.0-beta.12",
"@eyeseetea/d2-logger": "^1.1.0",
"@eyeseetea/d2-ui-components": "v2.9.0-beta.2",
"@eyeseetea/feedback-component": "0.0.3",
"@material-ui/core": "4.12.4",
Expand Down
23 changes: 23 additions & 0 deletions src/data/entities/Instance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export interface InstanceData {
url: string;
username?: string;
password?: string;
}

export class Instance {
public readonly url: string;
private username: string | undefined;
private password: string | undefined;

constructor(data: InstanceData) {
this.url = data.url;
this.username = data.username;
this.password = data.password;
}

public get auth(): { username: string; password: string } | undefined {
return this.username && this.password
? { username: this.username, password: this.password }
: undefined;
}
}
108 changes: 30 additions & 78 deletions src/data/repositories/AlertD2Repository.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { D2Api } from "@eyeseetea/d2-api/2.36";
import { apiToFuture, FutureData } from "../api-futures";
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_NATIONAL_INCIDENT_STATUS_TEA_ID,
RTSL_ZEBRA_ALERTS_PROGRAM_ID,
Expand All @@ -12,32 +10,26 @@ import { AlertOptions, AlertRepository } from "../../domain/repositories/AlertRe
import { Id } from "../../domain/entities/Ref";
import _ from "../../domain/entities/generic/Collection";
import { Future } from "../../domain/entities/generic/Future";
import {
D2TrackerTrackedEntity,
TrackedEntitiesGetResponse,
} from "@eyeseetea/d2-api/api/trackerTrackedEntities";
import { D2TrackerTrackedEntity } from "@eyeseetea/d2-api/api/trackerTrackedEntities";
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/OutbreakAlert";
import { getAllTrackedEntitiesAsync } from "./utils/getAllTrackedEntities";
import { outbreakDataSourceMapping, outbreakTEAMapping } from "./utils/AlertOutbreakMapper";

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,78 +71,38 @@ export class AlertD2Repository implements AlertRepository {
});
}

private async getTrackedEntitiesByTEACodeAsync(options: {
private getTrackedEntitiesByTEACode(options: {
program: Id;
orgUnit: Id;
ouMode: "SELECTED" | "DESCENDANTS";
filter?: Filter;
}): Promise<D2TrackerTrackedEntity[]> {
filter: OutbreakData;
}): FutureData<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 [];
}
return Future.fromPromise(
getAllTrackedEntitiesAsync(this.api, {
programId: program,
orgUnitId: orgUnit,
ouMode: ouMode,
filter: {
id: this.getOutbreakFilterId(filter),
value: filter.value,
},
})
);
}

getTrackedEntitiesByTEACode(options: {
program: Id;
orgUnit: Id;
ouMode: "SELECTED" | "DESCENDANTS";
filter?: Filter;
}): FutureData<D2TrackerTrackedEntity[]> {
return Future.fromPromise(this.getTrackedEntitiesByTEACodeAsync(options));
private getOutbreakFilterId(filter: OutbreakData): string {
return outbreakTEAMapping[filter.type];
}

private getAlertFilter(
private getAlertOutbreakData(
dataSource: DataSource,
suspectedDiseaseCode: Maybe<string>,
hazardTypeCode: Maybe<string>
): Filter {
switch (dataSource) {
case DataSource.RTSL_ZEB_OS_DATA_SOURCE_IBS:
return { id: RTSL_ZEBRA_ALERTS_DISEASE_TEA_ID, value: suspectedDiseaseCode };
case DataSource.RTSL_ZEB_OS_DATA_SOURCE_EBS:
return { id: RTSL_ZEBRA_ALERTS_EVENT_TYPE_TEA_ID, value: hazardTypeCode };
}
outbreakValue: Maybe<string>
): OutbreakData {
return {
type: outbreakDataSourceMapping[dataSource],
value: outbreakValue,
};
}
}
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
39 changes: 38 additions & 1 deletion 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, OutbreakDataType } from "../../domain/entities/alert/OutbreakAlert";

export class DiseaseOutbreakEventD2Repository implements DiseaseOutbreakEventRepository {
constructor(private api: D2Api) {}
Expand All @@ -34,7 +35,10 @@ export class DiseaseOutbreakEventD2Repository implements DiseaseOutbreakEventRep

getAll(): FutureData<DiseaseOutbreakEventBaseAttrs[]> {
return Future.fromPromise(
getAllTrackedEntitiesAsync(this.api, RTSL_ZEBRA_PROGRAM_ID, RTSL_ZEBRA_ORG_UNIT_ID)
getAllTrackedEntitiesAsync(this.api, {
programId: RTSL_ZEBRA_PROGRAM_ID,
orgUnitId: RTSL_ZEBRA_ORG_UNIT_ID,
})
).map(trackedEntities => {
return trackedEntities
.map(trackedEntity => {
Expand All @@ -44,6 +48,34 @@ export class DiseaseOutbreakEventD2Repository implements DiseaseOutbreakEventRep
});
}

getEventByDiseaseOrHazardType(
filter: OutbreakData
): FutureData<DiseaseOutbreakEventBaseAttrs[]> {
return Future.fromPromise(
getAllTrackedEntitiesAsync(this.api, {
programId: RTSL_ZEBRA_PROGRAM_ID,
orgUnitId: RTSL_ZEBRA_ORG_UNIT_ID,
filter: {
id: this.getDiseaseOutbreakFilterId(filter),
value: filter.value,
},
})
).map(trackedEntities => {
return trackedEntities.map(trackedEntity => {
return mapTrackedEntityAttributesToDiseaseOutbreak(trackedEntity);
});
});
}

private getDiseaseOutbreakFilterId(filter: OutbreakData): string {
const mapping: Record<OutbreakDataType, TrackedEntityAttributeId> = {
disease: RTSL_ZEBRA_DISEASE_TEA_ID,
hazard: RTSL_ZEBRA_HAZARD_TEA_ID,
};

return mapping[filter.type];
}

save(diseaseOutbreak: DiseaseOutbreakEventBaseAttrs): FutureData<Id> {
return getProgramTEAsMetadata(this.api, RTSL_ZEBRA_PROGRAM_ID).flatMap(
teasMetadataResponse => {
Expand Down Expand Up @@ -90,3 +122,8 @@ export class DiseaseOutbreakEventD2Repository implements DiseaseOutbreakEventRep

//TO DO : Implement delete/archive after requirement confirmation
}

const RTSL_ZEBRA_DISEASE_TEA_ID = "jLvbkuvPdZ6";
const RTSL_ZEBRA_HAZARD_TEA_ID = "Dzrw3Tf0ukB";

type TrackedEntityAttributeId = Id;
38 changes: 33 additions & 5 deletions src/data/repositories/NotificationD2Repository.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,45 @@
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 { OutbreakAlert } from "../../domain/entities/alert/OutbreakAlert";
import i18n from "../../utils/i18n";

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: OutbreakAlert,
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 i18n.t(`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}`);
}
Loading
Loading