diff --git a/src/CompositionRoot.ts b/src/CompositionRoot.ts index b34c916f..6944d4bd 100644 --- a/src/CompositionRoot.ts +++ b/src/CompositionRoot.ts @@ -37,12 +37,7 @@ function getCompositionRoot(repositories: Repositories) { }, diseaseOutbreakEvent: { get: new GetDiseaseOutbreakByIdUseCase(repositories), - getWithOptions: new GetDiseaseOutbreakWithOptionsUseCase( - repositories.diseaseOutbreakEventRepository, - repositories.optionsRepository, - repositories.teamMemberRepository, - repositories.orgUnitRepository - ), + getWithOptions: new GetDiseaseOutbreakWithOptionsUseCase(repositories), getAll: new GetAllDiseaseOutbreaksUseCase(repositories.diseaseOutbreakEventRepository), save: new SaveDiseaseOutbreakUseCase(repositories.diseaseOutbreakEventRepository), }, diff --git a/src/data/repositories/OptionsD2Repository.ts b/src/data/repositories/OptionsD2Repository.ts index 95dcb035..4019c2be 100644 --- a/src/data/repositories/OptionsD2Repository.ts +++ b/src/data/repositories/OptionsD2Repository.ts @@ -1,21 +1,9 @@ -import { D2OptionSetSchema, SelectedPick } from "@eyeseetea/d2-api/2.36"; -import { CodedNamedRef } from "../../domain/entities/Ref"; -import { D2Api } from "../../types/d2-api"; +import { D2Api, MetadataPick } from "../../types/d2-api"; import { Code, Option } from "../../domain/entities/Ref"; import { apiToFuture, FutureData } from "../api-futures"; import { OptionsRepository } from "../../domain/repositories/OptionsRepository"; import { assertOrError } from "./utils/AssertOrError"; -import { Future } from "../../domain/entities/generic/Future"; - -type D2OptionSet = SelectedPick< - D2OptionSetSchema, - { - id: true; - name: true; - code: true; - options: { id: true; name: true; code: true }; - } ->; +import { getHazardTypeByCode } from "./consts/DiseaseOutbreakConstants"; export class OptionsD2Repository implements OptionsRepository { constructor(private api: D2Api) {} @@ -36,111 +24,57 @@ export class OptionsD2Repository implements OptionsRepository { }); } - getAllHazardTypes(): FutureData { - return apiToFuture( - this.api.metadata.get({ - optionSets: { - fields: { - id: true, - name: true, - code: true, - options: { id: true, name: true, code: true }, - }, - filter: { code: { eq: "RTSL_ZEB_OS_HAZARD_TYPE" } }, - }, - }) - ).flatMap(response => { - if (!response.optionSets[0]) throw new Error("Hazard Types options not found"); - - return Future.success(this.mapD2OptionSetToCodedNamedRefs(response.optionSets[0])); + getAllHazardTypes(): FutureData { + return this.getOptionSetByCode("RTSL_ZEB_OS_HAZARD_TYPE").map(hazardTypes => { + return hazardTypes.map(hazardType => ({ + id: getHazardTypeByCode(hazardType.id), + name: hazardType.name, + })); }); } - getAllMainSyndromes(): FutureData { - return apiToFuture( - this.api.metadata.get({ - optionSets: { - fields: { - id: true, - name: true, - code: true, - options: { id: true, name: true, code: true }, - }, - filter: { code: { eq: "RTSL_ZEB_OS_SYNDROME" } }, - }, - }) - ).flatMap(response => { - if (!response.optionSets[0]) throw new Error("Main Syndromes options not found"); - return Future.success(this.mapD2OptionSetToCodedNamedRefs(response.optionSets[0])); - }); + getAllMainSyndromes(): FutureData { + return this.getOptionSetByCode("RTSL_ZEB_OS_SYNDROME"); } - getAllSuspectedDiseases(): FutureData { - return apiToFuture( - this.api.metadata.get({ - optionSets: { - fields: { - id: true, - name: true, - code: true, - options: { id: true, name: true, code: true }, - }, - filter: { code: { eq: "RTSL_ZEB_OS_DISEASE" } }, - }, - }) - ).flatMap(response => { - if (!response.optionSets[0]) throw new Error("Suspected Diseases options not found"); + getAllSuspectedDiseases(): FutureData { + return this.getOptionSetByCode("RTSL_ZEB_OS_DISEASE"); + } - return Future.success(this.mapD2OptionSetToCodedNamedRefs(response.optionSets[0])); - }); + getAllNotificationSources(): FutureData { + return this.getOptionSetByCode("RTSL_ZEB_OS_SOURCE"); } - getAllNotificationSources(): FutureData { - return apiToFuture( - this.api.metadata.get({ - optionSets: { - fields: { - id: true, - name: true, - code: true, - options: { id: true, name: true, code: true }, - }, - filter: { code: { eq: "RTSL_ZEB_OS_SOURCE" } }, - }, - }) - ).flatMap(response => { - if (!response.optionSets[0]) throw new Error("Notification Sources options not found"); - return Future.success(this.mapD2OptionSetToCodedNamedRefs(response.optionSets[0])); - }); + getAllIncidentStatus(): FutureData { + return this.getOptionSetByCode("RTSL_ZEB_OS_INCIDENT_STATUS"); } - getAllIncidentStatus(): FutureData { + private getOptionSetByCode(code: string): FutureData { return apiToFuture( this.api.metadata.get({ - optionSets: { - fields: { - id: true, - name: true, - code: true, - options: { id: true, name: true, code: true }, - }, - filter: { code: { eq: "RTSL_ZEB_OS_INCIDENT_STATUS" } }, - }, + optionSets: { fields: optionSetsFields, filter: { code: { eq: code } } }, }) - ).flatMap(response => { - if (!response.optionSets[0]) throw new Error("Incident Status options not found"); - - return Future.success(this.mapD2OptionSetToCodedNamedRefs(response.optionSets[0])); - }); + ) + .flatMap(response => assertOrError(response.optionSets[0], `OptionSet ${code}`)) + .map(d2Option => this.mapD2OptionSetToOptions(d2Option)); } - private mapD2OptionSetToCodedNamedRefs(optionSet: D2OptionSet): CodedNamedRef[] { - return optionSet.options.map(option => { - return { - id: option.id, + private mapD2OptionSetToOptions(optionSet: D2OptionSet): Option[] { + return optionSet.options.map( + (option): Option => ({ + id: option.code, name: option.name, - code: option.code, - }; - }); + }) + ); } } + +const optionSetsFields = { + name: true, + code: true, + options: { id: true, name: true, code: true }, +}; + +type D2OptionSet = MetadataPick<{ + optionSets: { fields: typeof optionSetsFields }; +}>["optionSets"][number]; diff --git a/src/data/repositories/OrgUnitD2Repository.ts b/src/data/repositories/OrgUnitD2Repository.ts index 5265d0c1..14ef9b4d 100644 --- a/src/data/repositories/OrgUnitD2Repository.ts +++ b/src/data/repositories/OrgUnitD2Repository.ts @@ -1,20 +1,9 @@ -import { D2OrganisationUnitSchema, SelectedPick } from "@eyeseetea/d2-api/2.36"; -import { D2Api } from "../../types/d2-api"; +import { D2Api, MetadataPick } from "../../types/d2-api"; import { OrgUnit } from "../../domain/entities/OrgUnit"; import { Id } from "../../domain/entities/Ref"; import { OrgUnitRepository } from "../../domain/repositories/OrgUnitRepository"; import { apiToFuture, FutureData } from "../api-futures"; -type D2OrgUnit = SelectedPick< - D2OrganisationUnitSchema, - { - id: true; - name: true; - code: true; - level: true; - } ->; - export class OrgUnitD2Repository implements OrgUnitRepository { constructor(private api: D2Api) {} @@ -22,12 +11,7 @@ export class OrgUnitD2Repository implements OrgUnitRepository { return apiToFuture( this.api.metadata.get({ organisationUnits: { - fields: { - id: true, - name: true, - code: true, - level: true, - }, + fields: d2OrgUnitFields, }, }) ).map(response => { @@ -42,37 +26,34 @@ export class OrgUnitD2Repository implements OrgUnitRepository { return apiToFuture( this.api.metadata.get({ organisationUnits: { - fields: { - id: true, - name: true, - code: true, - level: true, - }, + fields: d2OrgUnitFields, filter: { id: { in: ids } }, }, }) ).map(response => { - const orgUnits: OrgUnit[] = response.organisationUnits.map((ou): OrgUnit => { - return { - id: ou.id, - name: ou.name, - code: ou.code, - level: ou.level === 2 ? "Province" : "District", - }; - }); - - return orgUnits; + return this.mapD2OrgUnitsToOrgUnits(response.organisationUnits); }); } private mapD2OrgUnitsToOrgUnits(d2OrgUnit: D2OrgUnit[]): OrgUnit[] { - return d2OrgUnit.map(ou => { - return { + return d2OrgUnit.map( + (ou): OrgUnit => ({ id: ou.id, name: ou.name, code: ou.code, level: ou.level === 2 ? "Province" : "District", - }; - }); + }) + ); } } + +const d2OrgUnitFields = { + id: true, + name: true, + code: true, + level: true, +}; + +type D2OrgUnit = MetadataPick<{ + organisationUnits: { fields: typeof d2OrgUnitFields }; +}>["organisationUnits"][number]; diff --git a/src/data/repositories/TeamMemberD2Repository.ts b/src/data/repositories/TeamMemberD2Repository.ts index 663ef242..c40f2fab 100644 --- a/src/data/repositories/TeamMemberD2Repository.ts +++ b/src/data/repositories/TeamMemberD2Repository.ts @@ -1,22 +1,10 @@ -import { D2Api, D2UserSchema } from "../../types/d2-api"; +import { D2Api, MetadataPick } from "../../types/d2-api"; import { TeamMember } from "../../domain/entities/incident-management-team/TeamMember"; import { Id } from "../../domain/entities/Ref"; import { TeamMemberRepository } from "../../domain/repositories/TeamMemberRepository"; import { apiToFuture, FutureData } from "../api-futures"; -import { SelectedPick } from "@eyeseetea/d2-api/api"; import { assertOrError } from "./utils/AssertOrError"; -type D2User = SelectedPick< - D2UserSchema, - { - id: true; - name: true; - username: true; - email: true; - phoneNumber: true; - } ->; - export class TeamMemberD2Repository implements TeamMemberRepository { constructor(private api: D2Api) {} @@ -24,13 +12,7 @@ export class TeamMemberD2Repository implements TeamMemberRepository { return apiToFuture( this.api.metadata.get({ users: { - fields: { - id: true, - name: true, - email: true, - phoneNumber: true, - username: true, - }, + fields: d2UserFields, }, }) ).map(response => { @@ -43,13 +25,7 @@ export class TeamMemberD2Repository implements TeamMemberRepository { return apiToFuture( this.api.metadata.get({ users: { - fields: { - id: true, - name: true, - email: true, - phoneNumber: true, - username: true, - }, + fields: d2UserFields, filter: { username: { eq: id } }, }, }) @@ -74,3 +50,15 @@ export class TeamMemberD2Repository implements TeamMemberRepository { }); } } + +const d2UserFields = { + id: true, + name: true, + email: true, + phoneNumber: true, + username: true, +}; + +type _D2User = MetadataPick<{ + users: { fields: typeof d2UserFields }; +}>["users"][number]; diff --git a/src/data/repositories/consts/DiseaseOutbreakConstants.ts b/src/data/repositories/consts/DiseaseOutbreakConstants.ts index a9f98879..16528116 100644 --- a/src/data/repositories/consts/DiseaseOutbreakConstants.ts +++ b/src/data/repositories/consts/DiseaseOutbreakConstants.ts @@ -52,6 +52,7 @@ export function isHazardType(hazardType: string): hazardType is HazardType { return [ "Biological:Animal", "Biological:Human", + "Biological:HumanAndAnimal", "Chemical", "Environmental", "Unknown", @@ -87,28 +88,28 @@ export function getValueFromDiseaseOutbreak( diseaseOutbreak.earlyResponseActions.conductEpidemiologicalAnalysis.toISOString(), RTSL_ZEB_TEA_LABORATORY_CONFIRMATION: diseaseOutbreak.earlyResponseActions .laboratoryConfirmation.na - ? "true" - : "", + ? "" + : "true", RTSL_ZEB_TEA_SPECIFY_DATE1: diseaseOutbreak.earlyResponseActions.laboratoryConfirmation.date?.toISOString() ?? "", RTSL_ZEB_TEA_APPROPRIATE_CASE_MANAGEMENT: diseaseOutbreak.earlyResponseActions .appropriateCaseManagement.na - ? "true" - : "", + ? "" + : "true", RTSL_ZEB_TEA_SPECIFY_DATE2: diseaseOutbreak.earlyResponseActions.appropriateCaseManagement.date?.toISOString() ?? "", RTSL_ZEB_TEA_APPROPRIATE_PUBLIC_HEALTH: diseaseOutbreak.earlyResponseActions .initiatePublicHealthCounterMeasures.na - ? "true" - : "", + ? "" + : "true", RTSL_ZEB_TEA_SPECIFY_DATE3: diseaseOutbreak.earlyResponseActions.initiatePublicHealthCounterMeasures.date?.toISOString() ?? "", RTSL_ZEB_TEA_APPROPRIATE_RISK_COMMUNICATION: diseaseOutbreak.earlyResponseActions .initiateRiskCommunication.na - ? "true" - : "", + ? "" + : "true", RTSL_ZEB_TEA_SPECIFY_DATE4: diseaseOutbreak.earlyResponseActions.initiateRiskCommunication.date?.toISOString() ?? "", @@ -126,6 +127,8 @@ function getHazardTypeCode(hazardType: HazardType): string { return "BIOLOGICAL_ANIMAL"; case "Biological:Human": return "BIOLOGICAL_HUMAN"; + case "Biological:HumanAndAnimal": + return "BIOLOGICAL_HUM_ANM"; case "Chemical": return "CHEMICAL"; case "Environmental": @@ -135,6 +138,24 @@ function getHazardTypeCode(hazardType: HazardType): string { } } +export function getHazardTypeByCode(hazardTypeCode: string): HazardType { + switch (hazardTypeCode) { + case "BIOLOGICAL_ANIMAL": + return "Biological:Animal"; + case "BIOLOGICAL_HUMAN": + return "Biological:Human"; + case "BIOLOGICAL_HUM_ANM": + return "Biological:HumanAndAnimal"; + case "CHEMICAL": + return "Chemical"; + case "ENVIRONMENTAL": + return "Environmental"; + case "UNKNOWN": + default: + return "Unknown"; + } +} + function getOUTextFromList(OUs: string[]): string { return OUs[0] ?? ""; //TO DO : Handle multiple provinces/districts once metadata change is done } diff --git a/src/data/repositories/utils/DiseaseOutbreakMapper.ts b/src/data/repositories/utils/DiseaseOutbreakMapper.ts index bc2a3564..2711f1b1 100644 --- a/src/data/repositories/utils/DiseaseOutbreakMapper.ts +++ b/src/data/repositories/utils/DiseaseOutbreakMapper.ts @@ -15,7 +15,7 @@ import { RTSL_ZEBRA_PROGRAM_ID, RTSL_ZEBRA_TRACKED_ENTITY_TYPE_ID, } from "../consts/DiseaseOutbreakConstants"; -import _c from "../../../domain/entities/generic/Collection"; +import _ from "../../../domain/entities/generic/Collection"; import { SelectedPick } from "@eyeseetea/d2-api/api"; import { D2TrackedEntityAttributeSchema } from "../../../types/d2-api"; import { D2TrackerEnrollment } from "@eyeseetea/d2-api/api/trackerEnrollments"; @@ -123,6 +123,52 @@ function getMultipleOUFromText(text: string): string[] { return [text].filter(ou => ou !== ""); } +function getValueOfCodeIfSpecifyDateIsTrue( + attributeValues: Record, + code: DiseaseOutbreakCode, + specifyDateCode: DiseaseOutbreakCode +): string | undefined { + return attributeValues[specifyDateCode] === "true" ? attributeValues[code] : undefined; +} + +function getValueByDiseaseOutbreakCode( + attributeValues: Record, + code: DiseaseOutbreakCode +): string | undefined { + switch (code) { + case "RTSL_ZEB_TEA_LABORATORY_CONFIRMATION": + case "RTSL_ZEB_TEA_SPECIFY_DATE1": + return getValueOfCodeIfSpecifyDateIsTrue( + attributeValues, + code, + "RTSL_ZEB_TEA_LABORATORY_CONFIRMATION" + ); + case "RTSL_ZEB_TEA_APPROPRIATE_CASE_MANAGEMENT": + case "RTSL_ZEB_TEA_SPECIFY_DATE2": + return getValueOfCodeIfSpecifyDateIsTrue( + attributeValues, + code, + "RTSL_ZEB_TEA_APPROPRIATE_CASE_MANAGEMENT" + ); + case "RTSL_ZEB_TEA_APPROPRIATE_PUBLIC_HEALTH": + case "RTSL_ZEB_TEA_SPECIFY_DATE3": + return getValueOfCodeIfSpecifyDateIsTrue( + attributeValues, + code, + "RTSL_ZEB_TEA_APPROPRIATE_PUBLIC_HEALTH" + ); + case "RTSL_ZEB_TEA_APPROPRIATE_RISK_COMMUNICATION": + case "RTSL_ZEB_TEA_SPECIFY_DATE4": + return getValueOfCodeIfSpecifyDateIsTrue( + attributeValues, + code, + "RTSL_ZEB_TEA_APPROPRIATE_RISK_COMMUNICATION" + ); + default: + return attributeValues[code]; + } +} + export function mapDiseaseOutbreakEventToTrackedEntityAttributes( diseaseOutbreak: DiseaseOutbreakEventBaseAttrs, attributesMetadata: D2TrackedEntityAttribute[] @@ -130,17 +176,33 @@ export function mapDiseaseOutbreakEventToTrackedEntityAttributes( const attributeValues: Record = getValueFromDiseaseOutbreak(diseaseOutbreak); - const attributes: Attribute[] = attributesMetadata.map(attribute => { - if (!isStringInDiseaseOutbreakCodes(attribute.trackedEntityAttribute.code)) { - throw new Error("Attribute code not found in DiseaseOutbreakCodes"); - } - const typedCode: KeyCode = attribute.trackedEntityAttribute.code; - const populatedAttribute = { - attribute: attribute.trackedEntityAttribute.id, - value: attributeValues[typedCode], - }; - return populatedAttribute; - }); + const attributeValuesCleaned = (Object.keys(attributeValues) as DiseaseOutbreakCode[]).reduce( + (acc: Record, code: DiseaseOutbreakCode) => { + const value = getValueByDiseaseOutbreakCode(attributeValues, code); + return value + ? { ...acc, [code]: getValueByDiseaseOutbreakCode(attributeValues, code) } + : acc; + }, + {} as Record + ); + + const attributes: Attribute[] = _( + attributesMetadata.map((attribute): Attribute | null => { + if (!isStringInDiseaseOutbreakCodes(attribute.trackedEntityAttribute.code)) { + throw new Error("Attribute code not found in DiseaseOutbreakCodes"); + } + const typedCode: KeyCode = attribute.trackedEntityAttribute.code; + + return attributeValuesCleaned[typedCode] + ? { + attribute: attribute.trackedEntityAttribute.id, + value: attributeValuesCleaned[typedCode], + } + : null; + }) + ) + .compact() + .value(); const enrollment: D2TrackerEnrollment = { orgUnit: RTSL_ZEBRA_ORG_UNIT_ID, diff --git a/src/domain/entities/ValidationError.ts b/src/domain/entities/ValidationError.ts index 68ea0518..d6dab030 100644 --- a/src/domain/entities/ValidationError.ts +++ b/src/domain/entities/ValidationError.ts @@ -1,8 +1,4 @@ -export type ValidationErrorKey = - | "field_is_required" - | "field_is_required_na" - | "detected_before_emerged" - | "notified_before_emerged"; +export type ValidationErrorKey = "field_is_required" | "field_is_required_na"; export type ValidationError = { property: string; diff --git a/src/domain/entities/disease-outbreak-event/DiseaseOutbreakEvent.ts b/src/domain/entities/disease-outbreak-event/DiseaseOutbreakEvent.ts index 2d0dac40..e9261ab4 100644 --- a/src/domain/entities/disease-outbreak-event/DiseaseOutbreakEvent.ts +++ b/src/domain/entities/disease-outbreak-event/DiseaseOutbreakEvent.ts @@ -6,11 +6,12 @@ import { OrgUnit } from "../OrgUnit"; import { Id, NamedRef } from "../Ref"; import { RiskAssessment } from "../risk-assessment/RiskAssessment"; import { Maybe } from "../../../utils/ts-utils"; -import { ValidationError, ValidationErrorKey } from "../ValidationError"; +import { ValidationError } from "../ValidationError"; export type HazardType = | "Biological:Human" | "Biological:Animal" + | "Biological:HumanAndAnimal" | "Chemical" | "Environmental" | "Unknown"; @@ -77,45 +78,8 @@ export type DiseaseOutbreakEventAttrs = DiseaseOutbreakEventBaseAttrs & { **/ export class DiseaseOutbreakEvent extends Struct() { - //TODO: Add required validations, this is an example: - static validate(data: DiseaseOutbreakEventBaseAttrs): ValidationError[] { - const validationErrors: ValidationError[] = [ - { - property: "detected_date" as const, - errors: DiseaseOutbreakEvent.validateDateDetectedBeforeEmerged(data), - value: data.id, - }, - { - property: "notified_date" as const, - errors: DiseaseOutbreakEvent.validateDateNotifiedBeforeEmerged(data), - value: data.id, - }, - ]; - - const filteredValidationErrorsWithErrors = validationErrors.filter( - v => v.errors.length > 0 - ); - - return filteredValidationErrorsWithErrors.length ? validationErrors : []; - } - - static validateDateDetectedBeforeEmerged( - data: DiseaseOutbreakEventBaseAttrs - ): ValidationErrorKey[] { - return data.detected.date && - data.emerged.date && - data.detected.date.getTime() < data.emerged.date.getTime() - ? ["detected_before_emerged"] - : []; - } - - static validateDateNotifiedBeforeEmerged( - data: DiseaseOutbreakEventBaseAttrs - ): ValidationErrorKey[] { - return data.notified.date && - data.emerged.date && - data.notified.date.getTime() < data.emerged.date.getTime() - ? ["notified_before_emerged"] - : []; + //TODO: Add required validations if exists: + static validate(_data: DiseaseOutbreakEventBaseAttrs): ValidationError[] { + return []; } } diff --git a/src/domain/entities/disease-outbreak-event/DiseaseOutbreakEventWithOptions.ts b/src/domain/entities/disease-outbreak-event/DiseaseOutbreakEventWithOptions.ts index d275c0b9..49c41a6e 100644 --- a/src/domain/entities/disease-outbreak-event/DiseaseOutbreakEventWithOptions.ts +++ b/src/domain/entities/disease-outbreak-event/DiseaseOutbreakEventWithOptions.ts @@ -1,16 +1,16 @@ import { Maybe } from "../../../utils/ts-utils"; import { TeamMember } from "../incident-management-team/TeamMember"; import { OrgUnit } from "../OrgUnit"; -import { CodedNamedRef } from "../Ref"; +import { Option } from "../Ref"; import { DiseaseOutbreakEventBaseAttrs } from "./DiseaseOutbreakEvent"; export type DiseaseOutbreakEventOptions = { - hazardTypes: CodedNamedRef[]; - mainSyndromes: CodedNamedRef[]; - suspectedDiseases: CodedNamedRef[]; - notificationSources: CodedNamedRef[]; + hazardTypes: Option[]; + mainSyndromes: Option[]; + suspectedDiseases: Option[]; + notificationSources: Option[]; organisationUnits: OrgUnit[]; - incidentStatus: CodedNamedRef[]; + incidentStatus: Option[]; teamMembers: TeamMember[]; }; diff --git a/src/domain/repositories/OptionsRepository.ts b/src/domain/repositories/OptionsRepository.ts index a24485e3..ccc05468 100644 --- a/src/domain/repositories/OptionsRepository.ts +++ b/src/domain/repositories/OptionsRepository.ts @@ -1,11 +1,11 @@ import { FutureData } from "../../data/api-futures"; -import { CodedNamedRef, Id, Option } from "../entities/Ref"; +import { Id, Option } from "../entities/Ref"; export interface OptionsRepository { get(id: Id): FutureData