diff --git a/interfaces/IBF-dashboard/src/app/components/map/map.component.ts b/interfaces/IBF-dashboard/src/app/components/map/map.component.ts
index 6c589bf75a..7dbf8b319e 100644
--- a/interfaces/IBF-dashboard/src/app/components/map/map.component.ts
+++ b/interfaces/IBF-dashboard/src/app/components/map/map.component.ts
@@ -741,42 +741,6 @@ export class MapComponent implements AfterViewInit, OnDestroy {
);
}
- private createDefaultPopupAdminRegions(
- activeAggregateLayer: IbfLayer,
- feature,
- ): string {
- feature = activeAggregateLayer.data?.features.find(
- (f) => f.properties?.['placeCode'] === feature.properties.placeCode,
- );
- return (
- '' +
- feature.properties.name +
- (feature.properties.placeCode.includes('Disputed')
- ? ' (Disputed borders)'
- : '') +
- '' +
- (feature.properties.nameParent
- ? ` (${feature.properties.nameParent})`
- : '') +
- '
' +
- (!activeAggregateLayer
- ? ''
- : activeAggregateLayer.label +
- ': ' +
- this.numberFormat(
- typeof feature.properties[activeAggregateLayer.colorProperty] !==
- 'undefined'
- ? feature.properties[activeAggregateLayer.colorProperty]
- : feature.properties.indicators[
- activeAggregateLayer.colorProperty
- ],
- activeAggregateLayer,
- ) +
- ' ' +
- (activeAggregateLayer.unit || ''))
- );
- }
-
private createAdminRegionsLayer(layer: IbfLayer): GeoJSON {
if (!layer.data) {
return;
diff --git a/interfaces/IBF-dashboard/src/app/services/map.service.ts b/interfaces/IBF-dashboard/src/app/services/map.service.ts
index 7eb62b37ae..d5094ce71f 100644
--- a/interfaces/IBF-dashboard/src/app/services/map.service.ts
+++ b/interfaces/IBF-dashboard/src/app/services/map.service.ts
@@ -44,15 +44,6 @@ export class MapService {
public layers = [] as IbfLayer[];
private triggeredAreaColor = 'var(--ion-color-ibf-outline-red)';
private nonTriggeredAreaColor = 'var(--ion-color-ibf-no-alert-primary)';
- private disputedBorderStyle: {
- weight: number;
- dashArray: string;
- color: string;
- } = {
- weight: 2,
- dashArray: '5 5',
- color: null,
- };
private layerDataCache: Record = {};
public state = {
@@ -255,40 +246,35 @@ export class MapService {
layer: IbfLayerMetadata,
layerActive: boolean,
) => {
- const layerName =
- layer.name === IbfLayerName.redCrescentBranches
- ? IbfLayerName.redCrossBranches
- : layer.name;
if (this.country) {
if (layerActive) {
this.apiService
.getPointData(
this.country.countryCodeISO3,
- layerName,
+ layer.name,
this.disasterType.disasterType,
)
.subscribe((pointData) => {
- this.addPointDataLayer(layer, layerName, pointData);
+ this.addPointDataLayer(layer, pointData);
});
} else {
- this.addPointDataLayer(layer, layerName, null);
+ this.addPointDataLayer(layer, null);
}
}
};
private addPointDataLayer = (
layer: IbfLayerMetadata,
- layerName: IbfLayerName,
pointData: GeoJSON.FeatureCollection,
) => {
this.addLayer({
- name: layerName,
+ name: layer.name,
label: layer.label,
type: IbfLayerType.point,
group: IbfLayerGroup.point,
description: this.getPopoverText(layer),
active: this.adminLevelService.activeLayerNames.length
- ? this.adminLevelService.activeLayerNames.includes(layerName)
+ ? this.adminLevelService.activeLayerNames.includes(layer.name)
: this.getActiveState(layer),
show: true,
data: pointData,
@@ -632,14 +618,10 @@ export class MapService {
.pipe(shareReplay(1));
} else if (layer.type === IbfLayerType.point) {
// NOTE: any non-standard point layers should be placed above this 'else if'!
- const layerName =
- layer.name === IbfLayerName.redCrescentBranches
- ? IbfLayerName.redCrossBranches
- : layer.name;
layerData = this.apiService
.getPointData(
this.country.countryCodeISO3,
- layerName,
+ layer.name,
this.disasterType.disasterType,
)
.pipe(shareReplay(1));
@@ -943,20 +925,13 @@ export class MapService {
colorThreshold,
);
const fillOpacity = this.getAdminRegionFillOpacity(layer);
- let weight = this.getAdminRegionWeight(layer, placeCode);
- let color = this.getAdminRegionColor(layer);
- let dashArray: string;
- if (placeCode?.includes('Disputed')) {
- dashArray = this.disputedBorderStyle.dashArray;
- weight = this.disputedBorderStyle.weight;
- color = this.disputedBorderStyle.color;
- }
+ const weight = this.getAdminRegionWeight(layer, placeCode);
+ const color = this.getAdminRegionColor(layer);
return {
fillColor,
fillOpacity,
weight,
color,
- dashArray,
};
}
};
diff --git a/interfaces/IBF-dashboard/src/app/types/ibf-layer.ts b/interfaces/IBF-dashboard/src/app/types/ibf-layer.ts
index 4057605725..216a3963bb 100644
--- a/interfaces/IBF-dashboard/src/app/types/ibf-layer.ts
+++ b/interfaces/IBF-dashboard/src/app/types/ibf-layer.ts
@@ -106,7 +106,6 @@ export enum IbfLayerName {
rainfall = 'rainfall',
rainfallExtent = 'rainfall_extent',
rainfallForecast = 'rainfall_forecast',
- redCrescentBranches = 'red_crescent_branches',
redCrossBranches = 'red_cross_branches',
roads = 'roads',
roof_type = 'roof_type',
@@ -142,7 +141,6 @@ export enum IbfLayerLabel {
population = 'Population',
populationTotal = 'Total Population',
rainfallExtent = 'Rainfall extent',
- redCrescentBranches = 'Red Crescent branches',
redCrossBranches = 'Red Cross branches',
typhoonTrack = 'Typhoon track',
waterpoints = 'Waterpoints',
diff --git a/services/API-service/module-dependencies.md b/services/API-service/module-dependencies.md
new file mode 100644
index 0000000000..91899ed4a2
--- /dev/null
+++ b/services/API-service/module-dependencies.md
@@ -0,0 +1,43 @@
+# Module Dependencies Graph
+
+```mermaid
+graph LR
+ EapActionsModule-->UserModule
+ UserModule-->LookupModule
+ WaterpointsModule-->UserModule
+ WaterpointsModule-->CountryModule
+ CountryModule-->UserModule
+ AdminAreaModule-->UserModule
+ AdminAreaModule-->EventModule
+ EventModule-->UserModule
+ EventModule-->CountryModule
+ EventModule-->EapActionsModule
+ EventModule-->TyphoonTrackModule
+ TyphoonTrackModule-->UserModule
+ AdminAreaModule-->CountryModule
+ AdminAreaDynamicDataModule-->UserModule
+ AdminAreaDynamicDataModule-->EventModule
+ AdminAreaDynamicDataModule-->CountryModule
+ AdminAreaDynamicDataModule-->AdminAreaModule
+ MetadataModule-->UserModule
+ MetadataModule-->CountryModule
+ MetadataModule-->EventModule
+ PointDataModule-->UserModule
+ PointDataModule-->WhatsappModule
+ WhatsappModule-->LookupModule
+ WhatsappModule-->EventModule
+ WhatsappModule-->NotificationContentModule
+ NotificationContentModule-->EventModule
+ NotificationContentModule-->AdminAreaDynamicDataModule
+ NotificationContentModule-->AdminAreaDataModule
+ AdminAreaDataModule-->UserModule
+ NotificationContentModule-->AdminAreaModule
+ LinesDataModule-->UserModule
+ NotificationModule-->UserModule
+ NotificationModule-->EventModule
+ NotificationModule-->AdminAreaDynamicDataModule
+ NotificationModule-->WhatsappModule
+ NotificationModule-->NotificationContentModule
+ NotificationModule-->TyphoonTrackModule
+ CronjobModule-->AdminAreaDynamicDataModule
+```
diff --git a/services/API-service/package-lock.json b/services/API-service/package-lock.json
index 50e9f798e9..cea71113cf 100644
--- a/services/API-service/package-lock.json
+++ b/services/API-service/package-lock.json
@@ -26,6 +26,7 @@
"mailchimp-api-v3": "^1.15.0",
"mjml": "^4.15.3",
"mysql": "^2.15.0",
+ "nestjs-spelunker": "^1.3.1",
"pg": "^8.13.1",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.2.0",
@@ -2349,6 +2350,19 @@
"node": ">=8"
}
},
+ "node_modules/@ogma/common": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@ogma/common/-/common-1.2.0.tgz",
+ "integrity": "sha512-y2GHkT4t3B9CoI7ELfgZXeMHiiOC/X4XvdeqYLbP5A3keop7fANzjd5pQdtyYQvdQ0lPzcHJiMvOGkIINZHZHQ=="
+ },
+ "node_modules/@ogma/styler": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@ogma/styler/-/styler-1.1.0.tgz",
+ "integrity": "sha512-OtwJ8Ump3sKEAv2DIUi+R2G2Y5v3cEzqRO/sH11MTWikAhac31GZR/xzU+3BoPRT0u8oIob7N5dtOKTdPhqq3w==",
+ "dependencies": {
+ "@ogma/common": "^1.1.1"
+ }
+ },
"node_modules/@one-ini/wasm": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
@@ -9237,6 +9251,20 @@
"node": ">= 0.6"
}
},
+ "node_modules/nestjs-spelunker": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/nestjs-spelunker/-/nestjs-spelunker-1.3.1.tgz",
+ "integrity": "sha512-Vvye5jPdhKF+wyUDFlAxoEqXqWaXvpuyIIWN0/2OmJ3WNP1g7G2ljGM8pTnlWNsC8DGDdJcWB0OZ3y9IM07n7Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@ogma/styler": "^1.0.0"
+ },
+ "peerDependencies": {
+ "@nestjs/common": ">6.11.0",
+ "@nestjs/core": ">6.11.0",
+ "reflect-metadata": "^0.1.0"
+ }
+ },
"node_modules/no-case": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz",
@@ -13536,6 +13564,19 @@
}
}
},
+ "@ogma/common": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@ogma/common/-/common-1.2.0.tgz",
+ "integrity": "sha512-y2GHkT4t3B9CoI7ELfgZXeMHiiOC/X4XvdeqYLbP5A3keop7fANzjd5pQdtyYQvdQ0lPzcHJiMvOGkIINZHZHQ=="
+ },
+ "@ogma/styler": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@ogma/styler/-/styler-1.1.0.tgz",
+ "integrity": "sha512-OtwJ8Ump3sKEAv2DIUi+R2G2Y5v3cEzqRO/sH11MTWikAhac31GZR/xzU+3BoPRT0u8oIob7N5dtOKTdPhqq3w==",
+ "requires": {
+ "@ogma/common": "^1.1.1"
+ }
+ },
"@one-ini/wasm": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
@@ -18710,6 +18751,14 @@
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="
},
+ "nestjs-spelunker": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/nestjs-spelunker/-/nestjs-spelunker-1.3.1.tgz",
+ "integrity": "sha512-Vvye5jPdhKF+wyUDFlAxoEqXqWaXvpuyIIWN0/2OmJ3WNP1g7G2ljGM8pTnlWNsC8DGDdJcWB0OZ3y9IM07n7Q==",
+ "requires": {
+ "@ogma/styler": "^1.0.0"
+ }
+ },
"no-case": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz",
diff --git a/services/API-service/package.json b/services/API-service/package.json
index 2d23b939a4..8ae5b15142 100644
--- a/services/API-service/package.json
+++ b/services/API-service/package.json
@@ -44,6 +44,7 @@
"mailchimp-api-v3": "^1.15.0",
"mjml": "^4.15.3",
"mysql": "^2.15.0",
+ "nestjs-spelunker": "^1.3.1",
"pg": "^8.13.1",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.2.0",
diff --git a/services/API-service/src/api/country/country.service.ts b/services/API-service/src/api/country/country.service.ts
index 71a7b81628..44594ef2cf 100644
--- a/services/API-service/src/api/country/country.service.ts
+++ b/services/API-service/src/api/country/country.service.ts
@@ -1,4 +1,4 @@
-import { Injectable } from '@nestjs/common';
+import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { In, Repository } from 'typeorm';
@@ -222,6 +222,13 @@ export class CountryService {
where: { countryCodeISO3: notificationInfoCountry.countryCodeISO3 },
relations: ['notificationInfo'],
});
+ if (!existingCountry) {
+ // It is not ideal to throw an error halfway, but it's at least better than the 500 error that currently would occur
+ throw new HttpException(
+ `Country with code ${notificationInfoCountry.countryCodeISO3} not found. If multiple countries passed, then earlier countries have processed correctly, later countries have not.`,
+ HttpStatus.NOT_FOUND,
+ );
+ }
if (existingCountry.notificationInfo) {
existingCountry.notificationInfo = await this.createNotificationInfo(
diff --git a/services/API-service/src/api/metadata/dto/add-indicators.dto.ts b/services/API-service/src/api/metadata/dto/add-indicators.dto.ts
index 6bedca8436..3cb6865d7d 100644
--- a/services/API-service/src/api/metadata/dto/add-indicators.dto.ts
+++ b/services/API-service/src/api/metadata/dto/add-indicators.dto.ts
@@ -82,8 +82,7 @@ export class IndicatorDto {
@ApiProperty({
example: {
UGA: {
- floods:
- 'This layer represents the locations of the local branches, the source of this data comes from the National Society and may need updating.
Source link: Egyptian Red Crescent Society (ERCS). Year: 2020.',
+ floods: 'description',
},
},
})
diff --git a/services/API-service/src/api/metadata/dto/add-layers.dto.ts b/services/API-service/src/api/metadata/dto/add-layers.dto.ts
index 568dc799ec..eba9aaffe7 100644
--- a/services/API-service/src/api/metadata/dto/add-layers.dto.ts
+++ b/services/API-service/src/api/metadata/dto/add-layers.dto.ts
@@ -46,8 +46,7 @@ export class LayerDto {
@ApiProperty({
example: {
UGA: {
- 'heavy-rain':
- 'This layer represents the locations of the local branches, the source of this data comes from the National Society and may need updating.
Source link: Egyptian Red Crescent Society (ERCS). Year: 2020.',
+ 'heavy-rain': 'description',
},
},
})
diff --git a/services/API-service/src/main.ts b/services/API-service/src/main.ts
index 5e6bbc7b91..c53ccbc4b7 100644
--- a/services/API-service/src/main.ts
+++ b/services/API-service/src/main.ts
@@ -1,4 +1,9 @@
-import { BadRequestException, ValidationPipe } from '@nestjs/common';
+import fs from 'fs';
+import {
+ BadRequestException,
+ INestApplication,
+ ValidationPipe,
+} from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import {
DocumentBuilder,
@@ -8,9 +13,47 @@ import {
} from '@nestjs/swagger';
import * as bodyParser from 'body-parser';
+import { SpelunkerModule } from 'nestjs-spelunker';
import { ApplicationModule } from './app.module';
-import { EXTERNAL_API, PORT } from './config';
+import { DEBUG, EXTERNAL_API, PORT } from './config';
+
+/**
+ * A visualization of module dependencies is generated using `nestjs-spelunker`
+ * The file can be vied with [Mermaid](https://mermaid.live) or the VSCode extention "bierner.markdown-mermaid"
+ * See: https://github.com/jmcdo29/nestjs-spelunker
+ */
+function generateModuleDependencyGraph(app: INestApplication): void {
+ const tree = SpelunkerModule.explore(app);
+ const root = SpelunkerModule.graph(tree);
+ const edges = SpelunkerModule.findGraphEdges(root);
+ const genericModules = [
+ // Sorted alphabetically
+ 'ApplicationModule',
+ 'HealthModule',
+ 'HttpModule',
+ 'ScheduleModule',
+ 'ScriptsModule',
+ 'TerminusModule',
+ 'TypeOrmCoreModule',
+ 'TypeOrmModule',
+ ];
+ const mermaidEdges = edges
+ .filter(
+ ({ from, to }) =>
+ !genericModules.includes(from.module.name) &&
+ !genericModules.includes(to.module.name),
+ )
+ .map(({ from, to }) => ` ${from.module.name}-->${to.module.name}`);
+ const mermaidGraph =
+ '# Module Dependencies Graph\n\n```mermaid\ngraph LR\n' +
+ mermaidEdges.join('\n') +
+ '\n```\n';
+
+ fs.writeFile('module-dependencies.md', mermaidGraph, 'utf8', (err) => {
+ if (err) console.warn(`Writing API-graph failed!`, err);
+ });
+}
async function bootstrap(): Promise {
const appOptions = { cors: true };
@@ -67,6 +110,11 @@ async function bootstrap(): Promise {
extended: true,
}),
);
+
+ if (DEBUG) {
+ generateModuleDependencyGraph(app);
+ }
+
await app.listen(process.env.PORT || PORT);
}
bootstrap();
diff --git a/services/API-service/src/scripts/json/notification-info.json b/services/API-service/src/scripts/json/notification-info.json
index fef4f37088..fcd40e14e0 100644
--- a/services/API-service/src/scripts/json/notification-info.json
+++ b/services/API-service/src/scripts/json/notification-info.json
@@ -112,8 +112,8 @@
"triggerStatement": {
"drought": "TBD"
},
- "linkSocialMediaType": "WhatsApp",
- "linkSocialMediaUrl": "https://chat.whatsapp.com/FfVimuGRHHiJSk0BU7nGQT",
+ "linkSocialMediaType": null,
+ "linkSocialMediaUrl": null,
"linkVideo": "https://bit.ly/IBF-video-Zimbabwe",
"linkPdf": "https://510ibfsystem.blob.core.windows.net/manuals/IBF%20Manual-Zimbabwe-Published.pdf"
},
@@ -131,7 +131,6 @@
"linkSocialMediaUrl": "https://chat.whatsapp.com/Kjh3qxURJOQImAgUUmbmbR/",
"linkVideo": "https://bit.ly/IBF-video-Malawi",
"linkPdf": "https://510ibfsystem.blob.core.windows.net/manuals/IBF%20Manual-Malawi-Published.pdf",
-
"useWhatsapp": {
"flash-floods": true,
"floods": false
diff --git a/tests/integration/fixtures/country.const.ts b/tests/integration/fixtures/country.const.ts
new file mode 100644
index 0000000000..95a0a65768
--- /dev/null
+++ b/tests/integration/fixtures/country.const.ts
@@ -0,0 +1,101 @@
+import { CountryDto } from '../helpers/API-service/dto/create-country.dto';
+
+export const countryData: CountryDto[] = [
+ {
+ countryCodeISO3: 'MWI',
+ countryName: 'Malawi',
+ disasterTypes: ['floods', 'flash-floods'],
+ countryDisasterSettings: [
+ {
+ disasterType: 'floods',
+ adminLevels: [3],
+ defaultAdminLevel: 3,
+ activeLeadTimes: [
+ '0-day',
+ '1-day',
+ '2-day',
+ '3-day',
+ '4-day',
+ '5-day',
+ '6-day',
+ '7-day',
+ ],
+ eapLink:
+ 'https://510ibfsystem.blob.core.windows.net/about-trigger/MWI-EAP-document.pdf',
+ eapAlertClasses: {
+ no: {
+ label: 'No action',
+ color: 'ibf-no-alert-primary',
+ value: 0,
+ },
+ max: {
+ label: 'Trigger issued',
+ color: 'ibf-glofas-trigger',
+ value: 1,
+ },
+ },
+ isEventBased: true,
+ },
+ {
+ disasterType: 'flash-floods',
+ adminLevels: [3],
+ defaultAdminLevel: 3,
+ activeLeadTimes: [
+ '0-hour',
+ '1-hour',
+ '2-hour',
+ '3-hour',
+ '4-hour',
+ '5-hour',
+ '6-hour',
+ '7-hour',
+ '8-hour',
+ '9-hour',
+ '10-hour',
+ '11-hour',
+ '12-hour',
+ '15-hour',
+ '18-hour',
+ '21-hour',
+ '24-hour',
+ '48-hour',
+ ],
+ eapLink:
+ 'https://510ibfsystem.blob.core.windows.net/about-trigger/MWI-flashfloods-about.pdf',
+ enableEarlyActions: false,
+ enableStopTrigger: false,
+ isEventBased: true,
+ },
+ ],
+ adminRegionLabels: {
+ '1': {
+ singular: 'Region',
+ plural: 'Regions',
+ },
+ '2': {
+ singular: 'District',
+ plural: 'Districts',
+ },
+ '3': {
+ singular: 'Traditional Authority',
+ plural: 'Traditional Authorities',
+ },
+ },
+ countryLogos: {
+ floods: ['MWI-mrcs.png', 'ZWE-DanishRedCross.png'],
+ 'flash-floods': ['MWI-government.jpeg'],
+ },
+ countryBoundingBox: {
+ type: 'Polygon',
+ coordinates: [
+ [
+ [35.7719047381, -16.8012997372],
+ [35.7719047381, -9.23059905359],
+ [32.6881653175, -9.23059905359],
+ [32.6881653175, -16.8012997372],
+ [35.7719047381, -16.8012997372],
+ ],
+ ],
+ },
+ },
+];
diff --git a/tests/integration/fixtures/notification-info.const.ts b/tests/integration/fixtures/notification-info.const.ts
new file mode 100644
index 0000000000..d9fdbac536
--- /dev/null
+++ b/tests/integration/fixtures/notification-info.const.ts
@@ -0,0 +1,43 @@
+import { CreateNotificationInfoDto } from '../helpers/API-service/dto/create-notification-info.dto';
+
+export const notificationInfoData: CreateNotificationInfoDto[] = [
+ {
+ countryCodeISO3: 'MWI',
+ logo: {
+ floods:
+ 'https://mcusercontent.com/e71f3b134823403aa6fe0c6dc/images/31600ce7-b5e8-992f-8f53-e58f1b5dc955.png',
+ 'flash-floods':
+ 'https://mcusercontent.com/e71f3b134823403aa6fe0c6dc/images/d628fa5d-f4fa-bc51-9977-d6b464ff003b.png',
+ },
+ triggerStatement: {
+ floods:
+ 'An administrative area is triggered based on two parameters from the 6-days GloFAS forecast on a daily basis: the return period of the forecasted flood and the probability of occurrence. The trigger will activate when GloFAS issues a forecast of at least 60% probability of occurrence of a 5 year return period flood within the next 6 days. The GloFAS flood forecast triggers except in the Traditional Areas where the False Alarm Ratio (FAR) exceeds the predetermined maximum value which is 0.5.
Be aware that if a flood alert is issued with less than 6 days of lead time, the EAP may not be activated, please consider alternative response options.',
+ 'flash-floods':
+ 'A notification is issued when the model predicts that the rainfall forecasted can potentially lead to a flood, exposing at least 20 people. The trigger model updates every 6 hours based on rainfall forecasts. A warning notification will be issued based on the rainfall forecast, with a lead time of up to 48h. A trigger notification will be issued if the threshold is exceeded, with a lead time of 12 hours or less.',
+ },
+ linkSocialMediaType: 'WhatsApp',
+ linkSocialMediaUrl: 'https://chat.whatsapp.com/Kjh3qxURJOQImAgUUmbmbR/',
+ linkVideo: 'https://bit.ly/IBF-video-Malawi',
+ linkPdf:
+ 'https://510ibfsystem.blob.core.windows.net/manuals/IBF%20Manual-Malawi-Published.pdf',
+ useWhatsapp: {
+ 'flash-floods': true,
+ floods: false,
+ },
+ whatsappMessage: {
+ 'flash-floods': {
+ 'initial-single-event':
+ "*IBF [triggerState] notification*\n\nA [triggerState] for flash floods is forecasted in *[eventName]* for: *[startTimeEvent]*.\n\nTo receive more detailed information reply 'yes' to this message.",
+ 'initial-multi-event':
+ "*IBF notification*\n\nThere are *[nrEvents]* notifications issued for flash floods. The first notification is forecasted for: *[startTimeFirstEvent]*.\n\nTo receive more detailed information reply 'yes' to this message.",
+ 'follow-up':
+ '*IBF [triggerState] notification*\n\nA [triggerState] for flash floods is forecasted in *[eventName]*: *[startTimeEvent]*.\n\nThere are *[nrTriggeredAreas]* [adminAreaLabel] listed below in order of potentially exposed population.\n[areaList]\nOpen the IBF Portal on a computer to get more information about this [triggerState].',
+ 'whatsapp-group':
+ 'Please use the designated WhatsApp group ([whatsappGroupLink]) to communicate about this trigger.',
+ 'no-trigger-old-event':
+ 'The trigger warning formerly activated on *[startDate]* is now below trigger threshold.\n\n',
+ 'no-trigger': 'There is *no trigger* currently.',
+ },
+ },
+ },
+];
diff --git a/tests/integration/helpers/API-service/dto/create-country.dto.ts b/tests/integration/helpers/API-service/dto/create-country.dto.ts
new file mode 100644
index 0000000000..6109069e95
--- /dev/null
+++ b/tests/integration/helpers/API-service/dto/create-country.dto.ts
@@ -0,0 +1,32 @@
+import { AdminLevel } from '../enum/admin-level.enum';
+
+export interface CountryDto {
+ countryCodeISO3: string;
+ countryName: string;
+ countryDisasterSettings: CountryDisasterSettingsDto[];
+ adminRegionLabels: object;
+ countryLogos: object;
+ countryBoundingBox: any; //BoundingBox;
+ disasterTypes: string[];
+}
+
+export interface CountryDisasterSettingsDto {
+ disasterType: string;
+ adminLevels: AdminLevel[];
+ defaultAdminLevel: AdminLevel;
+ activeLeadTimes: string[];
+ droughtSeasonRegions?: object;
+ droughtEndOfMonthPipeline?: boolean;
+ showMonthlyEapActions?: boolean;
+ enableEarlyActions?: boolean;
+ enableStopTrigger?: boolean;
+ monthlyForecastInfo?: object;
+ eapLink: string;
+ eapAlertClasses?: object;
+ droughtRegions?: object;
+ isEventBased?: boolean;
+}
+
+export interface AddCountriesDto {
+ countries: CountryDto[];
+}
diff --git a/tests/integration/helpers/API-service/dto/create-notification-info.dto.ts b/tests/integration/helpers/API-service/dto/create-notification-info.dto.ts
new file mode 100644
index 0000000000..cc517f53a0
--- /dev/null
+++ b/tests/integration/helpers/API-service/dto/create-notification-info.dto.ts
@@ -0,0 +1,12 @@
+export interface CreateNotificationInfoDto {
+ countryCodeISO3: string;
+ logo: object;
+ triggerStatement: object;
+ linkSocialMediaType: string;
+ linkSocialMediaUrl: string;
+ linkVideo: string;
+ linkPdf: string;
+ useWhatsapp?: object;
+ whatsappMessage?: object;
+ externalEarlyActionForm?: string;
+}
diff --git a/tests/integration/helpers/API-service/enum/admin-level.enum.ts b/tests/integration/helpers/API-service/enum/admin-level.enum.ts
new file mode 100644
index 0000000000..d844594762
--- /dev/null
+++ b/tests/integration/helpers/API-service/enum/admin-level.enum.ts
@@ -0,0 +1,6 @@
+export enum AdminLevel {
+ adminLevel1 = 1,
+ adminLevel2 = 2,
+ adminLevel3 = 3,
+ adminLevel4 = 4,
+}
diff --git a/tests/integration/helpers/country.helper.ts b/tests/integration/helpers/country.helper.ts
new file mode 100644
index 0000000000..158020b131
--- /dev/null
+++ b/tests/integration/helpers/country.helper.ts
@@ -0,0 +1,35 @@
+import * as request from 'supertest';
+
+import { AddCountriesDto } from './API-service/dto/create-country.dto';
+import { CreateNotificationInfoDto } from './API-service/dto/create-notification-info.dto';
+import { getServer } from './utility.helper';
+
+export function addOrUpdateNotificationInfo(
+ notificationInfoData: CreateNotificationInfoDto[],
+ accessToken: string,
+): Promise {
+ return getServer()
+ .post(`/country/notification-info`)
+ .set('Authorization', `Bearer ${accessToken}`)
+ .send(notificationInfoData);
+}
+
+export function getCountries(
+ countryCodeISO3SArray: string[],
+ accessToken: string,
+): Promise {
+ return getServer()
+ .get(`/country`)
+ .set('Authorization', `Bearer ${accessToken}`)
+ .query({ countryCodesISO3: countryCodeISO3SArray.join(',') });
+}
+
+export function addOrUpdateCountries(
+ countryData: AddCountriesDto,
+ accessToken: string,
+): Promise {
+ return getServer()
+ .post(`/country`)
+ .set('Authorization', `Bearer ${accessToken}`)
+ .send(countryData);
+}
diff --git a/tests/integration/tests/country/create-country.test.ts b/tests/integration/tests/country/create-country.test.ts
new file mode 100644
index 0000000000..820af35237
--- /dev/null
+++ b/tests/integration/tests/country/create-country.test.ts
@@ -0,0 +1,92 @@
+import { countryData } from '../../fixtures/country.const';
+import { notificationInfoData } from '../../fixtures/notification-info.const';
+import {
+ addOrUpdateCountries,
+ addOrUpdateNotificationInfo,
+ getCountries,
+} from '../../helpers/country.helper';
+import { getAccessToken, resetDB } from '../../helpers/utility.helper';
+
+describe('create or update country and notification info', () => {
+ let accessToken: string;
+
+ beforeAll(async () => {
+ accessToken = await getAccessToken();
+ await resetDB(accessToken);
+ });
+
+ it('should update existing country and notification-info successfully', async () => {
+ // Arrange
+ const countryCodeISO3 = 'MWI';
+ const newLinkPdf = 'https://test-changed-link.com';
+ const newCountryName = 'Malawi-different-name';
+
+ const newCountryData = structuredClone(countryData);
+ const newNotificationInfoData = structuredClone(notificationInfoData);
+ newNotificationInfoData[0].linkPdf = newLinkPdf;
+ newCountryData[0].countryName = newCountryName;
+
+ // Act
+ const postCountryResult = await addOrUpdateCountries(
+ { countries: newCountryData },
+ accessToken,
+ );
+ const postNotificationInfoResult = await addOrUpdateNotificationInfo(
+ newNotificationInfoData,
+ accessToken,
+ );
+
+ const getResult = await getCountries([countryCodeISO3], accessToken);
+
+ // Assert
+ expect(postCountryResult.status).toBe(201);
+ expect(postNotificationInfoResult.status).toBe(201);
+ expect(getResult.status).toBe(200);
+ expect(getResult.body[0].countryName).toEqual(newCountryName);
+ expect(getResult.body[0].notificationInfo.linkPdf).toEqual(newLinkPdf);
+ });
+
+ it('should add new country and notification-info successfully', async () => {
+ // Arrange
+ const newCountryCodeISO3 = 'MWI-new-country';
+ const newCountryName = 'Malawi-new-country';
+
+ const newCountryData = structuredClone(countryData);
+ const newNotificationInfoData = structuredClone(notificationInfoData);
+ newCountryData[0].countryCodeISO3 = newCountryCodeISO3;
+ newCountryData[0].countryName = newCountryName;
+ newNotificationInfoData[0].countryCodeISO3 = newCountryCodeISO3;
+
+ // Act
+ const postCountryResult = await addOrUpdateCountries(
+ { countries: newCountryData },
+ accessToken,
+ );
+ const postNotificationInfoResult = await addOrUpdateNotificationInfo(
+ newNotificationInfoData,
+ accessToken,
+ );
+ const getResult = await getCountries([newCountryCodeISO3], accessToken);
+
+ // Assert
+ expect(postCountryResult.status).toBe(201);
+ expect(postNotificationInfoResult.status).toBe(201);
+ expect(getResult.status).toBe(200);
+ expect(getResult.body[0].countryName).toEqual(newCountryName);
+ });
+
+ it('should fail to create notification-info on unkown countryCodeISO3', async () => {
+ // Arrange
+ const newNotificationInfoData = structuredClone(notificationInfoData);
+ newNotificationInfoData[0].countryCodeISO3 = 'XXX';
+
+ // Act
+ const postResult = await addOrUpdateNotificationInfo(
+ newNotificationInfoData,
+ accessToken,
+ );
+
+ // Assert
+ expect(postResult.status).toBe(404);
+ });
+});