diff --git a/interfaces/IBF-dashboard/src/assets/i18n/en.json b/interfaces/IBF-dashboard/src/assets/i18n/en.json
index b833618f1..f31f21a6e 100644
--- a/interfaces/IBF-dashboard/src/assets/i18n/en.json
+++ b/interfaces/IBF-dashboard/src/assets/i18n/en.json
@@ -265,10 +265,10 @@
},
"active-event-active-trigger": {
"header": "Trigger: {{ eventName }} at {{firstLeadTimeDate}}",
- "header-ongoing": "Ongoing trigger: {{ eventName }} at {{firstLeadTimeDate}}",
+ "header-ongoing": "Ongoing trigger: {{ eventName }}",
"header-below-trigger": "Warning: {{ eventName }} at {{firstLeadTimeDate}}",
- "header-ongoing-below-trigger": "Ongoing warning: {{ eventName }} at {{firstLeadTimeDate}}",
- "welcome": "A trigger warning for {{ eventName }} was issued on {{ startDate }}.",
+ "header-ongoing-below-trigger": "Ongoing warning: {{ eventName }}",
+ "welcome": "A trigger for {{ eventName }} was issued on {{ startDate }}.",
"welcome-below-trigger": "A warning for {{ eventName }} was issued on {{ startDate }}, but it is currently not predicted to reach trigger thresholds.",
"upcoming-event": {
"landfall": "It is estimated to make landfall on {{ firstLeadTimeDate }}.
",
@@ -277,7 +277,7 @@
},
"ongoing-event": {
"landfall": "It has already made landfall.
",
- "no-landfall": "It has already reached the point closest to land.
"
+ "no-landfall": "It has already reached the point closest to land. It is not predicted to make landfall.
"
}
},
"active-event": {
diff --git a/services/API-service/src/api/notification/dto/notification-date-per-event.dto.ts b/services/API-service/src/api/notification/dto/notification-date-per-event.dto.ts
index b25fe9278..159bd68c7 100644
--- a/services/API-service/src/api/notification/dto/notification-date-per-event.dto.ts
+++ b/services/API-service/src/api/notification/dto/notification-date-per-event.dto.ts
@@ -1,10 +1,14 @@
-import { EapAlertClass, TriggeredArea } from '../../../shared/data.model';
+import {
+ DisasterSpecificProperties,
+ EapAlertClass,
+ TriggeredArea,
+} from '../../../shared/data.model';
import { LeadTime } from '../../admin-area-dynamic-data/enum/lead-time.enum';
export class NotificationDataPerEventDto {
triggerStatusLabel: TriggerStatusLabelEnum;
eventName: string;
- disasterSpecificCopy: DisasterSpecificCopy;
+ disasterSpecificProperties: DisasterSpecificProperties;
/**
* The day that the event starts.
diff --git a/services/API-service/src/api/notification/email/email.service.ts b/services/API-service/src/api/notification/email/email.service.ts
index 594ee44a6..32fbd22e4 100644
--- a/services/API-service/src/api/notification/email/email.service.ts
+++ b/services/API-service/src/api/notification/email/email.service.ts
@@ -64,7 +64,7 @@ export class EmailService {
if (isApiTest) {
// NOTE: use this to test the email output instead of using Mailchimp
// fs.writeFileSync(
- // `email-${country.countryCodeISO3}-${disasterType}.html`,
+ // `email-${country.countryCodeISO3}-${disasterType}-${new Date()}.html`,
// emailHtml,
// );
return emailHtml;
diff --git a/services/API-service/src/api/notification/email/mjml/body-event.ts b/services/API-service/src/api/notification/email/mjml/body-event.ts
index 2a6f968ac..b49ae46fc 100644
--- a/services/API-service/src/api/notification/email/mjml/body-event.ts
+++ b/services/API-service/src/api/notification/email/mjml/body-event.ts
@@ -1,5 +1,10 @@
+import { LeadTime } from '../../../admin-area-dynamic-data/enum/lead-time.enum';
+import { DisasterType } from '../../../disaster/disaster-type.enum';
import { ContentEventEmail } from '../../dto/content-trigger-email.dto';
-import { TriggerStatusLabelEnum } from '../../dto/notification-date-per-event.dto';
+import {
+ NotificationDataPerEventDto,
+ TriggerStatusLabelEnum,
+} from '../../dto/notification-date-per-event.dto';
import {
dateObjectToDateTimeString,
getDisasterIssuedLabel,
@@ -28,6 +33,7 @@ const getMjmlBodyEvent = ({
triangleIcon,
eapLink,
triggerStatusLabel,
+ disasterSpecificCopy,
}: {
color: string;
defaultAdminAreaLabel: string;
@@ -44,6 +50,7 @@ const getMjmlBodyEvent = ({
triangleIcon: string;
eapLink: string;
triggerStatusLabel: string;
+ disasterSpecificCopy: string;
}): object => {
const icon = getInlineImage({ src: triangleIcon, size: 16 });
@@ -54,23 +61,29 @@ const getMjmlBodyEvent = ({
const contentContent = [];
- if (firstTriggerLeadTimeFromNow) {
- // Trigger event
- if (firstLeadTimeString !== firstTriggerLeadTimeString) {
- // Warning-to-trigger event: show start of warning first
- contentContent.push(
- `${disasterTypeLabel}: Expected to start on ${firstLeadTimeString}, ${firstLeadTimeFromNow}.`,
- );
- }
- // Either way, show start of trigger next
+ if (disasterSpecificCopy) {
contentContent.push(
- `${disasterIssuedLabel}: Expected to trigger on ${firstTriggerLeadTimeString}, ${firstTriggerLeadTimeFromNow}.`,
+ `${disasterIssuedLabel}: ${disasterSpecificCopy}`,
);
} else {
- // Warning event
- contentContent.push(
- `${disasterIssuedLabel}: Expected to start on ${firstLeadTimeString}, ${firstLeadTimeFromNow}.`,
- );
+ if (triggerStatusLabel === TriggerStatusLabelEnum.Trigger) {
+ // Trigger event
+ if (firstLeadTimeString !== firstTriggerLeadTimeString) {
+ // Warning-to-trigger event: show start of warning first
+ contentContent.push(
+ `${disasterTypeLabel}: Expected to start on ${firstLeadTimeString}, ${firstLeadTimeFromNow}.`,
+ );
+ }
+ // Either way, show start of trigger next, the above line is optionally extra
+ contentContent.push(
+ `${disasterIssuedLabel}: Expected to start on ${firstTriggerLeadTimeString}, ${firstTriggerLeadTimeFromNow}.`,
+ );
+ } else {
+ // Warning event
+ contentContent.push(
+ `${disasterIssuedLabel}: Expected to start on ${firstLeadTimeString}, ${firstLeadTimeFromNow}.`,
+ );
+ }
}
contentContent.push(
@@ -88,7 +101,7 @@ const getMjmlBodyEvent = ({
});
const closingElement = getTextElement({
- content: `This ${triggerStatusLabel} was first issued by IBF on ${issuedDate} (${timeZone})`,
+ content: `This ${triggerStatusLabel.toLowerCase()} was first issued by IBF on ${issuedDate} (${timeZone})`,
attributes: {
'padding-top': '8px',
'font-size': '14px',
@@ -101,10 +114,48 @@ const getMjmlBodyEvent = ({
});
};
+const getTyphoonSpecificCopy = (event: NotificationDataPerEventDto): string => {
+ let disasterSpecificCopy: string;
+ if (event.firstLeadTime === LeadTime.hour0) {
+ if (event.disasterSpecificProperties.typhoonLandfall) {
+ disasterSpecificCopy = 'Has already made landfall.';
+ } else {
+ disasterSpecificCopy =
+ 'Has already reached the point closest to land. Not predicted to make landfall.';
+ }
+ } else {
+ if (event.disasterSpecificProperties.typhoonLandfall) {
+ disasterSpecificCopy = `Expected to make landfall on ${
+ event.triggerStatusLabel === TriggerStatusLabelEnum.Trigger
+ ? event.firstTriggerLeadTimeString
+ : event.firstLeadTimeString
+ }.`;
+ } else if (event.disasterSpecificProperties.typhoonNoLandfallYet) {
+ disasterSpecificCopy =
+ 'The landfall time prediction cannot be determined yet. Keep monitoring the event.';
+ } else {
+ disasterSpecificCopy = `Expected to reach the point closest to land on ${
+ event.triggerStatusLabel === TriggerStatusLabelEnum.Trigger
+ ? event.firstTriggerLeadTimeString
+ : event.firstLeadTimeString
+ }. Not predicted to make landfall.`;
+ }
+ }
+ if (event.triggerStatusLabel === TriggerStatusLabelEnum.Warning) {
+ disasterSpecificCopy += ' Not predicted to reach trigger thresholds.';
+ }
+ return disasterSpecificCopy;
+};
+
export const getMjmlEventListBody = (emailContent: ContentEventEmail) => {
const eventList = [];
for (const event of emailContent.dataPerEvent) {
+ let disasterSpecificCopy: string;
+ if (emailContent.disasterType === DisasterType.Typhoon) {
+ disasterSpecificCopy = getTyphoonSpecificCopy(event);
+ }
+
eventList.push(
getMjmlBodyEvent({
eventName: event.eventName,
@@ -137,12 +188,15 @@ export const getMjmlEventListBody = (emailContent: ContentEventEmail) => {
disasterIssuedLabel: getDisasterIssuedLabel(
event.eapAlertClass?.label,
- emailContent.disasterTypeLabel,
+ event.triggerStatusLabel,
),
color: getIbfHexColor(
event.eapAlertClass?.color,
event.triggerStatusLabel,
),
+
+ // Disaster-specific copy
+ disasterSpecificCopy,
}),
);
}
diff --git a/services/API-service/src/api/notification/helpers/mjml.helper.ts b/services/API-service/src/api/notification/helpers/mjml.helper.ts
index cc2e3d7b2..44a7a1298 100644
--- a/services/API-service/src/api/notification/helpers/mjml.helper.ts
+++ b/services/API-service/src/api/notification/helpers/mjml.helper.ts
@@ -268,11 +268,13 @@ export const getTimeFromNow = (leadTime: LeadTime) => {
const leadTimeQuantity = parseInt(leadTime.split('-')[0]);
- return [LeadTime.day0, LeadTime.month0, LeadTime.hour0].includes(leadTime)
- ? 'ongoing'
- : `${leadTime.replace('-', ' ')}${
- leadTimeQuantity === 1 ? '' : 's'
- } from now`;
+ if (leadTimeQuantity === 0) {
+ return 'ongoing';
+ }
+
+ return `${leadTime.replace('-', ' ')}${
+ leadTimeQuantity === 1 ? '' : 's'
+ } from now`;
};
export const getTriangleIcon = (
@@ -321,9 +323,9 @@ export const getPngImageAsDataURL = (relativePath: string) => {
export const getDisasterIssuedLabel = (
eapLabel: string,
- disasterTypeLabel: string,
+ triggerStatusLabel: TriggerStatusLabelEnum,
) => {
- return eapLabel || disasterTypeLabel;
+ return eapLabel || triggerStatusLabel;
};
export const getIbfHexColor = (
diff --git a/services/API-service/src/api/notification/notification-content/notification-content.service.ts b/services/API-service/src/api/notification/notification-content/notification-content.service.ts
index 41f33385a..a53b4f5aa 100644
--- a/services/API-service/src/api/notification/notification-content/notification-content.service.ts
+++ b/services/API-service/src/api/notification/notification-content/notification-content.service.ts
@@ -80,7 +80,7 @@ export class NotificationContentService {
});
}
- public async getDisaster(
+ private async getDisaster(
disasterType: DisasterType,
): Promise {
return await this.disasterRepository.findOne({
@@ -165,11 +165,7 @@ export class NotificationContentService {
: TriggerStatusLabelEnum.Warning;
data.eventName = await this.getFormattedEventName(event, disasterType);
- data.disasterSpecificCopy = await this.getDisasterSpecificCopy(
- disasterType,
- event.firstLeadTime,
- event,
- );
+ data.disasterSpecificProperties = event.disasterSpecificProperties;
data.firstLeadTime = event.firstLeadTime;
data.firstTriggerLeadTime = event.firstTriggerLeadTime;
data.triggeredAreas = await this.getSortedTriggeredAreas(
@@ -178,7 +174,7 @@ export class NotificationContentService {
event,
);
data.nrOfTriggeredAreas = data.triggeredAreas.length;
- // This looks weird, but as far as I understand the startDate of the event is the day it was first issued
+
data.issuedDate = new Date(event.startDate);
data.firstLeadTimeString = await this.getFirstLeadTimeString(
event,
@@ -348,109 +344,6 @@ export class NotificationContentService {
);
}
- private async getDisasterSpecificCopy(
- disasterType: DisasterType,
- leadTime: LeadTime,
- event: EventSummaryCountry,
- ): Promise<{
- eventStatus: string;
- extraInfo: string;
- leadTimeString?: string;
- timestamp?: string;
- }> {
- switch (disasterType) {
- case DisasterType.HeavyRain:
- return this.getHeavyRainCopy();
- case DisasterType.Typhoon:
- return await this.getTyphoonCopy(leadTime, event);
- case DisasterType.FlashFloods:
- return await this.getFlashFloodsCopy(leadTime, event);
- default:
- return { eventStatus: '', extraInfo: '' };
- }
- }
-
- private getHeavyRainCopy(): {
- eventStatus: string;
- extraInfo: string;
- } {
- return {
- eventStatus: 'Estimated',
- extraInfo: '',
- };
- }
-
- private async getTyphoonCopy(
- leadTime: LeadTime,
- event: EventSummaryCountry,
- ): Promise<{
- eventStatus: string;
- extraInfo: string;
- leadTimeString: string;
- timestamp: string;
- }> {
- const { typhoonLandfall, typhoonNoLandfallYet } =
- event.disasterSpecificProperties;
- let eventStatus = '';
- let extraInfo = '';
- let leadTimeString = null;
-
- if (leadTime === LeadTime.hour0) {
- if (typhoonLandfall) {
- eventStatus = 'Has already made landfall';
- leadTimeString = 'Already made landfall';
- } else {
- eventStatus = 'Has already reached the point closest to land';
- leadTimeString = 'reached the point closest to land';
- }
- } else {
- if (typhoonNoLandfallYet) {
- eventStatus =
- 'Landfall time prediction cannot be determined yet';
- extraInfo = 'Keep monitoring the event.';
- leadTimeString = 'Undetermined landfall';
- } else if (typhoonLandfall) {
- eventStatus = 'Estimated to make landfall';
- } else {
- eventStatus =
- 'Not predicted to make landfall. It is estimated to reach the point closest to land';
- }
- }
-
- const timestampString = await this.getLeadTimeTimestamp(
- leadTime,
- event.countryCodeISO3,
- DisasterType.Typhoon,
- );
-
- return {
- eventStatus: eventStatus,
- extraInfo: extraInfo,
- leadTimeString,
- timestamp: timestampString,
- };
- }
-
- private async getFlashFloodsCopy(
- leadTime: LeadTime,
- event: EventSummaryCountry,
- ): Promise<{
- eventStatus: string;
- extraInfo: string;
- timestamp: string;
- }> {
- const timestampString = await this.getLeadTimeTimestamp(
- leadTime,
- event.countryCodeISO3,
- DisasterType.FlashFloods,
- );
- return {
- eventStatus: 'The flash flood is forecasted: ',
- extraInfo: '',
- timestamp: timestampString,
- };
- }
-
private async getLeadTimeTimestamp(
leadTime: LeadTime,
countryCodeISO3: string,
diff --git a/tests/integration/email/typhoon/email-phl-typhoon.test.ts b/tests/integration/email/typhoon/email-phl-typhoon.test.ts
index 937888b75..dbba8f332 100644
--- a/tests/integration/email/typhoon/email-phl-typhoon.test.ts
+++ b/tests/integration/email/typhoon/email-phl-typhoon.test.ts
@@ -19,4 +19,40 @@ describe('Should send an email for phl typhoon', () => {
);
expect(result).toBeTruthy();
});
+
+ it('warning', async () => {
+ const result = await testTyphoonScenario(
+ TyphoonScenario.EventNoTrigger,
+ countryCodeISO3,
+ accessToken,
+ );
+ expect(result).toBeTruthy();
+ });
+
+ it('no landfall (trigger)', async () => {
+ const result = await testTyphoonScenario(
+ TyphoonScenario.EventNoLandfall,
+ countryCodeISO3,
+ accessToken,
+ );
+ expect(result).toBeTruthy();
+ });
+
+ it('no landfall yet (trigger)', async () => {
+ const result = await testTyphoonScenario(
+ TyphoonScenario.EventNoLandfallYet,
+ countryCodeISO3,
+ accessToken,
+ );
+ expect(result).toBeTruthy();
+ });
+
+ it('after landfall (trigger)', async () => {
+ const result = await testTyphoonScenario(
+ TyphoonScenario.EventAfterLandfall,
+ countryCodeISO3,
+ accessToken,
+ );
+ expect(result).toBeTruthy();
+ });
});
diff --git a/tests/integration/email/typhoon/test-typhoon-scenario.helper.ts b/tests/integration/email/typhoon/test-typhoon-scenario.helper.ts
index 6e5a39b82..0b6d7e19b 100644
--- a/tests/integration/email/typhoon/test-typhoon-scenario.helper.ts
+++ b/tests/integration/email/typhoon/test-typhoon-scenario.helper.ts
@@ -38,7 +38,8 @@ export async function testTyphoonScenario(
expect(response.body.finishedEvents).toBeFalsy();
// Parse the HTML content
- const dom = new JSDOM(response.body.activeEvents.email);
+ const emailContent = response.body.activeEvents.email;
+ const dom = new JSDOM(emailContent);
const document = dom.window.document;
// Get all span elements with data-testid="event-name" and their lower case text content
@@ -56,5 +57,23 @@ export async function testTyphoonScenario(
expect(hasEvent).toBe(true);
}
+ if (scenario === TyphoonScenario.EventTrigger) {
+ expect(emailContent).toContain('Expected to make landfall on');
+ } else if (scenario === TyphoonScenario.EventNoTrigger) {
+ expect(emailContent).toContain(
+ 'Not predicted to reach trigger thresholds.',
+ );
+ } else if (scenario === TyphoonScenario.EventNoLandfall) {
+ expect(emailContent).toContain(
+ 'Expected to reach the point closest to land on',
+ );
+ } else if (scenario === TyphoonScenario.EventNoLandfallYet) {
+ expect(emailContent).toContain(
+ 'The landfall time prediction cannot be determined yet',
+ );
+ } else if (scenario === TyphoonScenario.EventAfterLandfall) {
+ expect(emailContent).toContain('Has already made landfall');
+ }
+
return true;
}