From bcf5f4e2a6c317db25fa7a76efe1796240a41145 Mon Sep 17 00:00:00 2001 From: Ruben Date: Fri, 12 Apr 2024 16:48:28 +0200 Subject: [PATCH 01/55] WIP: Email multi event AB#27182 --- services/API-service/.gitignore | 1 - services/API-service/package-lock.json | 233 ++++++- services/API-service/package.json | 1 + .../api/country/country-time-zone-mapping.ts | 11 + .../src/api/event/event-map-image.entity.ts | 2 +- .../dto/admin-area-notification-info.dto.ts | 4 + .../dto/content-trigger-email.dto.ts | 16 + .../dto/notification-date-per-event.dto.ts | 26 + .../email/email-template.service.ts | 527 ++++++++++++++ .../api/notification/email/email.service.ts | 657 +----------------- .../src/api/notification/email/html/base.html | 8 +- .../email/html/email-body-trigger-event.html | 132 ++++ .../email/html/email-body-warning-event.html | 120 ++++ .../email/html/email-table-event.html | 96 +++ .../email/html/email-table-trigger-row.html | 36 + .../email/html/email-table-warning-row.html | 22 + .../email/html/header-event-overview.html | 2 +- .../notification/email/html/map-image.html | 8 +- .../email/html/social-media-link.html | 4 +- .../email/html/trigger-finished.html | 10 +- .../email/html/trigger-notification.html | 30 +- .../api/notification/email/icons/trigger.png | Bin 0 -> 760 bytes .../notification/email/icons/warning-low.png | Bin 0 -> 770 bytes .../email/icons/warning-medium.png | Bin 0 -> 785 bytes .../format-action-unit-value.helper.ts | 16 + .../notification-content.module.ts | 1 + .../notification-content.service.ts | 338 +++++---- .../notification/notification.controller.ts | 2 +- .../api/notification/notification.module.ts | 3 +- .../notification/whatsapp/whatsapp.service.ts | 11 +- 30 files changed, 1462 insertions(+), 855 deletions(-) create mode 100644 services/API-service/src/api/country/country-time-zone-mapping.ts create mode 100644 services/API-service/src/api/notification/dto/admin-area-notification-info.dto.ts create mode 100644 services/API-service/src/api/notification/dto/content-trigger-email.dto.ts create mode 100644 services/API-service/src/api/notification/dto/notification-date-per-event.dto.ts create mode 100644 services/API-service/src/api/notification/email/email-template.service.ts create mode 100644 services/API-service/src/api/notification/email/html/email-body-trigger-event.html create mode 100644 services/API-service/src/api/notification/email/html/email-body-warning-event.html create mode 100644 services/API-service/src/api/notification/email/html/email-table-event.html create mode 100644 services/API-service/src/api/notification/email/html/email-table-trigger-row.html create mode 100644 services/API-service/src/api/notification/email/html/email-table-warning-row.html create mode 100644 services/API-service/src/api/notification/email/icons/trigger.png create mode 100644 services/API-service/src/api/notification/email/icons/warning-low.png create mode 100644 services/API-service/src/api/notification/email/icons/warning-medium.png create mode 100644 services/API-service/src/api/notification/helpers/format-action-unit-value.helper.ts diff --git a/services/API-service/.gitignore b/services/API-service/.gitignore index 3aa5681ba..2747035a3 100644 --- a/services/API-service/.gitignore +++ b/services/API-service/.gitignore @@ -1,5 +1,4 @@ # IDE / Editors -.vscode .idea *.sublime-project *.sublime-workspace diff --git a/services/API-service/package-lock.json b/services/API-service/package-lock.json index 1ea0e3dcf..c54608ee2 100644 --- a/services/API-service/package-lock.json +++ b/services/API-service/package-lock.json @@ -22,6 +22,7 @@ "crypto-js": "^4.0.0", "cryptr": "^6.0.2", "csv-parser": "^3.0.0", + "ejs": "^3.1.9", "jsonwebtoken": "^8.1.1", "mailchimp-api-v3": "^1.15.0", "mysql": "^2.15.0", @@ -3043,6 +3044,11 @@ "node": ">=4" } }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, "node_modules/async-limiter": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", @@ -3453,7 +3459,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3959,8 +3964,7 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "node_modules/concat-stream": { "version": "1.6.2", @@ -4587,6 +4591,20 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "node_modules/ejs": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", + "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -5667,6 +5685,33 @@ "dev": true, "optional": true }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -6870,6 +6915,87 @@ "node": ">=6" } }, + "node_modules/jake": { + "version": "10.8.7", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", + "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jake/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jake/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jake/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jake/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jake/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jest": { "version": "24.9.0", "resolved": "https://registry.npmjs.org/jest/-/jest-24.9.0.tgz", @@ -9815,7 +9941,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -16138,6 +16263,11 @@ "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", "dev": true }, + "async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, "async-limiter": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", @@ -16479,7 +16609,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -16889,8 +17018,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "concat-stream": { "version": "1.6.2", @@ -17390,6 +17518,14 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "ejs": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", + "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", + "requires": { + "jake": "^10.8.5" + } + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -18211,6 +18347,32 @@ "dev": true, "optional": true }, + "filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "requires": { + "minimatch": "^5.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -19154,6 +19316,62 @@ "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==" }, + "jake": { + "version": "10.8.7", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", + "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", + "requires": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "jest": { "version": "24.9.0", "resolved": "https://registry.npmjs.org/jest/-/jest-24.9.0.tgz", @@ -21654,7 +21872,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "requires": { "brace-expansion": "^1.1.7" } diff --git a/services/API-service/package.json b/services/API-service/package.json index cbacf60be..74d68ab0d 100644 --- a/services/API-service/package.json +++ b/services/API-service/package.json @@ -40,6 +40,7 @@ "crypto-js": "^4.0.0", "cryptr": "^6.0.2", "csv-parser": "^3.0.0", + "ejs": "^3.1.9", "jsonwebtoken": "^8.1.1", "mailchimp-api-v3": "^1.15.0", "mysql": "^2.15.0", diff --git a/services/API-service/src/api/country/country-time-zone-mapping.ts b/services/API-service/src/api/country/country-time-zone-mapping.ts new file mode 100644 index 000000000..67cf4657c --- /dev/null +++ b/services/API-service/src/api/country/country-time-zone-mapping.ts @@ -0,0 +1,11 @@ +export const CountryTimeZoneMapping: { [key: string]: string } = { + UGA: 'Africa/Kampala', + KEN: 'Africa/Nairobi', + ETH: 'Africa/Addis_Ababa', + ZMB: 'Africa/Lusaka', + MWI: 'Africa/Blantyre', + ZWE: 'Africa/Harare', + EGY: 'Africa/Cairo', + PHL: 'Asia/Manila', + SSD: 'Africa/Juba', +}; diff --git a/services/API-service/src/api/event/event-map-image.entity.ts b/services/API-service/src/api/event/event-map-image.entity.ts index 9cc6b9a15..36c0f1587 100644 --- a/services/API-service/src/api/event/event-map-image.entity.ts +++ b/services/API-service/src/api/event/event-map-image.entity.ts @@ -16,7 +16,7 @@ export class EventMapImageEntity { public id: string; @Column({ type: 'bytea' }) - public image: any; + public image: Buffer; @ApiProperty({ example: 'SSD' }) @ManyToOne((): typeof CountryEntity => CountryEntity) diff --git a/services/API-service/src/api/notification/dto/admin-area-notification-info.dto.ts b/services/API-service/src/api/notification/dto/admin-area-notification-info.dto.ts new file mode 100644 index 000000000..c5afda2a9 --- /dev/null +++ b/services/API-service/src/api/notification/dto/admin-area-notification-info.dto.ts @@ -0,0 +1,4 @@ +export class AdminAreaLabel { + singular: string; + plural: string; +} diff --git a/services/API-service/src/api/notification/dto/content-trigger-email.dto.ts b/services/API-service/src/api/notification/dto/content-trigger-email.dto.ts new file mode 100644 index 000000000..e5bda8d48 --- /dev/null +++ b/services/API-service/src/api/notification/dto/content-trigger-email.dto.ts @@ -0,0 +1,16 @@ +import { CountryEntity } from '../../country/country.entity'; +import { DisasterType } from '../../disaster/disaster-type.enum'; +import { IndicatorMetadataEntity } from '../../metadata/indicator-metadata.entity'; +import { AdminAreaLabel } from './admin-area-notification-info.dto'; +import { NotificationDataPerEventDto } from './notification-date-per-event.dto'; + +export class ContentTriggerEmail { + public disasterType: DisasterType; + public disasterTypeLabel: string; + public indicatorMetadata: IndicatorMetadataEntity; + public dataPerEvent: NotificationDataPerEventDto[]; + public mapImageData: any[]; + public defaultAdminLevel: number; + public defaultAdminAreaLabel: AdminAreaLabel; + public country: CountryEntity; // Ensure that is has the following relations 'disasterTypes', 'notificationInfo','countryDisasterSettings','countryDisasterSettings.activeLeadTimes', +} 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 new file mode 100644 index 000000000..96baf5862 --- /dev/null +++ b/services/API-service/src/api/notification/dto/notification-date-per-event.dto.ts @@ -0,0 +1,26 @@ +import { 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; + firstLeadTime: LeadTime; + triggeredAreas: TriggeredArea[]; + nrOfTriggeredAreas: number; + startDateEventString: string; + totalAffectectedOfIndicator: number; + mapImage?: Buffer; +} + +export enum TriggerStatusLabelEnum { + Trigger = 'Trigger', + Warning = 'Warning', +} + +export class DisasterSpecificCopy { + eventStatus: string; + extraInfo: string; + leadTimeString?: string; + timestamp?: string; +} diff --git a/services/API-service/src/api/notification/email/email-template.service.ts b/services/API-service/src/api/notification/email/email-template.service.ts new file mode 100644 index 000000000..103489e2b --- /dev/null +++ b/services/API-service/src/api/notification/email/email-template.service.ts @@ -0,0 +1,527 @@ +import { Injectable } from '@nestjs/common'; +import { ContentTriggerEmail } from '../dto/content-trigger-email.dto'; +import { LeadTime } from '../../admin-area-dynamic-data/enum/lead-time.enum'; +import { + NotificationDataPerEventDto, + TriggerStatusLabelEnum, +} from '../dto/notification-date-per-event.dto'; +import * as ejs from 'ejs'; +import * as fs from 'fs'; +import { CountryTimeZoneMapping } from '../../country/country-time-zone-mapping'; +import { DisasterType } from '../../disaster/disaster-type.enum'; +import { EventSummaryCountry } from '../../../shared/data.model'; +import { CountryEntity } from '../../country/country.entity'; + +const emailFolder = './src/api/notification/email'; +const emailTemplateFolder = `${emailFolder}/html`; +const emailIconFolder = `${emailFolder}/icons`; + +class ReplaceKeyValue { + replaceKey: string; + replaceValue: string; +} + +@Injectable() +export class EmailTemplateService { + private placeholderToday = '(TODAY)'; + + public createHtmlForTriggerEmail( + contentForEmail: ContentTriggerEmail, + date: Date, + ): string { + const replaceKeyValues = this.createReplaceKeyValuesTrigger( + contentForEmail, + date, + ); + return this.formatEmail(replaceKeyValues); + } + + // TODO REFACTOR this to use a DTO (ContentTriggerFinishedEmail) instead of multiple parameters + public createHtmlForTriggerFinishedEmail( + country: CountryEntity, + disasterType: DisasterType, + finishedEvent: EventSummaryCountry, + disasterTypeLabel: string, + date: Date, + ): string { + const replaceKeyValues = this.createReplaceKeyValuesTriggerFinished( + country, + disasterType, + finishedEvent, + disasterTypeLabel, + date, + ); + return this.formatEmail(replaceKeyValues); + } + + private createReplaceKeyValuesTrigger( + contentForEmail: ContentTriggerEmail, + date: Date, + ): ReplaceKeyValue[] { + const country = contentForEmail.country; + const disasterType = contentForEmail.disasterType; + const keyValueReplaceList = [ + { + replaceKey: 'emailBody', + replaceValue: this.getEmailBody(false), + }, + { + replaceKey: 'headerEventOverview', + replaceValue: this.getHeaderEventOverview(contentForEmail.dataPerEvent), + }, + { + replaceKey: 'socialMediaPart', + replaceValue: this.getSocialMediaHtml(contentForEmail.country), + }, + { + replaceKey: 'tablesStacked', + replaceValue: this.getTablesForEvents(contentForEmail), + }, + { + replaceKey: this.placeholderToday, + replaceValue: date.toLocaleDateString('default', { + day: '2-digit', + month: 'short', + year: 'numeric', + }), + }, + { + replaceKey: 'eventListBody', + replaceValue: this.getEventListBody(contentForEmail), + }, + { + replaceKey: 'imgLogo', + replaceValue: country.notificationInfo.logo[disasterType], + }, + { + replaceKey: 'triggerStatement', + replaceValue: country.notificationInfo.triggerStatement[disasterType], + }, + { + replaceKey: 'mapImagePart', + replaceValue: this.getMapImageHtml(contentForEmail), + }, + { + replaceKey: 'linkDashboard', + replaceValue: process.env.DASHBOARD_URL, + }, + { + replaceKey: 'linkEapSop', + replaceValue: country.countryDisasterSettings.find( + (s) => s.disasterType === disasterType, + ).eapLink, + }, + { + replaceKey: 'socialMediaLink', + replaceValue: country.notificationInfo.linkSocialMediaUrl, + }, + { + replaceKey: 'socialMediaType', + replaceValue: country.notificationInfo.linkSocialMediaType, + }, + { + replaceKey: 'disasterType', + replaceValue: contentForEmail.disasterTypeLabel, + }, + { + replaceKey: 'videoPdfLinks', + replaceValue: this.getVideoPdfLinks( + country.notificationInfo.linkVideo, + country.notificationInfo.linkPdf, + ), + }, + ]; + return keyValueReplaceList; + } + + private createReplaceKeyValuesTriggerFinished( + country: CountryEntity, + disasterType: DisasterType, + event: EventSummaryCountry, + disasterTypeLabel: string, + date: Date, + ): ReplaceKeyValue[] { + const keyValueReplaceList = [ + { + replaceKey: 'emailBody', + replaceValue: this.getEmailBody(true), + }, + { + replaceKey: 'headerEventOverview', + replaceValue: '', + }, + { + replaceKey: 'imgLogo', + replaceValue: country.notificationInfo.logo[disasterType], + }, + { + replaceKey: 'startDate', + replaceValue: event.startDate, + }, + { + replaceKey: 'linkDashboard', + replaceValue: process.env.DASHBOARD_URL, + }, + { + replaceKey: 'socialMediaPart', + replaceValue: this.getSocialMediaHtml(country), + }, + { + replaceKey: 'linkEapSop', + replaceValue: country.countryDisasterSettings.find( + (s) => s.disasterType === disasterType, + ).eapLink, + }, + { + replaceKey: 'socialMediaLink', + replaceValue: country.notificationInfo.linkSocialMediaUrl, + }, + { + replaceKey: 'socialMediaType', + replaceValue: country.notificationInfo.linkSocialMediaType, + }, + { + replaceKey: 'videoPdfLinks', + replaceValue: this.getVideoPdfLinks( + country.notificationInfo.linkVideo, + country.notificationInfo.linkPdf, + ), + }, + { + replaceKey: 'disasterType', + replaceValue: disasterTypeLabel, + }, + { + replaceKey: this.placeholderToday, + replaceValue: date.toLocaleDateString('default', { + day: '2-digit', + month: 'short', + year: 'numeric', + }), + }, + ]; + return keyValueReplaceList; + } + + private getEmailBody(triggerFinished: boolean): string { + if (triggerFinished) { + return fs.readFileSync( + './src/api/notification/email/html/trigger-finished.html', + 'utf8', + ); + } else { + return fs.readFileSync( + './src/api/notification/email/html/trigger-notification.html', + 'utf8', + ); + } + } + + private getHeaderEventOverview( + eventsData: NotificationDataPerEventDto[], + ): string { + const leadTimeListShort = this.getEventListShort(eventsData); + let headerEventOverview = fs.readFileSync( + './src/api/notification/email/html/header-event-overview.html', + 'utf8', + ); + headerEventOverview = ejs.render(headerEventOverview, { + eventListHeader: leadTimeListShort, + }); + return headerEventOverview; + } + + private getVideoPdfLinks(videoLink: string, pdfLink: string) { + // TODO: Use ejs template + const linkVideoHTML = ` + video`; + + const linkPdfHTML = `PDF`; + let videoStr = ''; + if (videoLink) { + videoStr = ' ' + linkVideoHTML; + } + let pdfStr = ''; + if (pdfLink) { + pdfStr = ' ' + linkPdfHTML; + } + let orStr = ''; + if (videoStr && pdfStr) { + orStr = ' or'; + } + if (videoStr || pdfStr) { + return `See instructions for the IBF-portal in${videoStr}${orStr}${pdfStr}.`; + } + } + + private getSocialMediaHtml(country: CountryEntity): string { + if (country.notificationInfo.linkSocialMediaType) { + return fs.readFileSync( + './src/api/notification/email/html/social-media-link.html', + 'utf8', + ); + } else { + return ''; + } + } + + private getMapImageHtml(contentForEmail: ContentTriggerEmail): string { + let html = ''; + for (const event of contentForEmail.dataPerEvent) { + const mapImage = event.mapImage; + if (mapImage) { + let eventHtml = fs.readFileSync( + './src/api/notification/email/html/map-image.html', + 'utf8', + ); + const replacements = { + mapImgSrc: this.getMapImgSrc( + contentForEmail.country.countryCodeISO3, + contentForEmail.disasterType, + event.eventName, + ), + mapImgDescription: this.getMapImageDescription( + contentForEmail.disasterType, + ), + eventName: event.eventName ? ` for '${event.eventName}'` : '', + }; + eventHtml = ejs.render(eventHtml, replacements); + html += eventHtml; + } + } + return html; + } + + private getMapImgSrc( + countryCodeISO3: string, + disasterType: DisasterType, + eventName: string, + ): string { + const src = `${ + process.env.NG_API_URL + }/event/event-map-image/${countryCodeISO3}/${disasterType}/${ + eventName || 'no-name' + }`; + + return src; + } + + private getMapImageDescription(disasterType: DisasterType): string { + switch (disasterType) { + case DisasterType.Floods: + return 'The triggered areas are outlined in purple. The potential flood extent is shown in red.
'; + default: + return ''; + } + } + + private formatEmail(emailKeyValueReplaceList: ReplaceKeyValue[]): string { + let template = fs.readFileSync( + './src/api/notification/email/html/base.html', + 'utf8', + ); + const replacements = emailKeyValueReplaceList.reduce( + (acc, { replaceKey, replaceValue }) => { + acc[replaceKey] = replaceValue; + return acc; + }, + {}, + ); + + let emailHtml = template; + let previousHtml = null; + + // This loop is needed to handle nested EJS tags. It repeatedly renders the template + // until there are no more EJS tags left to render. This is necessary because EJS + // doesn't render nested tags in one pass. + while (emailHtml !== previousHtml) { + previousHtml = emailHtml; + emailHtml = ejs.render(previousHtml, replacements); + } + + return emailHtml; + } + + // TODO refactor this to use ejs package to render the html + private getEventListShort( + dataPerEvent: NotificationDataPerEventDto[], + ): string { + let text = ''; + for (const event of dataPerEvent) { + const leadTimeString = event.disasterSpecificCopy.leadTimeString + ? event.disasterSpecificCopy.leadTimeString + : event.firstLeadTime; + const timestamp = event.disasterSpecificCopy.timestamp + ? ` at ${event.disasterSpecificCopy.timestamp}` + : ''; + text += `${event.triggerStatusLabel} for ${event.eventName}: ${ + event.disasterSpecificCopy.extraInfo || + event.firstLeadTime === LeadTime.hour0 + ? leadTimeString + : `${event.firstLeadTime}${timestamp}` + }
`; + } + return text; + } + + private getTablesForEvents(emailContent: ContentTriggerEmail): string { + const adminAreaLabelsParent = + emailContent.country.adminRegionLabels[ + String(emailContent.defaultAdminLevel - 1) + ]; + + return emailContent.dataPerEvent + .map((event) => { + const data = { + hazard: emailContent.disasterTypeLabel, + triggerStatusLabel: event.triggerStatusLabel, + eventName: event.eventName, + expectedTriggerDate: event.firstLeadTime, + expectedExposedAdminBoundary: event.nrOfTriggeredAreas, + defaulAdminAreaLabelSingular: + emailContent.defaultAdminAreaLabel.singular, + defaulAdminAreaLabelPlural: + emailContent.defaultAdminAreaLabel.plural.toLocaleLowerCase(), + defaultAdminAreaLabelParent: adminAreaLabelsParent.singular, + indicatorLabel: emailContent.indicatorMetadata.label, + indicatorUnit: emailContent.indicatorMetadata.unit, + triangleIcon: this.getTriangleIcon(event.triggerStatusLabel), + tableRows: this.getTablesRows(event), + color: + event.triggerStatusLabel === TriggerStatusLabelEnum.Trigger + ? '#940000' + : '#da7c00', + }; + + const templatePath = `${emailTemplateFolder}/email-table-event.html`; + + let template = fs.readFileSync(templatePath, 'utf8'); + + const result = ejs.render(template, data); + return result; + }) + .join(''); + } + + private getTablesRows(event: NotificationDataPerEventDto) { + return event.triggeredAreas + .map((area) => { + const areaTemplatePath = + TriggerStatusLabelEnum.Trigger === event.triggerStatusLabel + ? `${emailTemplateFolder}/email-table-trigger-row.html` + : `${emailTemplateFolder}//email-table-warning-row.html`; + const areaTemplate = fs.readFileSync(areaTemplatePath, 'utf8'); + const areaData = { + affectectedOfIndicator: area.actionsValue, + adminBoundary: area.displayName ? area.displayName : area.name, + higherAdminBoundary: area.nameParent, + }; + + return ejs.render(areaTemplate, areaData); + }) + .join(''); + } + + private getEventListBody(emailContent: ContentTriggerEmail): string { + return emailContent.dataPerEvent + .map((event) => { + const data = { + hazard: emailContent.disasterTypeLabel, + triggerStatusLabel: event.triggerStatusLabel, + eventName: event.eventName, + level: event.disasterSpecificCopy.eventStatus, + expectedWarningDate: event.disasterSpecificCopy.leadTimeString, + nrOfTriggeredAreas: event.nrOfTriggeredAreas, + expectedTriggerDate: event.firstLeadTime, + expectedExposedAdminBoundary: event.nrOfTriggeredAreas, + issuedDate: event.disasterSpecificCopy.timestamp, + startDateEventString: event.startDateEventString, + defaulAdminAreaLabel: + emailContent.defaultAdminAreaLabel.plural.toLocaleLowerCase(), + indicatorLabel: emailContent.indicatorMetadata.label, + totalAffectectedOfIndicator: event.totalAffectectedOfIndicator, + indicatorUnit: emailContent.indicatorMetadata.unit, + currentDate: this.getCurrentDateTimeString( + emailContent.country.countryCodeISO3, + ), + timezone: + CountryTimeZoneMapping[emailContent.country.countryCodeISO3], + triangleIcon: this.getTriangleIcon(event.triggerStatusLabel), + leadTime: event.firstLeadTime.replace('-', ' '), + }; + + const templatePath = + TriggerStatusLabelEnum.Trigger === event.triggerStatusLabel + ? `${emailTemplateFolder}/email-body-trigger-event.html` + : `${emailTemplateFolder}/email-body-warning-event.html`; + + let template = fs.readFileSync(templatePath, 'utf8'); + + return ejs.render(template, data); + }) + .join(''); + } + + private getCurrentDateTimeString(countryCodeISO3: string): string { + const date = new Date(); + + const timeZone = CountryTimeZoneMapping[countryCodeISO3]; + + const options: Intl.DateTimeFormatOptions = { + weekday: 'long', + day: '2-digit', + month: 'long', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + timeZone: timeZone, + }; + + return date.toLocaleString('default', options); + } + + private getTriangleIcon(triggerStatusLabel) { + let fileName = ''; + // Still need implement the difference between medium and low warning + if (triggerStatusLabel === TriggerStatusLabelEnum.Trigger) { + fileName = 'trigger.png'; + } else { + fileName = 'warning-medium.png'; + } + const filePath = `${emailIconFolder}/${fileName}`; + const imageDataURL = this.getPngImageAsDataURL(filePath); + return imageDataURL; + } + + private getPngImageAsDataURL(relativePath) { + const imageBuffer = fs.readFileSync(relativePath); + const imageDataURL = `data:image/png;base64,${imageBuffer.toString( + 'base64', + )}`; + + return imageDataURL; + } +} 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 e88a2caf0..ca10269a8 100644 --- a/services/API-service/src/api/notification/email/email.service.ts +++ b/services/API-service/src/api/notification/email/email.service.ts @@ -1,34 +1,23 @@ -import { AdminAreaDynamicDataService } from './../../admin-area-dynamic-data/admin-area-dynamic-data.service'; -import { LeadTimeEntity } from './../../lead-time/lead-time.entity'; + import { CountryEntity } from './../../country/country.entity'; import { Injectable } from '@nestjs/common'; -import { EventService } from '../../event/event.service'; -import fs from 'fs'; import Mailchimp from 'mailchimp-api-v3'; -import { IndicatorMetadataEntity } from '../../metadata/indicator-metadata.entity'; -import { LeadTime } from '../../admin-area-dynamic-data/enum/lead-time.enum'; -import { DynamicIndicator } from '../../admin-area-dynamic-data/enum/dynamic-data-unit'; + import { DisasterType } from '../../disaster/disaster-type.enum'; import { EventSummaryCountry } from '../../../shared/data.model'; import { NotificationContentService } from './../notification-content/notification-content.service'; - -class ReplaceKeyValue { - replaceKey: string; - replaceValue: string; -} +import { EmailTemplateService } from './email-template.service'; @Injectable() export class EmailService { - private placeholderToday = '(TODAY)'; private fromEmail = process.env.SUPPORT_EMAIL_ADDRESS; private fromEmailName = 'IBF portal'; private mailchimp = new Mailchimp(process.env.MC_API); public constructor( - private readonly eventService: EventService, - private readonly adminAreaDynamicDataService: AdminAreaDynamicDataService, private readonly notificationContentService: NotificationContentService, + private readonly emailTemplateService: EmailTemplateService, ) {} private async getSegmentId( @@ -56,13 +45,18 @@ export class EmailService { activeEvents: EventSummaryCountry[], date?: Date, ): Promise { - const replaceKeyValues = await this.createReplaceKeyValuesTrigger( - country, - disasterType, - activeEvents, - date ? new Date(date) : new Date(), + date = date ? new Date(date) : new Date(); + + const contentForEmail = + await this.notificationContentService.getContentTriggerNotification( + country, + disasterType, + activeEvents, + ); + const emailHtml = this.emailTemplateService.createHtmlForTriggerEmail( + contentForEmail, + date, ); - const emailHtml = this.formatEmail(replaceKeyValues); const emailSubject = `IBF ${( await this.notificationContentService.getDisasterTypeLabel(disasterType) ).toLowerCase()} notification`; @@ -80,16 +74,17 @@ export class EmailService { finishedEvent: EventSummaryCountry, date?: Date, ): Promise { - const replaceKeyValues = await this.createReplaceKeyValuesTriggerFinished( - country, - disasterType, - finishedEvent, - date ? new Date(date) : new Date(), - ); - const emailHtml = this.formatEmail(replaceKeyValues); - const emailSubject = `IBF ${( - await this.notificationContentService.getDisasterTypeLabel(disasterType) - ).toLowerCase()} trigger is now below threshold`; + const disasterTypeLabel = + await this.notificationContentService.getDisasterTypeLabel(disasterType); + const emailHtml = + this.emailTemplateService.createHtmlForTriggerFinishedEmail( + country, + disasterType, + finishedEvent, + disasterTypeLabel, + date ? new Date(date) : new Date(), + ); + const emailSubject = `IBF ${disasterTypeLabel.toLowerCase()} trigger is now below threshold`; this.sendEmail( emailSubject, emailHtml, @@ -134,604 +129,4 @@ export class EmailService { ); await this.mailchimp.post(`/campaigns/${createResult.id}/actions/send`); } - - private async createReplaceKeyValuesTrigger( - country: CountryEntity, - disasterType: DisasterType, - events: EventSummaryCountry[], - date: Date, - ): Promise { - const keyValueReplaceList = [ - { - replaceKey: '(EMAIL-BODY)', - replaceValue: this.getEmailBody(false), - }, - { - replaceKey: '(HEADER-EVENT-OVERVIEW)', - replaceValue: await this.getHeaderEventOverview( - country, - disasterType, - events, - date, - ), - }, - { - replaceKey: '(SOCIAL-MEDIA-PART)', - replaceValue: this.getSocialMediaHtml(country), - }, - { - replaceKey: '(TABLES-stacked)', - replaceValue: await this.getTriggerOverviewTables( - country, - disasterType, - events, - date, - ), - }, - { - replaceKey: this.placeholderToday, - replaceValue: date.toLocaleDateString('default', { - day: '2-digit', - month: 'short', - year: 'numeric', - }), - }, - { - replaceKey: '(EVENT-LIST-BODY)', - replaceValue: ( - await this.getLeadTimeList(country, disasterType, events, date) - )['leadTimeListLong'], - }, - { - replaceKey: '(IMG-LOGO)', - replaceValue: country.notificationInfo.logo[disasterType], - }, - { - replaceKey: '(TRIGGER-STATEMENT)', - replaceValue: country.notificationInfo.triggerStatement[disasterType], - }, - { - replaceKey: '(MAP-IMAGE-PART)', - replaceValue: await this.getMapImageHtml(country, disasterType, events), - }, - { - replaceKey: '(LINK-DASHBOARD)', - replaceValue: process.env.DASHBOARD_URL, - }, - { - replaceKey: '(LINK-EAP-SOP)', - replaceValue: country.countryDisasterSettings.find( - (s) => s.disasterType === disasterType, - ).eapLink, - }, - { - replaceKey: '(SOCIAL-MEDIA-LINK)', - replaceValue: country.notificationInfo.linkSocialMediaUrl, - }, - { - replaceKey: '(SOCIAL-MEDIA-TYPE)', - replaceValue: country.notificationInfo.linkSocialMediaType, - }, - { - replaceKey: '(ADMIN-AREA-PLURAL)', - replaceValue: - country.adminRegionLabels[ - String( - country.countryDisasterSettings.find( - (s) => s.disasterType === disasterType, - ).defaultAdminLevel, - ) - ].plural.toLowerCase(), - }, - { - replaceKey: '(ADMIN-AREA-SINGULAR)', - replaceValue: - country.adminRegionLabels[ - String( - country.countryDisasterSettings.find( - (s) => s.disasterType === disasterType, - ).defaultAdminLevel, - ) - ].singular.toLowerCase(), - }, - { - replaceKey: '(DISASTER-TYPE)', - replaceValue: - await this.notificationContentService.getDisasterTypeLabel( - disasterType, - ), - }, - { - replaceKey: '(VIDEO-PDF-LINKS)', - replaceValue: this.getVideoPdfLinks( - country.notificationInfo.linkVideo, - country.notificationInfo.linkPdf, - ), - }, - { - replaceKey: '(EXPOSURE-UNIT)', - replaceValue: ( - await this.notificationContentService.getActionUnit(disasterType) - ).label.toLocaleLowerCase(), - }, - ]; - return keyValueReplaceList; - } - - private async createReplaceKeyValuesTriggerFinished( - country: CountryEntity, - disasterType: DisasterType, - event: EventSummaryCountry, - date: Date, - ): Promise { - const keyValueReplaceList = [ - { - replaceKey: '(EMAIL-BODY)', - replaceValue: this.getEmailBody(true), - }, - { - replaceKey: '(HEADER-EVENT-OVERVIEW)', - replaceValue: '', - }, - { - replaceKey: '(IMG-LOGO)', - replaceValue: country.notificationInfo.logo[disasterType], - }, - { - replaceKey: '(START-DATE)', - replaceValue: event.startDate, - }, - { - replaceKey: '(LINK-DASHBOARD)', - replaceValue: process.env.DASHBOARD_URL, - }, - { - replaceKey: '(SOCIAL-MEDIA-PART)', - replaceValue: this.getSocialMediaHtml(country), - }, - { - replaceKey: '(LINK-EAP-SOP)', - replaceValue: country.countryDisasterSettings.find( - (s) => s.disasterType === disasterType, - ).eapLink, - }, - { - replaceKey: '(SOCIAL-MEDIA-LINK)', - replaceValue: country.notificationInfo.linkSocialMediaUrl, - }, - { - replaceKey: '(SOCIAL-MEDIA-TYPE)', - replaceValue: country.notificationInfo.linkSocialMediaType, - }, - { - replaceKey: '(VIDEO-PDF-LINKS)', - replaceValue: this.getVideoPdfLinks( - country.notificationInfo.linkVideo, - country.notificationInfo.linkPdf, - ), - }, - { - replaceKey: '(DISASTER-TYPE)', - replaceValue: - await this.notificationContentService.getDisasterTypeLabel( - disasterType, - ), - }, - { - replaceKey: this.placeholderToday, - replaceValue: date.toLocaleDateString('default', { - day: '2-digit', - month: 'short', - year: 'numeric', - }), - }, - ]; - return keyValueReplaceList; - } - - private getEmailBody(triggerFinished: boolean): string { - if (triggerFinished) { - return fs.readFileSync( - './src/api/notification/email/html/trigger-finished.html', - 'utf8', - ); - } else { - return fs.readFileSync( - './src/api/notification/email/html/trigger-notification.html', - 'utf8', - ); - } - } - - private async getHeaderEventOverview( - country: CountryEntity, - disasterType: DisasterType, - activeEvents: EventSummaryCountry[], - date?: Date, - ): Promise { - const leadTimeListShort = ( - await this.getLeadTimeList( - country, - disasterType, - this.sortEventsByLeadTime(activeEvents), - date, - ) - )['leadTimeListShort']; - return fs - .readFileSync( - './src/api/notification/email/html/header-event-overview.html', - 'utf8', - ) - .replace('(EVENT-LIST-HEADER)', leadTimeListShort); - } - - private sortEventsByLeadTime( - arr: EventSummaryCountry[], - ): EventSummaryCountry[] { - const leadTimeValue = (leadTime: LeadTime): number => - Number(leadTime.split('-')[0]); - - return arr.sort((a, b) => { - if (leadTimeValue(a.firstLeadTime) < leadTimeValue(b.firstLeadTime)) { - return -1; - } - if (leadTimeValue(a.firstLeadTime) > leadTimeValue(b.firstLeadTime)) { - return 1; - } - - return 0; - }); - } - - private getVideoPdfLinks(videoLink: string, pdfLink: string) { - const linkVideoHTML = ` - video`; - - const linkPdfHTML = `PDF`; - let videoStr = ''; - if (videoLink) { - videoStr = ' ' + linkVideoHTML; - } - let pdfStr = ''; - if (pdfLink) { - pdfStr = ' ' + linkPdfHTML; - } - let orStr = ''; - if (videoStr && pdfStr) { - orStr = ' or'; - } - if (videoStr || pdfStr) { - return `See instructions for the IBF-portal in${videoStr}${orStr}${pdfStr}.`; - } - } - - private async getLeadTimeList( - country: CountryEntity, - disasterType: DisasterType, - events: EventSummaryCountry[], - date?: Date, - ): Promise { - const triggeredLeadTimes = - await this.notificationContentService.getLeadTimesAcrossEvents( - country.countryCodeISO3, - disasterType, - events, - ); - - let leadTimeListShort = ''; - let leadTimeListLong = ''; - for (const leadTime of country.countryDisasterSettings.find( - (s) => s.disasterType === disasterType, - ).activeLeadTimes) { - if (triggeredLeadTimes[leadTime.leadTimeName] === '1') { - for await (const event of events) { - // for each event .. - const triggeredLeadTimes = - await this.eventService.getTriggerPerLeadtime( - country.countryCodeISO3, - disasterType, - event.eventName, - ); - if (triggeredLeadTimes[leadTime.leadTimeName] === '1') { - const leadTimeListEvent = - await this.notificationContentService.getLeadTimeListEvent( - country, - event, - disasterType, - leadTime.leadTimeName as LeadTime, - date, - ); - - // We are hack-misusing 'extraInfo' being filled as a proxy for typhoonNoLandfallYet-boolean - leadTimeListShort = `${leadTimeListShort}${leadTimeListEvent.short}`; - leadTimeListLong = `${leadTimeListLong}${leadTimeListEvent.long}`; - } - } - } - } - return { leadTimeListShort, leadTimeListLong }; - } - - private async getTriggerOverviewTables( - country: CountryEntity, - disasterType: DisasterType, - events: EventSummaryCountry[], - date: Date, - ): Promise { - const triggeredLeadTimes = - await this.notificationContentService.getLeadTimesAcrossEvents( - country.countryCodeISO3, - disasterType, - events, - ); - - let leadTimeTables = ''; - for (const leadTime of country.countryDisasterSettings.find( - (s) => s.disasterType === disasterType, - ).activeLeadTimes) { - if (triggeredLeadTimes[leadTime.leadTimeName] === '1') { - for await (const event of events) { - // for each event .. - const triggeredLeadTimes = - await this.eventService.getTriggerPerLeadtime( - country.countryCodeISO3, - disasterType, - event.eventName, - ); - - if (triggeredLeadTimes[leadTime.leadTimeName] === '1') { - // .. find the right leadtime - const tableForLeadTime = await this.getTableForLeadTime( - country, - disasterType, - leadTime, - event, - date, - ); - leadTimeTables = leadTimeTables + tableForLeadTime; - } - } - } - } - return leadTimeTables; - } - - private getSocialMediaHtml(country: CountryEntity): string { - if (country.notificationInfo.linkSocialMediaType) { - return fs.readFileSync( - './src/api/notification/email/html/social-media-link.html', - 'utf8', - ); - } else { - return ''; - } - } - - private async getMapImageHtml( - country: CountryEntity, - disasterType: DisasterType, - events: EventSummaryCountry[], - ): Promise { - let html = ''; - for await (const event of events) { - const mapImage = await this.eventService.getEventMapImage( - country.countryCodeISO3, - disasterType, - event.eventName || 'no-name', - ); - if (mapImage) { - let eventHtml = fs.readFileSync( - './src/api/notification/email/html/map-image.html', - 'utf8', - ); - eventHtml = eventHtml - .replace( - '(MAP-IMG-SRC)', - this.getMapImgSrc( - country.countryCodeISO3, - disasterType, - event.eventName, - ), - ) - .replace( - '(MAP-IMG-DESCRIPTION)', - this.getMapImageDescription(disasterType), - ); - eventHtml = eventHtml.replace( - '(EVENT-NAME)', - event.eventName ? ` for '${event.eventName}'` : '', - ); - html += eventHtml; - } - } - return html; - } - - private getMapImgSrc( - countryCodeISO3: string, - disasterType: DisasterType, - eventName: string, - ): string { - const src = `${ - process.env.NG_API_URL - }/event/event-map-image/${countryCodeISO3}/${disasterType}/${ - eventName || 'no-name' - }`; - - return src; - } - - private getMapImageDescription(disasterType: DisasterType): string { - switch (disasterType) { - case DisasterType.Floods: - return 'The triggered areas are outlined in purple. The potential flood extent is shown in red.
'; - default: - return ''; - } - } - - private async getTableForLeadTime( - country: CountryEntity, - disasterType: DisasterType, - leadTime: LeadTimeEntity, - event: EventSummaryCountry, - date: Date, - ): Promise { - const adminLevel = country.countryDisasterSettings.find( - (s) => s.disasterType === disasterType, - ).defaultAdminLevel; - const adminAreaLabels = country.adminRegionLabels[String(adminLevel)]; - const adminAreaLabelsParent = - country.adminRegionLabels[String(adminLevel - 1)]; - - const actionsUnit = await this.notificationContentService.getActionUnit( - disasterType, - ); - - const tableForLeadTimeStart = `
- ${ - ( - await this.notificationContentService.getLeadTimeListEvent( - country, - event, - disasterType, - leadTime.leadTimeName as LeadTime, - date, - ) - ).short - } -
- - - - - - - - - - -
`; - const tableForLeadTimeMiddle = await this.getAreaTables( - country, - disasterType, - leadTime, - event.eventName, - actionsUnit, - ); - const tableForLeadTimeEnd = '
This table lists the potentially exposed ${adminAreaLabels.plural.toLowerCase()} in order of ${actionsUnit.label.toLowerCase()}:
Alert class${adminAreaLabels.singular}${ - adminAreaLabelsParent ? ' (' + adminAreaLabelsParent.singular + ')' : '' - }Predicted ${actionsUnit.label}


'; - const tableForLeadTime = - tableForLeadTimeStart + tableForLeadTimeMiddle + tableForLeadTimeEnd; - return tableForLeadTime; - } - - private async getAreaTables( - country: CountryEntity, - disasterType: DisasterType, - leadTime: LeadTimeEntity, - eventName: string, - actionsUnit: IndicatorMetadataEntity, - ): Promise { - const triggeredAreas = await this.eventService.getTriggeredAreas( - country.countryCodeISO3, - disasterType, - country.countryDisasterSettings.find( - (s) => s.disasterType === disasterType, - ).defaultAdminLevel, - leadTime.leadTimeName, - eventName, - ); - triggeredAreas.sort((a, b) => (a.triggerValue > b.triggerValue ? -1 : 1)); - const disaster = await this.notificationContentService.getDisaster( - disasterType, - ); - const hasEap = this.hasEap(disasterType); - let areaTableString = ''; - for (const area of triggeredAreas) { - const actionsUnitValue = - await this.adminAreaDynamicDataService.getDynamicAdminAreaDataPerPcode( - disaster.actionsUnit as DynamicIndicator, - area.placeCode, - leadTime.leadTimeName as LeadTime, - eventName, - ); - - const areaTable = ` - - ${this.mapTriggerValueToAlertClass( - area.triggerValue, - hasEap, - )} - ${area.name}${ - area.nameParent ? ' (' + area.nameParent + ')' : '' - } - ${this.notificationContentService.formatActionUnitValue( - actionsUnitValue, - actionsUnit, - )} - `; - areaTableString = areaTableString + areaTable; - } - return areaTableString; - } - - // TODO merge this with the front-end instance of this to some generic place in back-end - public hasEap(disasterType: DisasterType): boolean { - const eapDisasterTypes = [ - DisasterType.Floods, - DisasterType.Drought, - DisasterType.Typhoon, - DisasterType.FlashFloods, - ]; - return eapDisasterTypes.includes(disasterType); - } - - private mapTriggerValueToAlertClass( - triggerValue: number, - hasEap: boolean, - ): string { - if (triggerValue === 1) { - return hasEap ? 'Trigger issued' : 'Alert issued'; - } else if (triggerValue === 0.7) { - return 'Medium warning issued'; - } else if (triggerValue === 0.3) { - return 'Low warning issued'; - } - } - - private formatEmail(emailKeyValueReplaceList: ReplaceKeyValue[]): string { - let emailHtml = fs.readFileSync( - './src/api/notification/email/html/base.html', - 'utf8', - ); - for (const entry of emailKeyValueReplaceList) { - emailHtml = emailHtml.split(entry.replaceKey).join(entry.replaceValue); - } - return emailHtml; - } } diff --git a/services/API-service/src/api/notification/email/html/base.html b/services/API-service/src/api/notification/email/html/base.html index a73656531..4337a8c73 100644 --- a/services/API-service/src/api/notification/email/html/base.html +++ b/services/API-service/src/api/notification/email/html/base.html @@ -882,7 +882,7 @@ >

- (DISASTER-TYPE) IBF Notification + <%= disasterType %> IBF Notification

- (HEADER-EVENT-OVERVIEW) + <%- headerEventOverview %> @@ -922,7 +922,7 @@

- (EMAIL-BODY) + <%- emailBody %> + + Warning icon + <%= hazard %> <%= eventName %>
Trigger expected on: + <%= startDateEventString %>, <%= leadTime %>s from now.
Expected exposed <%= defaulAdminAreaLabel %>: + <%= nrOfTriggeredAreas %> (see list below)
+ <%= indicatorLabel %>: <%= totalAffectectedOfIndicator %> <%= indicatorUnit %>
Advisory: Activate + Early Action Protocol

This trigger was issued by the IBF portal on <%= currentDate %> (<%= + timezone %>) + +
diff --git a/services/API-service/src/api/notification/email/html/email-body-warning-event.html b/services/API-service/src/api/notification/email/html/email-body-warning-event.html new file mode 100644 index 000000000..77c9345f2 --- /dev/null +++ b/services/API-service/src/api/notification/email/html/email-body-warning-event.html @@ -0,0 +1,120 @@ +
+ + Warning icon + <%= hazard %> <%= eventName %>
Warning expected on: + <%= startDateEventString %>, <%= leadTime %>s from now.
Expected exposed <%= defaulAdminAreaLabel %>: + <%= nrOfTriggeredAreas %> (see list below)
+ <%= indicatorLabel %>: Information regarding exposed population not available for warnings
Advisory: + Inform all potentially exposed districts

This warning was issued by the IBF portal on <%= currentDate %> (<%= + timezone %>) +
diff --git a/services/API-service/src/api/notification/email/html/email-table-event.html b/services/API-service/src/api/notification/email/html/email-table-event.html new file mode 100644 index 000000000..e25ad4c7f --- /dev/null +++ b/services/API-service/src/api/notification/email/html/email-table-event.html @@ -0,0 +1,96 @@ +
+
+ Warning icon + <%= triggerStatusLabel %> <%= hazard %>: <%= eventName %> +
+
+ Expected exposed <%= defaulAdminAreaLabelPlural %><% if (triggerStatusLabel + === 'Trigger') { %> in order of <%= indicatorLabel.toLowerCase() %><% } %>: +
+
+
+ + + <% if (triggerStatusLabel === 'Trigger') { %> + + <% } %> + + + <%- tableRows %> +
+ <%= indicatorLabel %> + + <%= defaulAdminAreaLabelSingular %> (<%= defaultAdminAreaLabelParent + %>) +
+
+
+
diff --git a/services/API-service/src/api/notification/email/html/email-table-trigger-row.html b/services/API-service/src/api/notification/email/html/email-table-trigger-row.html new file mode 100644 index 000000000..38428b98e --- /dev/null +++ b/services/API-service/src/api/notification/email/html/email-table-trigger-row.html @@ -0,0 +1,36 @@ + + + <%= affectectedOfIndicator %> + + + <%= adminBoundary %> (<%= higherAdminBoundary %>) + + diff --git a/services/API-service/src/api/notification/email/html/email-table-warning-row.html b/services/API-service/src/api/notification/email/html/email-table-warning-row.html new file mode 100644 index 000000000..85d41824e --- /dev/null +++ b/services/API-service/src/api/notification/email/html/email-table-warning-row.html @@ -0,0 +1,22 @@ + + + <%= adminBoundary %> (<%= higherAdminBoundary %>) + + diff --git a/services/API-service/src/api/notification/email/html/header-event-overview.html b/services/API-service/src/api/notification/email/html/header-event-overview.html index db14059be..e5795b900 100644 --- a/services/API-service/src/api/notification/email/html/header-event-overview.html +++ b/services/API-service/src/api/notification/email/html/header-event-overview.html @@ -1,5 +1,5 @@ -

(EVENT-LIST-HEADER)

+

<%- eventListHeader %>

diff --git a/services/API-service/src/api/notification/email/html/map-image.html b/services/API-service/src/api/notification/email/html/map-image.html index 876034e1e..a64b1e9f5 100644 --- a/services/API-service/src/api/notification/email/html/map-image.html +++ b/services/API-service/src/api/notification/email/html/map-image.html @@ -1,6 +1,6 @@
-Map of the triggered area(EVENT-NAME): (click 'Download pictures' if it -does not show) +Map of the triggered area(<%= eventName %>): (click 'Download pictures' +if it does not show)
-(MAP-IMG-DESCRIPTION) - +<%= mapImgDescription %> + diff --git a/services/API-service/src/api/notification/email/html/social-media-link.html b/services/API-service/src/api/notification/email/html/social-media-link.html index b36c482e4..61a111b12 100644 --- a/services/API-service/src/api/notification/email/html/social-media-link.html +++ b/services/API-service/src/api/notification/email/html/social-media-link.html @@ -9,7 +9,7 @@ Join (SOCIAL-MEDIA-TYPE) groupJoin <%- socialMediaType %> group diff --git a/services/API-service/src/api/notification/email/html/trigger-finished.html b/services/API-service/src/api/notification/email/html/trigger-finished.html index 0c0ac46a9..301518ea0 100644 --- a/services/API-service/src/api/notification/email/html/trigger-finished.html +++ b/services/API-service/src/api/notification/email/html/trigger-finished.html @@ -86,7 +86,7 @@ Dear reader,

The trigger notification formerly activated on - (START-DATE) is now + <%= startDate %> is now below threshold.

The event will close in the IBF-portal when the forecast stays below threshold for 7 days in a @@ -112,7 +112,7 @@ align="center" >
- Dear Reader,

- (EVENT-LIST-BODY) +
+ Dear Reader, +
+

+ <%- eventListBody %> @@ -98,7 +110,7 @@ diff --git a/services/API-service/src/api/notification/email/html/trigger-finished.html b/services/API-service/src/api/notification/email/html/trigger-finished.html index 301518ea0..5df98542b 100644 --- a/services/API-service/src/api/notification/email/html/trigger-finished.html +++ b/services/API-service/src/api/notification/email/html/trigger-finished.html @@ -85,13 +85,8 @@
Dear reader,

- The trigger notification formerly activated on - <%= startDate %> is now - below threshold.

- The event will close in the IBF-portal when the - forecast stays below threshold for 7 days in a - row. Until that time, anticipatory actions can - still be managed in the IBF-portal. + + <%- eventOverview %>
{ + const countryAdminAreaIds = await this.getCountryAdminAreaIds( + countryCodeISO3, + ); - for await (const event of eventSummary) { + // I spend quite some time on trying to figure out what is the right query to get the event finished summary for the trigger closed email + // I came up with the following query but I am not sure if it is correct and how to test it properly + const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); + const sixDaysAgo = new Date(Date.now() - 6 * 24 * 60 * 60 * 1000); + const whereFilters = { + endDate: Between(sevenDaysAgo, sixDaysAgo), + adminArea: In(countryAdminAreaIds), + disasterType: disasterType, + closed: true, + }; + + const eventSummaryQueryBuilder = + this.createEventSummaryQueryBuilder(countryCodeISO3).andWhere( + whereFilters, + ); + + const rawEventSummaryTriggerFinished = + await eventSummaryQueryBuilder.getRawMany(); + const eventSummaryTriggerFinished = await this.populateEventsDetails( + rawEventSummaryTriggerFinished, + countryCodeISO3, + disasterType, + ); + return eventSummaryTriggerFinished; + } + + private async populateEventsDetails( + events: EventSummaryCountry[], + countryCodeISO3: string, + disasterType: DisasterType, + ) { + for (const event of events) { event.firstLeadTime = await this.getFirstLeadTime( countryCodeISO3, disasterType, @@ -106,7 +142,26 @@ export class EventService { ); } } - return eventSummary; + return events; + } + + private createEventSummaryQueryBuilder( + countryCodeISO3: string, + ): SelectQueryBuilder { + return this.eventPlaceCodeRepo + .createQueryBuilder('event') + .select(['area."countryCodeISO3"', 'event."eventName"']) + .leftJoin('event.adminArea', 'area') + .groupBy('area."countryCodeISO3"') + .addGroupBy('event."eventName"') + .addSelect([ + 'to_char(MIN("startDate") , \'yyyy-mm-dd\') AS "startDate"', + 'to_char(MAX("endDate") , \'yyyy-mm-dd\') AS "endDate"', + 'MAX(event."thresholdReached"::int)::boolean AS "thresholdReached"', + ]) + .andWhere('area."countryCodeISO3" = :countryCodeISO3', { + countryCodeISO3: countryCodeISO3, + }); } public async getRecentDate( @@ -887,7 +942,8 @@ export class EventService { where: whereFilters, }); - //Below threshold events can be removed from this table after closing + // Below threshold events can be removed from this table after closing + // Below threshold events are warnings an not triggered. I do not know why they are removed here const belowThresholdEvents = expiredEventAreas.filter( (a) => !a.thresholdReached, ); diff --git a/services/API-service/src/api/notification/dto/content-trigger-email.dto.ts b/services/API-service/src/api/notification/dto/content-trigger-email.dto.ts index e5bda8d48..f617ba219 100644 --- a/services/API-service/src/api/notification/dto/content-trigger-email.dto.ts +++ b/services/API-service/src/api/notification/dto/content-trigger-email.dto.ts @@ -4,7 +4,7 @@ import { IndicatorMetadataEntity } from '../../metadata/indicator-metadata.entit import { AdminAreaLabel } from './admin-area-notification-info.dto'; import { NotificationDataPerEventDto } from './notification-date-per-event.dto'; -export class ContentTriggerEmail { +export class ContentEventEmail { public disasterType: DisasterType; public disasterTypeLabel: string; public indicatorMetadata: IndicatorMetadataEntity; 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 96baf5862..33f17e713 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 @@ -8,9 +8,10 @@ export class NotificationDataPerEventDto { firstLeadTime: LeadTime; triggeredAreas: TriggeredArea[]; nrOfTriggeredAreas: number; - startDateEventString: string; + startDateDisasterString: string; totalAffectectedOfIndicator: number; mapImage?: Buffer; + issuedDate: Date; } export enum TriggerStatusLabelEnum { diff --git a/services/API-service/src/api/notification/email/email-template.service.ts b/services/API-service/src/api/notification/email/email-template.service.ts index 103489e2b..ab6eeb013 100644 --- a/services/API-service/src/api/notification/email/email-template.service.ts +++ b/services/API-service/src/api/notification/email/email-template.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { ContentTriggerEmail } from '../dto/content-trigger-email.dto'; +import { ContentEventEmail } from '../dto/content-trigger-email.dto'; import { LeadTime } from '../../admin-area-dynamic-data/enum/lead-time.enum'; import { NotificationDataPerEventDto, @@ -26,11 +26,11 @@ export class EmailTemplateService { private placeholderToday = '(TODAY)'; public createHtmlForTriggerEmail( - contentForEmail: ContentTriggerEmail, + emailContent: ContentEventEmail, date: Date, ): string { const replaceKeyValues = this.createReplaceKeyValuesTrigger( - contentForEmail, + emailContent, date, ); return this.formatEmail(replaceKeyValues); @@ -40,14 +40,14 @@ export class EmailTemplateService { public createHtmlForTriggerFinishedEmail( country: CountryEntity, disasterType: DisasterType, - finishedEvent: EventSummaryCountry, + finishedEvents: EventSummaryCountry[], disasterTypeLabel: string, date: Date, ): string { const replaceKeyValues = this.createReplaceKeyValuesTriggerFinished( country, disasterType, - finishedEvent, + finishedEvents, disasterTypeLabel, date, ); @@ -55,11 +55,11 @@ export class EmailTemplateService { } private createReplaceKeyValuesTrigger( - contentForEmail: ContentTriggerEmail, + emailContent: ContentEventEmail, date: Date, ): ReplaceKeyValue[] { - const country = contentForEmail.country; - const disasterType = contentForEmail.disasterType; + const country = emailContent.country; + const disasterType = emailContent.disasterType; const keyValueReplaceList = [ { replaceKey: 'emailBody', @@ -67,15 +67,15 @@ export class EmailTemplateService { }, { replaceKey: 'headerEventOverview', - replaceValue: this.getHeaderEventOverview(contentForEmail.dataPerEvent), + replaceValue: this.getHeaderEventStarted(emailContent), }, { replaceKey: 'socialMediaPart', - replaceValue: this.getSocialMediaHtml(contentForEmail.country), + replaceValue: this.getSocialMediaHtml(emailContent.country), }, { replaceKey: 'tablesStacked', - replaceValue: this.getTablesForEvents(contentForEmail), + replaceValue: this.getTablesForEvents(emailContent), }, { replaceKey: this.placeholderToday, @@ -87,7 +87,7 @@ export class EmailTemplateService { }, { replaceKey: 'eventListBody', - replaceValue: this.getEventListBody(contentForEmail), + replaceValue: this.getEventListBody(emailContent), }, { replaceKey: 'imgLogo', @@ -99,7 +99,7 @@ export class EmailTemplateService { }, { replaceKey: 'mapImagePart', - replaceValue: this.getMapImageHtml(contentForEmail), + replaceValue: this.getMapImageHtml(emailContent), }, { replaceKey: 'linkDashboard', @@ -121,7 +121,7 @@ export class EmailTemplateService { }, { replaceKey: 'disasterType', - replaceValue: contentForEmail.disasterTypeLabel, + replaceValue: emailContent.disasterTypeLabel, }, { replaceKey: 'videoPdfLinks', @@ -137,7 +137,7 @@ export class EmailTemplateService { private createReplaceKeyValuesTriggerFinished( country: CountryEntity, disasterType: DisasterType, - event: EventSummaryCountry, + events: EventSummaryCountry[], disasterTypeLabel: string, date: Date, ): ReplaceKeyValue[] { @@ -151,12 +151,16 @@ export class EmailTemplateService { replaceValue: '', }, { - replaceKey: 'imgLogo', - replaceValue: country.notificationInfo.logo[disasterType], + replaceKey: 'eventOverview', + replaceValue: this.geEventsFinishedOverview( + country, + events, + disasterTypeLabel, + ), }, { - replaceKey: 'startDate', - replaceValue: event.startDate, + replaceKey: 'imgLogo', + replaceValue: country.notificationInfo.logo[disasterType], }, { replaceKey: 'linkDashboard', @@ -217,16 +221,42 @@ export class EmailTemplateService { } } - private getHeaderEventOverview( - eventsData: NotificationDataPerEventDto[], + private geEventsFinishedOverview( + country: CountryEntity, + events: EventSummaryCountry[], + disasterTypeLabel: string, ): string { - const leadTimeListShort = this.getEventListShort(eventsData); + let html = ''; + + const template = fs.readFileSync( + './src/api/notification/email/html/event-finished.html', + 'utf-8', + ); + + for (const event of events) { + const eventFinshedHtml = ejs.render(template, { + disasterTypeLabel: disasterTypeLabel, + eventName: event.eventName, + issuedDate: 'yo', + timezone: CountryTimeZoneMapping[country.countryCodeISO3], + }); + html += eventFinshedHtml; + } + return html; + } + + private getHeaderEventStarted(emailContent: ContentEventEmail): string { let headerEventOverview = fs.readFileSync( './src/api/notification/email/html/header-event-overview.html', 'utf8', ); headerEventOverview = ejs.render(headerEventOverview, { - eventListHeader: leadTimeListShort, + sentOnDate: this.getCurrentDateTimeString( + emailContent.country.countryCodeISO3, + ), + disasterLabel: emailContent.disasterTypeLabel, + nrOfEvents: emailContent.dataPerEvent.length, + timezone: CountryTimeZoneMapping[emailContent.country.countryCodeISO3], }); return headerEventOverview; } @@ -288,9 +318,9 @@ export class EmailTemplateService { } } - private getMapImageHtml(contentForEmail: ContentTriggerEmail): string { + private getMapImageHtml(emailContent: ContentEventEmail): string { let html = ''; - for (const event of contentForEmail.dataPerEvent) { + for (const event of emailContent.dataPerEvent) { const mapImage = event.mapImage; if (mapImage) { let eventHtml = fs.readFileSync( @@ -299,12 +329,12 @@ export class EmailTemplateService { ); const replacements = { mapImgSrc: this.getMapImgSrc( - contentForEmail.country.countryCodeISO3, - contentForEmail.disasterType, + emailContent.country.countryCodeISO3, + emailContent.disasterType, event.eventName, ), mapImgDescription: this.getMapImageDescription( - contentForEmail.disasterType, + emailContent.disasterType, ), eventName: event.eventName ? ` for '${event.eventName}'` : '', }; @@ -387,7 +417,7 @@ export class EmailTemplateService { return text; } - private getTablesForEvents(emailContent: ContentTriggerEmail): string { + private getTablesForEvents(emailContent: ContentEventEmail): string { const adminAreaLabelsParent = emailContent.country.adminRegionLabels[ String(emailContent.defaultAdminLevel - 1) @@ -445,7 +475,7 @@ export class EmailTemplateService { .join(''); } - private getEventListBody(emailContent: ContentTriggerEmail): string { + private getEventListBody(emailContent: ContentEventEmail): string { return emailContent.dataPerEvent .map((event) => { const data = { @@ -457,8 +487,11 @@ export class EmailTemplateService { nrOfTriggeredAreas: event.nrOfTriggeredAreas, expectedTriggerDate: event.firstLeadTime, expectedExposedAdminBoundary: event.nrOfTriggeredAreas, - issuedDate: event.disasterSpecificCopy.timestamp, - startDateEventString: event.startDateEventString, + issuedDate: this.dateObjectToDateTimeString( + event.issuedDate, + emailContent.country.countryCodeISO3, + ), + startDateEventString: event.startDateDisasterString, defaulAdminAreaLabel: emailContent.defaultAdminAreaLabel.plural.toLocaleLowerCase(), indicatorLabel: emailContent.indicatorMetadata.label, @@ -487,9 +520,14 @@ export class EmailTemplateService { private getCurrentDateTimeString(countryCodeISO3: string): string { const date = new Date(); + return this.dateObjectToDateTimeString(date, countryCodeISO3); + } + private dateObjectToDateTimeString( + date: Date, + countryCodeISO3: string, + ): string { const timeZone = CountryTimeZoneMapping[countryCodeISO3]; - const options: Intl.DateTimeFormatOptions = { weekday: 'long', day: '2-digit', @@ -499,7 +537,6 @@ export class EmailTemplateService { minute: '2-digit', timeZone: timeZone, }; - return date.toLocaleString('default', options); } 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 ca10269a8..8ec3ea611 100644 --- a/services/API-service/src/api/notification/email/email.service.ts +++ b/services/API-service/src/api/notification/email/email.service.ts @@ -1,4 +1,3 @@ - import { CountryEntity } from './../../country/country.entity'; import { Injectable } from '@nestjs/common'; import Mailchimp from 'mailchimp-api-v3'; @@ -47,14 +46,14 @@ export class EmailService { ): Promise { date = date ? new Date(date) : new Date(); - const contentForEmail = + const emailContent = await this.notificationContentService.getContentTriggerNotification( country, disasterType, activeEvents, ); const emailHtml = this.emailTemplateService.createHtmlForTriggerEmail( - contentForEmail, + emailContent, date, ); const emailSubject = `IBF ${( @@ -71,7 +70,7 @@ export class EmailService { public async sendTriggerFinishedEmail( country: CountryEntity, disasterType: DisasterType, - finishedEvent: EventSummaryCountry, + finishedEvents: EventSummaryCountry[], date?: Date, ): Promise { const disasterTypeLabel = @@ -80,7 +79,7 @@ export class EmailService { this.emailTemplateService.createHtmlForTriggerFinishedEmail( country, disasterType, - finishedEvent, + finishedEvents, disasterTypeLabel, date ? new Date(date) : new Date(), ); diff --git a/services/API-service/src/api/notification/email/html/base.html b/services/API-service/src/api/notification/email/html/base.html index 4337a8c73..ca6072f8f 100644 --- a/services/API-service/src/api/notification/email/html/base.html +++ b/services/API-service/src/api/notification/email/html/base.html @@ -28,6 +28,9 @@ .notification-sub-title-left { color: white; text-align: center; + display: flex; + justify-content: center; + align-items: center; } .notification-content { diff --git a/services/API-service/src/api/notification/email/html/email-body-trigger-event.html b/services/API-service/src/api/notification/email/html/email-body-trigger-event.html index a052140f0..456687ad8 100644 --- a/services/API-service/src/api/notification/email/html/email-body-trigger-event.html +++ b/services/API-service/src/api/notification/email/html/email-body-trigger-event.html @@ -125,8 +125,8 @@ line-height: 19.2px; word-wrap: break-word; " - >This trigger was issued by the IBF portal on <%= currentDate %> (<%= + >This trigger was issued by the IBF portal on <%= issuedDate %> (<%= timezone %>) +
-
diff --git a/services/API-service/src/api/notification/email/html/email-body-warning-event.html b/services/API-service/src/api/notification/email/html/email-body-warning-event.html index 77c9345f2..ea20abf86 100644 --- a/services/API-service/src/api/notification/email/html/email-body-warning-event.html +++ b/services/API-service/src/api/notification/email/html/email-body-warning-event.html @@ -114,7 +114,8 @@ line-height: 19.2px; word-wrap: break-word; " - >This warning was issued by the IBF portal on <%= currentDate %> (<%= + >This warning was issued by the IBF portal on <%= issuedDate %> (<%= timezone %>) +
diff --git a/services/API-service/src/api/notification/email/html/event-finished.html b/services/API-service/src/api/notification/email/html/event-finished.html new file mode 100644 index 000000000..7a607dbe3 --- /dev/null +++ b/services/API-service/src/api/notification/email/html/event-finished.html @@ -0,0 +1,39 @@ +
+ + <%= disasterTypeLabel %>: <%= eventName %> is now below threshold
+ The events actions will continue to show on the IBF portal and can still be + managed.
+
This warning was issued by the IBF portal on <%= issuedDate %> (<%= + timezone %>) +
diff --git a/services/API-service/src/api/notification/email/html/header-event-overview.html b/services/API-service/src/api/notification/email/html/header-event-overview.html index e5795b900..5a5a87a43 100644 --- a/services/API-service/src/api/notification/email/html/header-event-overview.html +++ b/services/API-service/src/api/notification/email/html/header-event-overview.html @@ -1,5 +1,18 @@
-

<%- eventListHeader %>

+

+ <%= nrOfEvents %> <%= disasterLabel %> alerts +

+ IBF alert send on <%= sentOnDate %> (<%= timezone %>)
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 9cf9bd2ae..93c41add4 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 @@ -6,19 +6,16 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { IndicatorMetadataEntity } from '../../metadata/indicator-metadata.entity'; import { LeadTime } from '../../admin-area-dynamic-data/enum/lead-time.enum'; -import { DynamicIndicator } from '../../admin-area-dynamic-data/enum/dynamic-data-unit'; import { DisasterType } from '../../disaster/disaster-type.enum'; import { DisasterEntity } from '../../disaster/disaster.entity'; import { EventSummaryCountry, TriggeredArea } from '../../../shared/data.model'; -import { AdminAreaDataService } from '../../admin-area-data/admin-area-data.service'; -import { AdminAreaService } from '../../admin-area/admin-area.service'; import { HelperService } from '../../../shared/helper.service'; import { NotificationDataPerEventDto, TriggerStatusLabelEnum, } from '../dto/notification-date-per-event.dto'; import { AdminAreaLabel } from '../dto/admin-area-notification-info.dto'; -import { ContentTriggerEmail } from '../dto/content-trigger-email.dto'; +import { ContentEventEmail } from '../dto/content-trigger-email.dto'; @Injectable() export class NotificationContentService { @@ -38,8 +35,8 @@ export class NotificationContentService { country: CountryEntity, disasterType: DisasterType, activeEvents: EventSummaryCountry[], - ): Promise { - const content = new ContentTriggerEmail(); + ): Promise { + const content = new ContentEventEmail(); content.disasterType = disasterType; content.disasterTypeLabel = await this.getDisasterTypeLabel(disasterType); content.dataPerEvent = await this.getNotificationDataForEvents( @@ -169,17 +166,16 @@ export class NotificationContentService { data.nrOfTriggeredAreas = await this.getNrOfTriggeredAreas( data.triggeredAreas, ); - - data.startDateEventString = await this.getFirstLeadTimeDate( - Number(event.firstLeadTime.split('-')[0]), - event.firstLeadTime.split('-')[1], + // 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.startDateDisasterString = await this.getFirstLeadTimeString( + event, event.countryCodeISO3, disasterType, ); data.totalAffectectedOfIndicator = this.getTotalAffectedPerEvent( data.triggeredAreas, ); - data.mapImage = await this.eventService.getEventMapImage( country.countryCodeISO3, disasterType, @@ -270,7 +266,7 @@ export class NotificationContentService { }); } - public async getStartTimeEvent( + public async getFirstLeadTimeString( event: EventSummaryCountry, countryCodeISO3: string, disasterType: DisasterType, diff --git a/services/API-service/src/api/notification/notification.service.ts b/services/API-service/src/api/notification/notification.service.ts index 1352a256f..5cdf65f6c 100644 --- a/services/API-service/src/api/notification/notification.service.ts +++ b/services/API-service/src/api/notification/notification.service.ts @@ -23,94 +23,103 @@ export class NotificationService { disasterType: DisasterType, date?: Date, ): Promise { + await this.sendNotiFicationsActiveEvents( + disasterType, + countryCodeISO3, + date, + ); + + if (disasterType === DisasterType.Floods) { + // Sending finished events is now for floods only + await this.sendNotificationsFinishedEvents( + countryCodeISO3, + disasterType, + date, + ); + } + // REFACTOR: First close finished events. This is ideally done through separate endpoint called at end of pipeline, but that would require all pipelines to be updated. // Instead, making use of this endpoint which is already called at the end of every pipeline await this.eventService.closeEventsAutomatic(countryCodeISO3, disasterType); + } + private async sendNotiFicationsActiveEvents( + disasterType: DisasterType, + countryCodeISO3: string, + date?: Date, + ): Promise { const events = await this.eventService.getEventSummary( countryCodeISO3, disasterType, ); - - const activeEvents: EventSummaryCountry[] = []; - let finishedEvent: EventSummaryCountry; // This is now for floods only, so can only be 1 event, so not an array + const activeNotifiableEvents: EventSummaryCountry[] = []; for await (const event of events) { if ( - await this.getNotifiableActiveEvent( - event, - disasterType, - countryCodeISO3, - ) + await this.isNotifiableActiveEvent(event, disasterType, countryCodeISO3) ) { - activeEvents.push(event); - } else if (this.getFinishedEvent(event, disasterType, date)) { - finishedEvent = event; + activeNotifiableEvents.push(event); } } - if (activeEvents.length) { + + if (activeNotifiableEvents.length) { const country = await this.notificationContentService.getCountryNotificationInfo( countryCodeISO3, ); - this.emailService.sendTriggerEmail( + await this.emailService.sendTriggerEmail( country, disasterType, - activeEvents, + activeNotifiableEvents, date, ); - if (country.notificationInfo.useWhatsapp[disasterType]) { this.whatsappService.sendTriggerWhatsapp( country, - activeEvents, + activeNotifiableEvents, disasterType, ); } } + } - if (finishedEvent) { + private async sendNotificationsFinishedEvents( + countryCodeISO3: string, + disasterType: DisasterType, + date?: Date, + ): Promise { + const finishedNotifiableEvents = + await this.eventService.getEventsSummaryTriggerFinishedMail( + countryCodeISO3, + disasterType, + ); + + if (finishedNotifiableEvents.length > 0) { const country = await this.notificationContentService.getCountryNotificationInfo( countryCodeISO3, ); - this.emailService.sendTriggerFinishedEmail( + await this.emailService.sendTriggerFinishedEmail( country, disasterType, - finishedEvent, + finishedNotifiableEvents, date, ); if (country.notificationInfo.useWhatsapp[disasterType]) { - this.whatsappService.sendTriggerFinishedWhatsapp( - country, - finishedEvent, - disasterType, - ); - } - } - } - - private getFinishedEvent( - event: EventSummaryCountry, - disasterType: DisasterType, - uploadDate?: Date, - ) { - // For now only do this for floods - if (disasterType === DisasterType.Floods) { - const date = uploadDate ? new Date(uploadDate) : new Date(); - const yesterdayActiveDate = new Date(date.setDate(date.getDate() + 6)); // determine yesterday still active events by endDate lying (7 - 1) days in the future - if ( - new Date(event.endDate) >= - new Date(yesterdayActiveDate.setHours(0, 0, 0, 0)) - ) { - return true; + // TODO: Send one whatsapp message for all closing events + for (const event of finishedNotifiableEvents) { + await this.whatsappService.sendTriggerFinishedWhatsapp( + country, + event, + disasterType, + ); + } } } - return false; } - private async getNotifiableActiveEvent( + private async isNotifiableActiveEvent( event: EventSummaryCountry, disasterType: DisasterType, countryCodeISO3: string, diff --git a/services/API-service/src/api/notification/whatsapp/whatsapp.service.ts b/services/API-service/src/api/notification/whatsapp/whatsapp.service.ts index 7bbc72ec0..b71ea3f2d 100644 --- a/services/API-service/src/api/notification/whatsapp/whatsapp.service.ts +++ b/services/API-service/src/api/notification/whatsapp/whatsapp.service.ts @@ -16,7 +16,6 @@ import { TwilioStatusCallbackDto, } from './twilio.dto'; import { NotificationType, TwilioMessageEntity } from './twilio.entity'; -import { EmailTemplateService } from '../email/email-template.service'; import { formatActionUnitValue } from '../helpers/format-action-unit-value.helper'; @Injectable() @@ -89,7 +88,7 @@ export class WhatsappService { ? 'trigger' : 'warning'; const startTimeEvent = - await this.notificationContentService.getStartTimeEvent( + await this.notificationContentService.getFirstLeadTimeString( activeEvents[0], country.countryCodeISO3, disasterType, @@ -107,7 +106,7 @@ export class WhatsappService { ]; const startTimeFirstEvent = - await this.notificationContentService.getStartTimeEvent( + await this.notificationContentService.getFirstLeadTimeString( activeEvents[0], country.countryCodeISO3, disasterType, @@ -385,7 +384,7 @@ export class WhatsappService { } const startTimeEvent = - await this.notificationContentService.getStartTimeEvent( + await this.notificationContentService.getFirstLeadTimeString( event, country.countryCodeISO3, disasterType, From 3ff034156776e18321ffb3714df01b6538fe959e Mon Sep 17 00:00:00 2001 From: Ruben Date: Thu, 25 Apr 2024 17:11:09 +0200 Subject: [PATCH 03/55] AB#27285 Implement severity of warning --- .../dto/station-forecast.dto.ts | 3 +- .../dto/notification-date-per-event.dto.ts | 3 +- .../email/email-template.service.ts | 90 ++++++++----------- .../email/html/email-body-trigger-event.html | 4 +- .../email/html/email-body-warning-event.html | 4 +- .../email/html/email-table-event.html | 3 +- .../notification-content.service.ts | 2 +- services/API-service/src/shared/data.model.ts | 24 +++-- 8 files changed, 66 insertions(+), 67 deletions(-) diff --git a/services/API-service/src/api/glofas-station/dto/station-forecast.dto.ts b/services/API-service/src/api/glofas-station/dto/station-forecast.dto.ts index fc3e96d6e..1cdc7b59c 100644 --- a/services/API-service/src/api/glofas-station/dto/station-forecast.dto.ts +++ b/services/API-service/src/api/glofas-station/dto/station-forecast.dto.ts @@ -6,6 +6,7 @@ import { IsString, } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { EapAlertClassKeyEnum } from '../../../shared/data.model'; export class GlofasStationForecastDto { @ApiProperty({ example: 'G1374' }) @@ -20,7 +21,7 @@ export class GlofasStationForecastDto { @ApiProperty({ example: 1 }) @IsNotEmpty() - @IsIn(['no', 'min', 'med', 'max']) + @IsIn(Object.values(EapAlertClassKeyEnum)) public eapAlertClass: string; @ApiProperty({ example: 10 }) 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 33f17e713..4fde261a4 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,4 +1,4 @@ -import { TriggeredArea } from '../../../shared/data.model'; +import { EapAlertClass, TriggeredArea } from '../../../shared/data.model'; import { LeadTime } from '../../admin-area-dynamic-data/enum/lead-time.enum'; export class NotificationDataPerEventDto { @@ -12,6 +12,7 @@ export class NotificationDataPerEventDto { totalAffectectedOfIndicator: number; mapImage?: Buffer; issuedDate: Date; + eapAlertClass: EapAlertClass; } export enum TriggerStatusLabelEnum { diff --git a/services/API-service/src/api/notification/email/email-template.service.ts b/services/API-service/src/api/notification/email/email-template.service.ts index ab6eeb013..405428817 100644 --- a/services/API-service/src/api/notification/email/email-template.service.ts +++ b/services/API-service/src/api/notification/email/email-template.service.ts @@ -9,7 +9,10 @@ import * as ejs from 'ejs'; import * as fs from 'fs'; import { CountryTimeZoneMapping } from '../../country/country-time-zone-mapping'; import { DisasterType } from '../../disaster/disaster-type.enum'; -import { EventSummaryCountry } from '../../../shared/data.model'; +import { + EapAlertClassKeyEnum, + EventSummaryCountry, +} from '../../../shared/data.model'; import { CountryEntity } from '../../country/country.entity'; const emailFolder = './src/api/notification/email'; @@ -23,8 +26,6 @@ class ReplaceKeyValue { @Injectable() export class EmailTemplateService { - private placeholderToday = '(TODAY)'; - public createHtmlForTriggerEmail( emailContent: ContentEventEmail, date: Date, @@ -77,14 +78,6 @@ export class EmailTemplateService { replaceKey: 'tablesStacked', replaceValue: this.getTablesForEvents(emailContent), }, - { - replaceKey: this.placeholderToday, - replaceValue: date.toLocaleDateString('default', { - day: '2-digit', - month: 'short', - year: 'numeric', - }), - }, { replaceKey: 'eventListBody', replaceValue: this.getEventListBody(emailContent), @@ -195,14 +188,6 @@ export class EmailTemplateService { replaceKey: 'disasterType', replaceValue: disasterTypeLabel, }, - { - replaceKey: this.placeholderToday, - replaceValue: date.toLocaleDateString('default', { - day: '2-digit', - month: 'short', - year: 'numeric', - }), - }, ]; return keyValueReplaceList; } @@ -395,34 +380,11 @@ export class EmailTemplateService { return emailHtml; } - // TODO refactor this to use ejs package to render the html - private getEventListShort( - dataPerEvent: NotificationDataPerEventDto[], - ): string { - let text = ''; - for (const event of dataPerEvent) { - const leadTimeString = event.disasterSpecificCopy.leadTimeString - ? event.disasterSpecificCopy.leadTimeString - : event.firstLeadTime; - const timestamp = event.disasterSpecificCopy.timestamp - ? ` at ${event.disasterSpecificCopy.timestamp}` - : ''; - text += `${event.triggerStatusLabel} for ${event.eventName}: ${ - event.disasterSpecificCopy.extraInfo || - event.firstLeadTime === LeadTime.hour0 - ? leadTimeString - : `${event.firstLeadTime}${timestamp}` - }
`; - } - return text; - } - private getTablesForEvents(emailContent: ContentEventEmail): string { const adminAreaLabelsParent = emailContent.country.adminRegionLabels[ String(emailContent.defaultAdminLevel - 1) ]; - return emailContent.dataPerEvent .map((event) => { const data = { @@ -438,12 +400,10 @@ export class EmailTemplateService { defaultAdminAreaLabelParent: adminAreaLabelsParent.singular, indicatorLabel: emailContent.indicatorMetadata.label, indicatorUnit: emailContent.indicatorMetadata.unit, - triangleIcon: this.getTriangleIcon(event.triggerStatusLabel), + triangleIcon: this.getTriangleIcon(event.eapAlertClass?.key), tableRows: this.getTablesRows(event), - color: - event.triggerStatusLabel === TriggerStatusLabelEnum.Trigger - ? '#940000' - : '#da7c00', + color: this.ibfColorToHex(event.eapAlertClass?.color), + severityLabel: this.getEventSeverityLabel(event.eapAlertClass?.key), }; const templatePath = `${emailTemplateFolder}/email-table-event.html`; @@ -456,6 +416,16 @@ export class EmailTemplateService { .join(''); } + private getEventSeverityLabel(eapAlertClassKey: EapAlertClassKeyEnum) { + if (eapAlertClassKey === EapAlertClassKeyEnum.med) { + return 'Medium'; + } else if (eapAlertClassKey === EapAlertClassKeyEnum.min) { + return 'Low'; + } else { + return ''; + } + } + private getTablesRows(event: NotificationDataPerEventDto) { return event.triggeredAreas .map((area) => { @@ -502,8 +472,10 @@ export class EmailTemplateService { ), timezone: CountryTimeZoneMapping[emailContent.country.countryCodeISO3], - triangleIcon: this.getTriangleIcon(event.triggerStatusLabel), + triangleIcon: this.getTriangleIcon(event.eapAlertClass?.key), leadTime: event.firstLeadTime.replace('-', ' '), + disasterIssuedLabel: event.eapAlertClass.label, + color: this.ibfColorToHex(event.eapAlertClass?.color), }; const templatePath = @@ -518,6 +490,18 @@ export class EmailTemplateService { .join(''); } + private ibfColorToHex(color: string): string { + // TODO: Define in a place where FrontEnd and Backend can share this + switch (color) { + case 'ibf-orange': + return '#f0890d'; + case 'ibf-yellow': + return '#fff500'; + default: + return '#c70000'; + } + } + private getCurrentDateTimeString(countryCodeISO3: string): string { const date = new Date(); return this.dateObjectToDateTimeString(date, countryCodeISO3); @@ -540,13 +524,15 @@ export class EmailTemplateService { return date.toLocaleString('default', options); } - private getTriangleIcon(triggerStatusLabel) { + private getTriangleIcon(eapAlertClassKey: EapAlertClassKeyEnum) { let fileName = ''; // Still need implement the difference between medium and low warning - if (triggerStatusLabel === TriggerStatusLabelEnum.Trigger) { - fileName = 'trigger.png'; - } else { + if (eapAlertClassKey === EapAlertClassKeyEnum.med) { fileName = 'warning-medium.png'; + } else if (eapAlertClassKey === EapAlertClassKeyEnum.min) { + fileName = 'warning-low.png'; + } else { + fileName = 'trigger.png'; } const filePath = `${emailIconFolder}/${fileName}`; const imageDataURL = this.getPngImageAsDataURL(filePath); diff --git a/services/API-service/src/api/notification/email/html/email-body-trigger-event.html b/services/API-service/src/api/notification/email/html/email-body-trigger-event.html index 456687ad8..084bb3cc9 100644 --- a/services/API-service/src/api/notification/email/html/email-body-trigger-event.html +++ b/services/API-service/src/api/notification/email/html/email-body-trigger-event.html @@ -1,7 +1,7 @@
Trigger expected on:<%= disasterIssuedLabel %>: Warning expected on:<%= disasterIssuedLabel %> Warning icon - <%= triggerStatusLabel %> <%= hazard %>: <%= eventName %> + <%= severityLabel %> <%= triggerStatusLabel %> <%= hazard %>: <%= eventName + %>
Early Action Protocol diff --git a/services/API-service/src/api/notification/email/html/advisory-warning.html b/services/API-service/src/api/notification/email/html/advisory-warning.html new file mode 100644 index 000000000..90204931d --- /dev/null +++ b/services/API-service/src/api/notification/email/html/advisory-warning.html @@ -0,0 +1 @@ +Inform all potentially exposed districts diff --git a/services/API-service/src/api/notification/email/html/body-warning-event.html b/services/API-service/src/api/notification/email/html/body-event.html similarity index 68% rename from services/API-service/src/api/notification/email/html/body-warning-event.html rename to services/API-service/src/api/notification/email/html/body-event.html index ca5203753..b03ae3396 100644 --- a/services/API-service/src/api/notification/email/html/body-warning-event.html +++ b/services/API-service/src/api/notification/email/html/body-event.html @@ -9,9 +9,10 @@ word-wrap: break-word; " > - Warning icon - <%= hazard %> <%= eventName %>
+ <%= hazard %> <%= eventName %>
+
+ <%= disasterIssuedLabel %> + <%= disasterIssuedLabel %> + + - <%= startDateEventString %>, <%= leadTime %>s from now.
, <%= leadTime %>s from now.
+
+ Expected exposed <%= defaulAdminAreaLabel %>: + Expected exposed <%= defaulAdminAreaLabel %>: + + - <%= nrOfTriggeredAreas %> (see list below)
(see list below)
+
+ - <%= indicatorLabel %>: : + + Information regarding exposed population not available for warnings
+ <%- totalAffected %> + + Advisory: - Inform all potentially exposed districts
+
+ <%- advisory %>
+
+ This warning was issued by the IBF portal on <%= issuedDate %> (<%= - timezone %>) -
+
+ This <%= triggerStatusLabel %> was issued by the IBF portal on <%= + issuedDate %> (<%= timezone %>)
+
diff --git a/services/API-service/src/api/notification/email/html/body-total-affected-trigger.html b/services/API-service/src/api/notification/email/html/body-total-affected-trigger.html new file mode 100644 index 000000000..8849930fe --- /dev/null +++ b/services/API-service/src/api/notification/email/html/body-total-affected-trigger.html @@ -0,0 +1,11 @@ +<%= totalAffectectedOfIndicator %> <%= indicatorUnit %>
diff --git a/services/API-service/src/api/notification/email/html/body-total-affected-warning.html b/services/API-service/src/api/notification/email/html/body-total-affected-warning.html new file mode 100644 index 000000000..7ab140eb2 --- /dev/null +++ b/services/API-service/src/api/notification/email/html/body-total-affected-warning.html @@ -0,0 +1 @@ +Information regarding exposed population not available for warnings
diff --git a/services/API-service/src/api/notification/email/html/body-trigger-event.html b/services/API-service/src/api/notification/email/html/body-trigger-event.html deleted file mode 100644 index 084bb3cc9..000000000 --- a/services/API-service/src/api/notification/email/html/body-trigger-event.html +++ /dev/null @@ -1,132 +0,0 @@ -
- - Warning icon - <%= hazard %> <%= eventName %>
<%= disasterIssuedLabel %>: - <%= startDateEventString %>, <%= leadTime %>s from now.
Expected exposed <%= defaulAdminAreaLabel %>: - <%= nrOfTriggeredAreas %> (see list below)
- <%= indicatorLabel %>: <%= totalAffectectedOfIndicator %> <%= indicatorUnit %>
Advisory: Activate - Early Action Protocol

This trigger was issued by the IBF portal on <%= issuedDate %> (<%= - timezone %>) -
-
From 05acf46310c287b4e2ae561c9e84126a19586683 Mon Sep 17 00:00:00 2001 From: Ruben Date: Thu, 2 May 2024 14:04:46 +0200 Subject: [PATCH 06/55] AB#27182 Do not filter out action value 0 of warning flood email --- .../notification-content.service.ts | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) 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 f2d4bb1d0..59c2d335f 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 @@ -165,6 +165,8 @@ export class NotificationContentService { ); data.nrOfTriggeredAreas = await this.getNrOfTriggeredAreas( data.triggeredAreas, + data.triggerStatusLabel, + disasterType, ); // 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); @@ -187,14 +189,24 @@ export class NotificationContentService { private async getNrOfTriggeredAreas( triggeredAreas: TriggeredArea[], + statusLabel: TriggerStatusLabelEnum, + disasterType: DisasterType, ): Promise { // This filters out the areas that are affected by the event but do not have any affect action units // Affected action units are for example people_affected, houses_affected, etc (differs per disaster type) // We are not sure why this is done, but it is done in the original code - const triggeredAreaWithAffectedActionUnit = triggeredAreas.filter( - (a) => a.actionsValue > 0, - ); - return triggeredAreaWithAffectedActionUnit.length; + // For warning flood events this is not done, because there are no flood extens for warning events so we do not know any actions values + if ( + disasterType === DisasterType.Floods && + statusLabel === TriggerStatusLabelEnum.Warning + ) { + return triggeredAreas.length; + } else { + const triggeredAreasWithoutActionValue = triggeredAreas.filter( + (a) => a.actionsValue > 0, + ); + return triggeredAreasWithoutActionValue.length; + } } private async getSortedTriggeredAreas( From 1a1ad1bfa0d50e0eec583525c7cfa332443d6733 Mon Sep 17 00:00:00 2001 From: Ruben Date: Thu, 2 May 2024 14:59:20 +0200 Subject: [PATCH 07/55] Improved styling and moved to seperate file (styles.ejs) AB#27502 --- .../email/email-template.service.ts | 14 +- .../email/html/advisory-trigger.html | 6 +- .../src/api/notification/email/html/base.html | 4 +- .../notification/email/html/body-event.html | 138 +++--------------- .../html/body-total-affected-trigger.html | 15 +- .../email/html/event-finished.html | 48 ++---- .../api/notification/email/html/header.html | 9 +- .../api/notification/email/html/styles.ejs | 83 +++++++++++ .../notification/email/html/table-event.html | 96 +++--------- .../email/html/table-trigger-row.html | 34 +---- .../email/html/table-warning-row.html | 21 +-- .../email/html/trigger-finished.html | 2 +- .../email/html/trigger-notification.html | 15 +- 13 files changed, 170 insertions(+), 315 deletions(-) create mode 100644 services/API-service/src/api/notification/email/html/styles.ejs diff --git a/services/API-service/src/api/notification/email/email-template.service.ts b/services/API-service/src/api/notification/email/email-template.service.ts index 083ca869b..ac54718ff 100644 --- a/services/API-service/src/api/notification/email/email-template.service.ts +++ b/services/API-service/src/api/notification/email/email-template.service.ts @@ -190,6 +190,7 @@ export class EmailTemplateService { } private getEmailBody(triggerFinished: boolean): string { + // TODO: Also apply new EJS style templating to these files if (triggerFinished) { return this.readHtmlFile('trigger-finished.html'); } else { @@ -335,7 +336,10 @@ export class EmailTemplateService { } private formatEmail(emailKeyValueReplaceList: ReplaceKeyValue[]): string { + // TODO REFACTOR: Apply styles in a septerate file also for the base.html const template = this.readHtmlFile('base.html'); + const styles = this.readHtmlFile('styles.ejs'); + const templateWithStyle = styles + template; const replacements = emailKeyValueReplaceList.reduce( (acc, { replaceKey, replaceValue }) => { acc[replaceKey] = replaceValue; @@ -344,7 +348,7 @@ export class EmailTemplateService { {}, ); - let emailHtml = template; + let emailHtml = templateWithStyle; let previousHtml = null; // This loop is needed to handle nested EJS tags. It repeatedly renders the template @@ -428,7 +432,6 @@ export class EmailTemplateService { eventName: event.eventName, nrOfTriggeredAreas: event.nrOfTriggeredAreas, expectedTriggerDate: event.firstLeadTime, - expectedExposedAdminBoundary: event.nrOfTriggeredAreas, issuedDate: this.dateObjectToDateTimeString( event.issuedDate, emailContent.country.countryCodeISO3, @@ -454,7 +457,6 @@ export class EmailTemplateService { const templateFileName = 'body-event.html'; let template = this.readHtmlFile(templateFileName); - return ejs.render(template, data); }) .join(''); @@ -485,11 +487,11 @@ export class EmailTemplateService { // TODO: Define in a place where FrontEnd and Backend can share this switch (color) { case 'ibf-orange': - return '#f0890d'; + return '#aa6009'; case 'ibf-yellow': - return '#fff500'; + return '#7d6906'; default: - return '#c70000'; + return '#8a0f32'; } } diff --git a/services/API-service/src/api/notification/email/html/advisory-trigger.html b/services/API-service/src/api/notification/email/html/advisory-trigger.html index 40fdbbdd8..568dee3c8 100644 --- a/services/API-service/src/api/notification/email/html/advisory-trigger.html +++ b/services/API-service/src/api/notification/email/html/advisory-trigger.html @@ -1,2 +1,4 @@ -Activate Early Action Protocol +Activate + + Early Action Protocol diff --git a/services/API-service/src/api/notification/email/html/base.html b/services/API-service/src/api/notification/email/html/base.html index ca6072f8f..0a21277ce 100644 --- a/services/API-service/src/api/notification/email/html/base.html +++ b/services/API-service/src/api/notification/email/html/base.html @@ -397,7 +397,7 @@ #templateBody { /*@editable*/ - background-color: white; + background-color: #f5f5f594; /*@editable*/ background-image: none; /*@editable*/ @@ -564,7 +564,7 @@ @media only screen and (min-width: 768px) { .templateContainer { - width: 600px !important; + width: 800px !important; } } diff --git a/services/API-service/src/api/notification/email/html/body-event.html b/services/API-service/src/api/notification/email/html/body-event.html index b03ae3396..c5a33daec 100644 --- a/services/API-service/src/api/notification/email/html/body-event.html +++ b/services/API-service/src/api/notification/email/html/body-event.html @@ -1,125 +1,33 @@
- - triangleIcon - <%= hazard %> <%= eventName %>
+ + triangleIcon + <%= hazard %> <%= eventName %> + - - <%= disasterIssuedLabel %> + <%= disasterIssuedLabel %> + + <%= startDateEventString %>, <%= leadTime %>s from now. + - - <%= startDateEventString %>, <%= leadTime %>s from now.
-
- + Expected exposed <%= defaulAdminAreaLabel %>: - - <%= nrOfTriggeredAreas %> (see list below)
-
- - <%= indicatorLabel %>: - - - <%- totalAffected %> - - - Advisory: + + <%= nrOfTriggeredAreas %> (see list below) + - - <%- advisory %>
+ <%= indicatorLabel %>: + <%- totalAffected %> + Advisory: + + <%- advisory %> + - -
+
+ This <%= triggerStatusLabel %> was issued by the IBF portal on <%= - issuedDate %> (<%= timezone %>)
+ issuedDate %> (<%= timezone %>) +
+
diff --git a/services/API-service/src/api/notification/email/html/body-total-affected-trigger.html b/services/API-service/src/api/notification/email/html/body-total-affected-trigger.html index 8849930fe..f5e2bbe56 100644 --- a/services/API-service/src/api/notification/email/html/body-total-affected-trigger.html +++ b/services/API-service/src/api/notification/email/html/body-total-affected-trigger.html @@ -1,11 +1,4 @@ -<%= totalAffectectedOfIndicator %> <%= indicatorUnit %>
+ + <%= totalAffectectedOfIndicator %> <%= indicatorUnit %> + + diff --git a/services/API-service/src/api/notification/email/html/event-finished.html b/services/API-service/src/api/notification/email/html/event-finished.html index 7a607dbe3..12e17e678 100644 --- a/services/API-service/src/api/notification/email/html/event-finished.html +++ b/services/API-service/src/api/notification/email/html/event-finished.html @@ -1,39 +1,15 @@
- - <%= disasterTypeLabel %>: <%= eventName %> is now below threshold
+ + <%= disasterTypeLabel %>: <%= eventName %> is now below threshold + + + The events actions will continue to show on the IBF portal and can still be - managed.
-
This warning was issued by the IBF portal on <%= issuedDate %> (<%= - timezone %>) + managed. + + + + This warning was issued by the IBF portal on <%= issuedDate %> (<%= timezone + %>) +
diff --git a/services/API-service/src/api/notification/email/html/header.html b/services/API-service/src/api/notification/email/html/header.html index 5a5a87a43..1e75a6a6e 100644 --- a/services/API-service/src/api/notification/email/html/header.html +++ b/services/API-service/src/api/notification/email/html/header.html @@ -4,14 +4,7 @@

<%= nrOfEvents %> <%= disasterLabel %> alerts

IBF alert send on <%= sentOnDate %> (<%= timezone %>) diff --git a/services/API-service/src/api/notification/email/html/styles.ejs b/services/API-service/src/api/notification/email/html/styles.ejs new file mode 100644 index 000000000..cd532590e --- /dev/null +++ b/services/API-service/src/api/notification/email/html/styles.ejs @@ -0,0 +1,83 @@ + diff --git a/services/API-service/src/api/notification/email/html/table-event.html b/services/API-service/src/api/notification/email/html/table-event.html index 6c0cb02a5..6f87e4fdd 100644 --- a/services/API-service/src/api/notification/email/html/table-event.html +++ b/services/API-service/src/api/notification/email/html/table-event.html @@ -1,91 +1,34 @@ -
+
- Warning icon + Warning icon <%= severityLabel %> <%= triggerStatusLabel %> <%= hazard %>: <%= eventName %>
Expected exposed <%= defaulAdminAreaLabelPlural %><% if (triggerStatusLabel === 'Trigger') { %> in order of <%= indicatorLabel.toLowerCase() %><% } %>:
-
+
- + <% if (triggerStatusLabel === 'Trigger') { %> - <% } %> - @@ -94,4 +37,13 @@
+ <%= indicatorLabel %> + <%= defaulAdminAreaLabelSingular %> (<%= defaultAdminAreaLabelParent %>)
+ <% if (triggerStatusLabel === 'Warning') { %> +
+ Please note: Information regarding exposed population not available for + medium warning level. +
+ <% } %> +
diff --git a/services/API-service/src/api/notification/email/html/table-trigger-row.html b/services/API-service/src/api/notification/email/html/table-trigger-row.html index 38428b98e..b2881de88 100644 --- a/services/API-service/src/api/notification/email/html/table-trigger-row.html +++ b/services/API-service/src/api/notification/email/html/table-trigger-row.html @@ -1,36 +1,8 @@ - - + + <%= affectectedOfIndicator %> - + <%= adminBoundary %> (<%= higherAdminBoundary %>) diff --git a/services/API-service/src/api/notification/email/html/table-warning-row.html b/services/API-service/src/api/notification/email/html/table-warning-row.html index 85d41824e..a26f8d8ea 100644 --- a/services/API-service/src/api/notification/email/html/table-warning-row.html +++ b/services/API-service/src/api/notification/email/html/table-warning-row.html @@ -1,22 +1,5 @@ - - + + <%= adminBoundary %> (<%= higherAdminBoundary %>) diff --git a/services/API-service/src/api/notification/email/html/trigger-finished.html b/services/API-service/src/api/notification/email/html/trigger-finished.html index 5df98542b..0fd4f6d22 100644 --- a/services/API-service/src/api/notification/email/html/trigger-finished.html +++ b/services/API-service/src/api/notification/email/html/trigger-finished.html @@ -83,7 +83,7 @@ id="Editable_Content_Areas" >
- Dear reader, +
Dear Reader,


<%- eventOverview %> diff --git a/services/API-service/src/api/notification/email/html/trigger-notification.html b/services/API-service/src/api/notification/email/html/trigger-notification.html index d494e470c..f168c01cb 100644 --- a/services/API-service/src/api/notification/email/html/trigger-notification.html +++ b/services/API-service/src/api/notification/email/html/trigger-notification.html @@ -83,18 +83,7 @@ id="Editable_Content_Areas" >
-
- Dear Reader, -
+
Dear Reader,


<%- eventListBody %> @@ -140,6 +129,7 @@ border-radius: 3px; " align="left" + class="body-text" > Find more info on the potentially exposed areas on a map and manage @@ -192,6 +182,7 @@ border-radius: 3px; " align="left" + class="body-text" > Read about the trigger methodology and the anticipatory actions. From e9620159c2a5d33bd03efde64eae9bce09d8c226 Mon Sep 17 00:00:00 2001 From: Ruben Date: Thu, 2 May 2024 15:05:09 +0200 Subject: [PATCH 08/55] Tad of padding AB#27502 --- .../src/api/notification/email/html/table-event.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/API-service/src/api/notification/email/html/table-event.html b/services/API-service/src/api/notification/email/html/table-event.html index 6f87e4fdd..00790d217 100644 --- a/services/API-service/src/api/notification/email/html/table-event.html +++ b/services/API-service/src/api/notification/email/html/table-event.html @@ -13,12 +13,14 @@ <%= severityLabel %> <%= triggerStatusLabel %> <%= hazard %>: <%= eventName %> +
Expected exposed <%= defaulAdminAreaLabelPlural %><% if (triggerStatusLabel === 'Trigger') { %> in order of <%= indicatorLabel.toLowerCase() %><% } %>:
+
From 52188714455a0ac965be6838ab1d43ad469eea2b Mon Sep 17 00:00:00 2001 From: Ruben Date: Fri, 3 May 2024 14:16:11 +0200 Subject: [PATCH 09/55] AB#27502 Added juice to convert styles to inline html to support different email clients and browsers --- services/API-service/package-lock.json | 616 ++++++++++++++++++ services/API-service/package.json | 1 + .../email/email-template.service.ts | 29 +- .../api/notification/email/email.service.ts | 4 +- .../src/api/notification/email/html/base.html | 2 +- .../api/notification/email/html/styles.ejs | 10 +- .../notification/email/html/table-event.html | 106 +-- 7 files changed, 701 insertions(+), 67 deletions(-) diff --git a/services/API-service/package-lock.json b/services/API-service/package-lock.json index c54608ee2..000e24b72 100644 --- a/services/API-service/package-lock.json +++ b/services/API-service/package-lock.json @@ -24,6 +24,7 @@ "csv-parser": "^3.0.0", "ejs": "^3.1.9", "jsonwebtoken": "^8.1.1", + "juice": "^10.0.0", "mailchimp-api-v3": "^1.15.0", "mysql": "^2.15.0", "passport": "^0.4.1", @@ -2887,6 +2888,14 @@ "node": ">=6" } }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -3354,6 +3363,11 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, "node_modules/boxen": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", @@ -3683,6 +3697,65 @@ "node": ">=16" } }, + "node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio/node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "dependencies": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/chokidar": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.3.tgz", @@ -3955,6 +4028,14 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "engines": { + "node": ">= 6" + } + }, "node_modules/component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -4245,6 +4326,32 @@ "resolved": "https://registry.npmjs.org/cryptr/-/cryptr-6.0.2.tgz", "integrity": "sha512-1TRHI4bmuLIB8WgkH9eeYXzhEg1T4tonO4vVaMBKKde8Dre51J68nAgTVXTwMYXAf7+mV2gBCkm/9wksjSb2sA==" }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/cssom": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", @@ -4525,6 +4632,30 @@ "node": ">=6.0.0" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, "node_modules/domexception": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", @@ -4534,6 +4665,33 @@ "webidl-conversions": "^4.0.2" } }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dot-prop": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", @@ -4627,6 +4785,17 @@ "once": "^1.4.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -6248,6 +6417,24 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/http-cache-semantics": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", @@ -9543,6 +9730,24 @@ "verror": "1.10.0" } }, + "node_modules/juice": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/juice/-/juice-10.0.0.tgz", + "integrity": "sha512-9f68xmhGrnIi6DBkiiP3rUrQN33SEuaKu1+njX6VgMP+jwZAsnT33WIzlrWICL9matkhYu3OyrqSUP55YTIdGg==", + "dependencies": { + "cheerio": "^1.0.0-rc.12", + "commander": "^6.1.0", + "mensch": "^0.3.4", + "slick": "^1.12.2", + "web-resource-inliner": "^6.0.1" + }, + "bin": { + "juice": "bin/juice" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -9857,6 +10062,11 @@ "node": ">= 0.6" } }, + "node_modules/mensch": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/mensch/-/mensch-0.3.4.tgz", + "integrity": "sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==" + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -10324,6 +10534,17 @@ "node": ">=8" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/nwsapi": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", @@ -11980,6 +12201,14 @@ "node": ">=8" } }, + "node_modules/slick": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz", + "integrity": "sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==", + "engines": { + "node": "*" + } + }, "node_modules/slug": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/slug/-/slug-4.0.2.tgz", @@ -13684,6 +13913,14 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "devOptional": true }, + "node_modules/valid-data-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz", + "integrity": "sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==", + "engines": { + "node": ">=10" + } + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -13746,6 +13983,134 @@ "resolved": "https://registry.npmjs.org/weak-map/-/weak-map-1.0.8.tgz", "integrity": "sha512-lNR9aAefbGPpHO7AEnY0hCFjz1eTkWCXYvkTRrTHs9qv8zJp+SkVYpzfLIFXQQiG3tVvbNFQgVg2bQS8YGgxyw==" }, + "node_modules/web-resource-inliner": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-6.0.1.tgz", + "integrity": "sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==", + "dependencies": { + "ansi-colors": "^4.1.1", + "escape-goat": "^3.0.0", + "htmlparser2": "^5.0.0", + "mime": "^2.4.6", + "node-fetch": "^2.6.0", + "valid-data-url": "^3.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/web-resource-inliner/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/dom-serializer/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/domhandler": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", + "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", + "dependencies": { + "domelementtype": "^2.0.1" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/domutils/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/escape-goat": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz", + "integrity": "sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/web-resource-inliner/node_modules/htmlparser2": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-5.0.1.tgz", + "integrity": "sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^3.3.0", + "domutils": "^2.4.2", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/fb55/htmlparser2?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", @@ -16142,6 +16507,11 @@ } } }, + "ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==" + }, "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -16526,6 +16896,11 @@ "unpipe": "1.0.0" } }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, "boxen": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", @@ -16792,6 +17167,52 @@ "resolved": "https://registry.npmjs.org/check-disk-space/-/check-disk-space-3.4.0.tgz", "integrity": "sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw==" }, + "cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "requires": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "dependencies": { + "parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "requires": { + "entities": "^4.4.0" + } + }, + "parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "requires": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + } + } + } + }, + "cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "requires": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + } + }, "chokidar": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.3.tgz", @@ -17009,6 +17430,11 @@ "delayed-stream": "~1.0.0" } }, + "commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==" + }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -17245,6 +17671,23 @@ "resolved": "https://registry.npmjs.org/cryptr/-/cryptr-6.0.2.tgz", "integrity": "sha512-1TRHI4bmuLIB8WgkH9eeYXzhEg1T4tonO4vVaMBKKde8Dre51J68nAgTVXTwMYXAf7+mV2gBCkm/9wksjSb2sA==" }, + "css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + } + }, + "css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==" + }, "cssom": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", @@ -17461,6 +17904,21 @@ "esutils": "^2.0.2" } }, + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" + }, "domexception": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", @@ -17470,6 +17928,24 @@ "webidl-conversions": "^4.0.2" } }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, "dot-prop": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", @@ -17545,6 +18021,11 @@ "once": "^1.4.0" } }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -18792,6 +19273,17 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "http-cache-semantics": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", @@ -21545,6 +22037,18 @@ "verror": "1.10.0" } }, + "juice": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/juice/-/juice-10.0.0.tgz", + "integrity": "sha512-9f68xmhGrnIi6DBkiiP3rUrQN33SEuaKu1+njX6VgMP+jwZAsnT33WIzlrWICL9matkhYu3OyrqSUP55YTIdGg==", + "requires": { + "cheerio": "^1.0.0-rc.12", + "commander": "^6.1.0", + "mensch": "^0.3.4", + "slick": "^1.12.2", + "web-resource-inliner": "^6.0.1" + } + }, "jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -21812,6 +22316,11 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" }, + "mensch": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/mensch/-/mensch-0.3.4.tgz", + "integrity": "sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==" + }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -22207,6 +22716,14 @@ "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", "dev": true }, + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "requires": { + "boolbase": "^1.0.0" + } + }, "nwsapi": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", @@ -23513,6 +24030,11 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, + "slick": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz", + "integrity": "sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==" + }, "slug": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/slug/-/slug-4.0.2.tgz", @@ -24789,6 +25311,11 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "devOptional": true }, + "valid-data-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz", + "integrity": "sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==" + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -24842,6 +25369,95 @@ "resolved": "https://registry.npmjs.org/weak-map/-/weak-map-1.0.8.tgz", "integrity": "sha512-lNR9aAefbGPpHO7AEnY0hCFjz1eTkWCXYvkTRrTHs9qv8zJp+SkVYpzfLIFXQQiG3tVvbNFQgVg2bQS8YGgxyw==" }, + "web-resource-inliner": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-6.0.1.tgz", + "integrity": "sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==", + "requires": { + "ansi-colors": "^4.1.1", + "escape-goat": "^3.0.0", + "htmlparser2": "^5.0.0", + "mime": "^2.4.6", + "node-fetch": "^2.6.0", + "valid-data-url": "^3.0.0" + }, + "dependencies": { + "dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "dependencies": { + "domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "requires": { + "domelementtype": "^2.2.0" + } + } + } + }, + "domhandler": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", + "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", + "requires": { + "domelementtype": "^2.0.1" + } + }, + "domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "dependencies": { + "domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "requires": { + "domelementtype": "^2.2.0" + } + } + } + }, + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" + }, + "escape-goat": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz", + "integrity": "sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==" + }, + "htmlparser2": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-5.0.1.tgz", + "integrity": "sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^3.3.0", + "domutils": "^2.4.2", + "entities": "^2.0.0" + } + }, + "mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==" + } + } + }, "webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", diff --git a/services/API-service/package.json b/services/API-service/package.json index 74d68ab0d..3984b8b7d 100644 --- a/services/API-service/package.json +++ b/services/API-service/package.json @@ -42,6 +42,7 @@ "csv-parser": "^3.0.0", "ejs": "^3.1.9", "jsonwebtoken": "^8.1.1", + "juice": "^10.0.0", "mailchimp-api-v3": "^1.15.0", "mysql": "^2.15.0", "passport": "^0.4.1", diff --git a/services/API-service/src/api/notification/email/email-template.service.ts b/services/API-service/src/api/notification/email/email-template.service.ts index ac54718ff..ee8f1f33d 100644 --- a/services/API-service/src/api/notification/email/email-template.service.ts +++ b/services/API-service/src/api/notification/email/email-template.service.ts @@ -13,6 +13,7 @@ import { EventSummaryCountry, } from '../../../shared/data.model'; import { CountryEntity } from '../../country/country.entity'; +import * as juice from 'juice'; const emailFolder = './src/api/notification/email'; const emailTemplateFolder = `${emailFolder}/html`; @@ -25,10 +26,10 @@ class ReplaceKeyValue { @Injectable() export class EmailTemplateService { - public createHtmlForTriggerEmail( + public async createHtmlForTriggerEmail( emailContent: ContentEventEmail, date: Date, - ): string { + ): Promise { const replaceKeyValues = this.createReplaceKeyValuesTrigger( emailContent, date, @@ -37,25 +38,24 @@ export class EmailTemplateService { } // TODO REFACTOR this to use a DTO (ContentTriggerFinishedEmail) instead of multiple parameters - public createHtmlForTriggerFinishedEmail( + public async createHtmlForTriggerFinishedEmail( country: CountryEntity, disasterType: DisasterType, finishedEvents: EventSummaryCountry[], disasterTypeLabel: string, _date: Date, // I am not sure if this is needed and for what it was used before - ): string { + ): Promise { const replaceKeyValues = this.createReplaceKeyValuesTriggerFinished( country, disasterType, finishedEvents, disasterTypeLabel, ); - return this.formatEmail(replaceKeyValues); + return await this.formatEmail(replaceKeyValues); } private createReplaceKeyValuesTrigger( emailContent: ContentEventEmail, - date: Date, ): ReplaceKeyValue[] { const country = emailContent.country; const disasterType = emailContent.disasterType; @@ -335,7 +335,9 @@ export class EmailTemplateService { } } - private formatEmail(emailKeyValueReplaceList: ReplaceKeyValue[]): string { + private async formatEmail( + emailKeyValueReplaceList: ReplaceKeyValue[], + ): Promise { // TODO REFACTOR: Apply styles in a septerate file also for the base.html const template = this.readHtmlFile('base.html'); const styles = this.readHtmlFile('styles.ejs'); @@ -358,8 +360,19 @@ export class EmailTemplateService { previousHtml = emailHtml; emailHtml = ejs.render(previousHtml, replacements); } + // Inline the CSS + const inlinedHtml = await new Promise((resolve, reject) => { + juice.juiceResources(emailHtml, { webResources: {} }, (err, html) => { + if (err) reject(err); + else resolve(html); + }); + }); + fs.writeFile('output.html', inlinedHtml as string, (err) => { + if (err) throw err; + console.log('The file has been saved!'); + }); - return emailHtml; + return inlinedHtml as string; } private getTablesForEvents(emailContent: ContentEventEmail): string { 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 8ec3ea611..2149f6773 100644 --- a/services/API-service/src/api/notification/email/email.service.ts +++ b/services/API-service/src/api/notification/email/email.service.ts @@ -52,7 +52,7 @@ export class EmailService { disasterType, activeEvents, ); - const emailHtml = this.emailTemplateService.createHtmlForTriggerEmail( + const emailHtml = await this.emailTemplateService.createHtmlForTriggerEmail( emailContent, date, ); @@ -76,7 +76,7 @@ export class EmailService { const disasterTypeLabel = await this.notificationContentService.getDisasterTypeLabel(disasterType); const emailHtml = - this.emailTemplateService.createHtmlForTriggerFinishedEmail( + await this.emailTemplateService.createHtmlForTriggerFinishedEmail( country, disasterType, finishedEvents, diff --git a/services/API-service/src/api/notification/email/html/base.html b/services/API-service/src/api/notification/email/html/base.html index 0a21277ce..4980390c6 100644 --- a/services/API-service/src/api/notification/email/html/base.html +++ b/services/API-service/src/api/notification/email/html/base.html @@ -397,7 +397,7 @@ #templateBody { /*@editable*/ - background-color: #f5f5f594; + background-color: #f4f5f8; /*@editable*/ background-image: none; /*@editable*/ diff --git a/services/API-service/src/api/notification/email/html/styles.ejs b/services/API-service/src/api/notification/email/html/styles.ejs index cd532590e..8dc314a29 100644 --- a/services/API-service/src/api/notification/email/html/styles.ejs +++ b/services/API-service/src/api/notification/email/html/styles.ejs @@ -50,16 +50,16 @@ padding-bottom: 4px; border: 1px #f5f6f9 solid; } - .header-row { - justify-content: flex-start; - align-items: center; - gap: 40px; - display: inline-flex; + .header-row td { + vertical-align: middle; + padding-right: 20px; /* adjust as needed */ + padding-left: 20px; /* adjust as needed */ } .table-cell { height: 19px; padding: 2px 10px; width: 211px; + text-align: left; } /* Event table styles */ diff --git a/services/API-service/src/api/notification/email/html/table-event.html b/services/API-service/src/api/notification/email/html/table-event.html index 00790d217..cad642c59 100644 --- a/services/API-service/src/api/notification/email/html/table-event.html +++ b/services/API-service/src/api/notification/email/html/table-event.html @@ -1,51 +1,55 @@ -
-
- Warning icon - <%= severityLabel %> <%= triggerStatusLabel %> <%= hazard %>: <%= eventName - %> -
-
-
- Expected exposed <%= defaulAdminAreaLabelPlural %><% if (triggerStatusLabel - === 'Trigger') { %> in order of <%= indicatorLabel.toLowerCase() %><% } %>: -
-
-
-
-
- - <% if (triggerStatusLabel === 'Trigger') { %> - - <% } %> - - - <%- tableRows %> -
- <%= indicatorLabel %> - - <%= defaulAdminAreaLabelSingular %> (<%= defaultAdminAreaLabelParent - %>) -
-
-
- <% if (triggerStatusLabel === 'Warning') { %> -
- Please note: Information regarding exposed population not available for - medium warning level. -
- <% } %> - -
+ + + + +
+
+
+ Warning icon + <%= severityLabel %> <%= triggerStatusLabel %> <%= hazard %>: <%= + eventName %> +
+
+
+ Expected exposed <%= defaulAdminAreaLabelPlural %><% if + (triggerStatusLabel === 'Trigger') { %> in order of <%= + indicatorLabel.toLowerCase() %><% } %>: +
+
+
+ + + <% if (triggerStatusLabel === 'Trigger') { %> + + <% } %> + + + <%- tableRows %> +
+ <%= indicatorLabel %> + + <%= defaulAdminAreaLabelSingular %> (<%= defaultAdminAreaLabelParent %>) +
+
+ <% if (triggerStatusLabel === 'Warning') { %> +
+ Please note: Information regarding exposed population not available + for medium warning level. +
+ <% } %> +
+
+
From 9e4693c739e4a0c1734c74cee0ac54bafc00dc47 Mon Sep 17 00:00:00 2001 From: Ruben Date: Fri, 3 May 2024 14:17:29 +0200 Subject: [PATCH 10/55] remove write to file --- .../src/api/notification/email/email-template.service.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/services/API-service/src/api/notification/email/email-template.service.ts b/services/API-service/src/api/notification/email/email-template.service.ts index ee8f1f33d..7c671ed6d 100644 --- a/services/API-service/src/api/notification/email/email-template.service.ts +++ b/services/API-service/src/api/notification/email/email-template.service.ts @@ -56,6 +56,7 @@ export class EmailTemplateService { private createReplaceKeyValuesTrigger( emailContent: ContentEventEmail, + _date: Date, ): ReplaceKeyValue[] { const country = emailContent.country; const disasterType = emailContent.disasterType; @@ -367,10 +368,6 @@ export class EmailTemplateService { else resolve(html); }); }); - fs.writeFile('output.html', inlinedHtml as string, (err) => { - if (err) throw err; - console.log('The file has been saved!'); - }); return inlinedHtml as string; } From dac4d6210e2767dff028741238fd7d0dae87803d Mon Sep 17 00:00:00 2001 From: Ruben Date: Mon, 13 May 2024 13:58:03 +0200 Subject: [PATCH 11/55] Styling details and links to ibf portal AB#27502 --- .../email/email-template.service.ts | 143 ++++++++---------- .../email/html/advisory-trigger.html | 7 +- .../src/api/notification/email/html/base.html | 11 -- .../notification/email/html/body-event.html | 4 +- .../email/html/event-finished.html | 2 +- .../api/notification/email/html/footer.html | 20 +++ .../email/html/notification-actions.html | 63 ++++++++ .../api/notification/email/html/styles.ejs | 45 +++++- .../notification/email/html/table-event.html | 13 +- .../email/html/table-trigger-row.html | 4 +- .../email/html/table-warning-row.html | 2 +- .../email/html/trigger-finished.html | 122 +-------------- .../email/html/trigger-notification.html | 111 +------------- .../api/notification/email/logos/logo-IBF.png | Bin 0 -> 2838 bytes 14 files changed, 212 insertions(+), 335 deletions(-) create mode 100644 services/API-service/src/api/notification/email/html/footer.html create mode 100644 services/API-service/src/api/notification/email/html/notification-actions.html create mode 100644 services/API-service/src/api/notification/email/logos/logo-IBF.png diff --git a/services/API-service/src/api/notification/email/email-template.service.ts b/services/API-service/src/api/notification/email/email-template.service.ts index 7c671ed6d..f373d1dfc 100644 --- a/services/API-service/src/api/notification/email/email-template.service.ts +++ b/services/API-service/src/api/notification/email/email-template.service.ts @@ -18,6 +18,7 @@ import * as juice from 'juice'; const emailFolder = './src/api/notification/email'; const emailTemplateFolder = `${emailFolder}/html`; const emailIconFolder = `${emailFolder}/icons`; +const emailLogoFolder = `${emailFolder}/logos`; class ReplaceKeyValue { replaceKey: string; @@ -70,8 +71,8 @@ export class EmailTemplateService { replaceValue: this.getHeaderEventStarted(emailContent), }, { - replaceKey: 'socialMediaPart', - replaceValue: this.getSocialMediaHtml(emailContent.country), + replaceKey: 'notificationActions', + replaceValue: this.getNotificationActionsHtml(country, disasterType), }, { replaceKey: 'tablesStacked', @@ -97,12 +98,6 @@ export class EmailTemplateService { replaceKey: 'linkDashboard', replaceValue: process.env.DASHBOARD_URL, }, - { - replaceKey: 'linkEapSop', - replaceValue: country.countryDisasterSettings.find( - (s) => s.disasterType === disasterType, - ).eapLink, - }, { replaceKey: 'socialMediaLink', replaceValue: country.notificationInfo.linkSocialMediaUrl, @@ -116,11 +111,8 @@ export class EmailTemplateService { replaceValue: emailContent.disasterTypeLabel, }, { - replaceKey: 'videoPdfLinks', - replaceValue: this.getVideoPdfLinks( - country.notificationInfo.linkVideo, - country.notificationInfo.linkPdf, - ), + replaceKey: 'footer', + replaceValue: this.getFooterHtml(), }, ]; return keyValueReplaceList; @@ -161,12 +153,6 @@ export class EmailTemplateService { replaceKey: 'socialMediaPart', replaceValue: this.getSocialMediaHtml(country), }, - { - replaceKey: 'linkEapSop', - replaceValue: country.countryDisasterSettings.find( - (s) => s.disasterType === disasterType, - ).eapLink, - }, { replaceKey: 'socialMediaLink', replaceValue: country.notificationInfo.linkSocialMediaUrl, @@ -175,17 +161,14 @@ export class EmailTemplateService { replaceKey: 'socialMediaType', replaceValue: country.notificationInfo.linkSocialMediaType, }, - { - replaceKey: 'videoPdfLinks', - replaceValue: this.getVideoPdfLinks( - country.notificationInfo.linkVideo, - country.notificationInfo.linkPdf, - ), - }, { replaceKey: 'disasterType', replaceValue: disasterTypeLabel, }, + { + replaceKey: 'footer', + replaceValue: this.getFooterHtml(), + }, ]; return keyValueReplaceList; } @@ -235,50 +218,22 @@ export class EmailTemplateService { return headerEventOverview; } - private getVideoPdfLinks(videoLink: string, pdfLink: string) { - // TODO: Use ejs template - const linkVideoHTML = ` -
video`; - - const linkPdfHTML = `PDF`; - let videoStr = ''; - if (videoLink) { - videoStr = ' ' + linkVideoHTML; - } - let pdfStr = ''; - if (pdfLink) { - pdfStr = ' ' + linkPdfHTML; - } - let orStr = ''; - if (videoStr && pdfStr) { - orStr = ' or'; - } - if (videoStr || pdfStr) { - return `See instructions for the IBF-portal in${videoStr}${orStr}${pdfStr}.`; - } + private getNotificationActionsHtml( + country: CountryEntity, + disasterType: DisasterType, + ): string { + const socialMediaLinkHtml = this.getSocialMediaHtml(country); + + let html = this.readHtmlFile('notification-actions.html'); + const data = { + linkDashboard: process.env.DASHBOARD_URL, + linkEapSop: country.countryDisasterSettings.find( + (s) => s.disasterType === disasterType, + ).eapLink, + socialMediaPart: socialMediaLinkHtml, + }; + html = ejs.render(html, data); + return html; // } private getSocialMediaHtml(country: CountryEntity): string { @@ -364,8 +319,12 @@ export class EmailTemplateService { // Inline the CSS const inlinedHtml = await new Promise((resolve, reject) => { juice.juiceResources(emailHtml, { webResources: {} }, (err, html) => { - if (err) reject(err); - else resolve(html); + if (err) { + console.error('Error inlining CSS: ', err); + reject(err); + } else { + resolve(html); + } }); }); @@ -404,7 +363,9 @@ export class EmailTemplateService { .join(''); } - private getEventSeverityLabel(eapAlertClassKey: EapAlertClassKeyEnum) { + private getEventSeverityLabel( + eapAlertClassKey: EapAlertClassKeyEnum, + ): string { if (eapAlertClassKey === EapAlertClassKeyEnum.med) { return 'Medium'; } else if (eapAlertClassKey === EapAlertClassKeyEnum.min) { @@ -458,10 +419,14 @@ export class EmailTemplateService { leadTime: event.firstLeadTime.replace('-', ' '), disasterIssuedLabel: event.eapAlertClass.label, color: this.ibfColorToHex(event.eapAlertClass?.color), - advisory: this.getAdvisoryHtml(event.triggerStatusLabel), + advisory: this.getAdvisoryHtml( + event.triggerStatusLabel, + emailContent.country, + emailContent.disasterType, + ), totalAffected: this.getTotalAffectedHtml( event, - emailContent.indicatorMetadata.unit, + emailContent.indicatorMetadata.label.toLowerCase(), ), }; @@ -472,10 +437,19 @@ export class EmailTemplateService { .join(''); } - private getAdvisoryHtml(triggerStatusLabel: TriggerStatusLabelEnum): string { - return triggerStatusLabel === TriggerStatusLabelEnum.Trigger - ? this.readHtmlFile('advisory-trigger.html') - : this.readHtmlFile('advisory-warning.html'); + private getAdvisoryHtml( + triggerStatusLabel: TriggerStatusLabelEnum, + country: CountryEntity, + disasterType: DisasterType, + ): string { + const advisoryHtml = + triggerStatusLabel === TriggerStatusLabelEnum.Trigger + ? this.readHtmlFile('advisory-trigger.html') + : this.readHtmlFile('advisory-warning.html'); + const eapLink = country.countryDisasterSettings.find( + (s) => s.disasterType === disasterType, + ).eapLink; + return ejs.render(advisoryHtml, { eapLink: eapLink }); } private getTotalAffectedHtml( @@ -493,6 +467,12 @@ export class EmailTemplateService { } } + private getFooterHtml(): string { + const footerHtml = this.readHtmlFile('footer.html'); + const ibfLogo = this.getLogoImageAsDataURL(); + return ejs.render(footerHtml, { ibfLogo: ibfLogo }); + } + private ibfColorToHex(color: string): string { // TODO: Define in a place where FrontEnd and Backend can share this switch (color) { @@ -542,6 +522,11 @@ export class EmailTemplateService { return imageDataURL; } + private getLogoImageAsDataURL() { + const filePath = `${emailLogoFolder}/logo-IBF.png`; + return this.getPngImageAsDataURL(filePath); + } + private getPngImageAsDataURL(relativePath: string) { const imageBuffer = fs.readFileSync(relativePath); const imageDataURL = `data:image/png;base64,${imageBuffer.toString( diff --git a/services/API-service/src/api/notification/email/html/advisory-trigger.html b/services/API-service/src/api/notification/email/html/advisory-trigger.html index 568dee3c8..d9e896dda 100644 --- a/services/API-service/src/api/notification/email/html/advisory-trigger.html +++ b/services/API-service/src/api/notification/email/html/advisory-trigger.html @@ -1,4 +1,7 @@ Activate - - Early Action Protocol + Early Action Protocol + diff --git a/services/API-service/src/api/notification/email/html/base.html b/services/API-service/src/api/notification/email/html/base.html index 4980390c6..2771d5b3e 100644 --- a/services/API-service/src/api/notification/email/html/base.html +++ b/services/API-service/src/api/notification/email/html/base.html @@ -62,17 +62,6 @@ width: 220px; } - .notification-action-button { - height: 40px; - width: 200px; - background-color: #6200ee; - color: #ffffff; - font-weight: bold; - border: 0px; - border-radius: 40px; - cursor: pointer; - } - p { margin: 10px 0; padding: 0; diff --git a/services/API-service/src/api/notification/email/html/body-event.html b/services/API-service/src/api/notification/email/html/body-event.html index c5a33daec..df371dfaa 100644 --- a/services/API-service/src/api/notification/email/html/body-event.html +++ b/services/API-service/src/api/notification/email/html/body-event.html @@ -4,7 +4,7 @@ <%= hazard %> <%= eventName %> - <%= disasterIssuedLabel %> + <%= disasterIssuedLabel %>: <%= startDateEventString %>, <%= leadTime %>s from now. @@ -24,7 +24,7 @@
- + This <%= triggerStatusLabel %> was issued by the IBF portal on <%= issuedDate %> (<%= timezone %>) diff --git a/services/API-service/src/api/notification/email/html/event-finished.html b/services/API-service/src/api/notification/email/html/event-finished.html index 12e17e678..188cb295a 100644 --- a/services/API-service/src/api/notification/email/html/event-finished.html +++ b/services/API-service/src/api/notification/email/html/event-finished.html @@ -8,7 +8,7 @@ managed. - + This warning was issued by the IBF portal on <%= issuedDate %> (<%= timezone %>) diff --git a/services/API-service/src/api/notification/email/html/footer.html b/services/API-service/src/api/notification/email/html/footer.html new file mode 100644 index 000000000..615c4ff56 --- /dev/null +++ b/services/API-service/src/api/notification/email/html/footer.html @@ -0,0 +1,20 @@ + + + + + +
+ + +
+ Impact-Based Forecasting Portal (IBF) was co-developed by Netherlands + Red Cross 510 the together with the Uganda Red Cross National Society. + For questions contact us at ibf-support@510.global +
+
diff --git a/services/API-service/src/api/notification/email/html/notification-actions.html b/services/API-service/src/api/notification/email/html/notification-actions.html new file mode 100644 index 000000000..b7a42d92e --- /dev/null +++ b/services/API-service/src/api/notification/email/html/notification-actions.html @@ -0,0 +1,63 @@ + + + + + + + <%- socialMediaPart %> + + + + + +
+ + + + +
+ Go to the IBF-portal +
+
+ + + + +
+ Find more information about the potentially exposed areas, view + the map and manage anticipatory actions. +
+
+ + + + +
+ About trigger +
+
+ + + + +
+ Read about the trigger methodology and the anticipatory actions. +
+
diff --git a/services/API-service/src/api/notification/email/html/styles.ejs b/services/API-service/src/api/notification/email/html/styles.ejs index 8dc314a29..8575726c6 100644 --- a/services/API-service/src/api/notification/email/html/styles.ejs +++ b/services/API-service/src/api/notification/email/html/styles.ejs @@ -13,7 +13,7 @@ .body-text-normal { font-weight: 400; } - .body-text-smaller { + .body-text-16px { font-size: 16px; line-height: 19.2px; } @@ -25,6 +25,13 @@ .body-text-white { color: white; } + .body-text-12px { + font-size: 12px; + line-height: 14.4px; + } + .important-black { + color: black !important; + } /* Icon styles */ .body-text-icon { @@ -32,6 +39,13 @@ height: 14px; } + .ibf-logo { + width: 45px; + height: 45px; + display: block; + padding-right: 10px; + } + /* Text formatting styles */ .body-text-break { display: block; @@ -58,7 +72,6 @@ .table-cell { height: 19px; padding: 2px 10px; - width: 211px; text-align: left; } @@ -80,4 +93,32 @@ .padding-top { padding-top: 10px; } + + /* Notification action button styles */ + .notification-action-button { + height: 40px; + width: 200px; + font-weight: bold; + border: 0px; + border-radius: 40px; + cursor: pointer; + } + .notification-action-button-primary { + background-color: #6200ee; + } + .notification-action-button.notification-action-button-primary a { + color: #ffffff !important; + text-decoration: none !important; + } + .notification-action-button-secondary { + background-color: #ffffff; + } + .notification-action-button.notification-action-button-secondary a { + color: #6200ee !important; + text-decoration: none !important; + } + .button-text-padding { + padding: 12px 18px 12px 18px; + border-radius: 3px; + } diff --git a/services/API-service/src/api/notification/email/html/table-event.html b/services/API-service/src/api/notification/email/html/table-event.html index cad642c59..6072f2057 100644 --- a/services/API-service/src/api/notification/email/html/table-event.html +++ b/services/API-service/src/api/notification/email/html/table-event.html @@ -18,7 +18,7 @@
Expected exposed <%= defaulAdminAreaLabelPlural %><% if (triggerStatusLabel === 'Trigger') { %> in order of <%= @@ -26,15 +26,16 @@
- +
<% if (triggerStatusLabel === 'Trigger') { %> - <% } %> - <%- tableRows %> @@ -42,7 +43,7 @@ <% if (triggerStatusLabel === 'Warning') { %>
Please note: Information regarding exposed population not available for medium warning level. diff --git a/services/API-service/src/api/notification/email/html/table-trigger-row.html b/services/API-service/src/api/notification/email/html/table-trigger-row.html index b2881de88..8f942e29e 100644 --- a/services/API-service/src/api/notification/email/html/table-trigger-row.html +++ b/services/API-service/src/api/notification/email/html/table-trigger-row.html @@ -1,8 +1,8 @@
- - diff --git a/services/API-service/src/api/notification/email/html/table-warning-row.html b/services/API-service/src/api/notification/email/html/table-warning-row.html index a26f8d8ea..b551f1d03 100644 --- a/services/API-service/src/api/notification/email/html/table-warning-row.html +++ b/services/API-service/src/api/notification/email/html/table-warning-row.html @@ -1,5 +1,5 @@ - diff --git a/services/API-service/src/api/notification/email/html/trigger-finished.html b/services/API-service/src/api/notification/email/html/trigger-finished.html index 0fd4f6d22..fbd5ff3ea 100644 --- a/services/API-service/src/api/notification/email/html/trigger-finished.html +++ b/services/API-service/src/api/notification/email/html/trigger-finished.html @@ -86,126 +86,8 @@
Dear Reader,


- <%- eventOverview %> -
+ <%= indicatorLabel %> - <%= defaulAdminAreaLabelSingular %> (<%= defaultAdminAreaLabelParent %>) + + <%= defaulAdminAreaLabelSingular %> (<%= + defaultAdminAreaLabelParent %>)
+ <%= affectectedOfIndicator %> + <%= adminBoundary %> (<%= higherAdminBoundary %>)
+ <%= adminBoundary %> (<%= higherAdminBoundary %>)
- - - - - - - - - - <%- socialMediaPart %> - -
- - - - -
- Go to IBF-portal -
-
- - - - -
- 1. Manage the anticipatory actions - in the IBF-portal for 1 more week. - <%- videoPdfLinks %> -
-
- - - - -
- About trigger -
-
- - - - -
- 2. Read about the trigger - methodology and the anticipatory - actions. -
-
+ <%- eventOverview %> <%- notificationActions %> + <%- footer %>
diff --git a/services/API-service/src/api/notification/email/html/trigger-notification.html b/services/API-service/src/api/notification/email/html/trigger-notification.html index f168c01cb..110ceb0d1 100644 --- a/services/API-service/src/api/notification/email/html/trigger-notification.html +++ b/services/API-service/src/api/notification/email/html/trigger-notification.html @@ -85,114 +85,7 @@
Dear Reader,


- <%- eventListBody %> - - - - - - - <%- socialMediaPart %> - - - - - -
- - - - -
- Go to the IBF-portal -
-
- - - - -
- Find more info on the potentially - exposed areas on a map and manage - anticipatory actions in the - IBF-portal. <%- videoPdfLinks %> -
-
- - - - -
- About trigger -
-
- - - - -
- Read about the trigger methodology - and the anticipatory actions. -
-
+ <%- eventListBody %> <%- notificationActions %>
Trigger Statement: <%- triggerStatement %> @@ -200,7 +93,7 @@ <%= mapImagePart %>

- <%- tablesStacked %> + <%- tablesStacked %> <%- footer %>
diff --git a/services/API-service/src/api/notification/email/logos/logo-IBF.png b/services/API-service/src/api/notification/email/logos/logo-IBF.png new file mode 100644 index 0000000000000000000000000000000000000000..b006cbaca414ca383765a59458ad3b4a401e3849 GIT binary patch literal 2838 zcmV+x3+eQUP)Gzu1K$eeGIpYg-{jY)FZ(7C|)CN)RfNKq8OEV#K5gH9}go(I6T$nl>0Q z*jNaXsx(#tsf~adUwu%iSxPtEF0HoP-Mx2r@7~wUoa28UGxyA$S$4Oa&DryqnKR${ z&iDW4KXVDZ%k<{bug|ZL%9j}Nufe>ElAwoEzmtKV;lwA51c3cu$^fX%01_@W5x9hO zXwkPcQ8uZ9zbtY^GV2`ZCl%sRPg63GnXj z{p%!Q2l3H`$Rj?=K!jO3b)9+CA4qD;+I>OnKJK)c)8@5%oM%IB37G-(6CE$z29oSU zz-YdU?$ZEhe3I@&qRNx>j;Upsh&V8=j6a4 z%RtRLFcPF*Pk5oQ9s+)_aj=gFx?jPI3`$zI!8J06eaT`P7t+ysv=a4wF7w>>JKiF? zPOawa>e69adVISFf>J`YU!<1Vh;guOVusLla8@I_C#(m0!*ScORfyQmz+C;m_I&rj z*`<{7WBAf0wUsI`ANxu(Tz_{fwDwRK8K}aaewc%|4i@xQ%b0-m-{^)9-r4~yTa#C~ z34~=i@4@h~3Hav^2B3JMWM6Io&tKaG*KJz?t@E3qw)08ti)%X`_Dc!F2M3`4JBLu3 zX4BEvzYG^2$D6lO{g8y^>of4s%UvCs&P(ZkoGpHh(?|1{cvH4AabQz3M~w4aB0a$AL-j2Y_4g+_3bck zO*aBIz~_=G`6^s^eG)1Gf)1L1m#Mg!NEH?mP=B~}Py4WP>{1AA!S!isRKT_>U2&=FpZ%gMO9^60Fn4Cn*@% zO4%Y6wAm5WP|1l+jR|=D1y8?}@Ct&CUoQ;%F!(P|OT+~J@iFpes?dL6-0}Y+^5be< zsRgY1c|iQD6wnYbL6;{<`q@A_!0S^>H-*J(n?QI1R&Gcd*q?We!}u`OE>0P9ead!Z zp?g(ljOpTV1xmxE$i*tafnwUHmBm!Ctam}8V<>?GrzO<>wCZWOjKbDeyHl?{w4fh?0j2$0NQ$tm+4x>;I5~-U}~Jei9eLBre~KXM*x9lo%jdkQDsGsNuD);G3--; zIBA&%bQRfFnoquTWH5kjOLL$MOv$XJi^3DboxrSl@CE{(+t~)k5BNCcIowfA z%AfP$?@v!e#;bq>2|VtxQXYM8ivrDTyy}xmDrhAi9W14|EZ;-^tyfTgg>ea$f`I1E znonPF4TrY(a2Wqz67=8p6ycfs#3AqHMQ%`1HB4JO#t z2CxCHj4~Z`*@8|QD$fx3(8d|IW?cQzp&-==wO^3t072|-Af$;lg$8WoL8!REtGsZW z2!1CM3Uo#T_ok6Yl_8BPpq$3L%#@BDs6esk>%AZ?nX{&}pbdDTx3Z9V+!cLsh*pTd zx6CG?%rUEy6(Bb?nHaEA5>O^ZsL~Qll`1gTG&1DFFCHAztE6(Z6z98IRR3%7Phd~x zfj-P)7AEfTMe~bnZ1Tm4=F8UJiR^U?iUVRkJCxUqtp4zokjXGOe|8GKzwI!b>L1fa zIC!!E^AnZ5jqP=KQ zPCUmr`x*jmtdT4BzQ7S4x3AAjyNRI+P z*P6O116!A234tl8u=fPoP0ExEI-XH3RA|}g5Fz|}8kIWk+55D-{7Hp33g^(wc7kB{ zVBn-;DezgvEU}e~k`1GVaqiUPvN7IAeg1V_D6_2BR)RLYd)l!wuMwq!x( zTOrmcn)k>4o#U6S!eeUi5j|`DXPH8led<}p1I?^i1z)d7*e6L4v7VNtW!%)$Rw3*+ ztb-~w7V+p_vG9cuYb}ntZ60iuM1Y0mmdnUFo>;5LLYQ-UFqzuhnBr{nRV+{R5=)eJAl0 z`x%6n0SJE!34hdabELaS|AE!N0qyGFnFw(`P@VSi%t9cz_m7jGd?pRwv>(-F{^SYL zx#R>I!;Q#lbHs9t^*H#juHpqjL$b*)(sqZO0wI38t;3JoS7Hd+i>I3o^yj#75f2&8V;)7o o$APf_;4ck(%BO#G2Hr*bKkY_1nNM3loB#j-07*qoM6N<$g0*UN{Qv*} literal 0 HcmV?d00001 From 9f1a7c64daf66424bf1c43cf5a535ffb736a3ab7 Mon Sep 17 00:00:00 2001 From: Ruben Date: Mon, 13 May 2024 15:43:18 +0200 Subject: [PATCH 12/55] Changed key value array to object --- .../email/email-template.service.ts | 155 +++++------------- 1 file changed, 41 insertions(+), 114 deletions(-) diff --git a/services/API-service/src/api/notification/email/email-template.service.ts b/services/API-service/src/api/notification/email/email-template.service.ts index f373d1dfc..f72a146c1 100644 --- a/services/API-service/src/api/notification/email/email-template.service.ts +++ b/services/API-service/src/api/notification/email/email-template.service.ts @@ -38,7 +38,6 @@ export class EmailTemplateService { return this.formatEmail(replaceKeyValues); } - // TODO REFACTOR this to use a DTO (ContentTriggerFinishedEmail) instead of multiple parameters public async createHtmlForTriggerFinishedEmail( country: CountryEntity, disasterType: DisasterType, @@ -58,64 +57,29 @@ export class EmailTemplateService { private createReplaceKeyValuesTrigger( emailContent: ContentEventEmail, _date: Date, - ): ReplaceKeyValue[] { + ): Record { const country = emailContent.country; const disasterType = emailContent.disasterType; - const keyValueReplaceList = [ - { - replaceKey: 'emailBody', - replaceValue: this.getEmailBody(false), - }, - { - replaceKey: 'headerEventOverview', - replaceValue: this.getHeaderEventStarted(emailContent), - }, - { - replaceKey: 'notificationActions', - replaceValue: this.getNotificationActionsHtml(country, disasterType), - }, - { - replaceKey: 'tablesStacked', - replaceValue: this.getTablesForEvents(emailContent), - }, - { - replaceKey: 'eventListBody', - replaceValue: this.getEventListBody(emailContent), - }, - { - replaceKey: 'imgLogo', - replaceValue: country.notificationInfo.logo[disasterType], - }, - { - replaceKey: 'triggerStatement', - replaceValue: country.notificationInfo.triggerStatement[disasterType], - }, - { - replaceKey: 'mapImagePart', - replaceValue: this.getMapImageHtml(emailContent), - }, - { - replaceKey: 'linkDashboard', - replaceValue: process.env.DASHBOARD_URL, - }, - { - replaceKey: 'socialMediaLink', - replaceValue: country.notificationInfo.linkSocialMediaUrl, - }, - { - replaceKey: 'socialMediaType', - replaceValue: country.notificationInfo.linkSocialMediaType, - }, - { - replaceKey: 'disasterType', - replaceValue: emailContent.disasterTypeLabel, - }, - { - replaceKey: 'footer', - replaceValue: this.getFooterHtml(), - }, - ]; - return keyValueReplaceList; + + const keyValueReplaceObject = { + emailBody: this.getEmailBody(false), + headerEventOverview: this.getHeaderEventStarted(emailContent), + notificationActions: this.getNotificationActionsHtml( + country, + disasterType, + ), + tablesStacked: this.getTablesForEvents(emailContent), + eventListBody: this.getEventListBody(emailContent), + imgLogo: country.notificationInfo.logo[disasterType], + triggerStatement: country.notificationInfo.triggerStatement[disasterType], + mapImagePart: this.getMapImageHtml(emailContent), + linkDashboard: process.env.DASHBOARD_URL, + socialMediaLink: country.notificationInfo.linkSocialMediaUrl, + socialMediaType: country.notificationInfo.linkSocialMediaType, + disasterType: emailContent.disasterTypeLabel, + footer: this.getFooterHtml(), + }; + return keyValueReplaceObject; } private createReplaceKeyValuesTriggerFinished( @@ -123,54 +87,24 @@ export class EmailTemplateService { disasterType: DisasterType, events: EventSummaryCountry[], disasterTypeLabel: string, - ): ReplaceKeyValue[] { - const keyValueReplaceList = [ - { - replaceKey: 'emailBody', - replaceValue: this.getEmailBody(true), - }, - { - replaceKey: 'headerEventOverview', - replaceValue: '', - }, - { - replaceKey: 'eventOverview', - replaceValue: this.geEventsFinishedOverview( - country, - events, - disasterTypeLabel, - ), - }, - { - replaceKey: 'imgLogo', - replaceValue: country.notificationInfo.logo[disasterType], - }, - { - replaceKey: 'linkDashboard', - replaceValue: process.env.DASHBOARD_URL, - }, - { - replaceKey: 'socialMediaPart', - replaceValue: this.getSocialMediaHtml(country), - }, - { - replaceKey: 'socialMediaLink', - replaceValue: country.notificationInfo.linkSocialMediaUrl, - }, - { - replaceKey: 'socialMediaType', - replaceValue: country.notificationInfo.linkSocialMediaType, - }, - { - replaceKey: 'disasterType', - replaceValue: disasterTypeLabel, - }, - { - replaceKey: 'footer', - replaceValue: this.getFooterHtml(), - }, - ]; - return keyValueReplaceList; + ): Record { + const keyValueReplaceObject = { + emailBody: this.getEmailBody(true), + headerEventOverview: '', + eventOverview: this.geEventsFinishedOverview( + country, + events, + disasterTypeLabel, + ), + imgLogo: country.notificationInfo.logo[disasterType], + linkDashboard: process.env.DASHBOARD_URL, + socialMediaPart: this.getSocialMediaHtml(country), + socialMediaLink: country.notificationInfo.linkSocialMediaUrl, + socialMediaType: country.notificationInfo.linkSocialMediaType, + disasterType: disasterTypeLabel, + footer: this.getFooterHtml(), + }; + return keyValueReplaceObject; } private getEmailBody(triggerFinished: boolean): string { @@ -292,19 +226,12 @@ export class EmailTemplateService { } private async formatEmail( - emailKeyValueReplaceList: ReplaceKeyValue[], + emailKeyValueReplaceObject: Record, ): Promise { // TODO REFACTOR: Apply styles in a septerate file also for the base.html const template = this.readHtmlFile('base.html'); const styles = this.readHtmlFile('styles.ejs'); const templateWithStyle = styles + template; - const replacements = emailKeyValueReplaceList.reduce( - (acc, { replaceKey, replaceValue }) => { - acc[replaceKey] = replaceValue; - return acc; - }, - {}, - ); let emailHtml = templateWithStyle; let previousHtml = null; @@ -314,7 +241,7 @@ export class EmailTemplateService { // doesn't render nested tags in one pass. while (emailHtml !== previousHtml) { previousHtml = emailHtml; - emailHtml = ejs.render(previousHtml, replacements); + emailHtml = ejs.render(previousHtml, emailKeyValueReplaceObject); } // Inline the CSS const inlinedHtml = await new Promise((resolve, reject) => { From 42a6d7e22f0289c013b68b005fae3d2d8501a88b Mon Sep 17 00:00:00 2001 From: Ruben Date: Mon, 13 May 2024 17:38:40 +0200 Subject: [PATCH 13/55] AB#27263 First setup api test: uga floods --- services/API-service/jest.api.config.js | 16 ++++ services/API-service/package.json | 2 + .../src/scripts/geoserver-sync.service.ts | 4 + .../src/scripts/mock.controller.ts | 5 ++ .../API-service/src/scripts/mock.service.ts | 2 +- .../src/scripts/scripts.service.ts | 1 + .../test/email/email-uga-floods.test.ts | 39 ++++++++++ .../test/helpers/utility.helper.ts | 73 +++++++++++++++++++ services/API-service/test/tsconfig.json | 8 ++ 9 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 services/API-service/jest.api.config.js create mode 100644 services/API-service/test/email/email-uga-floods.test.ts create mode 100644 services/API-service/test/helpers/utility.helper.ts create mode 100644 services/API-service/test/tsconfig.json diff --git a/services/API-service/jest.api.config.js b/services/API-service/jest.api.config.js new file mode 100644 index 000000000..bdb761c21 --- /dev/null +++ b/services/API-service/jest.api.config.js @@ -0,0 +1,16 @@ +/** + * @type {import('@jest/types').Config.InitialOptions} + */ +module.exports = { + moduleFileExtensions: ['js', 'ts'], + transform: { + '^.+\\.ts?$': ['ts-jest', { tsconfig: '/test/tsconfig.json' }], + }, + rootDir: '.', + testMatch: ['/test/**/*.test.ts'], + coverageReporters: ['json', 'lcov'], + modulePathIgnorePatterns: ['/dist/'], + testTimeout: 30_000, + verbose: true, + reporters: ['default'], +}; diff --git a/services/API-service/package.json b/services/API-service/package.json index 3984b8b7d..9d7baa334 100644 --- a/services/API-service/package.json +++ b/services/API-service/package.json @@ -16,6 +16,8 @@ "test": "jest --config=jest.json --detectOpenHandles --forceExit --passWithNoTests", "test:dev": "npm test -- --watchAll", "test:coverage": "npm test -- --coverage --coverageDirectory=coverage", + "test:api:all": "node --expose-gc node_modules/.bin/jest --config=jest.api.config.js --runInBand --detectOpenHandles --logHeapUsage", + "test:api:watch": "npm run test:api:all -- --watchAll", "typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js", "migration:generate": "npm run typeorm migration:generate -- -d ./appdatasource.ts", "migration:run": "npm run typeorm migration:run -- -d ./appdatasource.ts", diff --git a/services/API-service/src/scripts/geoserver-sync.service.ts b/services/API-service/src/scripts/geoserver-sync.service.ts index ae32d844e..156eaf4ae 100644 --- a/services/API-service/src/scripts/geoserver-sync.service.ts +++ b/services/API-service/src/scripts/geoserver-sync.service.ts @@ -51,6 +51,10 @@ export class GeoserverSyncService { const foundStoreNames = await this.getStoreNamesFromGeoserver( workspaceName, ); + console.log( + '🚀 ~ GeoserverSyncService ~ syncStores ~ foundStoreNames:', + foundStoreNames, + ); const missingStoreNames = expectedStoreNameObjects.filter( (o) => !foundStoreNames.includes(o.resourceName), ); diff --git a/services/API-service/src/scripts/mock.controller.ts b/services/API-service/src/scripts/mock.controller.ts index 090ec78ed..6664bf5f7 100644 --- a/services/API-service/src/scripts/mock.controller.ts +++ b/services/API-service/src/scripts/mock.controller.ts @@ -53,6 +53,11 @@ export class MockBaseScenario { @ApiProperty({ example: new Date() }) @IsOptional() public readonly date: Date; + + // TODO change this to query param + @ApiProperty({ example: false }) + @IsOptional() + public readonly isApiTest: boolean = false; } export class MockFloodsScenario extends MockBaseScenario { diff --git a/services/API-service/src/scripts/mock.service.ts b/services/API-service/src/scripts/mock.service.ts index 4626a6555..8bec4ff7f 100644 --- a/services/API-service/src/scripts/mock.service.ts +++ b/services/API-service/src/scripts/mock.service.ts @@ -234,7 +234,7 @@ export class MockService { // Add the needed stores and layers to geoserver, only do this in debug mode // The resulting XML files should be commited to git and will end up on the servers that way - if (DEBUG) { + if (DEBUG && !mockBody.isApiTest) { await this.geoServerSyncService.sync( selectedCountry.countryCodeISO3, disasterType, diff --git a/services/API-service/src/scripts/scripts.service.ts b/services/API-service/src/scripts/scripts.service.ts index 38c6547d8..ccbafae6a 100644 --- a/services/API-service/src/scripts/scripts.service.ts +++ b/services/API-service/src/scripts/scripts.service.ts @@ -80,6 +80,7 @@ export class ScriptsService { removeEvents: true, date: mockAllInput.date || new Date(), scenario: null, // This is overwritten by useDefaultScenario=true anyway + isApiTest: false, }, disasterType.disasterType, true, diff --git a/services/API-service/test/email/email-uga-floods.test.ts b/services/API-service/test/email/email-uga-floods.test.ts new file mode 100644 index 000000000..590766a70 --- /dev/null +++ b/services/API-service/test/email/email-uga-floods.test.ts @@ -0,0 +1,39 @@ +import { + getAccessToken, + mockFloods, + resetDB, + sendNotification, +} from '../helpers/utility.helper'; +import { FloodsScenario } from '../../src/scripts/enum/mock-scenario.enum'; +import { DisasterType } from '../../src/api/disaster/disaster-type.enum'; + +describe('Should send an email for uga floods', () => { + let accessToken: string; + const countryCodeISO3 = 'UGA'; + + beforeEach(async () => { + accessToken = await getAccessToken(); + await resetDB(accessToken); + }); + + it('send trigger email default floods', async () => { + // Arrange + const mockResult = await mockFloods( + FloodsScenario.Default, + countryCodeISO3, + accessToken, + ); + // Act + const response = await sendNotification( + countryCodeISO3, + DisasterType.Floods, + accessToken, + ); + + // Assert + // Also checking the status of the mockResult here as I think it also breaks often + expect(mockResult.status).toBe(202); + + expect(response.status).toBe(201); + }); +}); diff --git a/services/API-service/test/helpers/utility.helper.ts b/services/API-service/test/helpers/utility.helper.ts new file mode 100644 index 000000000..9e241f836 --- /dev/null +++ b/services/API-service/test/helpers/utility.helper.ts @@ -0,0 +1,73 @@ +import * as request from 'supertest'; +import TestAgent from 'supertest/lib/agent'; +import users from '../../src/scripts/json/users.json'; +import { FloodsScenario } from '../../src/scripts/enum/mock-scenario.enum'; +import { DisasterType } from '../../src/api/disaster/disaster-type.enum'; + +export async function getAccessToken(): Promise { + const admin = users.find((user) => user.userRole === 'admin'); + const login = await loginApi(admin.email, admin.password); + + const accessToken = login.body.user.token; + console.log('🚀 ~ getAccessToken ~ login.body:', login.body); + return accessToken; +} + +export function loginApi( + email: string, + password: string, +): Promise { + return getServer().post(`/user/login`).send({ + email, + password, + }); +} + +export function getHostname(): string { + return 'http://localhost:3000/api'; +} + +export function getServer(): TestAgent { + return request.agent(getHostname()); +} + +export function resetDB(accessToken: string): Promise { + console.log('🚀 ~ resetDB ~ accessToken:', accessToken); + return getServer() + .post('/scripts/reset') + .set('Authorization', `Bearer ${accessToken}`) + .send({ + secret: process.env.RESET_SECRET, + }); +} + +export function mockFloods( + scenario: FloodsScenario, + countryCodeISO3: string, + accessToken: string, +): Promise { + return getServer() + .post('/mock/floods') + .set('Authorization', `Bearer ${accessToken}`) + .send({ + scenario, + secret: process.env.RESET_SECRET, + removeEvents: true, + date: new Date(), + countryCodeISO3, + }); +} + +export function sendNotification( + countryCodeISO3: string, + disasterType: DisasterType, + accessToken: string, +): Promise { + return getServer() + .post('/notification/send') + .set('Authorization', `Bearer ${accessToken}`) + .send({ + countryCodeISO3, + disasterType, + }); +} diff --git a/services/API-service/test/tsconfig.json b/services/API-service/test/tsconfig.json new file mode 100644 index 000000000..c05167595 --- /dev/null +++ b/services/API-service/test/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": ["node", "jest", "supertest"] + }, + "exclude": ["../node_modules", "../dist", "../src/**/*"], + "include": ["**/*.ts"] +} From 74085422544bac620b86b2ca417abd49e4febd9b Mon Sep 17 00:00:00 2001 From: Ruben Date: Thu, 16 May 2024 14:01:54 +0200 Subject: [PATCH 14/55] AB#27263 Test all uganda flood scenarios --- services/API-service/README.md | 4 + .../1710512991479-rename-mock-rasters.ts | 2 - .../dto/notification-api-test-response.dto.ts | 9 ++ .../api/notification/email/email.service.ts | 13 ++- .../notification/email/html/body-event.html | 6 +- .../notification/notification.controller.ts | 23 ++++- .../api/notification/notification.service.ts | 51 ++++++++-- .../src/scripts/geoserver-sync.service.ts | 4 - .../src/scripts/mock.controller.ts | 37 ++++++-- .../API-service/src/scripts/mock.service.ts | 3 +- .../src/scripts/scripts.service.ts | 4 +- .../test/email/email-uga-floods.test.ts | 92 +++++++++++++++---- .../test/helpers/utility.helper.ts | 4 +- services/API-service/tsconfig.json | 4 +- 14 files changed, 209 insertions(+), 47 deletions(-) create mode 100644 services/API-service/src/api/notification/dto/notification-api-test-response.dto.ts diff --git a/services/API-service/README.md b/services/API-service/README.md index 2125ac05b..6a16d5f2d 100644 --- a/services/API-service/README.md +++ b/services/API-service/README.md @@ -177,3 +177,7 @@ For the rest, follow the same instructions as above to receive initial and follo - Endpoint URL: `https://ibf.510.global/api/point-data/community-notification/${countryCodeISO3}` - To test this locally you can replace `ibf.510.global` by a local ngrok address - To demo on other environments, replace by respective environment-url, e.g. `ibf-test.510.global` + +## API tests + +1. Run them by `docker exec ibf-api-service npm run test:api:all` diff --git a/services/API-service/migration/1710512991479-rename-mock-rasters.ts b/services/API-service/migration/1710512991479-rename-mock-rasters.ts index 4ce5f8a52..6237128d9 100644 --- a/services/API-service/migration/1710512991479-rename-mock-rasters.ts +++ b/services/API-service/migration/1710512991479-rename-mock-rasters.ts @@ -13,8 +13,6 @@ export class RenameMockRasters1710512991479 implements MigrationInterface { if (fs.existsSync(directoryPath)) { const files = fs.readdirSync(directoryPath); - console.log('🚀 ~ RenameMockRasters1710512991479 ~ up ~ files:', files); - files.forEach((file) => { if (!file.includes('hour_MWI')) { const newFilename = file.replace( diff --git a/services/API-service/src/api/notification/dto/notification-api-test-response.dto.ts b/services/API-service/src/api/notification/dto/notification-api-test-response.dto.ts new file mode 100644 index 000000000..773d8595f --- /dev/null +++ b/services/API-service/src/api/notification/dto/notification-api-test-response.dto.ts @@ -0,0 +1,9 @@ +export class NotificationApiTestResponseDto { + activeEvents: NotificationApiTestResponseChannelDto; + finishedEvents: NotificationApiTestResponseChannelDto; +} + +export class NotificationApiTestResponseChannelDto { + email: string; + whatsapp: string; +} 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 2149f6773..3cad2dad4 100644 --- a/services/API-service/src/api/notification/email/email.service.ts +++ b/services/API-service/src/api/notification/email/email.service.ts @@ -42,8 +42,9 @@ export class EmailService { country: CountryEntity, disasterType: DisasterType, activeEvents: EventSummaryCountry[], + isApiTest: boolean, date?: Date, - ): Promise { + ): Promise { date = date ? new Date(date) : new Date(); const emailContent = @@ -56,6 +57,9 @@ export class EmailService { emailContent, date, ); + if (isApiTest) { + return emailHtml; + } const emailSubject = `IBF ${( await this.notificationContentService.getDisasterTypeLabel(disasterType) ).toLowerCase()} notification`; @@ -71,8 +75,9 @@ export class EmailService { country: CountryEntity, disasterType: DisasterType, finishedEvents: EventSummaryCountry[], + isApiTest: boolean, date?: Date, - ): Promise { + ): Promise { const disasterTypeLabel = await this.notificationContentService.getDisasterTypeLabel(disasterType); const emailHtml = @@ -83,6 +88,10 @@ export class EmailService { disasterTypeLabel, date ? new Date(date) : new Date(), ); + + if (isApiTest) { + return emailHtml; + } const emailSubject = `IBF ${disasterTypeLabel.toLowerCase()} trigger is now below threshold`; this.sendEmail( emailSubject, diff --git a/services/API-service/src/api/notification/email/html/body-event.html b/services/API-service/src/api/notification/email/html/body-event.html index df371dfaa..76f203826 100644 --- a/services/API-service/src/api/notification/email/html/body-event.html +++ b/services/API-service/src/api/notification/email/html/body-event.html @@ -1,5 +1,9 @@
- + triangleIcon <%= hazard %> <%= eventName %> diff --git a/services/API-service/src/api/notification/notification.controller.ts b/services/API-service/src/api/notification/notification.controller.ts index f894c9b45..e122e5b94 100644 --- a/services/API-service/src/api/notification/notification.controller.ts +++ b/services/API-service/src/api/notification/notification.controller.ts @@ -2,7 +2,9 @@ import { NotificationService } from './notification.service'; import { Body, Controller, + ParseBoolPipe, Post, + Query, UseGuards, UseInterceptors, } from '@nestjs/common'; @@ -10,6 +12,7 @@ import { ApiBearerAuth, ApiConsumes, ApiOperation, + ApiQuery, ApiResponse, ApiTags, } from '@nestjs/swagger'; @@ -17,6 +20,7 @@ import { RolesGuard } from '../../roles.guard'; import { SendNotificationDto } from './dto/send-notification.dto'; import { Roles } from '../../roles.decorator'; import { UserRole } from '../user/user-role.enum'; +import { NotificationApiTestResponseDto } from './dto/notification-api-test-response.dto'; @ApiBearerAuth() @UseGuards(RolesGuard) @@ -38,15 +42,30 @@ export class NotificationController { description: 'Notification request sent (actual e-mails/whatsapps sent only if there is an active event)', }) + @ApiQuery({ + name: 'isApiTest', + required: false, + type: 'boolean', + description: + 'If true, only returns the notification content without sending it', + }) @Post('send') @ApiConsumes() @UseInterceptors() public async send( @Body() sendNotification: SendNotificationDto, - ): Promise { - await this.notificationService.send( + @Query( + 'isApiTest', + new ParseBoolPipe({ + optional: true, + }), + ) + isApiTest: boolean, + ): Promise { + return await this.notificationService.send( sendNotification.countryCodeISO3, sendNotification.disasterType, + isApiTest, sendNotification.date, ); } diff --git a/services/API-service/src/api/notification/notification.service.ts b/services/API-service/src/api/notification/notification.service.ts index 5cdf65f6c..744d58610 100644 --- a/services/API-service/src/api/notification/notification.service.ts +++ b/services/API-service/src/api/notification/notification.service.ts @@ -7,6 +7,10 @@ import { EmailService } from './email/email.service'; import { TyphoonTrackService } from '../typhoon-track/typhoon-track.service'; import { EventSummaryCountry } from '../../shared/data.model'; import { LeadTime } from '../admin-area-dynamic-data/enum/lead-time.enum'; +import { + NotificationApiTestResponseChannelDto, + NotificationApiTestResponseDto, +} from './dto/notification-api-test-response.dto'; @Injectable() export class NotificationService { @@ -21,33 +25,50 @@ export class NotificationService { public async send( countryCodeISO3: string, disasterType: DisasterType, + isApiTest: boolean, date?: Date, - ): Promise { - await this.sendNotiFicationsActiveEvents( + ): Promise { + const apiTestResponse = new NotificationApiTestResponseDto(); + const apiTestReponseActive = await this.sendNotiFicationsActiveEvents( disasterType, countryCodeISO3, + isApiTest, date, ); + if (isApiTest && apiTestReponseActive) { + apiTestResponse.activeEvents = apiTestReponseActive; + } if (disasterType === DisasterType.Floods) { // Sending finished events is now for floods only - await this.sendNotificationsFinishedEvents( + const apiTestReponseFinished = await this.sendNotificationsFinishedEvents( countryCodeISO3, disasterType, + isApiTest, date, ); + if (isApiTest && apiTestReponseFinished) { + apiTestResponse.finishedEvents = apiTestReponseFinished; + } } // REFACTOR: First close finished events. This is ideally done through separate endpoint called at end of pipeline, but that would require all pipelines to be updated. // Instead, making use of this endpoint which is already called at the end of every pipeline await this.eventService.closeEventsAutomatic(countryCodeISO3, disasterType); + + if (isApiTest) { + return apiTestResponse; + } } private async sendNotiFicationsActiveEvents( disasterType: DisasterType, countryCodeISO3: string, + isApiTest: boolean, date?: Date, - ): Promise { + ): Promise { + const apiTestReponseActive = new NotificationApiTestResponseChannelDto(); + const events = await this.eventService.getEventSummary( countryCodeISO3, disasterType, @@ -66,12 +87,16 @@ export class NotificationService { await this.notificationContentService.getCountryNotificationInfo( countryCodeISO3, ); - await this.emailService.sendTriggerEmail( + const messageForApiTest = await this.emailService.sendTriggerEmail( country, disasterType, activeNotifiableEvents, + isApiTest, date, ); + if (isApiTest && messageForApiTest) { + apiTestReponseActive.email = messageForApiTest; + } if (country.notificationInfo.useWhatsapp[disasterType]) { this.whatsappService.sendTriggerWhatsapp( country, @@ -80,13 +105,18 @@ export class NotificationService { ); } } + if (isApiTest) { + return apiTestReponseActive; + } } private async sendNotificationsFinishedEvents( countryCodeISO3: string, disasterType: DisasterType, + isApiTest: boolean, date?: Date, - ): Promise { + ): Promise { + const apiTestReponseFinished = new NotificationApiTestResponseChannelDto(); const finishedNotifiableEvents = await this.eventService.getEventsSummaryTriggerFinishedMail( countryCodeISO3, @@ -99,12 +129,16 @@ export class NotificationService { countryCodeISO3, ); - await this.emailService.sendTriggerFinishedEmail( + const emailFinished = await this.emailService.sendTriggerFinishedEmail( country, disasterType, finishedNotifiableEvents, + isApiTest, date, ); + if (isApiTest && emailFinished) { + apiTestReponseFinished.email = emailFinished; + } if (country.notificationInfo.useWhatsapp[disasterType]) { // TODO: Send one whatsapp message for all closing events @@ -116,6 +150,9 @@ export class NotificationService { ); } } + if (isApiTest) { + return apiTestReponseFinished; + } } } diff --git a/services/API-service/src/scripts/geoserver-sync.service.ts b/services/API-service/src/scripts/geoserver-sync.service.ts index 156eaf4ae..ae32d844e 100644 --- a/services/API-service/src/scripts/geoserver-sync.service.ts +++ b/services/API-service/src/scripts/geoserver-sync.service.ts @@ -51,10 +51,6 @@ export class GeoserverSyncService { const foundStoreNames = await this.getStoreNamesFromGeoserver( workspaceName, ); - console.log( - '🚀 ~ GeoserverSyncService ~ syncStores ~ foundStoreNames:', - foundStoreNames, - ); const missingStoreNames = expectedStoreNameObjects.filter( (o) => !foundStoreNames.includes(o.resourceName), ); diff --git a/services/API-service/src/scripts/mock.controller.ts b/services/API-service/src/scripts/mock.controller.ts index 6664bf5f7..ae4699c61 100644 --- a/services/API-service/src/scripts/mock.controller.ts +++ b/services/API-service/src/scripts/mock.controller.ts @@ -5,6 +5,8 @@ import { Res, HttpStatus, UseGuards, + Query, + ParseBoolPipe, } from '@nestjs/common'; import { ApiBearerAuth, @@ -53,11 +55,6 @@ export class MockBaseScenario { @ApiProperty({ example: new Date() }) @IsOptional() public readonly date: Date; - - // TODO change this to query param - @ApiProperty({ example: false }) - @IsOptional() - public readonly isApiTest: boolean = false; } export class MockFloodsScenario extends MockBaseScenario { @@ -107,6 +104,13 @@ export class MockController { public async mockFloodsScenario( @Body() body: MockFloodsScenario, @Res() res, + @Query( + 'isApiTest', + new ParseBoolPipe({ + optional: true, + }), + ) + isApiTest: boolean, ): Promise { if (body.secret !== process.env.RESET_SECRET) { return res.status(HttpStatus.FORBIDDEN).send('Not allowed'); @@ -115,6 +119,7 @@ export class MockController { body, DisasterType.Floods, false, + isApiTest, ); return res.status(HttpStatus.ACCEPTED).send(result); @@ -132,6 +137,13 @@ export class MockController { public async mockFlashFloodsScenario( @Body() body: MockFlashFloodsScenario, @Res() res, + @Query( + 'isApiTest', + new ParseBoolPipe({ + optional: true, + }), + ) + isApiTest: boolean, ): Promise { if (body.secret !== process.env.RESET_SECRET) { return res.status(HttpStatus.FORBIDDEN).send('Not allowed'); @@ -140,6 +152,7 @@ export class MockController { body, DisasterType.FlashFloods, false, + isApiTest, ); return res.status(HttpStatus.ACCEPTED).send(result); @@ -157,6 +170,13 @@ export class MockController { public async mockEpidemicsScenario( @Body() body: MockEpidemicsScenario, @Res() res, + @Query( + 'isApiTest', + new ParseBoolPipe({ + optional: true, + }), + ) + isApiTest: boolean, ): Promise { if (body.secret !== process.env.RESET_SECRET) { return res.status(HttpStatus.FORBIDDEN).send('Not allowed'); @@ -166,7 +186,12 @@ export class MockController { body.countryCodeISO3 === 'PHL' ? DisasterType.Dengue : DisasterType.Malaria; - const result = await this.mockService.mock(body, disasterType, false); + const result = await this.mockService.mock( + body, + disasterType, + false, + isApiTest, + ); return res.status(HttpStatus.ACCEPTED).send(result); } diff --git a/services/API-service/src/scripts/mock.service.ts b/services/API-service/src/scripts/mock.service.ts index 8bec4ff7f..46f4ffaad 100644 --- a/services/API-service/src/scripts/mock.service.ts +++ b/services/API-service/src/scripts/mock.service.ts @@ -63,6 +63,7 @@ export class MockService { | MockFlashFloodsScenario, disasterType: DisasterType, useDefaultScenario: boolean, + isApiTest: boolean, ) { if (mockBody.removeEvents) { await this.removeEvents(mockBody.countryCodeISO3, disasterType); @@ -234,7 +235,7 @@ export class MockService { // Add the needed stores and layers to geoserver, only do this in debug mode // The resulting XML files should be commited to git and will end up on the servers that way - if (DEBUG && !mockBody.isApiTest) { + if (DEBUG && !isApiTest) { await this.geoServerSyncService.sync( selectedCountry.countryCodeISO3, disasterType, diff --git a/services/API-service/src/scripts/scripts.service.ts b/services/API-service/src/scripts/scripts.service.ts index ccbafae6a..e5a266d6d 100644 --- a/services/API-service/src/scripts/scripts.service.ts +++ b/services/API-service/src/scripts/scripts.service.ts @@ -56,6 +56,8 @@ export class ScriptsService { ) {} public async mockAll(mockAllInput: MockAll) { + const isApiTest = false + const envCountries = process.env.COUNTRIES.split(','); const newMockServiceDisasterTypes = [ @@ -80,10 +82,10 @@ export class ScriptsService { removeEvents: true, date: mockAllInput.date || new Date(), scenario: null, // This is overwritten by useDefaultScenario=true anyway - isApiTest: false, }, disasterType.disasterType, true, + isApiTest, ); } else { await this.mockCountry({ diff --git a/services/API-service/test/email/email-uga-floods.test.ts b/services/API-service/test/email/email-uga-floods.test.ts index 590766a70..f7bd6732e 100644 --- a/services/API-service/test/email/email-uga-floods.test.ts +++ b/services/API-service/test/email/email-uga-floods.test.ts @@ -6,34 +6,92 @@ import { } from '../helpers/utility.helper'; import { FloodsScenario } from '../../src/scripts/enum/mock-scenario.enum'; import { DisasterType } from '../../src/api/disaster/disaster-type.enum'; +import { JSDOM } from 'jsdom'; +import scenarios from '../../src/scripts/mock-data/floods/uga/scenarios.json'; +import disaters from '../../src/scripts/json/disasters.json'; +const disasterType = DisasterType.Floods; +const countryCodeISO3 = 'UGA'; describe('Should send an email for uga floods', () => { let accessToken: string; - const countryCodeISO3 = 'UGA'; beforeEach(async () => { accessToken = await getAccessToken(); await resetDB(accessToken); }); - it('send trigger email default floods', async () => { + it('default', async () => { // Arrange - const mockResult = await mockFloods( - FloodsScenario.Default, - countryCodeISO3, - accessToken, - ); - // Act - const response = await sendNotification( - countryCodeISO3, - DisasterType.Floods, - accessToken, - ); + const scenario = FloodsScenario.Default; + await testFloodScenario(scenario, accessToken); + }); + + it('warning', async () => { + // Arrange + const scenario = FloodsScenario.Warning; + await testFloodScenario(scenario, accessToken); + }); - // Assert - // Also checking the status of the mockResult here as I think it also breaks often - expect(mockResult.status).toBe(202); + it('warning-to-trigger', async () => { + // Arrange + const scenario = FloodsScenario.WarningToTrigger; + await testFloodScenario(scenario, accessToken); + }); - expect(response.status).toBe(201); + it('no-trigger', async () => { + // Arrange + const scenario = FloodsScenario.NoTrigger; + await testFloodScenario(scenario, accessToken); }); }); + +async function testFloodScenario( + scenario: FloodsScenario, + accessToken: string, +): Promise { + const disasterTypeLabel = disaters.find( + (d) => d.disasterType === disasterType, + ).label; + const scenarioSeed = scenarios.find((s) => s.scenarioName === scenario); + const mockResult = await mockFloods(scenario, countryCodeISO3, accessToken); + const eventsSeed = scenarioSeed.events ? scenarioSeed.events : []; + // Act + const response = await sendNotification( + countryCodeISO3, + DisasterType.Floods, + accessToken, + ); + + // Assert + // Also checking the status of the mockResult here as I think it also breaks often + expect(mockResult.status).toBe(202); + expect(response.status).toBe(201); + if (eventsSeed.length > 0) { + expect(response.body.activeEvents.email).toBeDefined(); + } else { + expect(response.body.activeEvents.email).toBeFalsy(); + } + expect(response.body.activeEvents.whatsapp).toBeFalsy(); + expect(response.body.finishedEvents).toBeFalsy(); + + // Parse the HTML content + const dom = new JSDOM(response.body.activeEvents.email); + const document = dom.window.document; + + // Get all span elements with apiTest="eventName" and their lower case text content + const eventNamesInEmail = Array.from( + document.querySelectorAll('span[apiTest="eventName"]'), + (el) => (el as Element).textContent.toLowerCase(), + ); + + expect(eventNamesInEmail.length).toBe(eventsSeed.length); + + // Check if there are elements with the desired text content + for (const event of eventsSeed) { + const eventTitle = `${disasterTypeLabel} ${event.eventName}`.toLowerCase(); + const hasEvent = eventNamesInEmail.some((eventName) => + eventName.includes(eventTitle), + ); + expect(hasEvent).toBe(true); + } +} diff --git a/services/API-service/test/helpers/utility.helper.ts b/services/API-service/test/helpers/utility.helper.ts index 9e241f836..a8e02db84 100644 --- a/services/API-service/test/helpers/utility.helper.ts +++ b/services/API-service/test/helpers/utility.helper.ts @@ -9,7 +9,6 @@ export async function getAccessToken(): Promise { const login = await loginApi(admin.email, admin.password); const accessToken = login.body.user.token; - console.log('🚀 ~ getAccessToken ~ login.body:', login.body); return accessToken; } @@ -32,7 +31,6 @@ export function getServer(): TestAgent { } export function resetDB(accessToken: string): Promise { - console.log('🚀 ~ resetDB ~ accessToken:', accessToken); return getServer() .post('/scripts/reset') .set('Authorization', `Bearer ${accessToken}`) @@ -49,6 +47,7 @@ export function mockFloods( return getServer() .post('/mock/floods') .set('Authorization', `Bearer ${accessToken}`) + .query({ isApiTest: true }) .send({ scenario, secret: process.env.RESET_SECRET, @@ -66,6 +65,7 @@ export function sendNotification( return getServer() .post('/notification/send') .set('Authorization', `Bearer ${accessToken}`) + .query({ isApiTest: true }) .send({ countryCodeISO3, disasterType, diff --git a/services/API-service/tsconfig.json b/services/API-service/tsconfig.json index 91fd4879a..d8166b82c 100644 --- a/services/API-service/tsconfig.json +++ b/services/API-service/tsconfig.json @@ -5,7 +5,7 @@ "noImplicitAny": false, "removeComments": true, "noLib": false, - "lib": ["es2017"], + "lib": ["es2017", "dom"], "emitDecoratorMetadata": true, "experimentalDecorators": true, "target": "es6", @@ -18,7 +18,7 @@ "baseUrl": "./", "skipLibCheck": true }, - "watchOptions": { + "watchOptions": { "watchFile": "fixedPollingInterval", "excludeDirectories": ["node_modules", "dist"], "excludeFiles": ["**/*.json"] From 8b1e0a60c1093e3e917c4c681232d79cfadce247 Mon Sep 17 00:00:00 2001 From: Ruben Date: Thu, 23 May 2024 09:43:08 +0200 Subject: [PATCH 15/55] Api test SSD Floods and PHL typhoon AB#27263 --- .../email/email-template.service.ts | 112 +++++++++++++----- .../html/body-total-affected-warning.html | 2 +- .../notification/email/html/table-event.html | 2 +- .../notification-content.service.ts | 9 +- .../src/scripts/enum/mock-scenario.enum.ts | 9 ++ .../src/scripts/scripts.controller.ts | 10 +- .../src/scripts/scripts.service.ts | 4 +- .../email/floods/email-ssd-floods.test.ts | 36 ++++++ .../email/floods/email-uga-floods.test.ts | 56 +++++++++ .../test-flood-scenario.helper.ts} | 61 +++------- .../email/typhoon/email-phl-typhoon.test.ts | 21 ++++ .../typhoon/test-typhoon-scenario.helper.ts | 53 +++++++++ .../test/helpers/utility.helper.ts | 24 +++- 13 files changed, 304 insertions(+), 95 deletions(-) create mode 100644 services/API-service/test/email/floods/email-ssd-floods.test.ts create mode 100644 services/API-service/test/email/floods/email-uga-floods.test.ts rename services/API-service/test/email/{email-uga-floods.test.ts => floods/test-flood-scenario.helper.ts} (54%) create mode 100644 services/API-service/test/email/typhoon/email-phl-typhoon.test.ts create mode 100644 services/API-service/test/email/typhoon/test-typhoon-scenario.helper.ts diff --git a/services/API-service/src/api/notification/email/email-template.service.ts b/services/API-service/src/api/notification/email/email-template.service.ts index f72a146c1..a0f151f44 100644 --- a/services/API-service/src/api/notification/email/email-template.service.ts +++ b/services/API-service/src/api/notification/email/email-template.service.ts @@ -20,11 +20,6 @@ const emailTemplateFolder = `${emailFolder}/html`; const emailIconFolder = `${emailFolder}/icons`; const emailLogoFolder = `${emailFolder}/logos`; -class ReplaceKeyValue { - replaceKey: string; - replaceValue: string; -} - @Injectable() export class EmailTemplateService { public async createHtmlForTriggerEmail( @@ -275,9 +270,15 @@ export class EmailTemplateService { emailContent.defaultAdminAreaLabel.plural.toLocaleLowerCase(), defaultAdminAreaLabelParent: adminAreaLabelsParent.singular, indicatorLabel: emailContent.indicatorMetadata.label, - triangleIcon: this.getTriangleIcon(event.eapAlertClass?.key), + triangleIcon: this.getTriangleIcon( + event.eapAlertClass?.key, + event.triggerStatusLabel, + ), tableRows: this.getTablesRows(event), - color: this.ibfColorToHex(event.eapAlertClass?.color), + color: this.getIbfHexColor( + event.eapAlertClass?.color, + event.triggerStatusLabel, + ), severityLabel: this.getEventSeverityLabel(event.eapAlertClass?.key), }; @@ -342,10 +343,19 @@ export class EmailTemplateService { indicatorUnit: emailContent.indicatorMetadata.unit, timezone: CountryTimeZoneMapping[emailContent.country.countryCodeISO3], - triangleIcon: this.getTriangleIcon(event.eapAlertClass?.key), + triangleIcon: this.getTriangleIcon( + event.eapAlertClass?.key, + event.triggerStatusLabel, + ), leadTime: event.firstLeadTime.replace('-', ' '), - disasterIssuedLabel: event.eapAlertClass.label, - color: this.ibfColorToHex(event.eapAlertClass?.color), + disasterIssuedLabel: this.getDisasterIssuedLabel( + event.eapAlertClass?.label, + event.triggerStatusLabel, + ), + color: this.getIbfHexColor( + event.eapAlertClass?.color, + event.triggerStatusLabel, + ), advisory: this.getAdvisoryHtml( event.triggerStatusLabel, emailContent.country, @@ -364,6 +374,17 @@ export class EmailTemplateService { .join(''); } + private getDisasterIssuedLabel( + eapLabel: string, + triggerStatusLabel: TriggerStatusLabelEnum, + ) { + if (eapLabel) { + return eapLabel; + } else { + return triggerStatusLabel; + } + } + private getAdvisoryHtml( triggerStatusLabel: TriggerStatusLabelEnum, country: CountryEntity, @@ -383,15 +404,18 @@ export class EmailTemplateService { event: NotificationDataPerEventDto, indicatorUnit: string, ): string { + let html = ''; if (event.triggerStatusLabel === TriggerStatusLabelEnum.Warning) { - return this.readHtmlFile('body-total-affected-warning.html'); + html = this.readHtmlFile('body-total-affected-warning.html'); } else { - let html = this.readHtmlFile('body-total-affected-trigger.html'); - return ejs.render(html, { - totalAffectectedOfIndicator: event.totalAffectectedOfIndicator, - indicatorUnit: indicatorUnit, - }); + + html = this.readHtmlFile('body-total-affected-trigger.html'); } + return ejs.render(html, { + totalAffectectedOfIndicator: event.totalAffectectedOfIndicator, + indicatorUnit: indicatorUnit, + }); + } private getFooterHtml(): string { @@ -400,16 +424,32 @@ export class EmailTemplateService { return ejs.render(footerHtml, { ibfLogo: ibfLogo }); } - private ibfColorToHex(color: string): string { - // TODO: Define in a place where FrontEnd and Backend can share this - switch (color) { - case 'ibf-orange': - return '#aa6009'; - case 'ibf-yellow': - return '#7d6906'; - default: - return '#8a0f32'; + private getIbfHexColor( + color: string, + triggerStatusLabel: TriggerStatusLabelEnum, + ): string { + const ibfOrange = '#aa6009'; + const ibfYellow = '#7d6906'; + const ibfRed = '#8a0f32'; + + // Color defined in the EAP Alert Class. This is only used for flood events + // For other events, the color is defined in the disaster settings + // So we decide it based on the trigger status label + + if (color) { + // TODO: Define in a place where FrontEnd and Backend can share this + switch (color) { + case 'ibf-orange': + return ibfOrange; + case 'ibf-yellow': + return ibfYellow; + default: + return ibfRed; + } } + return triggerStatusLabel === TriggerStatusLabelEnum.Trigger + ? ibfRed + : ibfOrange; } private getCurrentDateTimeString(countryCodeISO3: string): string { @@ -434,15 +474,25 @@ export class EmailTemplateService { return date.toLocaleString('default', options); } - private getTriangleIcon(eapAlertClassKey: EapAlertClassKeyEnum) { + private getTriangleIcon( + eapAlertClassKey: EapAlertClassKeyEnum, + triggerStatusLabel: TriggerStatusLabelEnum, + ) { let fileName = ''; // Still need implement the difference between medium and low warning - if (eapAlertClassKey === EapAlertClassKeyEnum.med) { - fileName = 'warning-medium.png'; - } else if (eapAlertClassKey === EapAlertClassKeyEnum.min) { - fileName = 'warning-low.png'; + if (eapAlertClassKey) { + if (eapAlertClassKey === EapAlertClassKeyEnum.med) { + fileName = 'warning-medium.png'; + } else if (eapAlertClassKey === EapAlertClassKeyEnum.min) { + fileName = 'warning-low.png'; + } else { + fileName = 'trigger.png'; + } } else { - fileName = 'trigger.png'; + fileName = + triggerStatusLabel === TriggerStatusLabelEnum.Trigger + ? 'trigger.png' + : 'warning-medium.png'; } const filePath = `${emailIconFolder}/${fileName}`; const imageDataURL = this.getPngImageAsDataURL(filePath); diff --git a/services/API-service/src/api/notification/email/html/body-total-affected-warning.html b/services/API-service/src/api/notification/email/html/body-total-affected-warning.html index 7ab140eb2..6572e5a8e 100644 --- a/services/API-service/src/api/notification/email/html/body-total-affected-warning.html +++ b/services/API-service/src/api/notification/email/html/body-total-affected-warning.html @@ -1 +1 @@ -Information regarding exposed population not available for warnings
+Information regarding <%= indicatorUnit %> not available for warnings
diff --git a/services/API-service/src/api/notification/email/html/table-event.html b/services/API-service/src/api/notification/email/html/table-event.html index 6072f2057..083b23c3a 100644 --- a/services/API-service/src/api/notification/email/html/table-event.html +++ b/services/API-service/src/api/notification/email/html/table-event.html @@ -45,7 +45,7 @@
- Please note: Information regarding exposed population not available + Please note: Information regarding <%= indicatorLabel %> not available for medium warning level.
<% } %> 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 59c2d335f..8d3fdd45f 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 @@ -1,4 +1,3 @@ -import { AdminAreaDynamicDataService } from '../../admin-area-dynamic-data/admin-area-dynamic-data.service'; import { CountryEntity } from '../../country/country.entity'; import { Injectable } from '@nestjs/common'; import { EventService } from '../../event/event.service'; @@ -246,8 +245,12 @@ export class NotificationContentService { }); } - private getTotalAffectedPerEvent(adminAreas: TriggeredArea[]) { - return adminAreas.reduce((acc, cur) => acc + cur.actionsValue, 0); + private getTotalAffectedPerEvent(adminAreas: TriggeredArea[]): number { + const total = adminAreas.reduce((acc, cur) => acc + cur.actionsValue, 0); + // Round to 2 decimals + if (total) { + return Math.floor(total * 100) / 100; + } } private async getFirstLeadTimeDate( diff --git a/services/API-service/src/scripts/enum/mock-scenario.enum.ts b/services/API-service/src/scripts/enum/mock-scenario.enum.ts index f849dec98..db494e37e 100644 --- a/services/API-service/src/scripts/enum/mock-scenario.enum.ts +++ b/services/API-service/src/scripts/enum/mock-scenario.enum.ts @@ -14,3 +14,12 @@ export enum EpidemicsScenario { Default = 'default', NoTrigger = 'no-trigger', } + +export enum TyphoonScenario { + NoEvent = 'noEvent', + EventNoLandfall = 'eventNoLandfall', + EventNoLandfallYet = 'eventNoLandfallYet', + EventNoTrigger = 'eventNoTrigger', + EventTrigger = 'eventTrigger', + EventAfterLandfall = 'eventAfterLandfall', +} diff --git a/services/API-service/src/scripts/scripts.controller.ts b/services/API-service/src/scripts/scripts.controller.ts index d5b6f694c..b8a6dda0f 100644 --- a/services/API-service/src/scripts/scripts.controller.ts +++ b/services/API-service/src/scripts/scripts.controller.ts @@ -26,6 +26,7 @@ import { RolesGuard } from '../roles.guard'; import { DisasterType } from '../api/disaster/disaster-type.enum'; import { Roles } from '../roles.decorator'; import { UserRole } from '../api/user/user-role.enum'; +import { TyphoonScenario } from './enum/mock-scenario.enum'; class ResetDto { @ApiProperty({ example: 'fill_in_secret' }) @@ -82,15 +83,6 @@ export class MockAll { public readonly date: Date; } -export enum TyphoonScenario { - NoEvent = 'noEvent', - EventNoLandfall = 'eventNoLandfall', - EventNoLandfallYet = 'eventNoLandfallYet', - EventNoTrigger = 'eventNoTrigger', - EventTrigger = 'eventTrigger', - EventAfterLandfall = 'eventAfterLandfall', -} - export class MockTyphoonScenario { @ApiProperty({ example: 'fill_in_secret' }) @IsNotEmpty() diff --git a/services/API-service/src/scripts/scripts.service.ts b/services/API-service/src/scripts/scripts.service.ts index e5a266d6d..c76e9893d 100644 --- a/services/API-service/src/scripts/scripts.service.ts +++ b/services/API-service/src/scripts/scripts.service.ts @@ -6,7 +6,6 @@ import { MockAll, MockDynamic, MockTyphoonScenario, - TyphoonScenario, } from './scripts.controller'; import countries from './json/countries.json'; import fs from 'fs'; @@ -26,6 +25,7 @@ import { AdminAreaDynamicDataEntity } from '../api/admin-area-dynamic-data/admin import { AdminAreaEntity } from '../api/admin-area/admin-area.entity'; import { MockHelperService } from './mock-helper.service'; import { MockService } from './mock.service'; +import { TyphoonScenario } from './enum/mock-scenario.enum'; @Injectable() export class ScriptsService { @@ -56,7 +56,7 @@ export class ScriptsService { ) {} public async mockAll(mockAllInput: MockAll) { - const isApiTest = false + const isApiTest = false; const envCountries = process.env.COUNTRIES.split(','); diff --git a/services/API-service/test/email/floods/email-ssd-floods.test.ts b/services/API-service/test/email/floods/email-ssd-floods.test.ts new file mode 100644 index 000000000..531d61f9a --- /dev/null +++ b/services/API-service/test/email/floods/email-ssd-floods.test.ts @@ -0,0 +1,36 @@ +import { getAccessToken, resetDB } from '../../helpers/utility.helper'; +import { FloodsScenario } from '../../../src/scripts/enum/mock-scenario.enum'; + +import scenarios from '../../../src/scripts/mock-data/floods/ssd/scenarios.json'; + +import { testFloodScenario } from './test-flood-scenario.helper'; + +const countryCodeISO3 = 'SSD'; +describe('Should send an email for ssd floods', () => { + let accessToken: string; + + beforeEach(async () => { + accessToken = await getAccessToken(); + await resetDB(accessToken); + }); + + it('default', async () => { + // Arrange + const scenario = FloodsScenario.Default; + await testFloodScenario(scenario, { + scenarios, + countryCodeISO3, + accessToken, + }); + }); + + it('no-trigger', async () => { + // Arrange + const scenario = FloodsScenario.NoTrigger; + await testFloodScenario(scenario, { + scenarios, + countryCodeISO3, + accessToken, + }); + }); +}); diff --git a/services/API-service/test/email/floods/email-uga-floods.test.ts b/services/API-service/test/email/floods/email-uga-floods.test.ts new file mode 100644 index 000000000..2b1788a8a --- /dev/null +++ b/services/API-service/test/email/floods/email-uga-floods.test.ts @@ -0,0 +1,56 @@ +import { getAccessToken, resetDB } from '../../helpers/utility.helper'; +import { FloodsScenario } from '../../../src/scripts/enum/mock-scenario.enum'; + +import scenarios from '../../../src/scripts/mock-data/floods/uga/scenarios.json'; + +import { testFloodScenario } from './test-flood-scenario.helper'; + +const countryCodeISO3 = 'UGA'; +describe('Should send an email for uga floods', () => { + let accessToken: string; + + beforeEach(async () => { + accessToken = await getAccessToken(); + await resetDB(accessToken); + }); + + it('default', async () => { + // Arrange + const scenario = FloodsScenario.Default; + await testFloodScenario(scenario, { + scenarios, + countryCodeISO3, + accessToken, + }); + }); + + it('warning', async () => { + // Arrange + const scenario = FloodsScenario.Warning; + await testFloodScenario(scenario, { + scenarios, + countryCodeISO3, + accessToken, + }); + }); + + it('warning-to-trigger', async () => { + // Arrange + const scenario = FloodsScenario.WarningToTrigger; + await testFloodScenario(scenario, { + scenarios, + countryCodeISO3, + accessToken, + }); + }); + + it('no-trigger', async () => { + // Arrange + const scenario = FloodsScenario.NoTrigger; + await testFloodScenario(scenario, { + scenarios, + countryCodeISO3, + accessToken, + }); + }); +}); diff --git a/services/API-service/test/email/email-uga-floods.test.ts b/services/API-service/test/email/floods/test-flood-scenario.helper.ts similarity index 54% rename from services/API-service/test/email/email-uga-floods.test.ts rename to services/API-service/test/email/floods/test-flood-scenario.helper.ts index f7bd6732e..91d7746d7 100644 --- a/services/API-service/test/email/email-uga-floods.test.ts +++ b/services/API-service/test/email/floods/test-flood-scenario.helper.ts @@ -1,55 +1,22 @@ -import { - getAccessToken, - mockFloods, - resetDB, - sendNotification, -} from '../helpers/utility.helper'; -import { FloodsScenario } from '../../src/scripts/enum/mock-scenario.enum'; -import { DisasterType } from '../../src/api/disaster/disaster-type.enum'; +import { DisasterType } from '../../../src/api/disaster/disaster-type.enum'; +import { FloodsScenario } from '../../../src/scripts/enum/mock-scenario.enum'; +import { mockFloods, sendNotification } from '../../helpers/utility.helper'; +import disasters from '../../../src/scripts/json/disasters.json'; import { JSDOM } from 'jsdom'; -import scenarios from '../../src/scripts/mock-data/floods/uga/scenarios.json'; -import disaters from '../../src/scripts/json/disasters.json'; -const disasterType = DisasterType.Floods; -const countryCodeISO3 = 'UGA'; -describe('Should send an email for uga floods', () => { - let accessToken: string; - - beforeEach(async () => { - accessToken = await getAccessToken(); - await resetDB(accessToken); - }); - - it('default', async () => { - // Arrange - const scenario = FloodsScenario.Default; - await testFloodScenario(scenario, accessToken); - }); - - it('warning', async () => { - // Arrange - const scenario = FloodsScenario.Warning; - await testFloodScenario(scenario, accessToken); - }); - - it('warning-to-trigger', async () => { - // Arrange - const scenario = FloodsScenario.WarningToTrigger; - await testFloodScenario(scenario, accessToken); - }); - - it('no-trigger', async () => { - // Arrange - const scenario = FloodsScenario.NoTrigger; - await testFloodScenario(scenario, accessToken); - }); -}); +export interface TestFloodScenarioDto { + scenarios: any[]; + countryCodeISO3: string; + accessToken: string; +} -async function testFloodScenario( +export async function testFloodScenario( scenario: FloodsScenario, - accessToken: string, + params: TestFloodScenarioDto, ): Promise { - const disasterTypeLabel = disaters.find( + const { scenarios, countryCodeISO3, accessToken } = params; + const disasterType = DisasterType.Floods; + const disasterTypeLabel = disasters.find( (d) => d.disasterType === disasterType, ).label; const scenarioSeed = scenarios.find((s) => s.scenarioName === scenario); diff --git a/services/API-service/test/email/typhoon/email-phl-typhoon.test.ts b/services/API-service/test/email/typhoon/email-phl-typhoon.test.ts new file mode 100644 index 000000000..b43595c5e --- /dev/null +++ b/services/API-service/test/email/typhoon/email-phl-typhoon.test.ts @@ -0,0 +1,21 @@ +import { TyphoonScenario } from '../../../src/scripts/enum/mock-scenario.enum'; +import { getAccessToken, resetDB } from '../../helpers/utility.helper'; +import { testTyphoonScenario } from './test-typhoon-scenario.helper'; + +const countryCodeISO3 = 'PHL'; +describe('Should send an email for phl typhoon', () => { + let accessToken: string; + + beforeEach(async () => { + accessToken = await getAccessToken(); + await resetDB(accessToken); + }); + + it('default', async () => { + await testTyphoonScenario( + TyphoonScenario.EventTrigger, + countryCodeISO3, + accessToken, + ); + }); +}); diff --git a/services/API-service/test/email/typhoon/test-typhoon-scenario.helper.ts b/services/API-service/test/email/typhoon/test-typhoon-scenario.helper.ts new file mode 100644 index 000000000..38fbb23d4 --- /dev/null +++ b/services/API-service/test/email/typhoon/test-typhoon-scenario.helper.ts @@ -0,0 +1,53 @@ +import { DisasterType } from '../../../src/api/disaster/disaster-type.enum'; +import { TyphoonScenario } from '../../../src/scripts/enum/mock-scenario.enum'; +import { mockTyphoon, sendNotification } from '../../helpers/utility.helper'; +import { JSDOM } from 'jsdom'; + +export async function testTyphoonScenario( + scenario: TyphoonScenario, + countryCodeISO3: string, + accessToken: string, +): Promise { + const nrOfEvents = 2; + const eventName = 'Mock typhoon' + const disasterTypeLabel = DisasterType.Typhoon + + // const disasterType = DisasterType.Typhoon; + // const disasterTypeLabel = disasters.find( + // (d) => d.disasterType === disasterType, + // ).label; + const mockResult = await mockTyphoon(scenario, countryCodeISO3, accessToken); + // Act + const response = await sendNotification( + countryCodeISO3, + DisasterType.Typhoon, + accessToken, + ); + // Assert + // Also checking the status of the mockResult here as I think it also breaks often + expect(mockResult.status).toBe(202); + expect(response.status).toBe(201); + expect(response.body.activeEvents.email).toBeDefined(); + + expect(response.body.activeEvents.whatsapp).toBeFalsy(); + expect(response.body.finishedEvents).toBeFalsy(); + + // Parse the HTML content + const dom = new JSDOM(response.body.activeEvents.email); + const document = dom.window.document; + + // Get all span elements with apiTest="eventName" and their lower case text content + const eventNamesInEmail = Array.from( + document.querySelectorAll('span[apiTest="eventName"]'), + (el) => (el as Element).textContent.toLowerCase(), + ); + + expect(eventNamesInEmail.length).toBe(nrOfEvents); + + // Check if there are elements with the desired text content + for (const eventNameInEmail of eventNamesInEmail) { + const eventTitle = `${disasterTypeLabel} ${eventName}`.toLowerCase(); + const hasEvent = eventNameInEmail.includes(eventTitle); + expect(hasEvent).toBe(true); + } +} diff --git a/services/API-service/test/helpers/utility.helper.ts b/services/API-service/test/helpers/utility.helper.ts index a8e02db84..7632558e4 100644 --- a/services/API-service/test/helpers/utility.helper.ts +++ b/services/API-service/test/helpers/utility.helper.ts @@ -1,7 +1,10 @@ import * as request from 'supertest'; import TestAgent from 'supertest/lib/agent'; import users from '../../src/scripts/json/users.json'; -import { FloodsScenario } from '../../src/scripts/enum/mock-scenario.enum'; +import { + FloodsScenario, + TyphoonScenario, +} from '../../src/scripts/enum/mock-scenario.enum'; import { DisasterType } from '../../src/api/disaster/disaster-type.enum'; export async function getAccessToken(): Promise { @@ -57,6 +60,25 @@ export function mockFloods( }); } +export function mockTyphoon( + scenario: TyphoonScenario, + countryCodeISO3: string, + accessToken: string, +): Promise { + return getServer() + .post('/scripts/mock-typhoon-scenario') + .set('Authorization', `Bearer ${accessToken}`) + .query({ isApiTest: true }) + .send({ + scenario, + eventNr: 1, + secret: process.env.RESET_SECRET, + removeEvents: true, + date: new Date(), + countryCodeISO3, + }); +} + export function sendNotification( countryCodeISO3: string, disasterType: DisasterType, From db589168c3da29137f539da6ceeab122b5b832f1 Mon Sep 17 00:00:00 2001 From: Ruben Date: Fri, 12 Apr 2024 16:48:28 +0200 Subject: [PATCH 16/55] WIP: Email multi event AB#27182 --- services/API-service/.gitignore | 1 - services/API-service/package-lock.json | 233 ++++++- services/API-service/package.json | 1 + .../api/country/country-time-zone-mapping.ts | 11 + .../src/api/event/event-map-image.entity.ts | 2 +- .../dto/admin-area-notification-info.dto.ts | 4 + .../dto/content-trigger-email.dto.ts | 16 + .../dto/notification-date-per-event.dto.ts | 26 + .../email/email-template.service.ts | 527 ++++++++++++++ .../api/notification/email/email.service.ts | 657 +----------------- .../src/api/notification/email/html/base.html | 8 +- .../email/html/email-body-trigger-event.html | 132 ++++ .../email/html/email-body-warning-event.html | 120 ++++ .../email/html/email-table-event.html | 96 +++ .../email/html/email-table-trigger-row.html | 36 + .../email/html/email-table-warning-row.html | 22 + .../email/html/header-event-overview.html | 2 +- .../notification/email/html/map-image.html | 8 +- .../email/html/social-media-link.html | 4 +- .../email/html/trigger-finished.html | 10 +- .../email/html/trigger-notification.html | 30 +- .../api/notification/email/icons/trigger.png | Bin 0 -> 760 bytes .../notification/email/icons/warning-low.png | Bin 0 -> 770 bytes .../email/icons/warning-medium.png | Bin 0 -> 785 bytes .../format-action-unit-value.helper.ts | 16 + .../notification-content.module.ts | 1 + .../notification-content.service.ts | 338 +++++---- .../notification/notification.controller.ts | 2 +- .../api/notification/notification.module.ts | 3 +- .../notification/whatsapp/whatsapp.service.ts | 11 +- 30 files changed, 1462 insertions(+), 855 deletions(-) create mode 100644 services/API-service/src/api/country/country-time-zone-mapping.ts create mode 100644 services/API-service/src/api/notification/dto/admin-area-notification-info.dto.ts create mode 100644 services/API-service/src/api/notification/dto/content-trigger-email.dto.ts create mode 100644 services/API-service/src/api/notification/dto/notification-date-per-event.dto.ts create mode 100644 services/API-service/src/api/notification/email/email-template.service.ts create mode 100644 services/API-service/src/api/notification/email/html/email-body-trigger-event.html create mode 100644 services/API-service/src/api/notification/email/html/email-body-warning-event.html create mode 100644 services/API-service/src/api/notification/email/html/email-table-event.html create mode 100644 services/API-service/src/api/notification/email/html/email-table-trigger-row.html create mode 100644 services/API-service/src/api/notification/email/html/email-table-warning-row.html create mode 100644 services/API-service/src/api/notification/email/icons/trigger.png create mode 100644 services/API-service/src/api/notification/email/icons/warning-low.png create mode 100644 services/API-service/src/api/notification/email/icons/warning-medium.png create mode 100644 services/API-service/src/api/notification/helpers/format-action-unit-value.helper.ts diff --git a/services/API-service/.gitignore b/services/API-service/.gitignore index 3aa5681ba..2747035a3 100644 --- a/services/API-service/.gitignore +++ b/services/API-service/.gitignore @@ -1,5 +1,4 @@ # IDE / Editors -.vscode .idea *.sublime-project *.sublime-workspace diff --git a/services/API-service/package-lock.json b/services/API-service/package-lock.json index 1cb8720ab..d59cf5c49 100644 --- a/services/API-service/package-lock.json +++ b/services/API-service/package-lock.json @@ -19,6 +19,7 @@ "class-transformer": "^0.3.1", "class-validator": "^0.14.0", "csv-parser": "^3.0.0", + "ejs": "^3.1.9", "jsonwebtoken": "^8.1.1", "mailchimp-api-v3": "^1.15.0", "mysql": "^2.15.0", @@ -3040,6 +3041,11 @@ "node": ">=4" } }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, "node_modules/async-limiter": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", @@ -3450,7 +3456,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3956,8 +3961,7 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "node_modules/concat-stream": { "version": "1.6.2", @@ -4569,6 +4573,20 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "node_modules/ejs": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", + "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -5649,6 +5667,33 @@ "dev": true, "optional": true }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -6852,6 +6897,87 @@ "node": ">=6" } }, + "node_modules/jake": { + "version": "10.8.7", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", + "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jake/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jake/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jake/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jake/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jake/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jest": { "version": "24.9.0", "resolved": "https://registry.npmjs.org/jest/-/jest-24.9.0.tgz", @@ -9798,7 +9924,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -16121,6 +16246,11 @@ "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", "dev": true }, + "async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, "async-limiter": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", @@ -16462,7 +16592,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -16872,8 +17001,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "concat-stream": { "version": "1.6.2", @@ -17358,6 +17486,14 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "ejs": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", + "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", + "requires": { + "jake": "^10.8.5" + } + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -18179,6 +18315,32 @@ "dev": true, "optional": true }, + "filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "requires": { + "minimatch": "^5.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -19122,6 +19284,62 @@ "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==" }, + "jake": { + "version": "10.8.7", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", + "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", + "requires": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "jest": { "version": "24.9.0", "resolved": "https://registry.npmjs.org/jest/-/jest-24.9.0.tgz", @@ -21622,7 +21840,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "requires": { "brace-expansion": "^1.1.7" } diff --git a/services/API-service/package.json b/services/API-service/package.json index d85d4cb1b..9b6217601 100644 --- a/services/API-service/package.json +++ b/services/API-service/package.json @@ -37,6 +37,7 @@ "class-transformer": "^0.3.1", "class-validator": "^0.14.0", "csv-parser": "^3.0.0", + "ejs": "^3.1.9", "jsonwebtoken": "^8.1.1", "mailchimp-api-v3": "^1.15.0", "mysql": "^2.15.0", diff --git a/services/API-service/src/api/country/country-time-zone-mapping.ts b/services/API-service/src/api/country/country-time-zone-mapping.ts new file mode 100644 index 000000000..67cf4657c --- /dev/null +++ b/services/API-service/src/api/country/country-time-zone-mapping.ts @@ -0,0 +1,11 @@ +export const CountryTimeZoneMapping: { [key: string]: string } = { + UGA: 'Africa/Kampala', + KEN: 'Africa/Nairobi', + ETH: 'Africa/Addis_Ababa', + ZMB: 'Africa/Lusaka', + MWI: 'Africa/Blantyre', + ZWE: 'Africa/Harare', + EGY: 'Africa/Cairo', + PHL: 'Asia/Manila', + SSD: 'Africa/Juba', +}; diff --git a/services/API-service/src/api/event/event-map-image.entity.ts b/services/API-service/src/api/event/event-map-image.entity.ts index 9cc6b9a15..36c0f1587 100644 --- a/services/API-service/src/api/event/event-map-image.entity.ts +++ b/services/API-service/src/api/event/event-map-image.entity.ts @@ -16,7 +16,7 @@ export class EventMapImageEntity { public id: string; @Column({ type: 'bytea' }) - public image: any; + public image: Buffer; @ApiProperty({ example: 'SSD' }) @ManyToOne((): typeof CountryEntity => CountryEntity) diff --git a/services/API-service/src/api/notification/dto/admin-area-notification-info.dto.ts b/services/API-service/src/api/notification/dto/admin-area-notification-info.dto.ts new file mode 100644 index 000000000..c5afda2a9 --- /dev/null +++ b/services/API-service/src/api/notification/dto/admin-area-notification-info.dto.ts @@ -0,0 +1,4 @@ +export class AdminAreaLabel { + singular: string; + plural: string; +} diff --git a/services/API-service/src/api/notification/dto/content-trigger-email.dto.ts b/services/API-service/src/api/notification/dto/content-trigger-email.dto.ts new file mode 100644 index 000000000..e5bda8d48 --- /dev/null +++ b/services/API-service/src/api/notification/dto/content-trigger-email.dto.ts @@ -0,0 +1,16 @@ +import { CountryEntity } from '../../country/country.entity'; +import { DisasterType } from '../../disaster/disaster-type.enum'; +import { IndicatorMetadataEntity } from '../../metadata/indicator-metadata.entity'; +import { AdminAreaLabel } from './admin-area-notification-info.dto'; +import { NotificationDataPerEventDto } from './notification-date-per-event.dto'; + +export class ContentTriggerEmail { + public disasterType: DisasterType; + public disasterTypeLabel: string; + public indicatorMetadata: IndicatorMetadataEntity; + public dataPerEvent: NotificationDataPerEventDto[]; + public mapImageData: any[]; + public defaultAdminLevel: number; + public defaultAdminAreaLabel: AdminAreaLabel; + public country: CountryEntity; // Ensure that is has the following relations 'disasterTypes', 'notificationInfo','countryDisasterSettings','countryDisasterSettings.activeLeadTimes', +} 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 new file mode 100644 index 000000000..96baf5862 --- /dev/null +++ b/services/API-service/src/api/notification/dto/notification-date-per-event.dto.ts @@ -0,0 +1,26 @@ +import { 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; + firstLeadTime: LeadTime; + triggeredAreas: TriggeredArea[]; + nrOfTriggeredAreas: number; + startDateEventString: string; + totalAffectectedOfIndicator: number; + mapImage?: Buffer; +} + +export enum TriggerStatusLabelEnum { + Trigger = 'Trigger', + Warning = 'Warning', +} + +export class DisasterSpecificCopy { + eventStatus: string; + extraInfo: string; + leadTimeString?: string; + timestamp?: string; +} diff --git a/services/API-service/src/api/notification/email/email-template.service.ts b/services/API-service/src/api/notification/email/email-template.service.ts new file mode 100644 index 000000000..103489e2b --- /dev/null +++ b/services/API-service/src/api/notification/email/email-template.service.ts @@ -0,0 +1,527 @@ +import { Injectable } from '@nestjs/common'; +import { ContentTriggerEmail } from '../dto/content-trigger-email.dto'; +import { LeadTime } from '../../admin-area-dynamic-data/enum/lead-time.enum'; +import { + NotificationDataPerEventDto, + TriggerStatusLabelEnum, +} from '../dto/notification-date-per-event.dto'; +import * as ejs from 'ejs'; +import * as fs from 'fs'; +import { CountryTimeZoneMapping } from '../../country/country-time-zone-mapping'; +import { DisasterType } from '../../disaster/disaster-type.enum'; +import { EventSummaryCountry } from '../../../shared/data.model'; +import { CountryEntity } from '../../country/country.entity'; + +const emailFolder = './src/api/notification/email'; +const emailTemplateFolder = `${emailFolder}/html`; +const emailIconFolder = `${emailFolder}/icons`; + +class ReplaceKeyValue { + replaceKey: string; + replaceValue: string; +} + +@Injectable() +export class EmailTemplateService { + private placeholderToday = '(TODAY)'; + + public createHtmlForTriggerEmail( + contentForEmail: ContentTriggerEmail, + date: Date, + ): string { + const replaceKeyValues = this.createReplaceKeyValuesTrigger( + contentForEmail, + date, + ); + return this.formatEmail(replaceKeyValues); + } + + // TODO REFACTOR this to use a DTO (ContentTriggerFinishedEmail) instead of multiple parameters + public createHtmlForTriggerFinishedEmail( + country: CountryEntity, + disasterType: DisasterType, + finishedEvent: EventSummaryCountry, + disasterTypeLabel: string, + date: Date, + ): string { + const replaceKeyValues = this.createReplaceKeyValuesTriggerFinished( + country, + disasterType, + finishedEvent, + disasterTypeLabel, + date, + ); + return this.formatEmail(replaceKeyValues); + } + + private createReplaceKeyValuesTrigger( + contentForEmail: ContentTriggerEmail, + date: Date, + ): ReplaceKeyValue[] { + const country = contentForEmail.country; + const disasterType = contentForEmail.disasterType; + const keyValueReplaceList = [ + { + replaceKey: 'emailBody', + replaceValue: this.getEmailBody(false), + }, + { + replaceKey: 'headerEventOverview', + replaceValue: this.getHeaderEventOverview(contentForEmail.dataPerEvent), + }, + { + replaceKey: 'socialMediaPart', + replaceValue: this.getSocialMediaHtml(contentForEmail.country), + }, + { + replaceKey: 'tablesStacked', + replaceValue: this.getTablesForEvents(contentForEmail), + }, + { + replaceKey: this.placeholderToday, + replaceValue: date.toLocaleDateString('default', { + day: '2-digit', + month: 'short', + year: 'numeric', + }), + }, + { + replaceKey: 'eventListBody', + replaceValue: this.getEventListBody(contentForEmail), + }, + { + replaceKey: 'imgLogo', + replaceValue: country.notificationInfo.logo[disasterType], + }, + { + replaceKey: 'triggerStatement', + replaceValue: country.notificationInfo.triggerStatement[disasterType], + }, + { + replaceKey: 'mapImagePart', + replaceValue: this.getMapImageHtml(contentForEmail), + }, + { + replaceKey: 'linkDashboard', + replaceValue: process.env.DASHBOARD_URL, + }, + { + replaceKey: 'linkEapSop', + replaceValue: country.countryDisasterSettings.find( + (s) => s.disasterType === disasterType, + ).eapLink, + }, + { + replaceKey: 'socialMediaLink', + replaceValue: country.notificationInfo.linkSocialMediaUrl, + }, + { + replaceKey: 'socialMediaType', + replaceValue: country.notificationInfo.linkSocialMediaType, + }, + { + replaceKey: 'disasterType', + replaceValue: contentForEmail.disasterTypeLabel, + }, + { + replaceKey: 'videoPdfLinks', + replaceValue: this.getVideoPdfLinks( + country.notificationInfo.linkVideo, + country.notificationInfo.linkPdf, + ), + }, + ]; + return keyValueReplaceList; + } + + private createReplaceKeyValuesTriggerFinished( + country: CountryEntity, + disasterType: DisasterType, + event: EventSummaryCountry, + disasterTypeLabel: string, + date: Date, + ): ReplaceKeyValue[] { + const keyValueReplaceList = [ + { + replaceKey: 'emailBody', + replaceValue: this.getEmailBody(true), + }, + { + replaceKey: 'headerEventOverview', + replaceValue: '', + }, + { + replaceKey: 'imgLogo', + replaceValue: country.notificationInfo.logo[disasterType], + }, + { + replaceKey: 'startDate', + replaceValue: event.startDate, + }, + { + replaceKey: 'linkDashboard', + replaceValue: process.env.DASHBOARD_URL, + }, + { + replaceKey: 'socialMediaPart', + replaceValue: this.getSocialMediaHtml(country), + }, + { + replaceKey: 'linkEapSop', + replaceValue: country.countryDisasterSettings.find( + (s) => s.disasterType === disasterType, + ).eapLink, + }, + { + replaceKey: 'socialMediaLink', + replaceValue: country.notificationInfo.linkSocialMediaUrl, + }, + { + replaceKey: 'socialMediaType', + replaceValue: country.notificationInfo.linkSocialMediaType, + }, + { + replaceKey: 'videoPdfLinks', + replaceValue: this.getVideoPdfLinks( + country.notificationInfo.linkVideo, + country.notificationInfo.linkPdf, + ), + }, + { + replaceKey: 'disasterType', + replaceValue: disasterTypeLabel, + }, + { + replaceKey: this.placeholderToday, + replaceValue: date.toLocaleDateString('default', { + day: '2-digit', + month: 'short', + year: 'numeric', + }), + }, + ]; + return keyValueReplaceList; + } + + private getEmailBody(triggerFinished: boolean): string { + if (triggerFinished) { + return fs.readFileSync( + './src/api/notification/email/html/trigger-finished.html', + 'utf8', + ); + } else { + return fs.readFileSync( + './src/api/notification/email/html/trigger-notification.html', + 'utf8', + ); + } + } + + private getHeaderEventOverview( + eventsData: NotificationDataPerEventDto[], + ): string { + const leadTimeListShort = this.getEventListShort(eventsData); + let headerEventOverview = fs.readFileSync( + './src/api/notification/email/html/header-event-overview.html', + 'utf8', + ); + headerEventOverview = ejs.render(headerEventOverview, { + eventListHeader: leadTimeListShort, + }); + return headerEventOverview; + } + + private getVideoPdfLinks(videoLink: string, pdfLink: string) { + // TODO: Use ejs template + const linkVideoHTML = ` + video`; + + const linkPdfHTML = `PDF`; + let videoStr = ''; + if (videoLink) { + videoStr = ' ' + linkVideoHTML; + } + let pdfStr = ''; + if (pdfLink) { + pdfStr = ' ' + linkPdfHTML; + } + let orStr = ''; + if (videoStr && pdfStr) { + orStr = ' or'; + } + if (videoStr || pdfStr) { + return `See instructions for the IBF-portal in${videoStr}${orStr}${pdfStr}.`; + } + } + + private getSocialMediaHtml(country: CountryEntity): string { + if (country.notificationInfo.linkSocialMediaType) { + return fs.readFileSync( + './src/api/notification/email/html/social-media-link.html', + 'utf8', + ); + } else { + return ''; + } + } + + private getMapImageHtml(contentForEmail: ContentTriggerEmail): string { + let html = ''; + for (const event of contentForEmail.dataPerEvent) { + const mapImage = event.mapImage; + if (mapImage) { + let eventHtml = fs.readFileSync( + './src/api/notification/email/html/map-image.html', + 'utf8', + ); + const replacements = { + mapImgSrc: this.getMapImgSrc( + contentForEmail.country.countryCodeISO3, + contentForEmail.disasterType, + event.eventName, + ), + mapImgDescription: this.getMapImageDescription( + contentForEmail.disasterType, + ), + eventName: event.eventName ? ` for '${event.eventName}'` : '', + }; + eventHtml = ejs.render(eventHtml, replacements); + html += eventHtml; + } + } + return html; + } + + private getMapImgSrc( + countryCodeISO3: string, + disasterType: DisasterType, + eventName: string, + ): string { + const src = `${ + process.env.NG_API_URL + }/event/event-map-image/${countryCodeISO3}/${disasterType}/${ + eventName || 'no-name' + }`; + + return src; + } + + private getMapImageDescription(disasterType: DisasterType): string { + switch (disasterType) { + case DisasterType.Floods: + return 'The triggered areas are outlined in purple. The potential flood extent is shown in red.
'; + default: + return ''; + } + } + + private formatEmail(emailKeyValueReplaceList: ReplaceKeyValue[]): string { + let template = fs.readFileSync( + './src/api/notification/email/html/base.html', + 'utf8', + ); + const replacements = emailKeyValueReplaceList.reduce( + (acc, { replaceKey, replaceValue }) => { + acc[replaceKey] = replaceValue; + return acc; + }, + {}, + ); + + let emailHtml = template; + let previousHtml = null; + + // This loop is needed to handle nested EJS tags. It repeatedly renders the template + // until there are no more EJS tags left to render. This is necessary because EJS + // doesn't render nested tags in one pass. + while (emailHtml !== previousHtml) { + previousHtml = emailHtml; + emailHtml = ejs.render(previousHtml, replacements); + } + + return emailHtml; + } + + // TODO refactor this to use ejs package to render the html + private getEventListShort( + dataPerEvent: NotificationDataPerEventDto[], + ): string { + let text = ''; + for (const event of dataPerEvent) { + const leadTimeString = event.disasterSpecificCopy.leadTimeString + ? event.disasterSpecificCopy.leadTimeString + : event.firstLeadTime; + const timestamp = event.disasterSpecificCopy.timestamp + ? ` at ${event.disasterSpecificCopy.timestamp}` + : ''; + text += `${event.triggerStatusLabel} for ${event.eventName}: ${ + event.disasterSpecificCopy.extraInfo || + event.firstLeadTime === LeadTime.hour0 + ? leadTimeString + : `${event.firstLeadTime}${timestamp}` + }
`; + } + return text; + } + + private getTablesForEvents(emailContent: ContentTriggerEmail): string { + const adminAreaLabelsParent = + emailContent.country.adminRegionLabels[ + String(emailContent.defaultAdminLevel - 1) + ]; + + return emailContent.dataPerEvent + .map((event) => { + const data = { + hazard: emailContent.disasterTypeLabel, + triggerStatusLabel: event.triggerStatusLabel, + eventName: event.eventName, + expectedTriggerDate: event.firstLeadTime, + expectedExposedAdminBoundary: event.nrOfTriggeredAreas, + defaulAdminAreaLabelSingular: + emailContent.defaultAdminAreaLabel.singular, + defaulAdminAreaLabelPlural: + emailContent.defaultAdminAreaLabel.plural.toLocaleLowerCase(), + defaultAdminAreaLabelParent: adminAreaLabelsParent.singular, + indicatorLabel: emailContent.indicatorMetadata.label, + indicatorUnit: emailContent.indicatorMetadata.unit, + triangleIcon: this.getTriangleIcon(event.triggerStatusLabel), + tableRows: this.getTablesRows(event), + color: + event.triggerStatusLabel === TriggerStatusLabelEnum.Trigger + ? '#940000' + : '#da7c00', + }; + + const templatePath = `${emailTemplateFolder}/email-table-event.html`; + + let template = fs.readFileSync(templatePath, 'utf8'); + + const result = ejs.render(template, data); + return result; + }) + .join(''); + } + + private getTablesRows(event: NotificationDataPerEventDto) { + return event.triggeredAreas + .map((area) => { + const areaTemplatePath = + TriggerStatusLabelEnum.Trigger === event.triggerStatusLabel + ? `${emailTemplateFolder}/email-table-trigger-row.html` + : `${emailTemplateFolder}//email-table-warning-row.html`; + const areaTemplate = fs.readFileSync(areaTemplatePath, 'utf8'); + const areaData = { + affectectedOfIndicator: area.actionsValue, + adminBoundary: area.displayName ? area.displayName : area.name, + higherAdminBoundary: area.nameParent, + }; + + return ejs.render(areaTemplate, areaData); + }) + .join(''); + } + + private getEventListBody(emailContent: ContentTriggerEmail): string { + return emailContent.dataPerEvent + .map((event) => { + const data = { + hazard: emailContent.disasterTypeLabel, + triggerStatusLabel: event.triggerStatusLabel, + eventName: event.eventName, + level: event.disasterSpecificCopy.eventStatus, + expectedWarningDate: event.disasterSpecificCopy.leadTimeString, + nrOfTriggeredAreas: event.nrOfTriggeredAreas, + expectedTriggerDate: event.firstLeadTime, + expectedExposedAdminBoundary: event.nrOfTriggeredAreas, + issuedDate: event.disasterSpecificCopy.timestamp, + startDateEventString: event.startDateEventString, + defaulAdminAreaLabel: + emailContent.defaultAdminAreaLabel.plural.toLocaleLowerCase(), + indicatorLabel: emailContent.indicatorMetadata.label, + totalAffectectedOfIndicator: event.totalAffectectedOfIndicator, + indicatorUnit: emailContent.indicatorMetadata.unit, + currentDate: this.getCurrentDateTimeString( + emailContent.country.countryCodeISO3, + ), + timezone: + CountryTimeZoneMapping[emailContent.country.countryCodeISO3], + triangleIcon: this.getTriangleIcon(event.triggerStatusLabel), + leadTime: event.firstLeadTime.replace('-', ' '), + }; + + const templatePath = + TriggerStatusLabelEnum.Trigger === event.triggerStatusLabel + ? `${emailTemplateFolder}/email-body-trigger-event.html` + : `${emailTemplateFolder}/email-body-warning-event.html`; + + let template = fs.readFileSync(templatePath, 'utf8'); + + return ejs.render(template, data); + }) + .join(''); + } + + private getCurrentDateTimeString(countryCodeISO3: string): string { + const date = new Date(); + + const timeZone = CountryTimeZoneMapping[countryCodeISO3]; + + const options: Intl.DateTimeFormatOptions = { + weekday: 'long', + day: '2-digit', + month: 'long', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + timeZone: timeZone, + }; + + return date.toLocaleString('default', options); + } + + private getTriangleIcon(triggerStatusLabel) { + let fileName = ''; + // Still need implement the difference between medium and low warning + if (triggerStatusLabel === TriggerStatusLabelEnum.Trigger) { + fileName = 'trigger.png'; + } else { + fileName = 'warning-medium.png'; + } + const filePath = `${emailIconFolder}/${fileName}`; + const imageDataURL = this.getPngImageAsDataURL(filePath); + return imageDataURL; + } + + private getPngImageAsDataURL(relativePath) { + const imageBuffer = fs.readFileSync(relativePath); + const imageDataURL = `data:image/png;base64,${imageBuffer.toString( + 'base64', + )}`; + + return imageDataURL; + } +} 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 e88a2caf0..ca10269a8 100644 --- a/services/API-service/src/api/notification/email/email.service.ts +++ b/services/API-service/src/api/notification/email/email.service.ts @@ -1,34 +1,23 @@ -import { AdminAreaDynamicDataService } from './../../admin-area-dynamic-data/admin-area-dynamic-data.service'; -import { LeadTimeEntity } from './../../lead-time/lead-time.entity'; + import { CountryEntity } from './../../country/country.entity'; import { Injectable } from '@nestjs/common'; -import { EventService } from '../../event/event.service'; -import fs from 'fs'; import Mailchimp from 'mailchimp-api-v3'; -import { IndicatorMetadataEntity } from '../../metadata/indicator-metadata.entity'; -import { LeadTime } from '../../admin-area-dynamic-data/enum/lead-time.enum'; -import { DynamicIndicator } from '../../admin-area-dynamic-data/enum/dynamic-data-unit'; + import { DisasterType } from '../../disaster/disaster-type.enum'; import { EventSummaryCountry } from '../../../shared/data.model'; import { NotificationContentService } from './../notification-content/notification-content.service'; - -class ReplaceKeyValue { - replaceKey: string; - replaceValue: string; -} +import { EmailTemplateService } from './email-template.service'; @Injectable() export class EmailService { - private placeholderToday = '(TODAY)'; private fromEmail = process.env.SUPPORT_EMAIL_ADDRESS; private fromEmailName = 'IBF portal'; private mailchimp = new Mailchimp(process.env.MC_API); public constructor( - private readonly eventService: EventService, - private readonly adminAreaDynamicDataService: AdminAreaDynamicDataService, private readonly notificationContentService: NotificationContentService, + private readonly emailTemplateService: EmailTemplateService, ) {} private async getSegmentId( @@ -56,13 +45,18 @@ export class EmailService { activeEvents: EventSummaryCountry[], date?: Date, ): Promise { - const replaceKeyValues = await this.createReplaceKeyValuesTrigger( - country, - disasterType, - activeEvents, - date ? new Date(date) : new Date(), + date = date ? new Date(date) : new Date(); + + const contentForEmail = + await this.notificationContentService.getContentTriggerNotification( + country, + disasterType, + activeEvents, + ); + const emailHtml = this.emailTemplateService.createHtmlForTriggerEmail( + contentForEmail, + date, ); - const emailHtml = this.formatEmail(replaceKeyValues); const emailSubject = `IBF ${( await this.notificationContentService.getDisasterTypeLabel(disasterType) ).toLowerCase()} notification`; @@ -80,16 +74,17 @@ export class EmailService { finishedEvent: EventSummaryCountry, date?: Date, ): Promise { - const replaceKeyValues = await this.createReplaceKeyValuesTriggerFinished( - country, - disasterType, - finishedEvent, - date ? new Date(date) : new Date(), - ); - const emailHtml = this.formatEmail(replaceKeyValues); - const emailSubject = `IBF ${( - await this.notificationContentService.getDisasterTypeLabel(disasterType) - ).toLowerCase()} trigger is now below threshold`; + const disasterTypeLabel = + await this.notificationContentService.getDisasterTypeLabel(disasterType); + const emailHtml = + this.emailTemplateService.createHtmlForTriggerFinishedEmail( + country, + disasterType, + finishedEvent, + disasterTypeLabel, + date ? new Date(date) : new Date(), + ); + const emailSubject = `IBF ${disasterTypeLabel.toLowerCase()} trigger is now below threshold`; this.sendEmail( emailSubject, emailHtml, @@ -134,604 +129,4 @@ export class EmailService { ); await this.mailchimp.post(`/campaigns/${createResult.id}/actions/send`); } - - private async createReplaceKeyValuesTrigger( - country: CountryEntity, - disasterType: DisasterType, - events: EventSummaryCountry[], - date: Date, - ): Promise { - const keyValueReplaceList = [ - { - replaceKey: '(EMAIL-BODY)', - replaceValue: this.getEmailBody(false), - }, - { - replaceKey: '(HEADER-EVENT-OVERVIEW)', - replaceValue: await this.getHeaderEventOverview( - country, - disasterType, - events, - date, - ), - }, - { - replaceKey: '(SOCIAL-MEDIA-PART)', - replaceValue: this.getSocialMediaHtml(country), - }, - { - replaceKey: '(TABLES-stacked)', - replaceValue: await this.getTriggerOverviewTables( - country, - disasterType, - events, - date, - ), - }, - { - replaceKey: this.placeholderToday, - replaceValue: date.toLocaleDateString('default', { - day: '2-digit', - month: 'short', - year: 'numeric', - }), - }, - { - replaceKey: '(EVENT-LIST-BODY)', - replaceValue: ( - await this.getLeadTimeList(country, disasterType, events, date) - )['leadTimeListLong'], - }, - { - replaceKey: '(IMG-LOGO)', - replaceValue: country.notificationInfo.logo[disasterType], - }, - { - replaceKey: '(TRIGGER-STATEMENT)', - replaceValue: country.notificationInfo.triggerStatement[disasterType], - }, - { - replaceKey: '(MAP-IMAGE-PART)', - replaceValue: await this.getMapImageHtml(country, disasterType, events), - }, - { - replaceKey: '(LINK-DASHBOARD)', - replaceValue: process.env.DASHBOARD_URL, - }, - { - replaceKey: '(LINK-EAP-SOP)', - replaceValue: country.countryDisasterSettings.find( - (s) => s.disasterType === disasterType, - ).eapLink, - }, - { - replaceKey: '(SOCIAL-MEDIA-LINK)', - replaceValue: country.notificationInfo.linkSocialMediaUrl, - }, - { - replaceKey: '(SOCIAL-MEDIA-TYPE)', - replaceValue: country.notificationInfo.linkSocialMediaType, - }, - { - replaceKey: '(ADMIN-AREA-PLURAL)', - replaceValue: - country.adminRegionLabels[ - String( - country.countryDisasterSettings.find( - (s) => s.disasterType === disasterType, - ).defaultAdminLevel, - ) - ].plural.toLowerCase(), - }, - { - replaceKey: '(ADMIN-AREA-SINGULAR)', - replaceValue: - country.adminRegionLabels[ - String( - country.countryDisasterSettings.find( - (s) => s.disasterType === disasterType, - ).defaultAdminLevel, - ) - ].singular.toLowerCase(), - }, - { - replaceKey: '(DISASTER-TYPE)', - replaceValue: - await this.notificationContentService.getDisasterTypeLabel( - disasterType, - ), - }, - { - replaceKey: '(VIDEO-PDF-LINKS)', - replaceValue: this.getVideoPdfLinks( - country.notificationInfo.linkVideo, - country.notificationInfo.linkPdf, - ), - }, - { - replaceKey: '(EXPOSURE-UNIT)', - replaceValue: ( - await this.notificationContentService.getActionUnit(disasterType) - ).label.toLocaleLowerCase(), - }, - ]; - return keyValueReplaceList; - } - - private async createReplaceKeyValuesTriggerFinished( - country: CountryEntity, - disasterType: DisasterType, - event: EventSummaryCountry, - date: Date, - ): Promise { - const keyValueReplaceList = [ - { - replaceKey: '(EMAIL-BODY)', - replaceValue: this.getEmailBody(true), - }, - { - replaceKey: '(HEADER-EVENT-OVERVIEW)', - replaceValue: '', - }, - { - replaceKey: '(IMG-LOGO)', - replaceValue: country.notificationInfo.logo[disasterType], - }, - { - replaceKey: '(START-DATE)', - replaceValue: event.startDate, - }, - { - replaceKey: '(LINK-DASHBOARD)', - replaceValue: process.env.DASHBOARD_URL, - }, - { - replaceKey: '(SOCIAL-MEDIA-PART)', - replaceValue: this.getSocialMediaHtml(country), - }, - { - replaceKey: '(LINK-EAP-SOP)', - replaceValue: country.countryDisasterSettings.find( - (s) => s.disasterType === disasterType, - ).eapLink, - }, - { - replaceKey: '(SOCIAL-MEDIA-LINK)', - replaceValue: country.notificationInfo.linkSocialMediaUrl, - }, - { - replaceKey: '(SOCIAL-MEDIA-TYPE)', - replaceValue: country.notificationInfo.linkSocialMediaType, - }, - { - replaceKey: '(VIDEO-PDF-LINKS)', - replaceValue: this.getVideoPdfLinks( - country.notificationInfo.linkVideo, - country.notificationInfo.linkPdf, - ), - }, - { - replaceKey: '(DISASTER-TYPE)', - replaceValue: - await this.notificationContentService.getDisasterTypeLabel( - disasterType, - ), - }, - { - replaceKey: this.placeholderToday, - replaceValue: date.toLocaleDateString('default', { - day: '2-digit', - month: 'short', - year: 'numeric', - }), - }, - ]; - return keyValueReplaceList; - } - - private getEmailBody(triggerFinished: boolean): string { - if (triggerFinished) { - return fs.readFileSync( - './src/api/notification/email/html/trigger-finished.html', - 'utf8', - ); - } else { - return fs.readFileSync( - './src/api/notification/email/html/trigger-notification.html', - 'utf8', - ); - } - } - - private async getHeaderEventOverview( - country: CountryEntity, - disasterType: DisasterType, - activeEvents: EventSummaryCountry[], - date?: Date, - ): Promise { - const leadTimeListShort = ( - await this.getLeadTimeList( - country, - disasterType, - this.sortEventsByLeadTime(activeEvents), - date, - ) - )['leadTimeListShort']; - return fs - .readFileSync( - './src/api/notification/email/html/header-event-overview.html', - 'utf8', - ) - .replace('(EVENT-LIST-HEADER)', leadTimeListShort); - } - - private sortEventsByLeadTime( - arr: EventSummaryCountry[], - ): EventSummaryCountry[] { - const leadTimeValue = (leadTime: LeadTime): number => - Number(leadTime.split('-')[0]); - - return arr.sort((a, b) => { - if (leadTimeValue(a.firstLeadTime) < leadTimeValue(b.firstLeadTime)) { - return -1; - } - if (leadTimeValue(a.firstLeadTime) > leadTimeValue(b.firstLeadTime)) { - return 1; - } - - return 0; - }); - } - - private getVideoPdfLinks(videoLink: string, pdfLink: string) { - const linkVideoHTML = ` - video`; - - const linkPdfHTML = `PDF`; - let videoStr = ''; - if (videoLink) { - videoStr = ' ' + linkVideoHTML; - } - let pdfStr = ''; - if (pdfLink) { - pdfStr = ' ' + linkPdfHTML; - } - let orStr = ''; - if (videoStr && pdfStr) { - orStr = ' or'; - } - if (videoStr || pdfStr) { - return `See instructions for the IBF-portal in${videoStr}${orStr}${pdfStr}.`; - } - } - - private async getLeadTimeList( - country: CountryEntity, - disasterType: DisasterType, - events: EventSummaryCountry[], - date?: Date, - ): Promise { - const triggeredLeadTimes = - await this.notificationContentService.getLeadTimesAcrossEvents( - country.countryCodeISO3, - disasterType, - events, - ); - - let leadTimeListShort = ''; - let leadTimeListLong = ''; - for (const leadTime of country.countryDisasterSettings.find( - (s) => s.disasterType === disasterType, - ).activeLeadTimes) { - if (triggeredLeadTimes[leadTime.leadTimeName] === '1') { - for await (const event of events) { - // for each event .. - const triggeredLeadTimes = - await this.eventService.getTriggerPerLeadtime( - country.countryCodeISO3, - disasterType, - event.eventName, - ); - if (triggeredLeadTimes[leadTime.leadTimeName] === '1') { - const leadTimeListEvent = - await this.notificationContentService.getLeadTimeListEvent( - country, - event, - disasterType, - leadTime.leadTimeName as LeadTime, - date, - ); - - // We are hack-misusing 'extraInfo' being filled as a proxy for typhoonNoLandfallYet-boolean - leadTimeListShort = `${leadTimeListShort}${leadTimeListEvent.short}`; - leadTimeListLong = `${leadTimeListLong}${leadTimeListEvent.long}`; - } - } - } - } - return { leadTimeListShort, leadTimeListLong }; - } - - private async getTriggerOverviewTables( - country: CountryEntity, - disasterType: DisasterType, - events: EventSummaryCountry[], - date: Date, - ): Promise { - const triggeredLeadTimes = - await this.notificationContentService.getLeadTimesAcrossEvents( - country.countryCodeISO3, - disasterType, - events, - ); - - let leadTimeTables = ''; - for (const leadTime of country.countryDisasterSettings.find( - (s) => s.disasterType === disasterType, - ).activeLeadTimes) { - if (triggeredLeadTimes[leadTime.leadTimeName] === '1') { - for await (const event of events) { - // for each event .. - const triggeredLeadTimes = - await this.eventService.getTriggerPerLeadtime( - country.countryCodeISO3, - disasterType, - event.eventName, - ); - - if (triggeredLeadTimes[leadTime.leadTimeName] === '1') { - // .. find the right leadtime - const tableForLeadTime = await this.getTableForLeadTime( - country, - disasterType, - leadTime, - event, - date, - ); - leadTimeTables = leadTimeTables + tableForLeadTime; - } - } - } - } - return leadTimeTables; - } - - private getSocialMediaHtml(country: CountryEntity): string { - if (country.notificationInfo.linkSocialMediaType) { - return fs.readFileSync( - './src/api/notification/email/html/social-media-link.html', - 'utf8', - ); - } else { - return ''; - } - } - - private async getMapImageHtml( - country: CountryEntity, - disasterType: DisasterType, - events: EventSummaryCountry[], - ): Promise { - let html = ''; - for await (const event of events) { - const mapImage = await this.eventService.getEventMapImage( - country.countryCodeISO3, - disasterType, - event.eventName || 'no-name', - ); - if (mapImage) { - let eventHtml = fs.readFileSync( - './src/api/notification/email/html/map-image.html', - 'utf8', - ); - eventHtml = eventHtml - .replace( - '(MAP-IMG-SRC)', - this.getMapImgSrc( - country.countryCodeISO3, - disasterType, - event.eventName, - ), - ) - .replace( - '(MAP-IMG-DESCRIPTION)', - this.getMapImageDescription(disasterType), - ); - eventHtml = eventHtml.replace( - '(EVENT-NAME)', - event.eventName ? ` for '${event.eventName}'` : '', - ); - html += eventHtml; - } - } - return html; - } - - private getMapImgSrc( - countryCodeISO3: string, - disasterType: DisasterType, - eventName: string, - ): string { - const src = `${ - process.env.NG_API_URL - }/event/event-map-image/${countryCodeISO3}/${disasterType}/${ - eventName || 'no-name' - }`; - - return src; - } - - private getMapImageDescription(disasterType: DisasterType): string { - switch (disasterType) { - case DisasterType.Floods: - return 'The triggered areas are outlined in purple. The potential flood extent is shown in red.
'; - default: - return ''; - } - } - - private async getTableForLeadTime( - country: CountryEntity, - disasterType: DisasterType, - leadTime: LeadTimeEntity, - event: EventSummaryCountry, - date: Date, - ): Promise { - const adminLevel = country.countryDisasterSettings.find( - (s) => s.disasterType === disasterType, - ).defaultAdminLevel; - const adminAreaLabels = country.adminRegionLabels[String(adminLevel)]; - const adminAreaLabelsParent = - country.adminRegionLabels[String(adminLevel - 1)]; - - const actionsUnit = await this.notificationContentService.getActionUnit( - disasterType, - ); - - const tableForLeadTimeStart = `
- ${ - ( - await this.notificationContentService.getLeadTimeListEvent( - country, - event, - disasterType, - leadTime.leadTimeName as LeadTime, - date, - ) - ).short - } -
- - - - - - - - - - -
`; - const tableForLeadTimeMiddle = await this.getAreaTables( - country, - disasterType, - leadTime, - event.eventName, - actionsUnit, - ); - const tableForLeadTimeEnd = '
This table lists the potentially exposed ${adminAreaLabels.plural.toLowerCase()} in order of ${actionsUnit.label.toLowerCase()}:
Alert class${adminAreaLabels.singular}${ - adminAreaLabelsParent ? ' (' + adminAreaLabelsParent.singular + ')' : '' - }Predicted ${actionsUnit.label}


'; - const tableForLeadTime = - tableForLeadTimeStart + tableForLeadTimeMiddle + tableForLeadTimeEnd; - return tableForLeadTime; - } - - private async getAreaTables( - country: CountryEntity, - disasterType: DisasterType, - leadTime: LeadTimeEntity, - eventName: string, - actionsUnit: IndicatorMetadataEntity, - ): Promise { - const triggeredAreas = await this.eventService.getTriggeredAreas( - country.countryCodeISO3, - disasterType, - country.countryDisasterSettings.find( - (s) => s.disasterType === disasterType, - ).defaultAdminLevel, - leadTime.leadTimeName, - eventName, - ); - triggeredAreas.sort((a, b) => (a.triggerValue > b.triggerValue ? -1 : 1)); - const disaster = await this.notificationContentService.getDisaster( - disasterType, - ); - const hasEap = this.hasEap(disasterType); - let areaTableString = ''; - for (const area of triggeredAreas) { - const actionsUnitValue = - await this.adminAreaDynamicDataService.getDynamicAdminAreaDataPerPcode( - disaster.actionsUnit as DynamicIndicator, - area.placeCode, - leadTime.leadTimeName as LeadTime, - eventName, - ); - - const areaTable = ` - - ${this.mapTriggerValueToAlertClass( - area.triggerValue, - hasEap, - )} - ${area.name}${ - area.nameParent ? ' (' + area.nameParent + ')' : '' - } - ${this.notificationContentService.formatActionUnitValue( - actionsUnitValue, - actionsUnit, - )} - `; - areaTableString = areaTableString + areaTable; - } - return areaTableString; - } - - // TODO merge this with the front-end instance of this to some generic place in back-end - public hasEap(disasterType: DisasterType): boolean { - const eapDisasterTypes = [ - DisasterType.Floods, - DisasterType.Drought, - DisasterType.Typhoon, - DisasterType.FlashFloods, - ]; - return eapDisasterTypes.includes(disasterType); - } - - private mapTriggerValueToAlertClass( - triggerValue: number, - hasEap: boolean, - ): string { - if (triggerValue === 1) { - return hasEap ? 'Trigger issued' : 'Alert issued'; - } else if (triggerValue === 0.7) { - return 'Medium warning issued'; - } else if (triggerValue === 0.3) { - return 'Low warning issued'; - } - } - - private formatEmail(emailKeyValueReplaceList: ReplaceKeyValue[]): string { - let emailHtml = fs.readFileSync( - './src/api/notification/email/html/base.html', - 'utf8', - ); - for (const entry of emailKeyValueReplaceList) { - emailHtml = emailHtml.split(entry.replaceKey).join(entry.replaceValue); - } - return emailHtml; - } } diff --git a/services/API-service/src/api/notification/email/html/base.html b/services/API-service/src/api/notification/email/html/base.html index a73656531..4337a8c73 100644 --- a/services/API-service/src/api/notification/email/html/base.html +++ b/services/API-service/src/api/notification/email/html/base.html @@ -882,7 +882,7 @@ >

- (DISASTER-TYPE) IBF Notification + <%= disasterType %> IBF Notification

- (HEADER-EVENT-OVERVIEW) + <%- headerEventOverview %> @@ -922,7 +922,7 @@

- (EMAIL-BODY) + <%- emailBody %> + + Warning icon + <%= hazard %> <%= eventName %>
Trigger expected on: + <%= startDateEventString %>, <%= leadTime %>s from now.
Expected exposed <%= defaulAdminAreaLabel %>: + <%= nrOfTriggeredAreas %> (see list below)
+ <%= indicatorLabel %>: <%= totalAffectectedOfIndicator %> <%= indicatorUnit %>
Advisory: Activate + Early Action Protocol

This trigger was issued by the IBF portal on <%= currentDate %> (<%= + timezone %>) + +
diff --git a/services/API-service/src/api/notification/email/html/email-body-warning-event.html b/services/API-service/src/api/notification/email/html/email-body-warning-event.html new file mode 100644 index 000000000..77c9345f2 --- /dev/null +++ b/services/API-service/src/api/notification/email/html/email-body-warning-event.html @@ -0,0 +1,120 @@ +
+ + Warning icon + <%= hazard %> <%= eventName %>
Warning expected on: + <%= startDateEventString %>, <%= leadTime %>s from now.
Expected exposed <%= defaulAdminAreaLabel %>: + <%= nrOfTriggeredAreas %> (see list below)
+ <%= indicatorLabel %>: Information regarding exposed population not available for warnings
Advisory: + Inform all potentially exposed districts

This warning was issued by the IBF portal on <%= currentDate %> (<%= + timezone %>) +
diff --git a/services/API-service/src/api/notification/email/html/email-table-event.html b/services/API-service/src/api/notification/email/html/email-table-event.html new file mode 100644 index 000000000..e25ad4c7f --- /dev/null +++ b/services/API-service/src/api/notification/email/html/email-table-event.html @@ -0,0 +1,96 @@ +
+
+ Warning icon + <%= triggerStatusLabel %> <%= hazard %>: <%= eventName %> +
+
+ Expected exposed <%= defaulAdminAreaLabelPlural %><% if (triggerStatusLabel + === 'Trigger') { %> in order of <%= indicatorLabel.toLowerCase() %><% } %>: +
+
+
+ + + <% if (triggerStatusLabel === 'Trigger') { %> + + <% } %> + + + <%- tableRows %> +
+ <%= indicatorLabel %> + + <%= defaulAdminAreaLabelSingular %> (<%= defaultAdminAreaLabelParent + %>) +
+
+
+
diff --git a/services/API-service/src/api/notification/email/html/email-table-trigger-row.html b/services/API-service/src/api/notification/email/html/email-table-trigger-row.html new file mode 100644 index 000000000..38428b98e --- /dev/null +++ b/services/API-service/src/api/notification/email/html/email-table-trigger-row.html @@ -0,0 +1,36 @@ + + + <%= affectectedOfIndicator %> + + + <%= adminBoundary %> (<%= higherAdminBoundary %>) + + diff --git a/services/API-service/src/api/notification/email/html/email-table-warning-row.html b/services/API-service/src/api/notification/email/html/email-table-warning-row.html new file mode 100644 index 000000000..85d41824e --- /dev/null +++ b/services/API-service/src/api/notification/email/html/email-table-warning-row.html @@ -0,0 +1,22 @@ + + + <%= adminBoundary %> (<%= higherAdminBoundary %>) + + diff --git a/services/API-service/src/api/notification/email/html/header-event-overview.html b/services/API-service/src/api/notification/email/html/header-event-overview.html index db14059be..e5795b900 100644 --- a/services/API-service/src/api/notification/email/html/header-event-overview.html +++ b/services/API-service/src/api/notification/email/html/header-event-overview.html @@ -1,5 +1,5 @@ -

(EVENT-LIST-HEADER)

+

<%- eventListHeader %>

diff --git a/services/API-service/src/api/notification/email/html/map-image.html b/services/API-service/src/api/notification/email/html/map-image.html index 876034e1e..a64b1e9f5 100644 --- a/services/API-service/src/api/notification/email/html/map-image.html +++ b/services/API-service/src/api/notification/email/html/map-image.html @@ -1,6 +1,6 @@
-Map of the triggered area(EVENT-NAME): (click 'Download pictures' if it -does not show) +Map of the triggered area(<%= eventName %>): (click 'Download pictures' +if it does not show)
-(MAP-IMG-DESCRIPTION) - +<%= mapImgDescription %> + diff --git a/services/API-service/src/api/notification/email/html/social-media-link.html b/services/API-service/src/api/notification/email/html/social-media-link.html index b36c482e4..61a111b12 100644 --- a/services/API-service/src/api/notification/email/html/social-media-link.html +++ b/services/API-service/src/api/notification/email/html/social-media-link.html @@ -9,7 +9,7 @@ Join (SOCIAL-MEDIA-TYPE) groupJoin <%- socialMediaType %> group diff --git a/services/API-service/src/api/notification/email/html/trigger-finished.html b/services/API-service/src/api/notification/email/html/trigger-finished.html index 0c0ac46a9..301518ea0 100644 --- a/services/API-service/src/api/notification/email/html/trigger-finished.html +++ b/services/API-service/src/api/notification/email/html/trigger-finished.html @@ -86,7 +86,7 @@ Dear reader,

The trigger notification formerly activated on - (START-DATE) is now + <%= startDate %> is now below threshold.

The event will close in the IBF-portal when the forecast stays below threshold for 7 days in a @@ -112,7 +112,7 @@ align="center" >
- Dear Reader,

- (EVENT-LIST-BODY) +
+ Dear Reader, +
+

+ <%- eventListBody %> @@ -98,7 +110,7 @@ diff --git a/services/API-service/src/api/notification/email/html/trigger-finished.html b/services/API-service/src/api/notification/email/html/trigger-finished.html index 301518ea0..5df98542b 100644 --- a/services/API-service/src/api/notification/email/html/trigger-finished.html +++ b/services/API-service/src/api/notification/email/html/trigger-finished.html @@ -85,13 +85,8 @@
Dear reader,

- The trigger notification formerly activated on - <%= startDate %> is now - below threshold.

- The event will close in the IBF-portal when the - forecast stays below threshold for 7 days in a - row. Until that time, anticipatory actions can - still be managed in the IBF-portal. + + <%- eventOverview %>
{ + const countryAdminAreaIds = await this.getCountryAdminAreaIds( + countryCodeISO3, + ); + + // I spend quite some time on trying to figure out what is the right query to get the event finished summary for the trigger closed email + // I came up with the following query but I am not sure if it is correct and how to test it properly + const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); + const sixDaysAgo = new Date(Date.now() - 6 * 24 * 60 * 60 * 1000); + const eventSummaryQueryBuilder = this.createEventSummaryQueryBuilder( + countryCodeISO3, + ) + .andWhere('event.endDate > :endDateMin', { endDateMin: sevenDaysAgo }) + .andWhere('event.endDate < :endDateMax', { endDateMax: sixDaysAgo }) + .andWhere('event.adminArea IN (:...adminAreaIds)', { + adminAreaIds: countryAdminAreaIds, }) - .andWhere('area."countryCodeISO3" = :countryCodeISO3', { - countryCodeISO3: countryCodeISO3, + .andWhere('event.disasterType = :disasterType', { + disasterType: disasterType, }) - .getRawMany(); + .andWhere('event.closed = :closed', { closed: true }); - const disasterSettings = await this.getCountryDisasterSettings( + return this.queryAndMapEventSummary( + eventSummaryQueryBuilder, countryCodeISO3, disasterType, ); + } - for await (const event of eventSummary) { + private async queryAndMapEventSummary( + qb: SelectQueryBuilder, + countryCodeISO3: string, + disasterType: DisasterType, + ): Promise { + const rawEventSummary = await qb.getRawMany(); + const eventSummary = await this.populateEventsDetails( + rawEventSummary, + countryCodeISO3, + disasterType, + ); + return eventSummary; + } + + private async populateEventsDetails( + rawEvents: any[], + countryCodeISO3: string, + disasterType: DisasterType, + ): Promise { + const disasterSettings = await this.getCountryDisasterSettings( + countryCodeISO3, + disasterType, + ); + for (const event of rawEvents) { event.firstLeadTime = await this.getFirstLeadTime( countryCodeISO3, disasterType, @@ -125,7 +166,34 @@ export class EventService { ); } } - return eventSummary; + return rawEvents; + } + + private createEventSummaryQueryBuilder( + countryCodeISO3: string, + ): SelectQueryBuilder { + return this.eventPlaceCodeRepo + .createQueryBuilder('event') + .select([ + 'area."countryCodeISO3"', + 'event."eventName"', + 'event."triggerValue"', + ]) + .leftJoin('event.adminArea', 'area') + .groupBy('area."countryCodeISO3"') + .addGroupBy('event."eventName"') + .addGroupBy('event."triggerValue"') + .addSelect([ + 'to_char(MIN("startDate") , \'yyyy-mm-dd\') AS "startDate"', + 'to_char(MAX("endDate") , \'yyyy-mm-dd\') AS "endDate"', + 'MAX(event."thresholdReached"::int)::boolean AS "thresholdReached"', + 'count(event."adminAreaId")::int AS "affectedAreas"', + 'MAX(event."triggerValue")::float AS "triggerValue"', + 'sum(event."actionsValue")::int AS "actionsValueSum"', + ]) + .andWhere('area."countryCodeISO3" = :countryCodeISO3', { + countryCodeISO3: countryCodeISO3, + }); } public async getRecentDate( @@ -907,7 +975,8 @@ export class EventService { where: whereFilters, }); - //Below threshold events can be removed from this table after closing + // Below threshold events can be removed from this table after closing + // Below threshold events are warnings an not triggered. I do not know why they are removed here const belowThresholdEvents = expiredEventAreas.filter( (a) => !a.thresholdReached, ); diff --git a/services/API-service/src/api/notification/dto/content-trigger-email.dto.ts b/services/API-service/src/api/notification/dto/content-trigger-email.dto.ts index e5bda8d48..f617ba219 100644 --- a/services/API-service/src/api/notification/dto/content-trigger-email.dto.ts +++ b/services/API-service/src/api/notification/dto/content-trigger-email.dto.ts @@ -4,7 +4,7 @@ import { IndicatorMetadataEntity } from '../../metadata/indicator-metadata.entit import { AdminAreaLabel } from './admin-area-notification-info.dto'; import { NotificationDataPerEventDto } from './notification-date-per-event.dto'; -export class ContentTriggerEmail { +export class ContentEventEmail { public disasterType: DisasterType; public disasterTypeLabel: string; public indicatorMetadata: IndicatorMetadataEntity; 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 96baf5862..33f17e713 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 @@ -8,9 +8,10 @@ export class NotificationDataPerEventDto { firstLeadTime: LeadTime; triggeredAreas: TriggeredArea[]; nrOfTriggeredAreas: number; - startDateEventString: string; + startDateDisasterString: string; totalAffectectedOfIndicator: number; mapImage?: Buffer; + issuedDate: Date; } export enum TriggerStatusLabelEnum { diff --git a/services/API-service/src/api/notification/email/email-template.service.ts b/services/API-service/src/api/notification/email/email-template.service.ts index 103489e2b..ab6eeb013 100644 --- a/services/API-service/src/api/notification/email/email-template.service.ts +++ b/services/API-service/src/api/notification/email/email-template.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { ContentTriggerEmail } from '../dto/content-trigger-email.dto'; +import { ContentEventEmail } from '../dto/content-trigger-email.dto'; import { LeadTime } from '../../admin-area-dynamic-data/enum/lead-time.enum'; import { NotificationDataPerEventDto, @@ -26,11 +26,11 @@ export class EmailTemplateService { private placeholderToday = '(TODAY)'; public createHtmlForTriggerEmail( - contentForEmail: ContentTriggerEmail, + emailContent: ContentEventEmail, date: Date, ): string { const replaceKeyValues = this.createReplaceKeyValuesTrigger( - contentForEmail, + emailContent, date, ); return this.formatEmail(replaceKeyValues); @@ -40,14 +40,14 @@ export class EmailTemplateService { public createHtmlForTriggerFinishedEmail( country: CountryEntity, disasterType: DisasterType, - finishedEvent: EventSummaryCountry, + finishedEvents: EventSummaryCountry[], disasterTypeLabel: string, date: Date, ): string { const replaceKeyValues = this.createReplaceKeyValuesTriggerFinished( country, disasterType, - finishedEvent, + finishedEvents, disasterTypeLabel, date, ); @@ -55,11 +55,11 @@ export class EmailTemplateService { } private createReplaceKeyValuesTrigger( - contentForEmail: ContentTriggerEmail, + emailContent: ContentEventEmail, date: Date, ): ReplaceKeyValue[] { - const country = contentForEmail.country; - const disasterType = contentForEmail.disasterType; + const country = emailContent.country; + const disasterType = emailContent.disasterType; const keyValueReplaceList = [ { replaceKey: 'emailBody', @@ -67,15 +67,15 @@ export class EmailTemplateService { }, { replaceKey: 'headerEventOverview', - replaceValue: this.getHeaderEventOverview(contentForEmail.dataPerEvent), + replaceValue: this.getHeaderEventStarted(emailContent), }, { replaceKey: 'socialMediaPart', - replaceValue: this.getSocialMediaHtml(contentForEmail.country), + replaceValue: this.getSocialMediaHtml(emailContent.country), }, { replaceKey: 'tablesStacked', - replaceValue: this.getTablesForEvents(contentForEmail), + replaceValue: this.getTablesForEvents(emailContent), }, { replaceKey: this.placeholderToday, @@ -87,7 +87,7 @@ export class EmailTemplateService { }, { replaceKey: 'eventListBody', - replaceValue: this.getEventListBody(contentForEmail), + replaceValue: this.getEventListBody(emailContent), }, { replaceKey: 'imgLogo', @@ -99,7 +99,7 @@ export class EmailTemplateService { }, { replaceKey: 'mapImagePart', - replaceValue: this.getMapImageHtml(contentForEmail), + replaceValue: this.getMapImageHtml(emailContent), }, { replaceKey: 'linkDashboard', @@ -121,7 +121,7 @@ export class EmailTemplateService { }, { replaceKey: 'disasterType', - replaceValue: contentForEmail.disasterTypeLabel, + replaceValue: emailContent.disasterTypeLabel, }, { replaceKey: 'videoPdfLinks', @@ -137,7 +137,7 @@ export class EmailTemplateService { private createReplaceKeyValuesTriggerFinished( country: CountryEntity, disasterType: DisasterType, - event: EventSummaryCountry, + events: EventSummaryCountry[], disasterTypeLabel: string, date: Date, ): ReplaceKeyValue[] { @@ -151,12 +151,16 @@ export class EmailTemplateService { replaceValue: '', }, { - replaceKey: 'imgLogo', - replaceValue: country.notificationInfo.logo[disasterType], + replaceKey: 'eventOverview', + replaceValue: this.geEventsFinishedOverview( + country, + events, + disasterTypeLabel, + ), }, { - replaceKey: 'startDate', - replaceValue: event.startDate, + replaceKey: 'imgLogo', + replaceValue: country.notificationInfo.logo[disasterType], }, { replaceKey: 'linkDashboard', @@ -217,16 +221,42 @@ export class EmailTemplateService { } } - private getHeaderEventOverview( - eventsData: NotificationDataPerEventDto[], + private geEventsFinishedOverview( + country: CountryEntity, + events: EventSummaryCountry[], + disasterTypeLabel: string, ): string { - const leadTimeListShort = this.getEventListShort(eventsData); + let html = ''; + + const template = fs.readFileSync( + './src/api/notification/email/html/event-finished.html', + 'utf-8', + ); + + for (const event of events) { + const eventFinshedHtml = ejs.render(template, { + disasterTypeLabel: disasterTypeLabel, + eventName: event.eventName, + issuedDate: 'yo', + timezone: CountryTimeZoneMapping[country.countryCodeISO3], + }); + html += eventFinshedHtml; + } + return html; + } + + private getHeaderEventStarted(emailContent: ContentEventEmail): string { let headerEventOverview = fs.readFileSync( './src/api/notification/email/html/header-event-overview.html', 'utf8', ); headerEventOverview = ejs.render(headerEventOverview, { - eventListHeader: leadTimeListShort, + sentOnDate: this.getCurrentDateTimeString( + emailContent.country.countryCodeISO3, + ), + disasterLabel: emailContent.disasterTypeLabel, + nrOfEvents: emailContent.dataPerEvent.length, + timezone: CountryTimeZoneMapping[emailContent.country.countryCodeISO3], }); return headerEventOverview; } @@ -288,9 +318,9 @@ export class EmailTemplateService { } } - private getMapImageHtml(contentForEmail: ContentTriggerEmail): string { + private getMapImageHtml(emailContent: ContentEventEmail): string { let html = ''; - for (const event of contentForEmail.dataPerEvent) { + for (const event of emailContent.dataPerEvent) { const mapImage = event.mapImage; if (mapImage) { let eventHtml = fs.readFileSync( @@ -299,12 +329,12 @@ export class EmailTemplateService { ); const replacements = { mapImgSrc: this.getMapImgSrc( - contentForEmail.country.countryCodeISO3, - contentForEmail.disasterType, + emailContent.country.countryCodeISO3, + emailContent.disasterType, event.eventName, ), mapImgDescription: this.getMapImageDescription( - contentForEmail.disasterType, + emailContent.disasterType, ), eventName: event.eventName ? ` for '${event.eventName}'` : '', }; @@ -387,7 +417,7 @@ export class EmailTemplateService { return text; } - private getTablesForEvents(emailContent: ContentTriggerEmail): string { + private getTablesForEvents(emailContent: ContentEventEmail): string { const adminAreaLabelsParent = emailContent.country.adminRegionLabels[ String(emailContent.defaultAdminLevel - 1) @@ -445,7 +475,7 @@ export class EmailTemplateService { .join(''); } - private getEventListBody(emailContent: ContentTriggerEmail): string { + private getEventListBody(emailContent: ContentEventEmail): string { return emailContent.dataPerEvent .map((event) => { const data = { @@ -457,8 +487,11 @@ export class EmailTemplateService { nrOfTriggeredAreas: event.nrOfTriggeredAreas, expectedTriggerDate: event.firstLeadTime, expectedExposedAdminBoundary: event.nrOfTriggeredAreas, - issuedDate: event.disasterSpecificCopy.timestamp, - startDateEventString: event.startDateEventString, + issuedDate: this.dateObjectToDateTimeString( + event.issuedDate, + emailContent.country.countryCodeISO3, + ), + startDateEventString: event.startDateDisasterString, defaulAdminAreaLabel: emailContent.defaultAdminAreaLabel.plural.toLocaleLowerCase(), indicatorLabel: emailContent.indicatorMetadata.label, @@ -487,9 +520,14 @@ export class EmailTemplateService { private getCurrentDateTimeString(countryCodeISO3: string): string { const date = new Date(); + return this.dateObjectToDateTimeString(date, countryCodeISO3); + } + private dateObjectToDateTimeString( + date: Date, + countryCodeISO3: string, + ): string { const timeZone = CountryTimeZoneMapping[countryCodeISO3]; - const options: Intl.DateTimeFormatOptions = { weekday: 'long', day: '2-digit', @@ -499,7 +537,6 @@ export class EmailTemplateService { minute: '2-digit', timeZone: timeZone, }; - return date.toLocaleString('default', options); } 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 ca10269a8..8ec3ea611 100644 --- a/services/API-service/src/api/notification/email/email.service.ts +++ b/services/API-service/src/api/notification/email/email.service.ts @@ -1,4 +1,3 @@ - import { CountryEntity } from './../../country/country.entity'; import { Injectable } from '@nestjs/common'; import Mailchimp from 'mailchimp-api-v3'; @@ -47,14 +46,14 @@ export class EmailService { ): Promise { date = date ? new Date(date) : new Date(); - const contentForEmail = + const emailContent = await this.notificationContentService.getContentTriggerNotification( country, disasterType, activeEvents, ); const emailHtml = this.emailTemplateService.createHtmlForTriggerEmail( - contentForEmail, + emailContent, date, ); const emailSubject = `IBF ${( @@ -71,7 +70,7 @@ export class EmailService { public async sendTriggerFinishedEmail( country: CountryEntity, disasterType: DisasterType, - finishedEvent: EventSummaryCountry, + finishedEvents: EventSummaryCountry[], date?: Date, ): Promise { const disasterTypeLabel = @@ -80,7 +79,7 @@ export class EmailService { this.emailTemplateService.createHtmlForTriggerFinishedEmail( country, disasterType, - finishedEvent, + finishedEvents, disasterTypeLabel, date ? new Date(date) : new Date(), ); diff --git a/services/API-service/src/api/notification/email/html/base.html b/services/API-service/src/api/notification/email/html/base.html index 4337a8c73..ca6072f8f 100644 --- a/services/API-service/src/api/notification/email/html/base.html +++ b/services/API-service/src/api/notification/email/html/base.html @@ -28,6 +28,9 @@ .notification-sub-title-left { color: white; text-align: center; + display: flex; + justify-content: center; + align-items: center; } .notification-content { diff --git a/services/API-service/src/api/notification/email/html/email-body-trigger-event.html b/services/API-service/src/api/notification/email/html/email-body-trigger-event.html index a052140f0..456687ad8 100644 --- a/services/API-service/src/api/notification/email/html/email-body-trigger-event.html +++ b/services/API-service/src/api/notification/email/html/email-body-trigger-event.html @@ -125,8 +125,8 @@ line-height: 19.2px; word-wrap: break-word; " - >This trigger was issued by the IBF portal on <%= currentDate %> (<%= + >This trigger was issued by the IBF portal on <%= issuedDate %> (<%= timezone %>) +
-
diff --git a/services/API-service/src/api/notification/email/html/email-body-warning-event.html b/services/API-service/src/api/notification/email/html/email-body-warning-event.html index 77c9345f2..ea20abf86 100644 --- a/services/API-service/src/api/notification/email/html/email-body-warning-event.html +++ b/services/API-service/src/api/notification/email/html/email-body-warning-event.html @@ -114,7 +114,8 @@ line-height: 19.2px; word-wrap: break-word; " - >This warning was issued by the IBF portal on <%= currentDate %> (<%= + >This warning was issued by the IBF portal on <%= issuedDate %> (<%= timezone %>) +
diff --git a/services/API-service/src/api/notification/email/html/event-finished.html b/services/API-service/src/api/notification/email/html/event-finished.html new file mode 100644 index 000000000..7a607dbe3 --- /dev/null +++ b/services/API-service/src/api/notification/email/html/event-finished.html @@ -0,0 +1,39 @@ +
+ + <%= disasterTypeLabel %>: <%= eventName %> is now below threshold
+ The events actions will continue to show on the IBF portal and can still be + managed.
+
This warning was issued by the IBF portal on <%= issuedDate %> (<%= + timezone %>) +
diff --git a/services/API-service/src/api/notification/email/html/header-event-overview.html b/services/API-service/src/api/notification/email/html/header-event-overview.html index e5795b900..5a5a87a43 100644 --- a/services/API-service/src/api/notification/email/html/header-event-overview.html +++ b/services/API-service/src/api/notification/email/html/header-event-overview.html @@ -1,5 +1,18 @@
-

<%- eventListHeader %>

+

+ <%= nrOfEvents %> <%= disasterLabel %> alerts +

+ IBF alert send on <%= sentOnDate %> (<%= timezone %>)
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 9cf9bd2ae..93c41add4 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 @@ -6,19 +6,16 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { IndicatorMetadataEntity } from '../../metadata/indicator-metadata.entity'; import { LeadTime } from '../../admin-area-dynamic-data/enum/lead-time.enum'; -import { DynamicIndicator } from '../../admin-area-dynamic-data/enum/dynamic-data-unit'; import { DisasterType } from '../../disaster/disaster-type.enum'; import { DisasterEntity } from '../../disaster/disaster.entity'; import { EventSummaryCountry, TriggeredArea } from '../../../shared/data.model'; -import { AdminAreaDataService } from '../../admin-area-data/admin-area-data.service'; -import { AdminAreaService } from '../../admin-area/admin-area.service'; import { HelperService } from '../../../shared/helper.service'; import { NotificationDataPerEventDto, TriggerStatusLabelEnum, } from '../dto/notification-date-per-event.dto'; import { AdminAreaLabel } from '../dto/admin-area-notification-info.dto'; -import { ContentTriggerEmail } from '../dto/content-trigger-email.dto'; +import { ContentEventEmail } from '../dto/content-trigger-email.dto'; @Injectable() export class NotificationContentService { @@ -38,8 +35,8 @@ export class NotificationContentService { country: CountryEntity, disasterType: DisasterType, activeEvents: EventSummaryCountry[], - ): Promise { - const content = new ContentTriggerEmail(); + ): Promise { + const content = new ContentEventEmail(); content.disasterType = disasterType; content.disasterTypeLabel = await this.getDisasterTypeLabel(disasterType); content.dataPerEvent = await this.getNotificationDataForEvents( @@ -169,17 +166,16 @@ export class NotificationContentService { data.nrOfTriggeredAreas = await this.getNrOfTriggeredAreas( data.triggeredAreas, ); - - data.startDateEventString = await this.getFirstLeadTimeDate( - Number(event.firstLeadTime.split('-')[0]), - event.firstLeadTime.split('-')[1], + // 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.startDateDisasterString = await this.getFirstLeadTimeString( + event, event.countryCodeISO3, disasterType, ); data.totalAffectectedOfIndicator = this.getTotalAffectedPerEvent( data.triggeredAreas, ); - data.mapImage = await this.eventService.getEventMapImage( country.countryCodeISO3, disasterType, @@ -270,7 +266,7 @@ export class NotificationContentService { }); } - public async getStartTimeEvent( + public async getFirstLeadTimeString( event: EventSummaryCountry, countryCodeISO3: string, disasterType: DisasterType, diff --git a/services/API-service/src/api/notification/notification.service.ts b/services/API-service/src/api/notification/notification.service.ts index 1352a256f..5cdf65f6c 100644 --- a/services/API-service/src/api/notification/notification.service.ts +++ b/services/API-service/src/api/notification/notification.service.ts @@ -23,94 +23,103 @@ export class NotificationService { disasterType: DisasterType, date?: Date, ): Promise { + await this.sendNotiFicationsActiveEvents( + disasterType, + countryCodeISO3, + date, + ); + + if (disasterType === DisasterType.Floods) { + // Sending finished events is now for floods only + await this.sendNotificationsFinishedEvents( + countryCodeISO3, + disasterType, + date, + ); + } + // REFACTOR: First close finished events. This is ideally done through separate endpoint called at end of pipeline, but that would require all pipelines to be updated. // Instead, making use of this endpoint which is already called at the end of every pipeline await this.eventService.closeEventsAutomatic(countryCodeISO3, disasterType); + } + private async sendNotiFicationsActiveEvents( + disasterType: DisasterType, + countryCodeISO3: string, + date?: Date, + ): Promise { const events = await this.eventService.getEventSummary( countryCodeISO3, disasterType, ); - - const activeEvents: EventSummaryCountry[] = []; - let finishedEvent: EventSummaryCountry; // This is now for floods only, so can only be 1 event, so not an array + const activeNotifiableEvents: EventSummaryCountry[] = []; for await (const event of events) { if ( - await this.getNotifiableActiveEvent( - event, - disasterType, - countryCodeISO3, - ) + await this.isNotifiableActiveEvent(event, disasterType, countryCodeISO3) ) { - activeEvents.push(event); - } else if (this.getFinishedEvent(event, disasterType, date)) { - finishedEvent = event; + activeNotifiableEvents.push(event); } } - if (activeEvents.length) { + + if (activeNotifiableEvents.length) { const country = await this.notificationContentService.getCountryNotificationInfo( countryCodeISO3, ); - this.emailService.sendTriggerEmail( + await this.emailService.sendTriggerEmail( country, disasterType, - activeEvents, + activeNotifiableEvents, date, ); - if (country.notificationInfo.useWhatsapp[disasterType]) { this.whatsappService.sendTriggerWhatsapp( country, - activeEvents, + activeNotifiableEvents, disasterType, ); } } + } - if (finishedEvent) { + private async sendNotificationsFinishedEvents( + countryCodeISO3: string, + disasterType: DisasterType, + date?: Date, + ): Promise { + const finishedNotifiableEvents = + await this.eventService.getEventsSummaryTriggerFinishedMail( + countryCodeISO3, + disasterType, + ); + + if (finishedNotifiableEvents.length > 0) { const country = await this.notificationContentService.getCountryNotificationInfo( countryCodeISO3, ); - this.emailService.sendTriggerFinishedEmail( + await this.emailService.sendTriggerFinishedEmail( country, disasterType, - finishedEvent, + finishedNotifiableEvents, date, ); if (country.notificationInfo.useWhatsapp[disasterType]) { - this.whatsappService.sendTriggerFinishedWhatsapp( - country, - finishedEvent, - disasterType, - ); - } - } - } - - private getFinishedEvent( - event: EventSummaryCountry, - disasterType: DisasterType, - uploadDate?: Date, - ) { - // For now only do this for floods - if (disasterType === DisasterType.Floods) { - const date = uploadDate ? new Date(uploadDate) : new Date(); - const yesterdayActiveDate = new Date(date.setDate(date.getDate() + 6)); // determine yesterday still active events by endDate lying (7 - 1) days in the future - if ( - new Date(event.endDate) >= - new Date(yesterdayActiveDate.setHours(0, 0, 0, 0)) - ) { - return true; + // TODO: Send one whatsapp message for all closing events + for (const event of finishedNotifiableEvents) { + await this.whatsappService.sendTriggerFinishedWhatsapp( + country, + event, + disasterType, + ); + } } } - return false; } - private async getNotifiableActiveEvent( + private async isNotifiableActiveEvent( event: EventSummaryCountry, disasterType: DisasterType, countryCodeISO3: string, diff --git a/services/API-service/src/api/notification/whatsapp/whatsapp.service.ts b/services/API-service/src/api/notification/whatsapp/whatsapp.service.ts index 7bbc72ec0..b71ea3f2d 100644 --- a/services/API-service/src/api/notification/whatsapp/whatsapp.service.ts +++ b/services/API-service/src/api/notification/whatsapp/whatsapp.service.ts @@ -16,7 +16,6 @@ import { TwilioStatusCallbackDto, } from './twilio.dto'; import { NotificationType, TwilioMessageEntity } from './twilio.entity'; -import { EmailTemplateService } from '../email/email-template.service'; import { formatActionUnitValue } from '../helpers/format-action-unit-value.helper'; @Injectable() @@ -89,7 +88,7 @@ export class WhatsappService { ? 'trigger' : 'warning'; const startTimeEvent = - await this.notificationContentService.getStartTimeEvent( + await this.notificationContentService.getFirstLeadTimeString( activeEvents[0], country.countryCodeISO3, disasterType, @@ -107,7 +106,7 @@ export class WhatsappService { ]; const startTimeFirstEvent = - await this.notificationContentService.getStartTimeEvent( + await this.notificationContentService.getFirstLeadTimeString( activeEvents[0], country.countryCodeISO3, disasterType, @@ -385,7 +384,7 @@ export class WhatsappService { } const startTimeEvent = - await this.notificationContentService.getStartTimeEvent( + await this.notificationContentService.getFirstLeadTimeString( event, country.countryCodeISO3, disasterType, From 5af3d114828f40036fc0759ad994d5995fe944f2 Mon Sep 17 00:00:00 2001 From: Ruben Date: Thu, 25 Apr 2024 17:11:09 +0200 Subject: [PATCH 18/55] AB#27285 Implement severity of warning --- .../dto/station-forecast.dto.ts | 3 +- .../dto/notification-date-per-event.dto.ts | 3 +- .../email/email-template.service.ts | 90 ++++++++----------- .../email/html/email-body-trigger-event.html | 4 +- .../email/html/email-body-warning-event.html | 4 +- .../email/html/email-table-event.html | 3 +- .../notification-content.service.ts | 2 +- services/API-service/src/shared/data.model.ts | 24 +++-- 8 files changed, 66 insertions(+), 67 deletions(-) diff --git a/services/API-service/src/api/glofas-station/dto/station-forecast.dto.ts b/services/API-service/src/api/glofas-station/dto/station-forecast.dto.ts index fc3e96d6e..1cdc7b59c 100644 --- a/services/API-service/src/api/glofas-station/dto/station-forecast.dto.ts +++ b/services/API-service/src/api/glofas-station/dto/station-forecast.dto.ts @@ -6,6 +6,7 @@ import { IsString, } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { EapAlertClassKeyEnum } from '../../../shared/data.model'; export class GlofasStationForecastDto { @ApiProperty({ example: 'G1374' }) @@ -20,7 +21,7 @@ export class GlofasStationForecastDto { @ApiProperty({ example: 1 }) @IsNotEmpty() - @IsIn(['no', 'min', 'med', 'max']) + @IsIn(Object.values(EapAlertClassKeyEnum)) public eapAlertClass: string; @ApiProperty({ example: 10 }) 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 33f17e713..4fde261a4 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,4 +1,4 @@ -import { TriggeredArea } from '../../../shared/data.model'; +import { EapAlertClass, TriggeredArea } from '../../../shared/data.model'; import { LeadTime } from '../../admin-area-dynamic-data/enum/lead-time.enum'; export class NotificationDataPerEventDto { @@ -12,6 +12,7 @@ export class NotificationDataPerEventDto { totalAffectectedOfIndicator: number; mapImage?: Buffer; issuedDate: Date; + eapAlertClass: EapAlertClass; } export enum TriggerStatusLabelEnum { diff --git a/services/API-service/src/api/notification/email/email-template.service.ts b/services/API-service/src/api/notification/email/email-template.service.ts index ab6eeb013..405428817 100644 --- a/services/API-service/src/api/notification/email/email-template.service.ts +++ b/services/API-service/src/api/notification/email/email-template.service.ts @@ -9,7 +9,10 @@ import * as ejs from 'ejs'; import * as fs from 'fs'; import { CountryTimeZoneMapping } from '../../country/country-time-zone-mapping'; import { DisasterType } from '../../disaster/disaster-type.enum'; -import { EventSummaryCountry } from '../../../shared/data.model'; +import { + EapAlertClassKeyEnum, + EventSummaryCountry, +} from '../../../shared/data.model'; import { CountryEntity } from '../../country/country.entity'; const emailFolder = './src/api/notification/email'; @@ -23,8 +26,6 @@ class ReplaceKeyValue { @Injectable() export class EmailTemplateService { - private placeholderToday = '(TODAY)'; - public createHtmlForTriggerEmail( emailContent: ContentEventEmail, date: Date, @@ -77,14 +78,6 @@ export class EmailTemplateService { replaceKey: 'tablesStacked', replaceValue: this.getTablesForEvents(emailContent), }, - { - replaceKey: this.placeholderToday, - replaceValue: date.toLocaleDateString('default', { - day: '2-digit', - month: 'short', - year: 'numeric', - }), - }, { replaceKey: 'eventListBody', replaceValue: this.getEventListBody(emailContent), @@ -195,14 +188,6 @@ export class EmailTemplateService { replaceKey: 'disasterType', replaceValue: disasterTypeLabel, }, - { - replaceKey: this.placeholderToday, - replaceValue: date.toLocaleDateString('default', { - day: '2-digit', - month: 'short', - year: 'numeric', - }), - }, ]; return keyValueReplaceList; } @@ -395,34 +380,11 @@ export class EmailTemplateService { return emailHtml; } - // TODO refactor this to use ejs package to render the html - private getEventListShort( - dataPerEvent: NotificationDataPerEventDto[], - ): string { - let text = ''; - for (const event of dataPerEvent) { - const leadTimeString = event.disasterSpecificCopy.leadTimeString - ? event.disasterSpecificCopy.leadTimeString - : event.firstLeadTime; - const timestamp = event.disasterSpecificCopy.timestamp - ? ` at ${event.disasterSpecificCopy.timestamp}` - : ''; - text += `${event.triggerStatusLabel} for ${event.eventName}: ${ - event.disasterSpecificCopy.extraInfo || - event.firstLeadTime === LeadTime.hour0 - ? leadTimeString - : `${event.firstLeadTime}${timestamp}` - }
`; - } - return text; - } - private getTablesForEvents(emailContent: ContentEventEmail): string { const adminAreaLabelsParent = emailContent.country.adminRegionLabels[ String(emailContent.defaultAdminLevel - 1) ]; - return emailContent.dataPerEvent .map((event) => { const data = { @@ -438,12 +400,10 @@ export class EmailTemplateService { defaultAdminAreaLabelParent: adminAreaLabelsParent.singular, indicatorLabel: emailContent.indicatorMetadata.label, indicatorUnit: emailContent.indicatorMetadata.unit, - triangleIcon: this.getTriangleIcon(event.triggerStatusLabel), + triangleIcon: this.getTriangleIcon(event.eapAlertClass?.key), tableRows: this.getTablesRows(event), - color: - event.triggerStatusLabel === TriggerStatusLabelEnum.Trigger - ? '#940000' - : '#da7c00', + color: this.ibfColorToHex(event.eapAlertClass?.color), + severityLabel: this.getEventSeverityLabel(event.eapAlertClass?.key), }; const templatePath = `${emailTemplateFolder}/email-table-event.html`; @@ -456,6 +416,16 @@ export class EmailTemplateService { .join(''); } + private getEventSeverityLabel(eapAlertClassKey: EapAlertClassKeyEnum) { + if (eapAlertClassKey === EapAlertClassKeyEnum.med) { + return 'Medium'; + } else if (eapAlertClassKey === EapAlertClassKeyEnum.min) { + return 'Low'; + } else { + return ''; + } + } + private getTablesRows(event: NotificationDataPerEventDto) { return event.triggeredAreas .map((area) => { @@ -502,8 +472,10 @@ export class EmailTemplateService { ), timezone: CountryTimeZoneMapping[emailContent.country.countryCodeISO3], - triangleIcon: this.getTriangleIcon(event.triggerStatusLabel), + triangleIcon: this.getTriangleIcon(event.eapAlertClass?.key), leadTime: event.firstLeadTime.replace('-', ' '), + disasterIssuedLabel: event.eapAlertClass.label, + color: this.ibfColorToHex(event.eapAlertClass?.color), }; const templatePath = @@ -518,6 +490,18 @@ export class EmailTemplateService { .join(''); } + private ibfColorToHex(color: string): string { + // TODO: Define in a place where FrontEnd and Backend can share this + switch (color) { + case 'ibf-orange': + return '#f0890d'; + case 'ibf-yellow': + return '#fff500'; + default: + return '#c70000'; + } + } + private getCurrentDateTimeString(countryCodeISO3: string): string { const date = new Date(); return this.dateObjectToDateTimeString(date, countryCodeISO3); @@ -540,13 +524,15 @@ export class EmailTemplateService { return date.toLocaleString('default', options); } - private getTriangleIcon(triggerStatusLabel) { + private getTriangleIcon(eapAlertClassKey: EapAlertClassKeyEnum) { let fileName = ''; // Still need implement the difference between medium and low warning - if (triggerStatusLabel === TriggerStatusLabelEnum.Trigger) { - fileName = 'trigger.png'; - } else { + if (eapAlertClassKey === EapAlertClassKeyEnum.med) { fileName = 'warning-medium.png'; + } else if (eapAlertClassKey === EapAlertClassKeyEnum.min) { + fileName = 'warning-low.png'; + } else { + fileName = 'trigger.png'; } const filePath = `${emailIconFolder}/${fileName}`; const imageDataURL = this.getPngImageAsDataURL(filePath); diff --git a/services/API-service/src/api/notification/email/html/email-body-trigger-event.html b/services/API-service/src/api/notification/email/html/email-body-trigger-event.html index 456687ad8..084bb3cc9 100644 --- a/services/API-service/src/api/notification/email/html/email-body-trigger-event.html +++ b/services/API-service/src/api/notification/email/html/email-body-trigger-event.html @@ -1,7 +1,7 @@
Trigger expected on:<%= disasterIssuedLabel %>: Warning expected on:<%= disasterIssuedLabel %> Warning icon - <%= triggerStatusLabel %> <%= hazard %>: <%= eventName %> + <%= severityLabel %> <%= triggerStatusLabel %> <%= hazard %>: <%= eventName + %>
Early Action Protocol diff --git a/services/API-service/src/api/notification/email/html/advisory-warning.html b/services/API-service/src/api/notification/email/html/advisory-warning.html new file mode 100644 index 000000000..90204931d --- /dev/null +++ b/services/API-service/src/api/notification/email/html/advisory-warning.html @@ -0,0 +1 @@ +Inform all potentially exposed districts diff --git a/services/API-service/src/api/notification/email/html/body-warning-event.html b/services/API-service/src/api/notification/email/html/body-event.html similarity index 68% rename from services/API-service/src/api/notification/email/html/body-warning-event.html rename to services/API-service/src/api/notification/email/html/body-event.html index ca5203753..b03ae3396 100644 --- a/services/API-service/src/api/notification/email/html/body-warning-event.html +++ b/services/API-service/src/api/notification/email/html/body-event.html @@ -9,9 +9,10 @@ word-wrap: break-word; " > - Warning icon - <%= hazard %> <%= eventName %>
+ <%= hazard %> <%= eventName %>
+
+ <%= disasterIssuedLabel %> + <%= disasterIssuedLabel %> + + - <%= startDateEventString %>, <%= leadTime %>s from now.
, <%= leadTime %>s from now.
+
+ Expected exposed <%= defaulAdminAreaLabel %>: + Expected exposed <%= defaulAdminAreaLabel %>: + + - <%= nrOfTriggeredAreas %> (see list below)
(see list below)
+
+ - <%= indicatorLabel %>: : + + Information regarding exposed population not available for warnings
+ <%- totalAffected %> + + Advisory: - Inform all potentially exposed districts
+
+ <%- advisory %>
+
+ This warning was issued by the IBF portal on <%= issuedDate %> (<%= - timezone %>) -
+
+ This <%= triggerStatusLabel %> was issued by the IBF portal on <%= + issuedDate %> (<%= timezone %>)
+
diff --git a/services/API-service/src/api/notification/email/html/body-total-affected-trigger.html b/services/API-service/src/api/notification/email/html/body-total-affected-trigger.html new file mode 100644 index 000000000..8849930fe --- /dev/null +++ b/services/API-service/src/api/notification/email/html/body-total-affected-trigger.html @@ -0,0 +1,11 @@ +<%= totalAffectectedOfIndicator %> <%= indicatorUnit %>
diff --git a/services/API-service/src/api/notification/email/html/body-total-affected-warning.html b/services/API-service/src/api/notification/email/html/body-total-affected-warning.html new file mode 100644 index 000000000..7ab140eb2 --- /dev/null +++ b/services/API-service/src/api/notification/email/html/body-total-affected-warning.html @@ -0,0 +1 @@ +Information regarding exposed population not available for warnings
diff --git a/services/API-service/src/api/notification/email/html/body-trigger-event.html b/services/API-service/src/api/notification/email/html/body-trigger-event.html deleted file mode 100644 index 084bb3cc9..000000000 --- a/services/API-service/src/api/notification/email/html/body-trigger-event.html +++ /dev/null @@ -1,132 +0,0 @@ -
- - Warning icon - <%= hazard %> <%= eventName %>
<%= disasterIssuedLabel %>: - <%= startDateEventString %>, <%= leadTime %>s from now.
Expected exposed <%= defaulAdminAreaLabel %>: - <%= nrOfTriggeredAreas %> (see list below)
- <%= indicatorLabel %>: <%= totalAffectectedOfIndicator %> <%= indicatorUnit %>
Advisory: Activate - Early Action Protocol

This trigger was issued by the IBF portal on <%= issuedDate %> (<%= - timezone %>) -
-
From d9be60d5505f0ea7c042e743dab3739df6d6a5f6 Mon Sep 17 00:00:00 2001 From: Ruben Date: Thu, 2 May 2024 14:04:46 +0200 Subject: [PATCH 21/55] AB#27182 Do not filter out action value 0 of warning flood email --- .../notification-content.service.ts | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) 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 f2d4bb1d0..59c2d335f 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 @@ -165,6 +165,8 @@ export class NotificationContentService { ); data.nrOfTriggeredAreas = await this.getNrOfTriggeredAreas( data.triggeredAreas, + data.triggerStatusLabel, + disasterType, ); // 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); @@ -187,14 +189,24 @@ export class NotificationContentService { private async getNrOfTriggeredAreas( triggeredAreas: TriggeredArea[], + statusLabel: TriggerStatusLabelEnum, + disasterType: DisasterType, ): Promise { // This filters out the areas that are affected by the event but do not have any affect action units // Affected action units are for example people_affected, houses_affected, etc (differs per disaster type) // We are not sure why this is done, but it is done in the original code - const triggeredAreaWithAffectedActionUnit = triggeredAreas.filter( - (a) => a.actionsValue > 0, - ); - return triggeredAreaWithAffectedActionUnit.length; + // For warning flood events this is not done, because there are no flood extens for warning events so we do not know any actions values + if ( + disasterType === DisasterType.Floods && + statusLabel === TriggerStatusLabelEnum.Warning + ) { + return triggeredAreas.length; + } else { + const triggeredAreasWithoutActionValue = triggeredAreas.filter( + (a) => a.actionsValue > 0, + ); + return triggeredAreasWithoutActionValue.length; + } } private async getSortedTriggeredAreas( From 84039a224f7b0450b29ec61a33f63aef3d58eabb Mon Sep 17 00:00:00 2001 From: Ruben Date: Thu, 2 May 2024 14:59:20 +0200 Subject: [PATCH 22/55] Improved styling and moved to seperate file (styles.ejs) AB#27502 --- .../email/email-template.service.ts | 14 +- .../email/html/advisory-trigger.html | 6 +- .../src/api/notification/email/html/base.html | 4 +- .../notification/email/html/body-event.html | 138 +++--------------- .../html/body-total-affected-trigger.html | 15 +- .../email/html/event-finished.html | 48 ++---- .../api/notification/email/html/header.html | 9 +- .../api/notification/email/html/styles.ejs | 83 +++++++++++ .../notification/email/html/table-event.html | 96 +++--------- .../email/html/table-trigger-row.html | 34 +---- .../email/html/table-warning-row.html | 21 +-- .../email/html/trigger-finished.html | 2 +- .../email/html/trigger-notification.html | 15 +- 13 files changed, 170 insertions(+), 315 deletions(-) create mode 100644 services/API-service/src/api/notification/email/html/styles.ejs diff --git a/services/API-service/src/api/notification/email/email-template.service.ts b/services/API-service/src/api/notification/email/email-template.service.ts index 083ca869b..ac54718ff 100644 --- a/services/API-service/src/api/notification/email/email-template.service.ts +++ b/services/API-service/src/api/notification/email/email-template.service.ts @@ -190,6 +190,7 @@ export class EmailTemplateService { } private getEmailBody(triggerFinished: boolean): string { + // TODO: Also apply new EJS style templating to these files if (triggerFinished) { return this.readHtmlFile('trigger-finished.html'); } else { @@ -335,7 +336,10 @@ export class EmailTemplateService { } private formatEmail(emailKeyValueReplaceList: ReplaceKeyValue[]): string { + // TODO REFACTOR: Apply styles in a septerate file also for the base.html const template = this.readHtmlFile('base.html'); + const styles = this.readHtmlFile('styles.ejs'); + const templateWithStyle = styles + template; const replacements = emailKeyValueReplaceList.reduce( (acc, { replaceKey, replaceValue }) => { acc[replaceKey] = replaceValue; @@ -344,7 +348,7 @@ export class EmailTemplateService { {}, ); - let emailHtml = template; + let emailHtml = templateWithStyle; let previousHtml = null; // This loop is needed to handle nested EJS tags. It repeatedly renders the template @@ -428,7 +432,6 @@ export class EmailTemplateService { eventName: event.eventName, nrOfTriggeredAreas: event.nrOfTriggeredAreas, expectedTriggerDate: event.firstLeadTime, - expectedExposedAdminBoundary: event.nrOfTriggeredAreas, issuedDate: this.dateObjectToDateTimeString( event.issuedDate, emailContent.country.countryCodeISO3, @@ -454,7 +457,6 @@ export class EmailTemplateService { const templateFileName = 'body-event.html'; let template = this.readHtmlFile(templateFileName); - return ejs.render(template, data); }) .join(''); @@ -485,11 +487,11 @@ export class EmailTemplateService { // TODO: Define in a place where FrontEnd and Backend can share this switch (color) { case 'ibf-orange': - return '#f0890d'; + return '#aa6009'; case 'ibf-yellow': - return '#fff500'; + return '#7d6906'; default: - return '#c70000'; + return '#8a0f32'; } } diff --git a/services/API-service/src/api/notification/email/html/advisory-trigger.html b/services/API-service/src/api/notification/email/html/advisory-trigger.html index 40fdbbdd8..568dee3c8 100644 --- a/services/API-service/src/api/notification/email/html/advisory-trigger.html +++ b/services/API-service/src/api/notification/email/html/advisory-trigger.html @@ -1,2 +1,4 @@ -Activate Early Action Protocol +Activate + + Early Action Protocol diff --git a/services/API-service/src/api/notification/email/html/base.html b/services/API-service/src/api/notification/email/html/base.html index ca6072f8f..0a21277ce 100644 --- a/services/API-service/src/api/notification/email/html/base.html +++ b/services/API-service/src/api/notification/email/html/base.html @@ -397,7 +397,7 @@ #templateBody { /*@editable*/ - background-color: white; + background-color: #f5f5f594; /*@editable*/ background-image: none; /*@editable*/ @@ -564,7 +564,7 @@ @media only screen and (min-width: 768px) { .templateContainer { - width: 600px !important; + width: 800px !important; } } diff --git a/services/API-service/src/api/notification/email/html/body-event.html b/services/API-service/src/api/notification/email/html/body-event.html index b03ae3396..c5a33daec 100644 --- a/services/API-service/src/api/notification/email/html/body-event.html +++ b/services/API-service/src/api/notification/email/html/body-event.html @@ -1,125 +1,33 @@
- - triangleIcon - <%= hazard %> <%= eventName %>
+ + triangleIcon + <%= hazard %> <%= eventName %> + - - <%= disasterIssuedLabel %> + <%= disasterIssuedLabel %> + + <%= startDateEventString %>, <%= leadTime %>s from now. + - - <%= startDateEventString %>, <%= leadTime %>s from now.
-
- + Expected exposed <%= defaulAdminAreaLabel %>: - - <%= nrOfTriggeredAreas %> (see list below)
-
- - <%= indicatorLabel %>: - - - <%- totalAffected %> - - - Advisory: + + <%= nrOfTriggeredAreas %> (see list below) + - - <%- advisory %>
+ <%= indicatorLabel %>: + <%- totalAffected %> + Advisory: + + <%- advisory %> + - -
+
+ This <%= triggerStatusLabel %> was issued by the IBF portal on <%= - issuedDate %> (<%= timezone %>)
+ issuedDate %> (<%= timezone %>) +
+
diff --git a/services/API-service/src/api/notification/email/html/body-total-affected-trigger.html b/services/API-service/src/api/notification/email/html/body-total-affected-trigger.html index 8849930fe..f5e2bbe56 100644 --- a/services/API-service/src/api/notification/email/html/body-total-affected-trigger.html +++ b/services/API-service/src/api/notification/email/html/body-total-affected-trigger.html @@ -1,11 +1,4 @@ -<%= totalAffectectedOfIndicator %> <%= indicatorUnit %>
+ + <%= totalAffectectedOfIndicator %> <%= indicatorUnit %> + + diff --git a/services/API-service/src/api/notification/email/html/event-finished.html b/services/API-service/src/api/notification/email/html/event-finished.html index 7a607dbe3..12e17e678 100644 --- a/services/API-service/src/api/notification/email/html/event-finished.html +++ b/services/API-service/src/api/notification/email/html/event-finished.html @@ -1,39 +1,15 @@
- - <%= disasterTypeLabel %>: <%= eventName %> is now below threshold
+ + <%= disasterTypeLabel %>: <%= eventName %> is now below threshold + + + The events actions will continue to show on the IBF portal and can still be - managed.
-
This warning was issued by the IBF portal on <%= issuedDate %> (<%= - timezone %>) + managed. + + + + This warning was issued by the IBF portal on <%= issuedDate %> (<%= timezone + %>) +
diff --git a/services/API-service/src/api/notification/email/html/header.html b/services/API-service/src/api/notification/email/html/header.html index 5a5a87a43..1e75a6a6e 100644 --- a/services/API-service/src/api/notification/email/html/header.html +++ b/services/API-service/src/api/notification/email/html/header.html @@ -4,14 +4,7 @@

<%= nrOfEvents %> <%= disasterLabel %> alerts

IBF alert send on <%= sentOnDate %> (<%= timezone %>) diff --git a/services/API-service/src/api/notification/email/html/styles.ejs b/services/API-service/src/api/notification/email/html/styles.ejs new file mode 100644 index 000000000..cd532590e --- /dev/null +++ b/services/API-service/src/api/notification/email/html/styles.ejs @@ -0,0 +1,83 @@ + diff --git a/services/API-service/src/api/notification/email/html/table-event.html b/services/API-service/src/api/notification/email/html/table-event.html index 6c0cb02a5..6f87e4fdd 100644 --- a/services/API-service/src/api/notification/email/html/table-event.html +++ b/services/API-service/src/api/notification/email/html/table-event.html @@ -1,91 +1,34 @@ -
+
- Warning icon + Warning icon <%= severityLabel %> <%= triggerStatusLabel %> <%= hazard %>: <%= eventName %>
Expected exposed <%= defaulAdminAreaLabelPlural %><% if (triggerStatusLabel === 'Trigger') { %> in order of <%= indicatorLabel.toLowerCase() %><% } %>:
-
+
- + <% if (triggerStatusLabel === 'Trigger') { %> - <% } %> - @@ -94,4 +37,13 @@
+ <%= indicatorLabel %> + <%= defaulAdminAreaLabelSingular %> (<%= defaultAdminAreaLabelParent %>)
+ <% if (triggerStatusLabel === 'Warning') { %> +
+ Please note: Information regarding exposed population not available for + medium warning level. +
+ <% } %> +
diff --git a/services/API-service/src/api/notification/email/html/table-trigger-row.html b/services/API-service/src/api/notification/email/html/table-trigger-row.html index 38428b98e..b2881de88 100644 --- a/services/API-service/src/api/notification/email/html/table-trigger-row.html +++ b/services/API-service/src/api/notification/email/html/table-trigger-row.html @@ -1,36 +1,8 @@ - - + + <%= affectectedOfIndicator %> - + <%= adminBoundary %> (<%= higherAdminBoundary %>) diff --git a/services/API-service/src/api/notification/email/html/table-warning-row.html b/services/API-service/src/api/notification/email/html/table-warning-row.html index 85d41824e..a26f8d8ea 100644 --- a/services/API-service/src/api/notification/email/html/table-warning-row.html +++ b/services/API-service/src/api/notification/email/html/table-warning-row.html @@ -1,22 +1,5 @@ - - + + <%= adminBoundary %> (<%= higherAdminBoundary %>) diff --git a/services/API-service/src/api/notification/email/html/trigger-finished.html b/services/API-service/src/api/notification/email/html/trigger-finished.html index 5df98542b..0fd4f6d22 100644 --- a/services/API-service/src/api/notification/email/html/trigger-finished.html +++ b/services/API-service/src/api/notification/email/html/trigger-finished.html @@ -83,7 +83,7 @@ id="Editable_Content_Areas" >
- Dear reader, +
Dear Reader,


<%- eventOverview %> diff --git a/services/API-service/src/api/notification/email/html/trigger-notification.html b/services/API-service/src/api/notification/email/html/trigger-notification.html index d494e470c..f168c01cb 100644 --- a/services/API-service/src/api/notification/email/html/trigger-notification.html +++ b/services/API-service/src/api/notification/email/html/trigger-notification.html @@ -83,18 +83,7 @@ id="Editable_Content_Areas" >
-
- Dear Reader, -
+
Dear Reader,


<%- eventListBody %> @@ -140,6 +129,7 @@ border-radius: 3px; " align="left" + class="body-text" > Find more info on the potentially exposed areas on a map and manage @@ -192,6 +182,7 @@ border-radius: 3px; " align="left" + class="body-text" > Read about the trigger methodology and the anticipatory actions. From 6ab3257f0014072b8c73276c1d7c80e668f80123 Mon Sep 17 00:00:00 2001 From: Ruben Date: Thu, 2 May 2024 15:05:09 +0200 Subject: [PATCH 23/55] Tad of padding AB#27502 --- .../src/api/notification/email/html/table-event.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/API-service/src/api/notification/email/html/table-event.html b/services/API-service/src/api/notification/email/html/table-event.html index 6f87e4fdd..00790d217 100644 --- a/services/API-service/src/api/notification/email/html/table-event.html +++ b/services/API-service/src/api/notification/email/html/table-event.html @@ -13,12 +13,14 @@ <%= severityLabel %> <%= triggerStatusLabel %> <%= hazard %>: <%= eventName %> +
Expected exposed <%= defaulAdminAreaLabelPlural %><% if (triggerStatusLabel === 'Trigger') { %> in order of <%= indicatorLabel.toLowerCase() %><% } %>:
+
From 2182b4bc2eaa4cb987c0b7874a5e30e61c28f56e Mon Sep 17 00:00:00 2001 From: Ruben Date: Fri, 3 May 2024 14:16:11 +0200 Subject: [PATCH 24/55] AB#27502 Added juice to convert styles to inline html to support different email clients and browsers --- services/API-service/package-lock.json | 626 ++++++++++++++++++ services/API-service/package.json | 1 + .../email/email-template.service.ts | 29 +- .../api/notification/email/email.service.ts | 4 +- .../src/api/notification/email/html/base.html | 2 +- .../api/notification/email/html/styles.ejs | 10 +- .../notification/email/html/table-event.html | 106 +-- 7 files changed, 711 insertions(+), 67 deletions(-) diff --git a/services/API-service/package-lock.json b/services/API-service/package-lock.json index d59cf5c49..2a926b0d3 100644 --- a/services/API-service/package-lock.json +++ b/services/API-service/package-lock.json @@ -21,6 +21,7 @@ "csv-parser": "^3.0.0", "ejs": "^3.1.9", "jsonwebtoken": "^8.1.1", + "juice": "^10.0.0", "mailchimp-api-v3": "^1.15.0", "mysql": "^2.15.0", "passport": "^0.4.1", @@ -2884,6 +2885,14 @@ "node": ">=6" } }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -3351,6 +3360,11 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, "node_modules/boxen": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", @@ -3680,6 +3694,65 @@ "node": ">=16" } }, + "node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio/node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "dependencies": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/chokidar": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.3.tgz", @@ -3952,6 +4025,14 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "engines": { + "node": ">= 6" + } + }, "node_modules/component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -4227,6 +4308,37 @@ "node": ">=8" } }, + "node_modules/cryptr": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/cryptr/-/cryptr-6.0.2.tgz", + "integrity": "sha512-1TRHI4bmuLIB8WgkH9eeYXzhEg1T4tonO4vVaMBKKde8Dre51J68nAgTVXTwMYXAf7+mV2gBCkm/9wksjSb2sA==" + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/cssom": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", @@ -4507,6 +4619,30 @@ "node": ">=6.0.0" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, "node_modules/domexception": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", @@ -4516,6 +4652,33 @@ "webidl-conversions": "^4.0.2" } }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dot-prop": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", @@ -4609,6 +4772,17 @@ "once": "^1.4.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -6230,6 +6404,24 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/http-cache-semantics": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", @@ -9526,6 +9718,24 @@ "verror": "1.10.0" } }, + "node_modules/juice": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/juice/-/juice-10.0.0.tgz", + "integrity": "sha512-9f68xmhGrnIi6DBkiiP3rUrQN33SEuaKu1+njX6VgMP+jwZAsnT33WIzlrWICL9matkhYu3OyrqSUP55YTIdGg==", + "dependencies": { + "cheerio": "^1.0.0-rc.12", + "commander": "^6.1.0", + "mensch": "^0.3.4", + "slick": "^1.12.2", + "web-resource-inliner": "^6.0.1" + }, + "bin": { + "juice": "bin/juice" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -9840,6 +10050,11 @@ "node": ">= 0.6" } }, + "node_modules/mensch": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/mensch/-/mensch-0.3.4.tgz", + "integrity": "sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==" + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -10307,6 +10522,17 @@ "node": ">=8" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/nwsapi": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", @@ -11963,6 +12189,14 @@ "node": ">=8" } }, + "node_modules/slick": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz", + "integrity": "sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==", + "engines": { + "node": "*" + } + }, "node_modules/slug": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/slug/-/slug-4.0.2.tgz", @@ -13667,6 +13901,14 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "devOptional": true }, + "node_modules/valid-data-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz", + "integrity": "sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==", + "engines": { + "node": ">=10" + } + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -13729,6 +13971,134 @@ "resolved": "https://registry.npmjs.org/weak-map/-/weak-map-1.0.8.tgz", "integrity": "sha512-lNR9aAefbGPpHO7AEnY0hCFjz1eTkWCXYvkTRrTHs9qv8zJp+SkVYpzfLIFXQQiG3tVvbNFQgVg2bQS8YGgxyw==" }, + "node_modules/web-resource-inliner": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-6.0.1.tgz", + "integrity": "sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==", + "dependencies": { + "ansi-colors": "^4.1.1", + "escape-goat": "^3.0.0", + "htmlparser2": "^5.0.0", + "mime": "^2.4.6", + "node-fetch": "^2.6.0", + "valid-data-url": "^3.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/web-resource-inliner/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/dom-serializer/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/domhandler": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", + "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", + "dependencies": { + "domelementtype": "^2.0.1" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/domutils/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/escape-goat": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz", + "integrity": "sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/web-resource-inliner/node_modules/htmlparser2": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-5.0.1.tgz", + "integrity": "sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^3.3.0", + "domutils": "^2.4.2", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/fb55/htmlparser2?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", @@ -16125,6 +16495,11 @@ } } }, + "ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==" + }, "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -16509,6 +16884,11 @@ "unpipe": "1.0.0" } }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, "boxen": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", @@ -16775,6 +17155,52 @@ "resolved": "https://registry.npmjs.org/check-disk-space/-/check-disk-space-3.4.0.tgz", "integrity": "sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw==" }, + "cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "requires": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "dependencies": { + "parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "requires": { + "entities": "^4.4.0" + } + }, + "parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "requires": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + } + } + } + }, + "cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "requires": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + } + }, "chokidar": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.3.tgz", @@ -16992,6 +17418,11 @@ "delayed-stream": "~1.0.0" } }, + "commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==" + }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -17213,6 +17644,28 @@ "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", "dev": true }, + "cryptr": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/cryptr/-/cryptr-6.0.2.tgz", + "integrity": "sha512-1TRHI4bmuLIB8WgkH9eeYXzhEg1T4tonO4vVaMBKKde8Dre51J68nAgTVXTwMYXAf7+mV2gBCkm/9wksjSb2sA==" + }, + "css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + } + }, + "css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==" + }, "cssom": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", @@ -17429,6 +17882,21 @@ "esutils": "^2.0.2" } }, + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" + }, "domexception": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", @@ -17438,6 +17906,24 @@ "webidl-conversions": "^4.0.2" } }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, "dot-prop": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", @@ -17513,6 +17999,11 @@ "once": "^1.4.0" } }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -18760,6 +19251,17 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "http-cache-semantics": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", @@ -21513,6 +22015,18 @@ "verror": "1.10.0" } }, + "juice": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/juice/-/juice-10.0.0.tgz", + "integrity": "sha512-9f68xmhGrnIi6DBkiiP3rUrQN33SEuaKu1+njX6VgMP+jwZAsnT33WIzlrWICL9matkhYu3OyrqSUP55YTIdGg==", + "requires": { + "cheerio": "^1.0.0-rc.12", + "commander": "^6.1.0", + "mensch": "^0.3.4", + "slick": "^1.12.2", + "web-resource-inliner": "^6.0.1" + } + }, "jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -21780,6 +22294,11 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" }, + "mensch": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/mensch/-/mensch-0.3.4.tgz", + "integrity": "sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==" + }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -22175,6 +22694,14 @@ "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", "dev": true }, + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "requires": { + "boolbase": "^1.0.0" + } + }, "nwsapi": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", @@ -23481,6 +24008,11 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, + "slick": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz", + "integrity": "sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==" + }, "slug": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/slug/-/slug-4.0.2.tgz", @@ -24757,6 +25289,11 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "devOptional": true }, + "valid-data-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz", + "integrity": "sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==" + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -24810,6 +25347,95 @@ "resolved": "https://registry.npmjs.org/weak-map/-/weak-map-1.0.8.tgz", "integrity": "sha512-lNR9aAefbGPpHO7AEnY0hCFjz1eTkWCXYvkTRrTHs9qv8zJp+SkVYpzfLIFXQQiG3tVvbNFQgVg2bQS8YGgxyw==" }, + "web-resource-inliner": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-6.0.1.tgz", + "integrity": "sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==", + "requires": { + "ansi-colors": "^4.1.1", + "escape-goat": "^3.0.0", + "htmlparser2": "^5.0.0", + "mime": "^2.4.6", + "node-fetch": "^2.6.0", + "valid-data-url": "^3.0.0" + }, + "dependencies": { + "dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "dependencies": { + "domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "requires": { + "domelementtype": "^2.2.0" + } + } + } + }, + "domhandler": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", + "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", + "requires": { + "domelementtype": "^2.0.1" + } + }, + "domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "dependencies": { + "domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "requires": { + "domelementtype": "^2.2.0" + } + } + } + }, + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" + }, + "escape-goat": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz", + "integrity": "sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==" + }, + "htmlparser2": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-5.0.1.tgz", + "integrity": "sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^3.3.0", + "domutils": "^2.4.2", + "entities": "^2.0.0" + } + }, + "mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==" + } + } + }, "webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", diff --git a/services/API-service/package.json b/services/API-service/package.json index 9b6217601..eb169fb6c 100644 --- a/services/API-service/package.json +++ b/services/API-service/package.json @@ -39,6 +39,7 @@ "csv-parser": "^3.0.0", "ejs": "^3.1.9", "jsonwebtoken": "^8.1.1", + "juice": "^10.0.0", "mailchimp-api-v3": "^1.15.0", "mysql": "^2.15.0", "passport": "^0.4.1", diff --git a/services/API-service/src/api/notification/email/email-template.service.ts b/services/API-service/src/api/notification/email/email-template.service.ts index ac54718ff..ee8f1f33d 100644 --- a/services/API-service/src/api/notification/email/email-template.service.ts +++ b/services/API-service/src/api/notification/email/email-template.service.ts @@ -13,6 +13,7 @@ import { EventSummaryCountry, } from '../../../shared/data.model'; import { CountryEntity } from '../../country/country.entity'; +import * as juice from 'juice'; const emailFolder = './src/api/notification/email'; const emailTemplateFolder = `${emailFolder}/html`; @@ -25,10 +26,10 @@ class ReplaceKeyValue { @Injectable() export class EmailTemplateService { - public createHtmlForTriggerEmail( + public async createHtmlForTriggerEmail( emailContent: ContentEventEmail, date: Date, - ): string { + ): Promise { const replaceKeyValues = this.createReplaceKeyValuesTrigger( emailContent, date, @@ -37,25 +38,24 @@ export class EmailTemplateService { } // TODO REFACTOR this to use a DTO (ContentTriggerFinishedEmail) instead of multiple parameters - public createHtmlForTriggerFinishedEmail( + public async createHtmlForTriggerFinishedEmail( country: CountryEntity, disasterType: DisasterType, finishedEvents: EventSummaryCountry[], disasterTypeLabel: string, _date: Date, // I am not sure if this is needed and for what it was used before - ): string { + ): Promise { const replaceKeyValues = this.createReplaceKeyValuesTriggerFinished( country, disasterType, finishedEvents, disasterTypeLabel, ); - return this.formatEmail(replaceKeyValues); + return await this.formatEmail(replaceKeyValues); } private createReplaceKeyValuesTrigger( emailContent: ContentEventEmail, - date: Date, ): ReplaceKeyValue[] { const country = emailContent.country; const disasterType = emailContent.disasterType; @@ -335,7 +335,9 @@ export class EmailTemplateService { } } - private formatEmail(emailKeyValueReplaceList: ReplaceKeyValue[]): string { + private async formatEmail( + emailKeyValueReplaceList: ReplaceKeyValue[], + ): Promise { // TODO REFACTOR: Apply styles in a septerate file also for the base.html const template = this.readHtmlFile('base.html'); const styles = this.readHtmlFile('styles.ejs'); @@ -358,8 +360,19 @@ export class EmailTemplateService { previousHtml = emailHtml; emailHtml = ejs.render(previousHtml, replacements); } + // Inline the CSS + const inlinedHtml = await new Promise((resolve, reject) => { + juice.juiceResources(emailHtml, { webResources: {} }, (err, html) => { + if (err) reject(err); + else resolve(html); + }); + }); + fs.writeFile('output.html', inlinedHtml as string, (err) => { + if (err) throw err; + console.log('The file has been saved!'); + }); - return emailHtml; + return inlinedHtml as string; } private getTablesForEvents(emailContent: ContentEventEmail): string { 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 8ec3ea611..2149f6773 100644 --- a/services/API-service/src/api/notification/email/email.service.ts +++ b/services/API-service/src/api/notification/email/email.service.ts @@ -52,7 +52,7 @@ export class EmailService { disasterType, activeEvents, ); - const emailHtml = this.emailTemplateService.createHtmlForTriggerEmail( + const emailHtml = await this.emailTemplateService.createHtmlForTriggerEmail( emailContent, date, ); @@ -76,7 +76,7 @@ export class EmailService { const disasterTypeLabel = await this.notificationContentService.getDisasterTypeLabel(disasterType); const emailHtml = - this.emailTemplateService.createHtmlForTriggerFinishedEmail( + await this.emailTemplateService.createHtmlForTriggerFinishedEmail( country, disasterType, finishedEvents, diff --git a/services/API-service/src/api/notification/email/html/base.html b/services/API-service/src/api/notification/email/html/base.html index 0a21277ce..4980390c6 100644 --- a/services/API-service/src/api/notification/email/html/base.html +++ b/services/API-service/src/api/notification/email/html/base.html @@ -397,7 +397,7 @@ #templateBody { /*@editable*/ - background-color: #f5f5f594; + background-color: #f4f5f8; /*@editable*/ background-image: none; /*@editable*/ diff --git a/services/API-service/src/api/notification/email/html/styles.ejs b/services/API-service/src/api/notification/email/html/styles.ejs index cd532590e..8dc314a29 100644 --- a/services/API-service/src/api/notification/email/html/styles.ejs +++ b/services/API-service/src/api/notification/email/html/styles.ejs @@ -50,16 +50,16 @@ padding-bottom: 4px; border: 1px #f5f6f9 solid; } - .header-row { - justify-content: flex-start; - align-items: center; - gap: 40px; - display: inline-flex; + .header-row td { + vertical-align: middle; + padding-right: 20px; /* adjust as needed */ + padding-left: 20px; /* adjust as needed */ } .table-cell { height: 19px; padding: 2px 10px; width: 211px; + text-align: left; } /* Event table styles */ diff --git a/services/API-service/src/api/notification/email/html/table-event.html b/services/API-service/src/api/notification/email/html/table-event.html index 00790d217..cad642c59 100644 --- a/services/API-service/src/api/notification/email/html/table-event.html +++ b/services/API-service/src/api/notification/email/html/table-event.html @@ -1,51 +1,55 @@ -
-
- Warning icon - <%= severityLabel %> <%= triggerStatusLabel %> <%= hazard %>: <%= eventName - %> -
-
-
- Expected exposed <%= defaulAdminAreaLabelPlural %><% if (triggerStatusLabel - === 'Trigger') { %> in order of <%= indicatorLabel.toLowerCase() %><% } %>: -
-
-
-
-
- - <% if (triggerStatusLabel === 'Trigger') { %> - - <% } %> - - - <%- tableRows %> -
- <%= indicatorLabel %> - - <%= defaulAdminAreaLabelSingular %> (<%= defaultAdminAreaLabelParent - %>) -
-
-
- <% if (triggerStatusLabel === 'Warning') { %> -
- Please note: Information regarding exposed population not available for - medium warning level. -
- <% } %> - -
+ + + + +
+
+
+ Warning icon + <%= severityLabel %> <%= triggerStatusLabel %> <%= hazard %>: <%= + eventName %> +
+
+
+ Expected exposed <%= defaulAdminAreaLabelPlural %><% if + (triggerStatusLabel === 'Trigger') { %> in order of <%= + indicatorLabel.toLowerCase() %><% } %>: +
+
+
+ + + <% if (triggerStatusLabel === 'Trigger') { %> + + <% } %> + + + <%- tableRows %> +
+ <%= indicatorLabel %> + + <%= defaulAdminAreaLabelSingular %> (<%= defaultAdminAreaLabelParent %>) +
+
+ <% if (triggerStatusLabel === 'Warning') { %> +
+ Please note: Information regarding exposed population not available + for medium warning level. +
+ <% } %> +
+
+
From 48f7adf66e40199671b1e5e95ca1d58f1e195697 Mon Sep 17 00:00:00 2001 From: Ruben Date: Fri, 3 May 2024 14:17:29 +0200 Subject: [PATCH 25/55] remove write to file --- .../src/api/notification/email/email-template.service.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/services/API-service/src/api/notification/email/email-template.service.ts b/services/API-service/src/api/notification/email/email-template.service.ts index ee8f1f33d..7c671ed6d 100644 --- a/services/API-service/src/api/notification/email/email-template.service.ts +++ b/services/API-service/src/api/notification/email/email-template.service.ts @@ -56,6 +56,7 @@ export class EmailTemplateService { private createReplaceKeyValuesTrigger( emailContent: ContentEventEmail, + _date: Date, ): ReplaceKeyValue[] { const country = emailContent.country; const disasterType = emailContent.disasterType; @@ -367,10 +368,6 @@ export class EmailTemplateService { else resolve(html); }); }); - fs.writeFile('output.html', inlinedHtml as string, (err) => { - if (err) throw err; - console.log('The file has been saved!'); - }); return inlinedHtml as string; } From cc43046cb776d42464e3377d0601e51e4d316cef Mon Sep 17 00:00:00 2001 From: Ruben Date: Mon, 13 May 2024 13:58:03 +0200 Subject: [PATCH 26/55] Styling details and links to ibf portal AB#27502 --- .../email/email-template.service.ts | 143 ++++++++---------- .../email/html/advisory-trigger.html | 7 +- .../src/api/notification/email/html/base.html | 11 -- .../notification/email/html/body-event.html | 4 +- .../email/html/event-finished.html | 2 +- .../api/notification/email/html/footer.html | 20 +++ .../email/html/notification-actions.html | 63 ++++++++ .../api/notification/email/html/styles.ejs | 45 +++++- .../notification/email/html/table-event.html | 13 +- .../email/html/table-trigger-row.html | 4 +- .../email/html/table-warning-row.html | 2 +- .../email/html/trigger-finished.html | 122 +-------------- .../email/html/trigger-notification.html | 111 +------------- .../api/notification/email/logos/logo-IBF.png | Bin 0 -> 2838 bytes 14 files changed, 212 insertions(+), 335 deletions(-) create mode 100644 services/API-service/src/api/notification/email/html/footer.html create mode 100644 services/API-service/src/api/notification/email/html/notification-actions.html create mode 100644 services/API-service/src/api/notification/email/logos/logo-IBF.png diff --git a/services/API-service/src/api/notification/email/email-template.service.ts b/services/API-service/src/api/notification/email/email-template.service.ts index 7c671ed6d..f373d1dfc 100644 --- a/services/API-service/src/api/notification/email/email-template.service.ts +++ b/services/API-service/src/api/notification/email/email-template.service.ts @@ -18,6 +18,7 @@ import * as juice from 'juice'; const emailFolder = './src/api/notification/email'; const emailTemplateFolder = `${emailFolder}/html`; const emailIconFolder = `${emailFolder}/icons`; +const emailLogoFolder = `${emailFolder}/logos`; class ReplaceKeyValue { replaceKey: string; @@ -70,8 +71,8 @@ export class EmailTemplateService { replaceValue: this.getHeaderEventStarted(emailContent), }, { - replaceKey: 'socialMediaPart', - replaceValue: this.getSocialMediaHtml(emailContent.country), + replaceKey: 'notificationActions', + replaceValue: this.getNotificationActionsHtml(country, disasterType), }, { replaceKey: 'tablesStacked', @@ -97,12 +98,6 @@ export class EmailTemplateService { replaceKey: 'linkDashboard', replaceValue: process.env.DASHBOARD_URL, }, - { - replaceKey: 'linkEapSop', - replaceValue: country.countryDisasterSettings.find( - (s) => s.disasterType === disasterType, - ).eapLink, - }, { replaceKey: 'socialMediaLink', replaceValue: country.notificationInfo.linkSocialMediaUrl, @@ -116,11 +111,8 @@ export class EmailTemplateService { replaceValue: emailContent.disasterTypeLabel, }, { - replaceKey: 'videoPdfLinks', - replaceValue: this.getVideoPdfLinks( - country.notificationInfo.linkVideo, - country.notificationInfo.linkPdf, - ), + replaceKey: 'footer', + replaceValue: this.getFooterHtml(), }, ]; return keyValueReplaceList; @@ -161,12 +153,6 @@ export class EmailTemplateService { replaceKey: 'socialMediaPart', replaceValue: this.getSocialMediaHtml(country), }, - { - replaceKey: 'linkEapSop', - replaceValue: country.countryDisasterSettings.find( - (s) => s.disasterType === disasterType, - ).eapLink, - }, { replaceKey: 'socialMediaLink', replaceValue: country.notificationInfo.linkSocialMediaUrl, @@ -175,17 +161,14 @@ export class EmailTemplateService { replaceKey: 'socialMediaType', replaceValue: country.notificationInfo.linkSocialMediaType, }, - { - replaceKey: 'videoPdfLinks', - replaceValue: this.getVideoPdfLinks( - country.notificationInfo.linkVideo, - country.notificationInfo.linkPdf, - ), - }, { replaceKey: 'disasterType', replaceValue: disasterTypeLabel, }, + { + replaceKey: 'footer', + replaceValue: this.getFooterHtml(), + }, ]; return keyValueReplaceList; } @@ -235,50 +218,22 @@ export class EmailTemplateService { return headerEventOverview; } - private getVideoPdfLinks(videoLink: string, pdfLink: string) { - // TODO: Use ejs template - const linkVideoHTML = ` -
video`; - - const linkPdfHTML = `PDF`; - let videoStr = ''; - if (videoLink) { - videoStr = ' ' + linkVideoHTML; - } - let pdfStr = ''; - if (pdfLink) { - pdfStr = ' ' + linkPdfHTML; - } - let orStr = ''; - if (videoStr && pdfStr) { - orStr = ' or'; - } - if (videoStr || pdfStr) { - return `See instructions for the IBF-portal in${videoStr}${orStr}${pdfStr}.`; - } + private getNotificationActionsHtml( + country: CountryEntity, + disasterType: DisasterType, + ): string { + const socialMediaLinkHtml = this.getSocialMediaHtml(country); + + let html = this.readHtmlFile('notification-actions.html'); + const data = { + linkDashboard: process.env.DASHBOARD_URL, + linkEapSop: country.countryDisasterSettings.find( + (s) => s.disasterType === disasterType, + ).eapLink, + socialMediaPart: socialMediaLinkHtml, + }; + html = ejs.render(html, data); + return html; // } private getSocialMediaHtml(country: CountryEntity): string { @@ -364,8 +319,12 @@ export class EmailTemplateService { // Inline the CSS const inlinedHtml = await new Promise((resolve, reject) => { juice.juiceResources(emailHtml, { webResources: {} }, (err, html) => { - if (err) reject(err); - else resolve(html); + if (err) { + console.error('Error inlining CSS: ', err); + reject(err); + } else { + resolve(html); + } }); }); @@ -404,7 +363,9 @@ export class EmailTemplateService { .join(''); } - private getEventSeverityLabel(eapAlertClassKey: EapAlertClassKeyEnum) { + private getEventSeverityLabel( + eapAlertClassKey: EapAlertClassKeyEnum, + ): string { if (eapAlertClassKey === EapAlertClassKeyEnum.med) { return 'Medium'; } else if (eapAlertClassKey === EapAlertClassKeyEnum.min) { @@ -458,10 +419,14 @@ export class EmailTemplateService { leadTime: event.firstLeadTime.replace('-', ' '), disasterIssuedLabel: event.eapAlertClass.label, color: this.ibfColorToHex(event.eapAlertClass?.color), - advisory: this.getAdvisoryHtml(event.triggerStatusLabel), + advisory: this.getAdvisoryHtml( + event.triggerStatusLabel, + emailContent.country, + emailContent.disasterType, + ), totalAffected: this.getTotalAffectedHtml( event, - emailContent.indicatorMetadata.unit, + emailContent.indicatorMetadata.label.toLowerCase(), ), }; @@ -472,10 +437,19 @@ export class EmailTemplateService { .join(''); } - private getAdvisoryHtml(triggerStatusLabel: TriggerStatusLabelEnum): string { - return triggerStatusLabel === TriggerStatusLabelEnum.Trigger - ? this.readHtmlFile('advisory-trigger.html') - : this.readHtmlFile('advisory-warning.html'); + private getAdvisoryHtml( + triggerStatusLabel: TriggerStatusLabelEnum, + country: CountryEntity, + disasterType: DisasterType, + ): string { + const advisoryHtml = + triggerStatusLabel === TriggerStatusLabelEnum.Trigger + ? this.readHtmlFile('advisory-trigger.html') + : this.readHtmlFile('advisory-warning.html'); + const eapLink = country.countryDisasterSettings.find( + (s) => s.disasterType === disasterType, + ).eapLink; + return ejs.render(advisoryHtml, { eapLink: eapLink }); } private getTotalAffectedHtml( @@ -493,6 +467,12 @@ export class EmailTemplateService { } } + private getFooterHtml(): string { + const footerHtml = this.readHtmlFile('footer.html'); + const ibfLogo = this.getLogoImageAsDataURL(); + return ejs.render(footerHtml, { ibfLogo: ibfLogo }); + } + private ibfColorToHex(color: string): string { // TODO: Define in a place where FrontEnd and Backend can share this switch (color) { @@ -542,6 +522,11 @@ export class EmailTemplateService { return imageDataURL; } + private getLogoImageAsDataURL() { + const filePath = `${emailLogoFolder}/logo-IBF.png`; + return this.getPngImageAsDataURL(filePath); + } + private getPngImageAsDataURL(relativePath: string) { const imageBuffer = fs.readFileSync(relativePath); const imageDataURL = `data:image/png;base64,${imageBuffer.toString( diff --git a/services/API-service/src/api/notification/email/html/advisory-trigger.html b/services/API-service/src/api/notification/email/html/advisory-trigger.html index 568dee3c8..d9e896dda 100644 --- a/services/API-service/src/api/notification/email/html/advisory-trigger.html +++ b/services/API-service/src/api/notification/email/html/advisory-trigger.html @@ -1,4 +1,7 @@ Activate - - Early Action Protocol + Early Action Protocol + diff --git a/services/API-service/src/api/notification/email/html/base.html b/services/API-service/src/api/notification/email/html/base.html index 4980390c6..2771d5b3e 100644 --- a/services/API-service/src/api/notification/email/html/base.html +++ b/services/API-service/src/api/notification/email/html/base.html @@ -62,17 +62,6 @@ width: 220px; } - .notification-action-button { - height: 40px; - width: 200px; - background-color: #6200ee; - color: #ffffff; - font-weight: bold; - border: 0px; - border-radius: 40px; - cursor: pointer; - } - p { margin: 10px 0; padding: 0; diff --git a/services/API-service/src/api/notification/email/html/body-event.html b/services/API-service/src/api/notification/email/html/body-event.html index c5a33daec..df371dfaa 100644 --- a/services/API-service/src/api/notification/email/html/body-event.html +++ b/services/API-service/src/api/notification/email/html/body-event.html @@ -4,7 +4,7 @@ <%= hazard %> <%= eventName %> - <%= disasterIssuedLabel %> + <%= disasterIssuedLabel %>: <%= startDateEventString %>, <%= leadTime %>s from now. @@ -24,7 +24,7 @@
- + This <%= triggerStatusLabel %> was issued by the IBF portal on <%= issuedDate %> (<%= timezone %>) diff --git a/services/API-service/src/api/notification/email/html/event-finished.html b/services/API-service/src/api/notification/email/html/event-finished.html index 12e17e678..188cb295a 100644 --- a/services/API-service/src/api/notification/email/html/event-finished.html +++ b/services/API-service/src/api/notification/email/html/event-finished.html @@ -8,7 +8,7 @@ managed. - + This warning was issued by the IBF portal on <%= issuedDate %> (<%= timezone %>) diff --git a/services/API-service/src/api/notification/email/html/footer.html b/services/API-service/src/api/notification/email/html/footer.html new file mode 100644 index 000000000..615c4ff56 --- /dev/null +++ b/services/API-service/src/api/notification/email/html/footer.html @@ -0,0 +1,20 @@ + + + + + +
+ + +
+ Impact-Based Forecasting Portal (IBF) was co-developed by Netherlands + Red Cross 510 the together with the Uganda Red Cross National Society. + For questions contact us at ibf-support@510.global +
+
diff --git a/services/API-service/src/api/notification/email/html/notification-actions.html b/services/API-service/src/api/notification/email/html/notification-actions.html new file mode 100644 index 000000000..b7a42d92e --- /dev/null +++ b/services/API-service/src/api/notification/email/html/notification-actions.html @@ -0,0 +1,63 @@ + + + + + + + <%- socialMediaPart %> + + + + + +
+ + + + +
+ Go to the IBF-portal +
+
+ + + + +
+ Find more information about the potentially exposed areas, view + the map and manage anticipatory actions. +
+
+ + + + +
+ About trigger +
+
+ + + + +
+ Read about the trigger methodology and the anticipatory actions. +
+
diff --git a/services/API-service/src/api/notification/email/html/styles.ejs b/services/API-service/src/api/notification/email/html/styles.ejs index 8dc314a29..8575726c6 100644 --- a/services/API-service/src/api/notification/email/html/styles.ejs +++ b/services/API-service/src/api/notification/email/html/styles.ejs @@ -13,7 +13,7 @@ .body-text-normal { font-weight: 400; } - .body-text-smaller { + .body-text-16px { font-size: 16px; line-height: 19.2px; } @@ -25,6 +25,13 @@ .body-text-white { color: white; } + .body-text-12px { + font-size: 12px; + line-height: 14.4px; + } + .important-black { + color: black !important; + } /* Icon styles */ .body-text-icon { @@ -32,6 +39,13 @@ height: 14px; } + .ibf-logo { + width: 45px; + height: 45px; + display: block; + padding-right: 10px; + } + /* Text formatting styles */ .body-text-break { display: block; @@ -58,7 +72,6 @@ .table-cell { height: 19px; padding: 2px 10px; - width: 211px; text-align: left; } @@ -80,4 +93,32 @@ .padding-top { padding-top: 10px; } + + /* Notification action button styles */ + .notification-action-button { + height: 40px; + width: 200px; + font-weight: bold; + border: 0px; + border-radius: 40px; + cursor: pointer; + } + .notification-action-button-primary { + background-color: #6200ee; + } + .notification-action-button.notification-action-button-primary a { + color: #ffffff !important; + text-decoration: none !important; + } + .notification-action-button-secondary { + background-color: #ffffff; + } + .notification-action-button.notification-action-button-secondary a { + color: #6200ee !important; + text-decoration: none !important; + } + .button-text-padding { + padding: 12px 18px 12px 18px; + border-radius: 3px; + } diff --git a/services/API-service/src/api/notification/email/html/table-event.html b/services/API-service/src/api/notification/email/html/table-event.html index cad642c59..6072f2057 100644 --- a/services/API-service/src/api/notification/email/html/table-event.html +++ b/services/API-service/src/api/notification/email/html/table-event.html @@ -18,7 +18,7 @@
Expected exposed <%= defaulAdminAreaLabelPlural %><% if (triggerStatusLabel === 'Trigger') { %> in order of <%= @@ -26,15 +26,16 @@
- +
<% if (triggerStatusLabel === 'Trigger') { %> - <% } %> - <%- tableRows %> @@ -42,7 +43,7 @@ <% if (triggerStatusLabel === 'Warning') { %>
Please note: Information regarding exposed population not available for medium warning level. diff --git a/services/API-service/src/api/notification/email/html/table-trigger-row.html b/services/API-service/src/api/notification/email/html/table-trigger-row.html index b2881de88..8f942e29e 100644 --- a/services/API-service/src/api/notification/email/html/table-trigger-row.html +++ b/services/API-service/src/api/notification/email/html/table-trigger-row.html @@ -1,8 +1,8 @@
- - diff --git a/services/API-service/src/api/notification/email/html/table-warning-row.html b/services/API-service/src/api/notification/email/html/table-warning-row.html index a26f8d8ea..b551f1d03 100644 --- a/services/API-service/src/api/notification/email/html/table-warning-row.html +++ b/services/API-service/src/api/notification/email/html/table-warning-row.html @@ -1,5 +1,5 @@ - diff --git a/services/API-service/src/api/notification/email/html/trigger-finished.html b/services/API-service/src/api/notification/email/html/trigger-finished.html index 0fd4f6d22..fbd5ff3ea 100644 --- a/services/API-service/src/api/notification/email/html/trigger-finished.html +++ b/services/API-service/src/api/notification/email/html/trigger-finished.html @@ -86,126 +86,8 @@
Dear Reader,


- <%- eventOverview %> -
+ <%= indicatorLabel %> - <%= defaulAdminAreaLabelSingular %> (<%= defaultAdminAreaLabelParent %>) + + <%= defaulAdminAreaLabelSingular %> (<%= + defaultAdminAreaLabelParent %>)
+ <%= affectectedOfIndicator %> + <%= adminBoundary %> (<%= higherAdminBoundary %>)
+ <%= adminBoundary %> (<%= higherAdminBoundary %>)
- - - - - - - - - - <%- socialMediaPart %> - -
- - - - -
- Go to IBF-portal -
-
- - - - -
- 1. Manage the anticipatory actions - in the IBF-portal for 1 more week. - <%- videoPdfLinks %> -
-
- - - - -
- About trigger -
-
- - - - -
- 2. Read about the trigger - methodology and the anticipatory - actions. -
-
+ <%- eventOverview %> <%- notificationActions %> + <%- footer %>
diff --git a/services/API-service/src/api/notification/email/html/trigger-notification.html b/services/API-service/src/api/notification/email/html/trigger-notification.html index f168c01cb..110ceb0d1 100644 --- a/services/API-service/src/api/notification/email/html/trigger-notification.html +++ b/services/API-service/src/api/notification/email/html/trigger-notification.html @@ -85,114 +85,7 @@
Dear Reader,


- <%- eventListBody %> - - - - - - - <%- socialMediaPart %> - - - - - -
- - - - -
- Go to the IBF-portal -
-
- - - - -
- Find more info on the potentially - exposed areas on a map and manage - anticipatory actions in the - IBF-portal. <%- videoPdfLinks %> -
-
- - - - -
- About trigger -
-
- - - - -
- Read about the trigger methodology - and the anticipatory actions. -
-
+ <%- eventListBody %> <%- notificationActions %>
Trigger Statement: <%- triggerStatement %> @@ -200,7 +93,7 @@ <%= mapImagePart %>

- <%- tablesStacked %> + <%- tablesStacked %> <%- footer %>
diff --git a/services/API-service/src/api/notification/email/logos/logo-IBF.png b/services/API-service/src/api/notification/email/logos/logo-IBF.png new file mode 100644 index 0000000000000000000000000000000000000000..b006cbaca414ca383765a59458ad3b4a401e3849 GIT binary patch literal 2838 zcmV+x3+eQUP)Gzu1K$eeGIpYg-{jY)FZ(7C|)CN)RfNKq8OEV#K5gH9}go(I6T$nl>0Q z*jNaXsx(#tsf~adUwu%iSxPtEF0HoP-Mx2r@7~wUoa28UGxyA$S$4Oa&DryqnKR${ z&iDW4KXVDZ%k<{bug|ZL%9j}Nufe>ElAwoEzmtKV;lwA51c3cu$^fX%01_@W5x9hO zXwkPcQ8uZ9zbtY^GV2`ZCl%sRPg63GnXj z{p%!Q2l3H`$Rj?=K!jO3b)9+CA4qD;+I>OnKJK)c)8@5%oM%IB37G-(6CE$z29oSU zz-YdU?$ZEhe3I@&qRNx>j;Upsh&V8=j6a4 z%RtRLFcPF*Pk5oQ9s+)_aj=gFx?jPI3`$zI!8J06eaT`P7t+ysv=a4wF7w>>JKiF? zPOawa>e69adVISFf>J`YU!<1Vh;guOVusLla8@I_C#(m0!*ScORfyQmz+C;m_I&rj z*`<{7WBAf0wUsI`ANxu(Tz_{fwDwRK8K}aaewc%|4i@xQ%b0-m-{^)9-r4~yTa#C~ z34~=i@4@h~3Hav^2B3JMWM6Io&tKaG*KJz?t@E3qw)08ti)%X`_Dc!F2M3`4JBLu3 zX4BEvzYG^2$D6lO{g8y^>of4s%UvCs&P(ZkoGpHh(?|1{cvH4AabQz3M~w4aB0a$AL-j2Y_4g+_3bck zO*aBIz~_=G`6^s^eG)1Gf)1L1m#Mg!NEH?mP=B~}Py4WP>{1AA!S!isRKT_>U2&=FpZ%gMO9^60Fn4Cn*@% zO4%Y6wAm5WP|1l+jR|=D1y8?}@Ct&CUoQ;%F!(P|OT+~J@iFpes?dL6-0}Y+^5be< zsRgY1c|iQD6wnYbL6;{<`q@A_!0S^>H-*J(n?QI1R&Gcd*q?We!}u`OE>0P9ead!Z zp?g(ljOpTV1xmxE$i*tafnwUHmBm!Ctam}8V<>?GrzO<>wCZWOjKbDeyHl?{w4fh?0j2$0NQ$tm+4x>;I5~-U}~Jei9eLBre~KXM*x9lo%jdkQDsGsNuD);G3--; zIBA&%bQRfFnoquTWH5kjOLL$MOv$XJi^3DboxrSl@CE{(+t~)k5BNCcIowfA z%AfP$?@v!e#;bq>2|VtxQXYM8ivrDTyy}xmDrhAi9W14|EZ;-^tyfTgg>ea$f`I1E znonPF4TrY(a2Wqz67=8p6ycfs#3AqHMQ%`1HB4JO#t z2CxCHj4~Z`*@8|QD$fx3(8d|IW?cQzp&-==wO^3t072|-Af$;lg$8WoL8!REtGsZW z2!1CM3Uo#T_ok6Yl_8BPpq$3L%#@BDs6esk>%AZ?nX{&}pbdDTx3Z9V+!cLsh*pTd zx6CG?%rUEy6(Bb?nHaEA5>O^ZsL~Qll`1gTG&1DFFCHAztE6(Z6z98IRR3%7Phd~x zfj-P)7AEfTMe~bnZ1Tm4=F8UJiR^U?iUVRkJCxUqtp4zokjXGOe|8GKzwI!b>L1fa zIC!!E^AnZ5jqP=KQ zPCUmr`x*jmtdT4BzQ7S4x3AAjyNRI+P z*P6O116!A234tl8u=fPoP0ExEI-XH3RA|}g5Fz|}8kIWk+55D-{7Hp33g^(wc7kB{ zVBn-;DezgvEU}e~k`1GVaqiUPvN7IAeg1V_D6_2BR)RLYd)l!wuMwq!x( zTOrmcn)k>4o#U6S!eeUi5j|`DXPH8led<}p1I?^i1z)d7*e6L4v7VNtW!%)$Rw3*+ ztb-~w7V+p_vG9cuYb}ntZ60iuM1Y0mmdnUFo>;5LLYQ-UFqzuhnBr{nRV+{R5=)eJAl0 z`x%6n0SJE!34hdabELaS|AE!N0qyGFnFw(`P@VSi%t9cz_m7jGd?pRwv>(-F{^SYL zx#R>I!;Q#lbHs9t^*H#juHpqjL$b*)(sqZO0wI38t;3JoS7Hd+i>I3o^yj#75f2&8V;)7o o$APf_;4ck(%BO#G2Hr*bKkY_1nNM3loB#j-07*qoM6N<$g0*UN{Qv*} literal 0 HcmV?d00001 From cb19babc3496f307b1e0f011bf55827e3d876247 Mon Sep 17 00:00:00 2001 From: Ruben Date: Mon, 13 May 2024 15:43:18 +0200 Subject: [PATCH 27/55] Changed key value array to object --- .../email/email-template.service.ts | 155 +++++------------- 1 file changed, 41 insertions(+), 114 deletions(-) diff --git a/services/API-service/src/api/notification/email/email-template.service.ts b/services/API-service/src/api/notification/email/email-template.service.ts index f373d1dfc..f72a146c1 100644 --- a/services/API-service/src/api/notification/email/email-template.service.ts +++ b/services/API-service/src/api/notification/email/email-template.service.ts @@ -38,7 +38,6 @@ export class EmailTemplateService { return this.formatEmail(replaceKeyValues); } - // TODO REFACTOR this to use a DTO (ContentTriggerFinishedEmail) instead of multiple parameters public async createHtmlForTriggerFinishedEmail( country: CountryEntity, disasterType: DisasterType, @@ -58,64 +57,29 @@ export class EmailTemplateService { private createReplaceKeyValuesTrigger( emailContent: ContentEventEmail, _date: Date, - ): ReplaceKeyValue[] { + ): Record { const country = emailContent.country; const disasterType = emailContent.disasterType; - const keyValueReplaceList = [ - { - replaceKey: 'emailBody', - replaceValue: this.getEmailBody(false), - }, - { - replaceKey: 'headerEventOverview', - replaceValue: this.getHeaderEventStarted(emailContent), - }, - { - replaceKey: 'notificationActions', - replaceValue: this.getNotificationActionsHtml(country, disasterType), - }, - { - replaceKey: 'tablesStacked', - replaceValue: this.getTablesForEvents(emailContent), - }, - { - replaceKey: 'eventListBody', - replaceValue: this.getEventListBody(emailContent), - }, - { - replaceKey: 'imgLogo', - replaceValue: country.notificationInfo.logo[disasterType], - }, - { - replaceKey: 'triggerStatement', - replaceValue: country.notificationInfo.triggerStatement[disasterType], - }, - { - replaceKey: 'mapImagePart', - replaceValue: this.getMapImageHtml(emailContent), - }, - { - replaceKey: 'linkDashboard', - replaceValue: process.env.DASHBOARD_URL, - }, - { - replaceKey: 'socialMediaLink', - replaceValue: country.notificationInfo.linkSocialMediaUrl, - }, - { - replaceKey: 'socialMediaType', - replaceValue: country.notificationInfo.linkSocialMediaType, - }, - { - replaceKey: 'disasterType', - replaceValue: emailContent.disasterTypeLabel, - }, - { - replaceKey: 'footer', - replaceValue: this.getFooterHtml(), - }, - ]; - return keyValueReplaceList; + + const keyValueReplaceObject = { + emailBody: this.getEmailBody(false), + headerEventOverview: this.getHeaderEventStarted(emailContent), + notificationActions: this.getNotificationActionsHtml( + country, + disasterType, + ), + tablesStacked: this.getTablesForEvents(emailContent), + eventListBody: this.getEventListBody(emailContent), + imgLogo: country.notificationInfo.logo[disasterType], + triggerStatement: country.notificationInfo.triggerStatement[disasterType], + mapImagePart: this.getMapImageHtml(emailContent), + linkDashboard: process.env.DASHBOARD_URL, + socialMediaLink: country.notificationInfo.linkSocialMediaUrl, + socialMediaType: country.notificationInfo.linkSocialMediaType, + disasterType: emailContent.disasterTypeLabel, + footer: this.getFooterHtml(), + }; + return keyValueReplaceObject; } private createReplaceKeyValuesTriggerFinished( @@ -123,54 +87,24 @@ export class EmailTemplateService { disasterType: DisasterType, events: EventSummaryCountry[], disasterTypeLabel: string, - ): ReplaceKeyValue[] { - const keyValueReplaceList = [ - { - replaceKey: 'emailBody', - replaceValue: this.getEmailBody(true), - }, - { - replaceKey: 'headerEventOverview', - replaceValue: '', - }, - { - replaceKey: 'eventOverview', - replaceValue: this.geEventsFinishedOverview( - country, - events, - disasterTypeLabel, - ), - }, - { - replaceKey: 'imgLogo', - replaceValue: country.notificationInfo.logo[disasterType], - }, - { - replaceKey: 'linkDashboard', - replaceValue: process.env.DASHBOARD_URL, - }, - { - replaceKey: 'socialMediaPart', - replaceValue: this.getSocialMediaHtml(country), - }, - { - replaceKey: 'socialMediaLink', - replaceValue: country.notificationInfo.linkSocialMediaUrl, - }, - { - replaceKey: 'socialMediaType', - replaceValue: country.notificationInfo.linkSocialMediaType, - }, - { - replaceKey: 'disasterType', - replaceValue: disasterTypeLabel, - }, - { - replaceKey: 'footer', - replaceValue: this.getFooterHtml(), - }, - ]; - return keyValueReplaceList; + ): Record { + const keyValueReplaceObject = { + emailBody: this.getEmailBody(true), + headerEventOverview: '', + eventOverview: this.geEventsFinishedOverview( + country, + events, + disasterTypeLabel, + ), + imgLogo: country.notificationInfo.logo[disasterType], + linkDashboard: process.env.DASHBOARD_URL, + socialMediaPart: this.getSocialMediaHtml(country), + socialMediaLink: country.notificationInfo.linkSocialMediaUrl, + socialMediaType: country.notificationInfo.linkSocialMediaType, + disasterType: disasterTypeLabel, + footer: this.getFooterHtml(), + }; + return keyValueReplaceObject; } private getEmailBody(triggerFinished: boolean): string { @@ -292,19 +226,12 @@ export class EmailTemplateService { } private async formatEmail( - emailKeyValueReplaceList: ReplaceKeyValue[], + emailKeyValueReplaceObject: Record, ): Promise { // TODO REFACTOR: Apply styles in a septerate file also for the base.html const template = this.readHtmlFile('base.html'); const styles = this.readHtmlFile('styles.ejs'); const templateWithStyle = styles + template; - const replacements = emailKeyValueReplaceList.reduce( - (acc, { replaceKey, replaceValue }) => { - acc[replaceKey] = replaceValue; - return acc; - }, - {}, - ); let emailHtml = templateWithStyle; let previousHtml = null; @@ -314,7 +241,7 @@ export class EmailTemplateService { // doesn't render nested tags in one pass. while (emailHtml !== previousHtml) { previousHtml = emailHtml; - emailHtml = ejs.render(previousHtml, replacements); + emailHtml = ejs.render(previousHtml, emailKeyValueReplaceObject); } // Inline the CSS const inlinedHtml = await new Promise((resolve, reject) => { From 57b6a90be8b4353d4b630783bde424b114701b87 Mon Sep 17 00:00:00 2001 From: Ruben Date: Thu, 23 May 2024 12:04:47 +0200 Subject: [PATCH 28/55] Changes based on feedback PR#1496 --- .../src/api/event/event.service.ts | 1 - .../dto/content-trigger-email.dto.ts | 1 + .../dto/notification-date-per-event.dto.ts | 2 +- .../email/email-template.service.ts | 45 +++++++++---------- .../notification/email/html/body-event.html | 2 +- .../html/body-total-affected-trigger.html | 2 +- .../api/notification/email/html/footer.html | 2 +- .../notification/email/html/table-event.html | 6 +-- .../notification-content.service.ts | 18 +++++--- .../api/notification/notification.service.ts | 13 +++--- 10 files changed, 47 insertions(+), 45 deletions(-) diff --git a/services/API-service/src/api/event/event.service.ts b/services/API-service/src/api/event/event.service.ts index 075eb93c5..794a2e8b5 100644 --- a/services/API-service/src/api/event/event.service.ts +++ b/services/API-service/src/api/event/event.service.ts @@ -16,7 +16,6 @@ import { IsNull, DataSource, SelectQueryBuilder, - Between, } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; diff --git a/services/API-service/src/api/notification/dto/content-trigger-email.dto.ts b/services/API-service/src/api/notification/dto/content-trigger-email.dto.ts index f617ba219..8d802b036 100644 --- a/services/API-service/src/api/notification/dto/content-trigger-email.dto.ts +++ b/services/API-service/src/api/notification/dto/content-trigger-email.dto.ts @@ -8,6 +8,7 @@ export class ContentEventEmail { public disasterType: DisasterType; public disasterTypeLabel: string; public indicatorMetadata: IndicatorMetadataEntity; + public linkEapSop: string; public dataPerEvent: NotificationDataPerEventDto[]; public mapImageData: any[]; public defaultAdminLevel: number; 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 4fde261a4..8389691b0 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 @@ -9,7 +9,7 @@ export class NotificationDataPerEventDto { triggeredAreas: TriggeredArea[]; nrOfTriggeredAreas: number; startDateDisasterString: string; - totalAffectectedOfIndicator: number; + totalAffectedOfIndicator: number; mapImage?: Buffer; issuedDate: Date; eapAlertClass: EapAlertClass; diff --git a/services/API-service/src/api/notification/email/email-template.service.ts b/services/API-service/src/api/notification/email/email-template.service.ts index f72a146c1..5b676aaec 100644 --- a/services/API-service/src/api/notification/email/email-template.service.ts +++ b/services/API-service/src/api/notification/email/email-template.service.ts @@ -14,6 +14,7 @@ import { } from '../../../shared/data.model'; import { CountryEntity } from '../../country/country.entity'; import * as juice from 'juice'; +import { formatActionUnitValue } from '../helpers/format-action-unit-value.helper'; const emailFolder = './src/api/notification/email'; const emailTemplateFolder = `${emailFolder}/html`; @@ -66,7 +67,7 @@ export class EmailTemplateService { headerEventOverview: this.getHeaderEventStarted(emailContent), notificationActions: this.getNotificationActionsHtml( country, - disasterType, + emailContent.linkEapSop, ), tablesStacked: this.getTablesForEvents(emailContent), eventListBody: this.getEventListBody(emailContent), @@ -77,7 +78,7 @@ export class EmailTemplateService { socialMediaLink: country.notificationInfo.linkSocialMediaUrl, socialMediaType: country.notificationInfo.linkSocialMediaType, disasterType: emailContent.disasterTypeLabel, - footer: this.getFooterHtml(), + footer: this.getFooterHtml(country.countryName), }; return keyValueReplaceObject; } @@ -102,13 +103,12 @@ export class EmailTemplateService { socialMediaLink: country.notificationInfo.linkSocialMediaUrl, socialMediaType: country.notificationInfo.linkSocialMediaType, disasterType: disasterTypeLabel, - footer: this.getFooterHtml(), + footer: this.getFooterHtml(country.countryName), }; return keyValueReplaceObject; } private getEmailBody(triggerFinished: boolean): string { - // TODO: Also apply new EJS style templating to these files if (triggerFinished) { return this.readHtmlFile('trigger-finished.html'); } else { @@ -154,16 +154,14 @@ export class EmailTemplateService { private getNotificationActionsHtml( country: CountryEntity, - disasterType: DisasterType, + linkEapSop: string, ): string { const socialMediaLinkHtml = this.getSocialMediaHtml(country); let html = this.readHtmlFile('notification-actions.html'); const data = { linkDashboard: process.env.DASHBOARD_URL, - linkEapSop: country.countryDisasterSettings.find( - (s) => s.disasterType === disasterType, - ).eapLink, + linkEapSop: linkEapSop, socialMediaPart: socialMediaLinkHtml, }; html = ejs.render(html, data); @@ -269,9 +267,9 @@ export class EmailTemplateService { hazard: emailContent.disasterTypeLabel, triggerStatusLabel: event.triggerStatusLabel, eventName: event.eventName, - defaulAdminAreaLabelSingular: + defaultAdminAreaLabelSingular: emailContent.defaultAdminAreaLabel.singular, - defaulAdminAreaLabelPlural: + defaultAdminAreaLabelPlural: emailContent.defaultAdminAreaLabel.plural.toLocaleLowerCase(), defaultAdminAreaLabelParent: adminAreaLabelsParent.singular, indicatorLabel: emailContent.indicatorMetadata.label, @@ -335,10 +333,13 @@ export class EmailTemplateService { emailContent.country.countryCodeISO3, ), startDateEventString: event.startDateDisasterString, - defaulAdminAreaLabel: + defaultAdminAreaLabel: emailContent.defaultAdminAreaLabel.plural.toLocaleLowerCase(), indicatorLabel: emailContent.indicatorMetadata.label, - totalAffectectedOfIndicator: event.totalAffectectedOfIndicator, + totalAffectedOfIndicator: formatActionUnitValue( + event.totalAffectedOfIndicator, + emailContent.indicatorMetadata.numberFormatMap, + ), indicatorUnit: emailContent.indicatorMetadata.unit, timezone: CountryTimeZoneMapping[emailContent.country.countryCodeISO3], @@ -348,8 +349,7 @@ export class EmailTemplateService { color: this.ibfColorToHex(event.eapAlertClass?.color), advisory: this.getAdvisoryHtml( event.triggerStatusLabel, - emailContent.country, - emailContent.disasterType, + emailContent.linkEapSop, ), totalAffected: this.getTotalAffectedHtml( event, @@ -366,16 +366,13 @@ export class EmailTemplateService { private getAdvisoryHtml( triggerStatusLabel: TriggerStatusLabelEnum, - country: CountryEntity, - disasterType: DisasterType, + eapLink: string, ): string { const advisoryHtml = triggerStatusLabel === TriggerStatusLabelEnum.Trigger ? this.readHtmlFile('advisory-trigger.html') : this.readHtmlFile('advisory-warning.html'); - const eapLink = country.countryDisasterSettings.find( - (s) => s.disasterType === disasterType, - ).eapLink; + return ejs.render(advisoryHtml, { eapLink: eapLink }); } @@ -388,16 +385,19 @@ export class EmailTemplateService { } else { let html = this.readHtmlFile('body-total-affected-trigger.html'); return ejs.render(html, { - totalAffectectedOfIndicator: event.totalAffectectedOfIndicator, + totalAffectedOfIndicator: event.totalAffectedOfIndicator, indicatorUnit: indicatorUnit, }); } } - private getFooterHtml(): string { + private getFooterHtml(countryName: string): string { const footerHtml = this.readHtmlFile('footer.html'); const ibfLogo = this.getLogoImageAsDataURL(); - return ejs.render(footerHtml, { ibfLogo: ibfLogo }); + return ejs.render(footerHtml, { + ibfLogo: ibfLogo, + countryName: countryName, + }); } private ibfColorToHex(color: string): string { @@ -436,7 +436,6 @@ export class EmailTemplateService { private getTriangleIcon(eapAlertClassKey: EapAlertClassKeyEnum) { let fileName = ''; - // Still need implement the difference between medium and low warning if (eapAlertClassKey === EapAlertClassKeyEnum.med) { fileName = 'warning-medium.png'; } else if (eapAlertClassKey === EapAlertClassKeyEnum.min) { diff --git a/services/API-service/src/api/notification/email/html/body-event.html b/services/API-service/src/api/notification/email/html/body-event.html index df371dfaa..fc7439767 100644 --- a/services/API-service/src/api/notification/email/html/body-event.html +++ b/services/API-service/src/api/notification/email/html/body-event.html @@ -10,7 +10,7 @@
- Expected exposed <%= defaulAdminAreaLabel %>: + Expected exposed <%= defaultAdminAreaLabel %>: <%= nrOfTriggeredAreas %> (see list below) diff --git a/services/API-service/src/api/notification/email/html/body-total-affected-trigger.html b/services/API-service/src/api/notification/email/html/body-total-affected-trigger.html index f5e2bbe56..e5f37e53d 100644 --- a/services/API-service/src/api/notification/email/html/body-total-affected-trigger.html +++ b/services/API-service/src/api/notification/email/html/body-total-affected-trigger.html @@ -1,4 +1,4 @@ - <%= totalAffectectedOfIndicator %> <%= indicatorUnit %> + <%= totalAffectedOfIndicator %> <%= indicatorUnit %> diff --git a/services/API-service/src/api/notification/email/html/footer.html b/services/API-service/src/api/notification/email/html/footer.html index 615c4ff56..d021bf6e6 100644 --- a/services/API-service/src/api/notification/email/html/footer.html +++ b/services/API-service/src/api/notification/email/html/footer.html @@ -12,7 +12,7 @@
Impact-Based Forecasting Portal (IBF) was co-developed by Netherlands - Red Cross 510 the together with the Uganda Red Cross National Society. + Red Cross 510 the together with the <%= countryName %> Red Cross National Society. For questions contact us at ibf-support@510.global
diff --git a/services/API-service/src/api/notification/email/html/table-event.html b/services/API-service/src/api/notification/email/html/table-event.html index 6072f2057..61055f046 100644 --- a/services/API-service/src/api/notification/email/html/table-event.html +++ b/services/API-service/src/api/notification/email/html/table-event.html @@ -20,7 +20,7 @@
- Expected exposed <%= defaulAdminAreaLabelPlural %><% if + Expected exposed <%= defaultAdminAreaLabelPlural %><% if (triggerStatusLabel === 'Trigger') { %> in order of <%= indicatorLabel.toLowerCase() %><% } %>:
@@ -34,7 +34,7 @@ <% } %> - <%= defaulAdminAreaLabelSingular %> (<%= + <%= defaultAdminAreaLabelSingular %> (<%= defaultAdminAreaLabelParent %>) @@ -46,7 +46,7 @@ class="body-text body-text-normal body-text-normal-stretch text-centered padding-top body-text-12px" > Please note: Information regarding exposed population not available - for medium warning level. + for warning levels. <% } %> 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 59c2d335f..c1a3669fc 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 @@ -1,4 +1,3 @@ -import { AdminAreaDynamicDataService } from '../../admin-area-dynamic-data/admin-area-dynamic-data.service'; import { CountryEntity } from '../../country/country.entity'; import { Injectable } from '@nestjs/common'; import { EventService } from '../../event/event.service'; @@ -51,7 +50,8 @@ export class NotificationContentService { content.country = country; content.indicatorMetadata = await this.getIndicatorMetadata(disasterType); - content.defaultAdminAreaLabel = this.getDefaulAdminAreaLabels( + content.linkEapSop = this.getLinkEapSop(country, disasterType); + content.defaultAdminAreaLabel = this.getdefaultAdminAreaLabel( country, content.defaultAdminLevel, ); @@ -94,7 +94,13 @@ export class NotificationContentService { ).defaultAdminLevel; } - private getDefaulAdminAreaLabels( + private getLinkEapSop(country: CountryEntity, disasterType: DisasterType): string { + return country.countryDisasterSettings.find( + (s) => s.disasterType === disasterType, + ).eapLink + } + + private getdefaultAdminAreaLabel( country: CountryEntity, adminAreaDefaultLevel: number, ): AdminAreaLabel { @@ -175,7 +181,7 @@ export class NotificationContentService { event.countryCodeISO3, disasterType, ); - data.totalAffectectedOfIndicator = this.getTotalAffectedPerEvent( + data.totalAffectedOfIndicator = this.getTotalAffectedPerEvent( data.triggeredAreas, ); data.mapImage = await this.eventService.getEventMapImage( @@ -214,9 +220,7 @@ export class NotificationContentService { disasterType: DisasterType, event: EventSummaryCountry, ): Promise { - const defaultAdminLevel = country.countryDisasterSettings.find( - (s) => s.disasterType === disasterType, - ).defaultAdminLevel; + const defaultAdminLevel = this.getDefaultAdminLevel(country, disasterType) const triggeredAreas = await this.eventService.getTriggeredAreas( country.countryCodeISO3, disasterType, diff --git a/services/API-service/src/api/notification/notification.service.ts b/services/API-service/src/api/notification/notification.service.ts index 5cdf65f6c..ba08436e3 100644 --- a/services/API-service/src/api/notification/notification.service.ts +++ b/services/API-service/src/api/notification/notification.service.ts @@ -23,7 +23,11 @@ export class NotificationService { disasterType: DisasterType, date?: Date, ): Promise { - await this.sendNotiFicationsActiveEvents( + // REFACTOR: First close finished events. This is ideally done through separate endpoint called at end of pipeline, but that would require all pipelines to be updated. + // Instead, making use of this endpoint which is already called at the end of every pipeline + await this.eventService.closeEventsAutomatic(countryCodeISO3, disasterType); + + await this.sendNotificationsActiveEvents( disasterType, countryCodeISO3, date, @@ -37,13 +41,9 @@ export class NotificationService { date, ); } - - // REFACTOR: First close finished events. This is ideally done through separate endpoint called at end of pipeline, but that would require all pipelines to be updated. - // Instead, making use of this endpoint which is already called at the end of every pipeline - await this.eventService.closeEventsAutomatic(countryCodeISO3, disasterType); } - private async sendNotiFicationsActiveEvents( + private async sendNotificationsActiveEvents( disasterType: DisasterType, countryCodeISO3: string, date?: Date, @@ -107,7 +107,6 @@ export class NotificationService { ); if (country.notificationInfo.useWhatsapp[disasterType]) { - // TODO: Send one whatsapp message for all closing events for (const event of finishedNotifiableEvents) { await this.whatsappService.sendTriggerFinishedWhatsapp( country, From 34deb35747b290607c08e63fd6bd0cfba18bb70a Mon Sep 17 00:00:00 2001 From: Ruben Date: Thu, 23 May 2024 14:19:58 +0200 Subject: [PATCH 29/55] Styling of header email make it work for chrome --- .../src/api/notification/email/html/base.html | 8 -------- .../api/notification/email/html/footer.html | 4 ++-- .../api/notification/email/html/header.html | 19 +++++++++++------- .../api/notification/email/html/styles.ejs | 20 +++++++++++++++++++ .../notification-content.service.ts | 9 ++++++--- 5 files changed, 40 insertions(+), 20 deletions(-) diff --git a/services/API-service/src/api/notification/email/html/base.html b/services/API-service/src/api/notification/email/html/base.html index 2771d5b3e..4a57d5cf5 100644 --- a/services/API-service/src/api/notification/email/html/base.html +++ b/services/API-service/src/api/notification/email/html/base.html @@ -25,14 +25,6 @@ text-align: center; } - .notification-sub-title-left { - color: white; - text-align: center; - display: flex; - justify-content: center; - align-items: center; - } - .notification-content { color: #000000; line-height: 1.5; diff --git a/services/API-service/src/api/notification/email/html/footer.html b/services/API-service/src/api/notification/email/html/footer.html index d021bf6e6..c742a474e 100644 --- a/services/API-service/src/api/notification/email/html/footer.html +++ b/services/API-service/src/api/notification/email/html/footer.html @@ -12,8 +12,8 @@
Impact-Based Forecasting Portal (IBF) was co-developed by Netherlands - Red Cross 510 the together with the <%= countryName %> Red Cross National Society. - For questions contact us at ibf-support@510.global + Red Cross 510 the together with the <%= countryName %> Red Cross + National Society. For questions contact us at ibf-support@510.global
diff --git a/services/API-service/src/api/notification/email/html/header.html b/services/API-service/src/api/notification/email/html/header.html index 1e75a6a6e..2fcee9bc5 100644 --- a/services/API-service/src/api/notification/email/html/header.html +++ b/services/API-service/src/api/notification/email/html/header.html @@ -1,11 +1,16 @@ -

- <%= nrOfEvents %> <%= disasterLabel %> alerts -

- IBF alert send on <%= sentOnDate %> (<%= timezone %>) + + + + +
+

+ <%= nrOfEvents %> <%= disasterLabel %> alerts +

+ + IBF alert send on <%= sentOnDate %> (<%= timezone %>) + +
diff --git a/services/API-service/src/api/notification/email/html/styles.ejs b/services/API-service/src/api/notification/email/html/styles.ejs index 8575726c6..74a3f2b0f 100644 --- a/services/API-service/src/api/notification/email/html/styles.ejs +++ b/services/API-service/src/api/notification/email/html/styles.ejs @@ -121,4 +121,24 @@ padding: 12px 18px 12px 18px; border-radius: 3px; } + + /* Notification title styles */ + .notification-title { + color: white; + text-align: center; + } + + .full-width-table { + width: 100%; + border: 0; + cellspacing: 0; + cellpadding: 0; + } + .centered-text { + text-align: center; + } + + .white-text { + color: white; + } 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 c1a3669fc..ec5eaffdf 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 @@ -94,10 +94,13 @@ export class NotificationContentService { ).defaultAdminLevel; } - private getLinkEapSop(country: CountryEntity, disasterType: DisasterType): string { + private getLinkEapSop( + country: CountryEntity, + disasterType: DisasterType, + ): string { return country.countryDisasterSettings.find( (s) => s.disasterType === disasterType, - ).eapLink + ).eapLink; } private getdefaultAdminAreaLabel( @@ -220,7 +223,7 @@ export class NotificationContentService { disasterType: DisasterType, event: EventSummaryCountry, ): Promise { - const defaultAdminLevel = this.getDefaultAdminLevel(country, disasterType) + const defaultAdminLevel = this.getDefaultAdminLevel(country, disasterType); const triggeredAreas = await this.eventService.getTriggeredAreas( country.countryCodeISO3, disasterType, From a2eb16b450154f8af8feb1aca8791b873e63f72e Mon Sep 17 00:00:00 2001 From: Ruben Date: Fri, 24 May 2024 10:51:58 +0200 Subject: [PATCH 30/55] AB#27286 warning date trigger event --- .../dto/notification-date-per-event.dto.ts | 21 +++++++- .../email/email-template.service.ts | 34 ++++++++----- .../notification/email/html/body-event.html | 17 ++++++- .../notification-content.service.ts | 49 +++++++++++++++++-- 4 files changed, 103 insertions(+), 18 deletions(-) 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 8389691b0..373beeabc 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 @@ -5,10 +5,29 @@ export class NotificationDataPerEventDto { triggerStatusLabel: TriggerStatusLabelEnum; eventName: string; disasterSpecificCopy: DisasterSpecificCopy; + + /** + * The day that the event starts. + */ firstLeadTime: LeadTime; + + /** + * The day that the event triggers. This could be different from firstLeadTimeString. + * For example, a flood could transition from a warning (a chance of a small flood) + * to an EAP trigger (a larger chance of a bigger flood). + */ + firstTriggerLeadTime: LeadTime; + + firstLeadTimeString: string; + firstTriggerLeadTimeString: string; + triggeredAreas: TriggeredArea[]; + + /** + * The number of areas where the event triggers. + */ nrOfTriggeredAreas: number; - startDateDisasterString: string; + totalAffectedOfIndicator: number; mapImage?: Buffer; issuedDate: Date; diff --git a/services/API-service/src/api/notification/email/email-template.service.ts b/services/API-service/src/api/notification/email/email-template.service.ts index 5b676aaec..c58a3a66e 100644 --- a/services/API-service/src/api/notification/email/email-template.service.ts +++ b/services/API-service/src/api/notification/email/email-template.service.ts @@ -323,38 +323,50 @@ export class EmailTemplateService { return emailContent.dataPerEvent .map((event) => { const data = { + // Event details + eventName: event.eventName, hazard: emailContent.disasterTypeLabel, triggerStatusLabel: event.triggerStatusLabel, - eventName: event.eventName, - nrOfTriggeredAreas: event.nrOfTriggeredAreas, - expectedTriggerDate: event.firstLeadTime, issuedDate: this.dateObjectToDateTimeString( event.issuedDate, emailContent.country.countryCodeISO3, ), - startDateEventString: event.startDateDisasterString, + timezone: + CountryTimeZoneMapping[emailContent.country.countryCodeISO3], + + // Lead time details + firstLeadTimeString: event.firstLeadTimeString, + firstTriggerLeadTimeString: event.firstTriggerLeadTimeString, + firstLeadTimeQuantity: event.firstLeadTime.replace('-', ' '), + firstTriggerLeadTimeQuantity: event.firstTriggerLeadTime + ? event.firstTriggerLeadTime.replace('-', ' ') + : '', + + // Area details + nrOfTriggeredAreas: event.nrOfTriggeredAreas, defaultAdminAreaLabel: emailContent.defaultAdminAreaLabel.plural.toLocaleLowerCase(), + + // Indicator details indicatorLabel: emailContent.indicatorMetadata.label, totalAffectedOfIndicator: formatActionUnitValue( event.totalAffectedOfIndicator, emailContent.indicatorMetadata.numberFormatMap, ), indicatorUnit: emailContent.indicatorMetadata.unit, - timezone: - CountryTimeZoneMapping[emailContent.country.countryCodeISO3], + totalAffected: this.getTotalAffectedHtml( + event, + emailContent.indicatorMetadata.label.toLowerCase(), + ), + + // EAP details triangleIcon: this.getTriangleIcon(event.eapAlertClass?.key), - leadTime: event.firstLeadTime.replace('-', ' '), disasterIssuedLabel: event.eapAlertClass.label, color: this.ibfColorToHex(event.eapAlertClass?.color), advisory: this.getAdvisoryHtml( event.triggerStatusLabel, emailContent.linkEapSop, ), - totalAffected: this.getTotalAffectedHtml( - event, - emailContent.indicatorMetadata.label.toLowerCase(), - ), }; const templateFileName = 'body-event.html'; diff --git a/services/API-service/src/api/notification/email/html/body-event.html b/services/API-service/src/api/notification/email/html/body-event.html index fc7439767..8df8f290c 100644 --- a/services/API-service/src/api/notification/email/html/body-event.html +++ b/services/API-service/src/api/notification/email/html/body-event.html @@ -4,11 +4,26 @@ <%= hazard %> <%= eventName %>
+ <% if (firstTriggerLeadTimeString) { %> + + <%= hazard %> expected to start on <%= firstLeadTimeString %>, <%= + firstLeadTimeQuantity %>s from now. + + + <%= disasterIssuedLabel %>: + + expected to reach threshold on <%= firstTriggerLeadTimeString %>, <%= + firstTriggerLeadTimeQuantity %>s from now. + + + <% } else { %> <%= disasterIssuedLabel %>: - <%= startDateEventString %>, <%= leadTime %>s from now. + expected on <%= firstLeadTimeString %>, <%= firstLeadTimeQuantity %>s from + now. + <% } %> Expected exposed <%= defaultAdminAreaLabel %>: 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 ec5eaffdf..e221a63e9 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 @@ -167,6 +167,7 @@ export class NotificationContentService { event, ); data.firstLeadTime = event.firstLeadTime; + data.firstTriggerLeadTime = event.firstTriggerLeadTime; data.triggeredAreas = await this.getSortedTriggeredAreas( country, disasterType, @@ -179,11 +180,17 @@ export class NotificationContentService { ); // 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.startDateDisasterString = await this.getFirstLeadTimeString( + data.firstLeadTimeString = await this.getFirstLeadTimeString( event, event.countryCodeISO3, disasterType, ); + data.firstTriggerLeadTimeString = await this.getFirstTriggerLeadTimeString( + event, + event.countryCodeISO3, + disasterType, + ); + data.totalAffectedOfIndicator = this.getTotalAffectedPerEvent( data.triggeredAreas, ); @@ -290,16 +297,48 @@ export class NotificationContentService { countryCodeISO3: string, disasterType: DisasterType, date?: Date, - ) { + ): Promise { + return this.getEventTimeString( + event.firstLeadTime, + countryCodeISO3, + disasterType, + date, + ); + } + + public async getFirstTriggerLeadTimeString( + event: EventSummaryCountry, + countryCodeISO3: string, + disasterType: DisasterType, + date?: Date, + ): Promise { + if (event.firstTriggerLeadTime) { + return this.getEventTimeString( + event.firstTriggerLeadTime, + countryCodeISO3, + disasterType, + date, + ); + } else { + return null; + } + } + + private async getEventTimeString( + leadTime: LeadTime, + countryCodeISO3: string, + disasterType: DisasterType, + date?: Date, + ): Promise { const startDateFirstEvent = await this.getFirstLeadTimeDate( - Number(event.firstLeadTime.split('-')[0]), - event.firstLeadTime.split('-')[1], + Number(leadTime.split('-')[0]), + leadTime.split('-')[1], countryCodeISO3, disasterType, date, ); const startTimeFirstEvent = await this.getLeadTimeTimestamp( - event.firstLeadTime, + leadTime, countryCodeISO3, disasterType, ); From 521abc2c933b8a040f4d885c5c02e15791c9cc7d Mon Sep 17 00:00:00 2001 From: Ruben Date: Fri, 24 May 2024 12:01:31 +0200 Subject: [PATCH 31/55] Styling details --- .../API-service/src/api/notification/email/html/base.html | 4 ---- .../api/notification/email/html/notification-actions.html | 5 ++++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/services/API-service/src/api/notification/email/html/base.html b/services/API-service/src/api/notification/email/html/base.html index 4a57d5cf5..2ef820fc4 100644 --- a/services/API-service/src/api/notification/email/html/base.html +++ b/services/API-service/src/api/notification/email/html/base.html @@ -50,10 +50,6 @@ padding: 10px 0; } - .notification-actions-table td:first-child { - width: 220px; - } - p { margin: 10px 0; padding: 0; diff --git a/services/API-service/src/api/notification/email/html/notification-actions.html b/services/API-service/src/api/notification/email/html/notification-actions.html index b7a42d92e..be5074f41 100644 --- a/services/API-service/src/api/notification/email/html/notification-actions.html +++ b/services/API-service/src/api/notification/email/html/notification-actions.html @@ -23,7 +23,10 @@ - From f08df08772dfd2126a868d7b8a49dbdde700ca25 Mon Sep 17 00:00:00 2001 From: Ruben Date: Fri, 24 May 2024 13:46:29 +0200 Subject: [PATCH 32/55] typo --- .../src/api/notification/email/email-template.service.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services/API-service/src/api/notification/email/email-template.service.ts b/services/API-service/src/api/notification/email/email-template.service.ts index 0f28dc726..1db22f41b 100644 --- a/services/API-service/src/api/notification/email/email-template.service.ts +++ b/services/API-service/src/api/notification/email/email-template.service.ts @@ -262,9 +262,9 @@ export class EmailTemplateService { hazard: emailContent.disasterTypeLabel, triggerStatusLabel: event.triggerStatusLabel, eventName: event.eventName, - defaulAdminAreaLabelSingular: + defaultAdminAreaLabelSingular: emailContent.defaultAdminAreaLabel.singular, - defaulAdminAreaLabelPlural: + defaultAdminAreaLabelPlural: emailContent.defaultAdminAreaLabel.plural.toLocaleLowerCase(), defaultAdminAreaLabelParent: adminAreaLabelsParent.singular, indicatorLabel: emailContent.indicatorMetadata.label, @@ -334,7 +334,7 @@ export class EmailTemplateService { emailContent.country.countryCodeISO3, ), startDateEventString: event.startDateDisasterString, - defaulAdminAreaLabel: + defaultAdminAreaLabel: emailContent.defaultAdminAreaLabel.plural.toLocaleLowerCase(), indicatorLabel: emailContent.indicatorMetadata.label, totalAffectedOfIndicator: event.totalAffectedOfIndicator, @@ -404,7 +404,7 @@ export class EmailTemplateService { html = this.readHtmlFile('body-total-affected-trigger.html'); } return ejs.render(html, { - totalAffectectedOfIndicator: event.totalAffectedOfIndicator, + totalAffectedOfIndicator: event.totalAffectedOfIndicator, indicatorUnit: indicatorUnit, }); } From 60dd995b3d68f6ad1ab367e927f8b2013f6d5e91 Mon Sep 17 00:00:00 2001 From: Ruben Date: Fri, 31 May 2024 16:11:03 +0200 Subject: [PATCH 33/55] Api test drought AB#27890 --- .../notification-content.service.ts | 2 +- .../email/drought/email-uga-drought.test.ts | 101 ++++++++++++++++++ .../test/helpers/utility.helper.ts | 21 ++++ 3 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 services/API-service/test/email/drought/email-uga-drought.test.ts 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 51f8199bd..5adc44489 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 @@ -192,7 +192,7 @@ export class NotificationContentService { disasterType, event.eventName || 'no-name', ); - data.eapAlertClass = event.disasterSpecificProperties.eapAlertClass; + data.eapAlertClass = event.disasterSpecificProperties?.eapAlertClass; return data; } diff --git a/services/API-service/test/email/drought/email-uga-drought.test.ts b/services/API-service/test/email/drought/email-uga-drought.test.ts new file mode 100644 index 000000000..be9795664 --- /dev/null +++ b/services/API-service/test/email/drought/email-uga-drought.test.ts @@ -0,0 +1,101 @@ +import { + getAccessToken, + mockDynamicData, + resetDB, + sendNotification, +} from '../../helpers/utility.helper'; +import { DisasterType } from '../../../src/api/disaster/disaster-type.enum'; +import { JSDOM } from 'jsdom'; + +const countryCodeISO3 = 'UGA'; +const disasterType = DisasterType.Drought; + +describe('Should send an email for uga drought', () => { + let accessToken: string; + + beforeEach(async () => { + accessToken = await getAccessToken(); + await resetDB(accessToken); + }); + + it('triggered in january', async () => { + // Mock settings + const dateJanuary = new Date(new Date().getFullYear(), 0, 1); + const triggered = true; + + // TODO: Do not hard code this but get it from the seed data + const expectedEventNames = ['Mam', 'Karamoja']; + + const nrOfEvents = 2; + const mockResult = await mockDynamicData( + disasterType, + countryCodeISO3, + triggered, + accessToken, + dateJanuary, + ); + const response = await sendNotification( + countryCodeISO3, + disasterType, + accessToken, + ); + + // Assert + // Also checking the status of the mockResult here as I think it also breaks often + expect(mockResult.status).toBe(202); + expect(response.status).toBe(201); + expect(response.body.activeEvents.email).toBeDefined(); + + expect(response.body.activeEvents.whatsapp).toBeFalsy(); + expect(response.body.finishedEvents).toBeFalsy(); + + // Parse the HTML content + const dom = new JSDOM(response.body.activeEvents.email); + const document = dom.window.document; + + // Get all span elements with apiTest="eventName" and their lower case text content + const eventNamesInEmail = Array.from( + document.querySelectorAll('span[apiTest="eventName"]'), + (el) => (el as Element).textContent.toLowerCase(), + ); + + expect(eventNamesInEmail.length).toBe(nrOfEvents); + + // Check if each expected event name is included in at least one title + for (const expectedEventName of expectedEventNames) { + const eventTitle = `${disasterType} ${expectedEventName}`.toLowerCase(); + const hasEvent = eventNamesInEmail.some((eventNameInEmail) => + eventNameInEmail.includes(eventTitle), + ); + expect(hasEvent).toBe(true); + } + }); + + it('non triggered any month', async () => { + // Mock settings + const currentDate = new Date(); + const triggered = false; + + const mockResult = await mockDynamicData( + disasterType, + countryCodeISO3, + triggered, + accessToken, + currentDate, + ); + const response = await sendNotification( + countryCodeISO3, + disasterType, + accessToken, + ); + + // Assert + // Also checking the status of the mockResult here as I think it also breaks often + expect(mockResult.status).toBe(202); + expect(response.status).toBe(201); + expect(response.body.activeEvents.email).toBeFalsy(); + expect(response.body.activeEvents.whatsapp).toBeFalsy(); + }); + + // TODO: Add more tests for different months when this issue is fixed AB#27890 +}); diff --git a/services/API-service/test/helpers/utility.helper.ts b/services/API-service/test/helpers/utility.helper.ts index 7632558e4..9235c4737 100644 --- a/services/API-service/test/helpers/utility.helper.ts +++ b/services/API-service/test/helpers/utility.helper.ts @@ -79,6 +79,27 @@ export function mockTyphoon( }); } +export function mockDynamicData( + disasterType: DisasterType, + countryCodeISO3: string, + triggered: boolean, + accessToken: string, + date?: Date, +): Promise { + return getServer() + .post('/scripts/mock-dynamic-data') + .set('Authorization', `Bearer ${accessToken}`) + .query({ isApiTest: true }) + .send({ + disasterType, + secret: process.env.RESET_SECRET, + triggered, + removeEvents: true, + date: date ? date : new Date(), + countryCodeISO3, + }); +} + export function sendNotification( countryCodeISO3: string, disasterType: DisasterType, From f549632bc7254fa888e0175b24617ea5121cbff4 Mon Sep 17 00:00:00 2001 From: arsforza Date: Fri, 28 Jun 2024 16:04:30 +0200 Subject: [PATCH 34/55] fix: use correct ejs syntax AB#26923 --- .../api/notification/email/email-template.service.ts | 2 +- .../src/api/notification/email/html/map-image.html | 11 +++++++---- .../notification/email/html/trigger-notification.html | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/services/API-service/src/api/notification/email/email-template.service.ts b/services/API-service/src/api/notification/email/email-template.service.ts index 5b676aaec..faa6ae1a2 100644 --- a/services/API-service/src/api/notification/email/email-template.service.ts +++ b/services/API-service/src/api/notification/email/email-template.service.ts @@ -191,7 +191,7 @@ export class EmailTemplateService { mapImgDescription: this.getMapImageDescription( emailContent.disasterType, ), - eventName: event.eventName ? ` for '${event.eventName}'` : '', + eventName: event.eventName ? `(for ${event.eventName})` : '', }; eventHtml = ejs.render(eventHtml, replacements); html += eventHtml; diff --git a/services/API-service/src/api/notification/email/html/map-image.html b/services/API-service/src/api/notification/email/html/map-image.html index a64b1e9f5..4af2b1715 100644 --- a/services/API-service/src/api/notification/email/html/map-image.html +++ b/services/API-service/src/api/notification/email/html/map-image.html @@ -1,6 +1,9 @@
-Map of the triggered area(<%= eventName %>): (click 'Download pictures' -if it does not show) +Map of the triggered area <%- eventName %>
-<%= mapImgDescription %> - +<%- mapImgDescription %> +click 'Download pictures' if it does not show diff --git a/services/API-service/src/api/notification/email/html/trigger-notification.html b/services/API-service/src/api/notification/email/html/trigger-notification.html index 110ceb0d1..0570573bf 100644 --- a/services/API-service/src/api/notification/email/html/trigger-notification.html +++ b/services/API-service/src/api/notification/email/html/trigger-notification.html @@ -90,7 +90,7 @@ Trigger Statement: <%- triggerStatement %>
- <%= mapImagePart %> + <%- mapImagePart %>

<%- tablesStacked %> <%- footer %> From 4512c07c1dec0b928e075d7c8393c96f2be1eb55 Mon Sep 17 00:00:00 2001 From: arsforza Date: Fri, 28 Jun 2024 16:04:45 +0200 Subject: [PATCH 35/55] fix: lint AB#26923 --- .../src/api/notification/email/email-template.service.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/services/API-service/src/api/notification/email/email-template.service.ts b/services/API-service/src/api/notification/email/email-template.service.ts index faa6ae1a2..c3ae60ed0 100644 --- a/services/API-service/src/api/notification/email/email-template.service.ts +++ b/services/API-service/src/api/notification/email/email-template.service.ts @@ -21,11 +21,6 @@ const emailTemplateFolder = `${emailFolder}/html`; const emailIconFolder = `${emailFolder}/icons`; const emailLogoFolder = `${emailFolder}/logos`; -class ReplaceKeyValue { - replaceKey: string; - replaceValue: string; -} - @Injectable() export class EmailTemplateService { public async createHtmlForTriggerEmail( @@ -358,7 +353,7 @@ export class EmailTemplateService { }; const templateFileName = 'body-event.html'; - let template = this.readHtmlFile(templateFileName); + const template = this.readHtmlFile(templateFileName); return ejs.render(template, data); }) .join(''); @@ -383,7 +378,7 @@ export class EmailTemplateService { if (event.triggerStatusLabel === TriggerStatusLabelEnum.Warning) { return this.readHtmlFile('body-total-affected-warning.html'); } else { - let html = this.readHtmlFile('body-total-affected-trigger.html'); + const html = this.readHtmlFile('body-total-affected-trigger.html'); return ejs.render(html, { totalAffectedOfIndicator: event.totalAffectedOfIndicator, indicatorUnit: indicatorUnit, From 07ff5ed9d6dd2b355471e18f2b14fd2646bf1bb6 Mon Sep 17 00:00:00 2001 From: arsforza Date: Fri, 28 Jun 2024 16:05:04 +0200 Subject: [PATCH 36/55] chore: add comment to example.env --- example.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example.env b/example.env index 9594846a1..34b343731 100644 --- a/example.env +++ b/example.env @@ -28,7 +28,7 @@ GEOSERVER_ADMIN_PASSWORD= # interfaces/IBF-dashboard NG_CONFIGURATION= -NG_API_URL= +NG_API_URL= # URL should not end with trailing slash NG_USE_SERVICE_WORKER= NG_GEOSERVER_URL= NG_IBF_SYSTEM_VERSION= From 0415d9630610c897e4751dbd8cbe557972c38ea5 Mon Sep 17 00:00:00 2001 From: Gulfaraz Rahman Date: Mon, 1 Jul 2024 14:37:51 +0200 Subject: [PATCH 37/55] fix: email module should not assume that eap alert class is required --- services/API-service/src/api/event/event.service.ts | 5 ++++- .../src/api/notification/email/email-template.service.ts | 5 ++++- .../notification-content/notification-content.service.ts | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/services/API-service/src/api/event/event.service.ts b/services/API-service/src/api/event/event.service.ts index 794a2e8b5..9b203b0e9 100644 --- a/services/API-service/src/api/event/event.service.ts +++ b/services/API-service/src/api/event/event.service.ts @@ -158,7 +158,10 @@ export class EventService { event.eventName, ); } - if (disasterSettings.eapAlertClasses) { + if (disasterType === DisasterType.Floods) { + // REFACTOR: either make eapAlertClass a requirement across all hazard + // types or reimplement such that eapAlertClass is not needed in the + // backend (it is a VIEW of the DATA in the dashboard and email) event.disasterSpecificProperties = await this.getEventEapAlertClass( disasterSettings, event.triggerValue, diff --git a/services/API-service/src/api/notification/email/email-template.service.ts b/services/API-service/src/api/notification/email/email-template.service.ts index c3ae60ed0..e3b0aca78 100644 --- a/services/API-service/src/api/notification/email/email-template.service.ts +++ b/services/API-service/src/api/notification/email/email-template.service.ts @@ -340,7 +340,10 @@ export class EmailTemplateService { CountryTimeZoneMapping[emailContent.country.countryCodeISO3], triangleIcon: this.getTriangleIcon(event.eapAlertClass?.key), leadTime: event.firstLeadTime.replace('-', ' '), - disasterIssuedLabel: event.eapAlertClass.label, + disasterIssuedLabel: + event.eapAlertClass?.label ?? event.triggerStatusLabel, + // REFACTOR: avoid the logic fork in disasterIssuedLabel + // use the same label variable across all hazard types color: this.ibfColorToHex(event.eapAlertClass?.color), advisory: this.getAdvisoryHtml( event.triggerStatusLabel, 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 ec5eaffdf..24c491191 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 @@ -192,7 +192,7 @@ export class NotificationContentService { disasterType, event.eventName || 'no-name', ); - data.eapAlertClass = event.disasterSpecificProperties.eapAlertClass; + data.eapAlertClass = event.disasterSpecificProperties?.eapAlertClass; return data; } From 050f0baaa9db75c8aa9c443dd0fd2328db9fa1b4 Mon Sep 17 00:00:00 2001 From: Gulfaraz Rahman Date: Mon, 1 Jul 2024 14:38:26 +0200 Subject: [PATCH 38/55] style: fix function name case --- .../notification-content/notification-content.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 24c491191..5adc44489 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 @@ -51,7 +51,7 @@ export class NotificationContentService { content.country = country; content.indicatorMetadata = await this.getIndicatorMetadata(disasterType); content.linkEapSop = this.getLinkEapSop(country, disasterType); - content.defaultAdminAreaLabel = this.getdefaultAdminAreaLabel( + content.defaultAdminAreaLabel = this.getDefaultAdminAreaLabel( country, content.defaultAdminLevel, ); @@ -103,7 +103,7 @@ export class NotificationContentService { ).eapLink; } - private getdefaultAdminAreaLabel( + private getDefaultAdminAreaLabel( country: CountryEntity, adminAreaDefaultLevel: number, ): AdminAreaLabel { From 1029dc109b512a43683f7e0168456583cda1f903 Mon Sep 17 00:00:00 2001 From: Gulfaraz Rahman Date: Mon, 1 Jul 2024 14:42:14 +0200 Subject: [PATCH 39/55] fix: lint error remove unused import --- .../notification-content/notification-content.module.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/services/API-service/src/api/notification/notification-content/notification-content.module.ts b/services/API-service/src/api/notification/notification-content/notification-content.module.ts index e98bdff42..6369045e5 100644 --- a/services/API-service/src/api/notification/notification-content/notification-content.module.ts +++ b/services/API-service/src/api/notification/notification-content/notification-content.module.ts @@ -9,7 +9,6 @@ import { EventModule } from '../../event/event.module'; import { IndicatorMetadataEntity } from '../../metadata/indicator-metadata.entity'; import { NotificationContentService } from './notification-content.service'; import { HelperService } from '../../../shared/helper.service'; -import { EmailTemplateService } from '../email/email-template.service'; @Module({ imports: [ From ab556a705ebb78c6b222ebb671d6396564b883aa Mon Sep 17 00:00:00 2001 From: Gulfaraz Rahman Date: Mon, 1 Jul 2024 15:39:21 +0200 Subject: [PATCH 40/55] style: fix lint warnings --- .../admin-area-dynamic-data.service.ts | 13 ++++++--- .../api/admin-area/admin-area.controller.ts | 2 +- .../src/api/admin-area/admin-area.service.ts | 2 +- .../api/eap-actions/eap-actions.controller.ts | 4 +-- .../api/eap-actions/eap-actions.service.ts | 12 ++++++--- .../src/api/event/event.controller.ts | 3 ++- .../src/api/event/event.service.ts | 3 ++- .../glofas-station.controller.ts | 2 +- .../glofas-station/glofas-station.service.ts | 2 +- .../src/api/lines-data/lines-data.service.ts | 3 ++- .../dto/content-trigger-email.dto.ts | 2 +- .../whatsapp/auth.middlewareTwilio.ts | 6 +---- .../notification/whatsapp/whatsapp.service.ts | 5 ++-- .../api/point-data/point-data.controller.ts | 4 +-- .../src/api/point-data/point-data.service.ts | 21 +++++++++++---- .../src/api/user/user.controller.ts | 2 +- services/API-service/src/roles.decorator.ts | 2 +- .../src/scripts/geoserver-sync.service.ts | 27 ++++++++++--------- .../API-service/src/scripts/mock.service.ts | 3 ++- .../src/scripts/scripts.service.ts | 18 ++++++++----- 20 files changed, 80 insertions(+), 56 deletions(-) diff --git a/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.service.ts b/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.service.ts index e917a073c..d424227b2 100644 --- a/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.service.ts +++ b/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.service.ts @@ -17,6 +17,11 @@ import { HelperService } from '../../shared/helper.service'; import { EventAreaService } from '../admin-area/services/event-area.service'; import { DisasterTypeGeoServerMapper } from '../../scripts/disaster-type-geoserver-file.mapper'; +interface RasterData { + originalname: string; + buffer: Buffer; +} + @Injectable() export class AdminAreaDynamicDataService { @InjectRepository(AdminAreaDynamicDataEntity) @@ -232,9 +237,9 @@ export class AdminAreaDynamicDataService { const result = await this.adminAreaDynamicDataRepo .createQueryBuilder('dynamic') .where({ - indicator: indicator, - placeCode: placeCode, - leadTime: leadTime, + indicator, + placeCode, + leadTime, eventName: eventName === 'no-name' || !eventName ? IsNull() : eventName, }) .select(['dynamic.value AS value']) @@ -244,7 +249,7 @@ export class AdminAreaDynamicDataService { } public async postRaster( - data: any, + data: RasterData, disasterType: DisasterType, ): Promise { const subfolder = diff --git a/services/API-service/src/api/admin-area/admin-area.controller.ts b/services/API-service/src/api/admin-area/admin-area.controller.ts index 8146c0893..bd03cb3e1 100644 --- a/services/API-service/src/api/admin-area/admin-area.controller.ts +++ b/services/API-service/src/api/admin-area/admin-area.controller.ts @@ -69,7 +69,7 @@ export class AdminAreaController { type: [AdminAreaEntity], }) @Get('raw/:countryCodeISO3') - public async getAdminAreasRaw(@Param() params): Promise { + public async getAdminAreasRaw(@Param() params) { return await this.adminAreaService.getAdminAreasRaw(params.countryCodeISO3); } diff --git a/services/API-service/src/api/admin-area/admin-area.service.ts b/services/API-service/src/api/admin-area/admin-area.service.ts index 959f18ce8..f11ca8d4a 100644 --- a/services/API-service/src/api/admin-area/admin-area.service.ts +++ b/services/API-service/src/api/admin-area/admin-area.service.ts @@ -292,7 +292,7 @@ export class AdminAreaService { }); } - public async getAdminAreasRaw(countryCodeISO3): Promise { + public async getAdminAreasRaw(countryCodeISO3) { return await this.adminAreaRepository.find({ select: [ 'countryCodeISO3', diff --git a/services/API-service/src/api/eap-actions/eap-actions.controller.ts b/services/API-service/src/api/eap-actions/eap-actions.controller.ts index d2bb7d277..ae535b920 100644 --- a/services/API-service/src/api/eap-actions/eap-actions.controller.ts +++ b/services/API-service/src/api/eap-actions/eap-actions.controller.ts @@ -1,5 +1,5 @@ import { Controller, Post, Body, Get, UseGuards, Param } from '@nestjs/common'; -import { EapActionsService } from './eap-actions.service'; +import { EapAction, EapActionsService } from './eap-actions.service'; import { UserDecorator } from '../user/user.decorator'; import { ApiBearerAuth, @@ -70,7 +70,7 @@ export class EapActionsController { @Post('check-external/:countryCodeISO3/:disasterType') public async checkActionExternally( @Param() params, - @Body() eapActions: any, + @Body() eapActions: EapAction[], ): Promise { return await this.eapActionsService.checkActionExternally( params.countryCodeISO3, diff --git a/services/API-service/src/api/eap-actions/eap-actions.service.ts b/services/API-service/src/api/eap-actions/eap-actions.service.ts index 3d44879fe..f3f43c1ee 100644 --- a/services/API-service/src/api/eap-actions/eap-actions.service.ts +++ b/services/API-service/src/api/eap-actions/eap-actions.service.ts @@ -11,6 +11,11 @@ import { AdminAreaEntity } from '../admin-area/admin-area.entity'; import { AddEapActionsDto } from './dto/eap-action.dto'; import { DisasterType } from '../disaster/disaster-type.enum'; +export interface EapAction { + Early_action: string; + placeCode: string; +} + @Injectable() export class EapActionsService { @InjectRepository(UserEntity) @@ -110,9 +115,8 @@ export class EapActionsService { public async checkActionExternally( countryCodeISO3: string, disasterType: DisasterType, - eapActions, + eapActions: EapAction[], ): Promise { - console.log('eapAction: ', eapActions); const eapActionIds = eapActions['Early_action'].split(' '); const actionIds = await this.eapActionRepository.find({ where: { @@ -129,7 +133,7 @@ export class EapActionsService { const placeCode = eapActions['placeCode']; const adminArea = await this.adminAreaRepository.findOne({ select: ['id'], - where: { placeCode: placeCode }, + where: { placeCode }, }); // note: the below will not be able to distinguish between different open events (= typhoon only) @@ -223,7 +227,7 @@ export class EapActionsService { '(' + eapActionsStates.getQuery() + ')', 'status', 'action.id = status."actionCheckedId" AND status."placeCode" = :placeCode', - { placeCode: placeCode }, + { placeCode }, ) .setParameters(eapActionsStates.getParameters()) .leftJoin('action.areaOfFocus', 'area') diff --git a/services/API-service/src/api/event/event.controller.ts b/services/API-service/src/api/event/event.controller.ts index 6dd6bb368..539107eeb 100644 --- a/services/API-service/src/api/event/event.controller.ts +++ b/services/API-service/src/api/event/event.controller.ts @@ -263,7 +263,8 @@ export class EventController { ); } const bufferStream = new stream.PassThrough(); - bufferStream.end(Buffer.from(blob, 'binary')); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + bufferStream.end(Buffer.from(blob as any, 'binary')); response.writeHead(HttpStatus.OK, { 'Content-Type': 'image/png', }); diff --git a/services/API-service/src/api/event/event.service.ts b/services/API-service/src/api/event/event.service.ts index 9b203b0e9..48393be00 100644 --- a/services/API-service/src/api/event/event.service.ts +++ b/services/API-service/src/api/event/event.service.ts @@ -130,6 +130,7 @@ export class EventService { } private async populateEventsDetails( + // eslint-disable-next-line @typescript-eslint/no-explicit-any rawEvents: any[], countryCodeISO3: string, disasterType: DisasterType, @@ -1025,7 +1026,7 @@ export class EventService { countryCodeISO3: string, disasterType: DisasterType, eventName: string, - ): Promise { + ): Promise { const eventMapImageEntity = await this.eventMapImageRepository.findOne({ where: { countryCodeISO3: countryCodeISO3, diff --git a/services/API-service/src/api/glofas-station/glofas-station.controller.ts b/services/API-service/src/api/glofas-station/glofas-station.controller.ts index e6baa24a3..50c2d3b13 100644 --- a/services/API-service/src/api/glofas-station/glofas-station.controller.ts +++ b/services/API-service/src/api/glofas-station/glofas-station.controller.ts @@ -34,7 +34,7 @@ export class GlofasStationController { description: 'Glofas station locations and attributes for given country.', }) @Get(':countryCodeISO3') - public async getStationsByCountry(@Param() params): Promise { + public async getStationsByCountry(@Param() params) { return await this.glofasStationService.getStationsByCountry( params.countryCodeISO3, ); diff --git a/services/API-service/src/api/glofas-station/glofas-station.service.ts b/services/API-service/src/api/glofas-station/glofas-station.service.ts index 5489cf7d3..dd7f407cf 100644 --- a/services/API-service/src/api/glofas-station/glofas-station.service.ts +++ b/services/API-service/src/api/glofas-station/glofas-station.service.ts @@ -15,7 +15,7 @@ export class GlofasStationService { public constructor(private readonly pointDataService: PointDataService) {} - public async getStationsByCountry(countryCodeISO3: string): Promise { + public async getStationsByCountry(countryCodeISO3: string) { const stations = await this.pointDataService.getPointDataByCountry( PointDataEnum.glofasStations, countryCodeISO3, diff --git a/services/API-service/src/api/lines-data/lines-data.service.ts b/services/API-service/src/api/lines-data/lines-data.service.ts index 864e46c1d..f52ee6375 100644 --- a/services/API-service/src/api/lines-data/lines-data.service.ts +++ b/services/API-service/src/api/lines-data/lines-data.service.ts @@ -17,7 +17,7 @@ export class LinesDataService { public constructor(private readonly helperService: HelperService) {} - private getDtoPerLinesDataCategory(linesDataCategory: LinesDataEnum): any { + private getDtoPerLinesDataCategory(linesDataCategory: LinesDataEnum) { switch (linesDataCategory) { case LinesDataEnum.roads: return new RoadDto(); @@ -34,6 +34,7 @@ export class LinesDataService { public async uploadJson( linesDataCategory: LinesDataEnum, countryCodeISO3: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any validatedObjArray: any, deleteExisting = true, ) { diff --git a/services/API-service/src/api/notification/dto/content-trigger-email.dto.ts b/services/API-service/src/api/notification/dto/content-trigger-email.dto.ts index 8d802b036..44bc7b412 100644 --- a/services/API-service/src/api/notification/dto/content-trigger-email.dto.ts +++ b/services/API-service/src/api/notification/dto/content-trigger-email.dto.ts @@ -10,7 +10,7 @@ export class ContentEventEmail { public indicatorMetadata: IndicatorMetadataEntity; public linkEapSop: string; public dataPerEvent: NotificationDataPerEventDto[]; - public mapImageData: any[]; + public mapImageData: unknown[]; public defaultAdminLevel: number; public defaultAdminAreaLabel: AdminAreaLabel; public country: CountryEntity; // Ensure that is has the following relations 'disasterTypes', 'notificationInfo','countryDisasterSettings','countryDisasterSettings.activeLeadTimes', diff --git a/services/API-service/src/api/notification/whatsapp/auth.middlewareTwilio.ts b/services/API-service/src/api/notification/whatsapp/auth.middlewareTwilio.ts index 7f6ea7ea5..fbd5bdf68 100644 --- a/services/API-service/src/api/notification/whatsapp/auth.middlewareTwilio.ts +++ b/services/API-service/src/api/notification/whatsapp/auth.middlewareTwilio.ts @@ -8,11 +8,7 @@ import { twilio } from './twilio.client'; export class AuthMiddlewareTwilio implements NestMiddleware { public constructor() {} - public async use( - req: Request, - res: Response, - next: NextFunction, - ): Promise { + public async use(req: Request, res: Response, next: NextFunction) { const twilioSignature = req.headers['x-twilio-signature']; if (DEBUG) { diff --git a/services/API-service/src/api/notification/whatsapp/whatsapp.service.ts b/services/API-service/src/api/notification/whatsapp/whatsapp.service.ts index b71ea3f2d..3359046c2 100644 --- a/services/API-service/src/api/notification/whatsapp/whatsapp.service.ts +++ b/services/API-service/src/api/notification/whatsapp/whatsapp.service.ts @@ -49,7 +49,7 @@ export class WhatsappService { message: string, recipientPhoneNr: string, mediaUrl?: string, - ): Promise { + ) { const payload = { body: message, messagingServiceSid: process.env.TWILIO_MESSAGING_SID, @@ -256,8 +256,7 @@ export class WhatsappService { events, disasterType.disasterType, ); - await this.sendWhatsapp(noTriggerMessage, fromNumber); - return; + return await this.sendWhatsapp(noTriggerMessage, fromNumber); } for (const event of sortedEvents) { diff --git a/services/API-service/src/api/point-data/point-data.controller.ts b/services/API-service/src/api/point-data/point-data.controller.ts index aa7e0b42b..aa51421b1 100644 --- a/services/API-service/src/api/point-data/point-data.controller.ts +++ b/services/API-service/src/api/point-data/point-data.controller.ts @@ -25,7 +25,7 @@ import { Roles } from '../../roles.decorator'; import { RolesGuard } from '../../roles.guard'; import { GeoJson } from '../../shared/geo.model'; import { UserRole } from '../user/user-role.enum'; -import { PointDataService } from './point-data.service'; +import { CommunityNotification, PointDataService } from './point-data.service'; import { UploadAssetExposureStatusDto, UploadDynamicPointDataDto, @@ -101,7 +101,7 @@ export class PointDataController { @Post('community-notification/:countryCodeISO3') public async uploadCommunityNotification( @Param() params, - @Body() communityNotification: any, + @Body() communityNotification: CommunityNotification, ): Promise { return await this.pointDataService.uploadCommunityNotification( params.countryCodeISO3, diff --git a/services/API-service/src/api/point-data/point-data.service.ts b/services/API-service/src/api/point-data/point-data.service.ts index ae14ec8a7..d08c16421 100644 --- a/services/API-service/src/api/point-data/point-data.service.ts +++ b/services/API-service/src/api/point-data/point-data.service.ts @@ -22,6 +22,16 @@ import { GaugeDto } from './dto/upload-gauge.dto'; import { DynamicPointDataEntity } from './dynamic-point-data.entity'; import { GlofasStationDto } from './dto/upload-glofas-station.dto'; +export interface CommunityNotification { + nameVolunteer: string; + nameVillage: string; + disasterType: string; + description: string; + end: Date; + _attachments: [{ download_url: string }]; + _geolocation: [number, number]; +} + @Injectable() export class PointDataService { @InjectRepository(PointDataEntity) @@ -89,7 +99,7 @@ export class PointDataService { return this.helperService.toGeojson(pointData); } - private getDtoPerPointDataCategory(pointDataCategory: PointDataEnum): any { + private getDtoPerPointDataCategory(pointDataCategory: PointDataEnum) { switch (pointDataCategory) { case PointDataEnum.dams: return new DamSiteDto(); @@ -120,6 +130,7 @@ export class PointDataService { public async uploadJson( pointDataCategory: PointDataEnum, countryCodeISO3: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any validatedObjArray: any, deleteExisting = true, ) { @@ -170,7 +181,7 @@ export class PointDataService { csvArray, ): Promise { const errors = []; - const validatatedArray = []; + const validatedArray = []; for (const [i, row] of csvArray.entries()) { const dto = this.getDtoPerPointDataCategory(pointDataCategory); for (const attribute in dto) { @@ -185,12 +196,12 @@ export class PointDataService { const errorObj = { lineNumber: i + 1, validationError: result }; errors.push(errorObj); } - validatatedArray.push(dto); + validatedArray.push(dto); } if (errors.length > 0) { throw new HttpException(errors, HttpStatus.BAD_REQUEST); } - return validatatedArray; + return validatedArray; } public async dismissCommunityNotification(pointDataId: string) { @@ -209,7 +220,7 @@ export class PointDataService { public async uploadCommunityNotification( countryCodeISO3: string, - communityNotification: any, + communityNotification: CommunityNotification, ): Promise { const notification = new CommunityNotificationDto(); notification.nameVolunteer = communityNotification['nameVolunteer']; diff --git a/services/API-service/src/api/user/user.controller.ts b/services/API-service/src/api/user/user.controller.ts index aced6e8b4..cb29a5bf6 100644 --- a/services/API-service/src/api/user/user.controller.ts +++ b/services/API-service/src/api/user/user.controller.ts @@ -86,7 +86,7 @@ export class UserController { public async update( @UserDecorator('userId') loggedInUserId: string, @Body() userData: UpdatePasswordDto, - ): Promise { + ) { return this.userService.update(loggedInUserId, userData); } } diff --git a/services/API-service/src/roles.decorator.ts b/services/API-service/src/roles.decorator.ts index a57ffef79..ea6255c28 100644 --- a/services/API-service/src/roles.decorator.ts +++ b/services/API-service/src/roles.decorator.ts @@ -1,4 +1,4 @@ import { SetMetadata } from '@nestjs/common'; import { UserRole } from './api/user/user-role.enum'; -export const Roles = (...roles: UserRole[]): any => SetMetadata('roles', roles); +export const Roles = (...roles: UserRole[]) => SetMetadata('roles', roles); diff --git a/services/API-service/src/scripts/geoserver-sync.service.ts b/services/API-service/src/scripts/geoserver-sync.service.ts index ae32d844e..e032d52f1 100644 --- a/services/API-service/src/scripts/geoserver-sync.service.ts +++ b/services/API-service/src/scripts/geoserver-sync.service.ts @@ -9,7 +9,7 @@ import fs from 'fs'; const workspaceName = 'ibf-system'; -class RecourceNameObject { +class ResourceNameObject { resourceName: string; disasterType: DisasterType; countryCodeISO3: string; @@ -24,7 +24,7 @@ export class GeoserverSyncService { disasterType?: DisasterType, ): Promise { const countriesCopy = JSON.parse(JSON.stringify(countries)); - const filteredCountries = countriesCopy.filter((country: any) => { + const filteredCountries = countriesCopy.filter((country) => { return countryCodeISO3 ? country.countryCodeISO3 === countryCodeISO3 : true; @@ -32,7 +32,7 @@ export class GeoserverSyncService { // also filter by disaster type for (const country of filteredCountries) { const disasterSettings = country.countryDisasterSettings.filter( - (disasterSetting: any) => { + (disasterSetting) => { return disasterType ? disasterSetting.disasterType === disasterType : true; @@ -47,7 +47,7 @@ export class GeoserverSyncService { await this.syncLayers(geoserverResourceNameObjects); } - private async syncStores(expectedStoreNameObjects: RecourceNameObject[]) { + private async syncStores(expectedStoreNameObjects: ResourceNameObject[]) { const foundStoreNames = await this.getStoreNamesFromGeoserver( workspaceName, ); @@ -58,8 +58,9 @@ export class GeoserverSyncService { } private generateGeoserverResourceNames( + // eslint-disable-next-line @typescript-eslint/no-explicit-any filteredCountries: any[], - ): RecourceNameObject[] { + ): ResourceNameObject[] { const resourceNameObjects = []; for (const country of filteredCountries) { resourceNameObjects.push(...this.generateStoreNameForCountry(country)); @@ -67,7 +68,7 @@ export class GeoserverSyncService { return resourceNameObjects; } - private generateStoreNameForCountry(country: any): RecourceNameObject[] { + private generateStoreNameForCountry(country): ResourceNameObject[] { const resourceNameObjects = []; const countryCode = country.countryCodeISO3; for (const disasterSetting of country.countryDisasterSettings) { @@ -92,13 +93,13 @@ export class GeoserverSyncService { private async getStoreNamesFromGeoserver(workspaceName: string) { const data = await this.get(`workspaces/${workspaceName}/coveragestores`); const storeNames = data.coverageStores.coverageStore.map( - (store: any) => store.name, + (store) => store.name, ); return storeNames; } private async postStoreNamesToGeoserver( - resourceNameObjects: RecourceNameObject[], + resourceNameObjects: ResourceNameObject[], ) { for (const resourceNameObject of resourceNameObjects) { const subfolder = DisasterTypeGeoServerMapper.getSubfolderForDisasterType( @@ -135,7 +136,7 @@ export class GeoserverSyncService { } } - public async syncLayers(expectedLayerNames: RecourceNameObject[]) { + public async syncLayers(expectedLayerNames: ResourceNameObject[]) { const foundLayerNames = await this.getLayerNamesFromGeoserver( workspaceName, ); @@ -147,12 +148,12 @@ export class GeoserverSyncService { private async getLayerNamesFromGeoserver(workspaceName: string) { const data = await this.get(`workspaces/${workspaceName}/layers`); - const layerNames = data.layers.layer.map((layer: any) => layer.name); + const layerNames = data.layers.layer.map((layer) => layer.name); return layerNames; } private async postLayerNamesToGeoserver( - resourceNameObjects: RecourceNameObject[], + resourceNameObjects: ResourceNameObject[], ) { for (const resourceNameObject of resourceNameObjects) { const publishLayerUrl = `workspaces/${workspaceName}/coveragestores/${resourceNameObject.resourceName}/coverages`; @@ -182,7 +183,7 @@ export class GeoserverSyncService { } } - private async post(path: string, body: any) { + private async post(path: string, body: unknown) { const url = `${INTERNAL_GEOSERVER_API_URL}/${path}`; const headers = this.getHeaders(); const result = await firstValueFrom( @@ -191,7 +192,7 @@ export class GeoserverSyncService { return result.data; } - private async put(path: string, body: any) { + private async put(path: string, body: unknown) { const url = `${INTERNAL_GEOSERVER_API_URL}/${path}`; const headers = this.getHeaders(); const result = await firstValueFrom( diff --git a/services/API-service/src/scripts/mock.service.ts b/services/API-service/src/scripts/mock.service.ts index 4626a6555..dd6795844 100644 --- a/services/API-service/src/scripts/mock.service.ts +++ b/services/API-service/src/scripts/mock.service.ts @@ -68,7 +68,7 @@ export class MockService { await this.removeEvents(mockBody.countryCodeISO3, disasterType); } - const selectedCountry = countries.find((country): any => { + const selectedCountry = countries.find((country) => { if (mockBody.countryCodeISO3 === country.countryCodeISO3) { return country; } @@ -244,6 +244,7 @@ export class MockService { private getLeadTimesForNoTrigger( disasterType: DisasterType, + // eslint-disable-next-line @typescript-eslint/no-explicit-any selectedCountry: any, ): LeadTime[] { // NOTE: this reflects agreements with pipelines that are in place. This is ugly, and should be refactored better. diff --git a/services/API-service/src/scripts/scripts.service.ts b/services/API-service/src/scripts/scripts.service.ts index 38c6547d8..daee18356 100644 --- a/services/API-service/src/scripts/scripts.service.ts +++ b/services/API-service/src/scripts/scripts.service.ts @@ -155,7 +155,7 @@ export class ScriptsService { await this.eventPlaceCodeRepo.remove(allCountryEvents); } - const selectedCountry = countries.find((country): any => { + const selectedCountry = countries.find((country) => { if (mockInput.countryCodeISO3 === country.countryCodeISO3) { return country; } @@ -407,6 +407,7 @@ export class ScriptsService { } private getLeadTimes( + // eslint-disable-next-line @typescript-eslint/no-explicit-any selectedCountry: any, disasterType: DisasterType, eventNr: number, @@ -456,6 +457,7 @@ export class ScriptsService { typhoonScenario?: TyphoonScenario, eventRegion?: string, leadTime?: LeadTime, + // eslint-disable-next-line @typescript-eslint/no-explicit-any selectedCountry?: any, date?: Date, triggered?: boolean, @@ -494,6 +496,7 @@ export class ScriptsService { } private filterLeadTimesPerDisasterType( + // eslint-disable-next-line @typescript-eslint/no-explicit-any selectedCountry: any, leadTime: string, disasterType: DisasterType, @@ -552,6 +555,7 @@ export class ScriptsService { } private getDroughtLeadTime( + // eslint-disable-next-line @typescript-eslint/no-explicit-any selectedCountry: any, leadTime: string, disasterType: DisasterType, @@ -582,6 +586,7 @@ export class ScriptsService { forecastSeasons, leadTime: string, date: Date, + // eslint-disable-next-line @typescript-eslint/no-explicit-any selectedCountry: any, ) { const { currentYear, currentUTCMonth, leadTimeMonthFirstDay } = @@ -650,7 +655,8 @@ export class ScriptsService { } private async mockAmount( - exposurePlacecodes: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + exposurePlaceCodes: any, exposureUnit: DynamicIndicator, triggered: boolean, disasterType: DisasterType, @@ -658,8 +664,8 @@ export class ScriptsService { activeLeadTime: LeadTime, date: Date, eventRegion?: string, - ): Promise { - let copyOfExposureUnit = JSON.parse(JSON.stringify(exposurePlacecodes)); + ) { + let copyOfExposureUnit = JSON.parse(JSON.stringify(exposurePlaceCodes)); if ( disasterType === DisasterType.Drought && selectedCountry.countryCodeISO3 !== 'ZWE' && // exclude ZWE drought from this rule @@ -718,9 +724,7 @@ export class ScriptsService { const month = leadTimeMonthFirstDay.getMonth() + 1; const triggeredAreas = droughtRegionAreas[droughtRegion].map( - (placeCode) => { - return { placeCode: placeCode, triggered: false }; - }, + (placeCode) => ({ placeCode, triggered: false }), ); for (const season of Object.values(forecastSeasonAreas[droughtRegion])) { const filteredSeason = season[this.rainMonthsKey].filter( From 18edcb6f8c2335286ef15e4202effe04cf4e6477 Mon Sep 17 00:00:00 2001 From: Gulfaraz Rahman Date: Mon, 1 Jul 2024 16:16:49 +0200 Subject: [PATCH 41/55] refactor: simply 3-way logical split into a 2-way split --- services/API-service/src/api/event/event.service.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/services/API-service/src/api/event/event.service.ts b/services/API-service/src/api/event/event.service.ts index 48393be00..b072999d0 100644 --- a/services/API-service/src/api/event/event.service.ts +++ b/services/API-service/src/api/event/event.service.ts @@ -91,15 +91,11 @@ export class EventService { countryCodeISO3, ); - // I spend quite some time on trying to figure out what is the right query to get the event finished summary for the trigger closed email - // I came up with the following query but I am not sure if it is correct and how to test it properly - const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); const sixDaysAgo = new Date(Date.now() - 6 * 24 * 60 * 60 * 1000); const eventSummaryQueryBuilder = this.createEventSummaryQueryBuilder( countryCodeISO3, ) - .andWhere('event.endDate > :endDateMin', { endDateMin: sevenDaysAgo }) - .andWhere('event.endDate < :endDateMax', { endDateMax: sixDaysAgo }) + .andWhere('event.endDate > :endDate', { endDate: sixDaysAgo }) .andWhere('event.adminArea IN (:...adminAreaIds)', { adminAreaIds: countryAdminAreaIds, }) From f3c57860f1f763216c724be87caa2eb9ede88188 Mon Sep 17 00:00:00 2001 From: Gulfaraz Rahman Date: Mon, 1 Jul 2024 16:17:42 +0200 Subject: [PATCH 42/55] style: readability fixes --- .../src/api/event/event.service.ts | 83 ++++++++----------- 1 file changed, 36 insertions(+), 47 deletions(-) diff --git a/services/API-service/src/api/event/event.service.ts b/services/API-service/src/api/event/event.service.ts index b072999d0..a2b615fb3 100644 --- a/services/API-service/src/api/event/event.service.ts +++ b/services/API-service/src/api/event/event.service.ts @@ -87,21 +87,15 @@ export class EventService { countryCodeISO3: string, disasterType: DisasterType, ): Promise { - const countryAdminAreaIds = await this.getCountryAdminAreaIds( - countryCodeISO3, - ); + const adminAreaIds = await this.getCountryAdminAreaIds(countryCodeISO3); const sixDaysAgo = new Date(Date.now() - 6 * 24 * 60 * 60 * 1000); const eventSummaryQueryBuilder = this.createEventSummaryQueryBuilder( countryCodeISO3, ) .andWhere('event.endDate > :endDate', { endDate: sixDaysAgo }) - .andWhere('event.adminArea IN (:...adminAreaIds)', { - adminAreaIds: countryAdminAreaIds, - }) - .andWhere('event.disasterType = :disasterType', { - disasterType: disasterType, - }) + .andWhere('event.adminArea IN (:...adminAreaIds)', { adminAreaIds }) + .andWhere('event.disasterType = :disasterType', { disasterType }) .andWhere('event.closed = :closed', { closed: true }); return this.queryAndMapEventSummary( @@ -270,7 +264,7 @@ export class EventService { ); const deleteFilters = { adminArea: In(countryAdminAreaIds), - disasterType: disasterType, + disasterType, startDate: MoreThanOrEqual( this.helperService.getUploadCutoffMoment(disasterType, date), ), @@ -288,7 +282,7 @@ export class EventService { return ( await this.disasterTypeRepository.findOne({ select: ['triggerUnit'], - where: { disasterType: disasterType }, + where: { disasterType }, }) ).triggerUnit; } @@ -299,7 +293,7 @@ export class EventService { ) { return ( await this.countryRepository.findOne({ - where: { countryCodeISO3: countryCodeISO3 }, + where: { countryCodeISO3 }, relations: ['countryDisasterSettings'], }) ).countryDisasterSettings.find((d) => d.disasterType === disasterType); @@ -322,11 +316,11 @@ export class EventService { ).defaultAdminLevel; const whereFiltersDynamicData = { - indicator: triggerUnit, + indicator: triggerUnit, // REFACTOR: trigger unit and indicator should not be used interchangeably value: MoreThan(0), - adminLevel: adminLevel, - disasterType: disasterType, - countryCodeISO3: countryCodeISO3, + adminLevel, + disasterType, + countryCodeISO3, timestamp: MoreThanOrEqual( this.helperService.getUploadCutoffMoment( disasterType, @@ -576,14 +570,14 @@ export class EventService { disasterType, ); const whereFilters = { - countryCodeISO3: countryCodeISO3, + countryCodeISO3, timestamp: MoreThanOrEqual( this.helperService.getUploadCutoffMoment( disasterType, lastTriggeredDate.timestamp, ), ), - disasterType: disasterType, + disasterType, }; if (eventName) { whereFilters['eventName'] = eventName; @@ -663,7 +657,7 @@ export class EventService { return ( await this.disasterTypeRepository.findOne({ select: ['actionsUnit'], - where: { disasterType: disasterType }, + where: { disasterType }, }) ).actionsUnit; } @@ -718,16 +712,16 @@ export class EventService { ); const whereFilters = { - indicator: triggerUnit, + indicator: triggerUnit, // REFACTOR: trigger unit and indicator should not be used interchangeably timestamp: MoreThanOrEqual( this.helperService.getUploadCutoffMoment( disasterType, lastTriggeredDate.timestamp, ), ), - countryCodeISO3: countryCodeISO3, - adminLevel: adminLevel, - disasterType: disasterType, + countryCodeISO3, + adminLevel, + disasterType, eventName: eventName || IsNull(), }; @@ -752,16 +746,16 @@ export class EventService { const whereOptions = { placeCode: In(triggerPlaceCodesArray), - indicator: actionUnit, + indicator: actionUnit, // REFACTOR: action unit and indicator should not be used interchangeably timestamp: MoreThanOrEqual( this.helperService.getUploadCutoffMoment( disasterType, lastTriggeredDate.timestamp, ), ), - countryCodeISO3: countryCodeISO3, - adminLevel: adminLevel, - disasterType: disasterType, + countryCodeISO3, + adminLevel, + disasterType, }; if (eventName) { whereFilters['eventName'] = eventName; @@ -778,7 +772,7 @@ export class EventService { for (const area of affectedAreas) { area.triggerValue = triggeredPlaceCodes.find( - (p) => p.placeCode === area.placeCode, + ({ placeCode }) => placeCode === area.placeCode, ).triggerValue; } @@ -804,7 +798,7 @@ export class EventService { where: { closed: false, adminArea: In(countryAdminAreaIds), - disasterType: disasterType, + disasterType, eventName: eventName || IsNull(), }, relations: ['adminArea'], @@ -849,17 +843,14 @@ export class EventService { private async updateEvents( eventPlaceCodeIds: string[], - aboveThreshold: boolean, + thresholdReached: boolean, endDate: Date, ) { if (eventPlaceCodeIds.length) { await this.eventPlaceCodeRepo .createQueryBuilder() .update() - .set({ - thresholdReached: aboveThreshold, - endDate: endDate, - }) + .set({ thresholdReached, endDate }) .where({ eventPlaceCodeId: In(eventPlaceCodeIds) }) .execute(); } @@ -920,7 +911,7 @@ export class EventService { where: { closed: false, adminArea: In(countryAdminAreaIds), - disasterType: disasterType, + disasterType, eventName: eventName || IsNull(), }, relations: ['adminArea'], @@ -964,28 +955,26 @@ export class EventService { countryCodeISO3, disasterType, ); - const whereFilters = { - endDate: LessThan(uploadDate.timestamp), // If the area was not prolongued earlier, then the endDate is not updated and is therefore less than the uploadDate + const where = { + endDate: LessThan(uploadDate.timestamp), // If the area was not prolonged earlier, then the endDate is not updated and is therefore less than the uploadDate adminArea: In(countryAdminAreaIds), - disasterType: disasterType, + disasterType, closed: false, }; - const expiredEventAreas = await this.eventPlaceCodeRepo.find({ - where: whereFilters, - }); + const expiredEventAreas = await this.eventPlaceCodeRepo.find({ where }); // Below threshold events can be removed from this table after closing // Below threshold events are warnings an not triggered. I do not know why they are removed here const belowThresholdEvents = expiredEventAreas.filter( - (a) => !a.thresholdReached, + ({ thresholdReached }) => !thresholdReached, ); await this.eventPlaceCodeRepo.remove(belowThresholdEvents); //For the other ones update 'closed = true' const aboveThresholdEvents = expiredEventAreas.filter( - (a) => a.thresholdReached, + ({ thresholdReached }) => thresholdReached, ); - for await (const area of aboveThresholdEvents) { + for (const area of aboveThresholdEvents) { area.closed = true; } await this.eventPlaceCodeRepo.save(aboveThresholdEvents); @@ -995,7 +984,7 @@ export class EventService { countryCodeISO3: string, disasterType: DisasterType, eventName: string, - imageFileBlob, + imageFileBlob: { buffer: Buffer }, ): Promise { let eventMapImageEntity = await this.eventMapImageRepository.findOne({ where: { @@ -1025,8 +1014,8 @@ export class EventService { ): Promise { const eventMapImageEntity = await this.eventMapImageRepository.findOne({ where: { - countryCodeISO3: countryCodeISO3, - disasterType: disasterType, + countryCodeISO3, + disasterType, eventName: eventName === 'no-name' || !eventName ? IsNull() : eventName, }, }); From e2683973b1f365becb1c8c772b3dbf58a96f1111 Mon Sep 17 00:00:00 2001 From: Gulfaraz Rahman Date: Mon, 1 Jul 2024 16:35:50 +0200 Subject: [PATCH 43/55] feat: use date-fns library for date operations --- services/API-service/package-lock.json | 83 ++++++++++--------- services/API-service/package.json | 1 + .../src/api/event/event.service.ts | 3 +- 3 files changed, 47 insertions(+), 40 deletions(-) diff --git a/services/API-service/package-lock.json b/services/API-service/package-lock.json index 2a926b0d3..a66c8d0d7 100644 --- a/services/API-service/package-lock.json +++ b/services/API-service/package-lock.json @@ -19,6 +19,7 @@ "class-transformer": "^0.3.1", "class-validator": "^0.14.0", "csv-parser": "^3.0.0", + "date-fns": "^3.6.0", "ejs": "^3.1.9", "jsonwebtoken": "^8.1.1", "juice": "^10.0.0", @@ -326,9 +327,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.10.tgz", - "integrity": "sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -4308,11 +4309,6 @@ "node": ">=8" } }, - "node_modules/cryptr": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/cryptr/-/cryptr-6.0.2.tgz", - "integrity": "sha512-1TRHI4bmuLIB8WgkH9eeYXzhEg1T4tonO4vVaMBKKde8Dre51J68nAgTVXTwMYXAf7+mV2gBCkm/9wksjSb2sA==" - }, "node_modules/css-select": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", @@ -4402,18 +4398,12 @@ } }, "node_modules/date-fns": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", - "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", - "dependencies": { - "@babel/runtime": "^7.21.0" - }, - "engines": { - "node": ">=0.11" - }, + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", "funding": { - "type": "opencollective", - "url": "https://opencollective.com/date-fns" + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" } }, "node_modules/dayjs": { @@ -11451,9 +11441,9 @@ "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" }, "node_modules/regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regex-not": { "version": "1.0.2", @@ -13504,6 +13494,21 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/typeorm/node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/typeorm/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -14611,9 +14616,9 @@ } }, "@babel/runtime": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.10.tgz", - "integrity": "sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", "requires": { "regenerator-runtime": "^0.14.0" } @@ -17644,11 +17649,6 @@ "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", "dev": true }, - "cryptr": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/cryptr/-/cryptr-6.0.2.tgz", - "integrity": "sha512-1TRHI4bmuLIB8WgkH9eeYXzhEg1T4tonO4vVaMBKKde8Dre51J68nAgTVXTwMYXAf7+mV2gBCkm/9wksjSb2sA==" - }, "css-select": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", @@ -17722,12 +17722,9 @@ } }, "date-fns": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", - "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", - "requires": { - "@babel/runtime": "^7.21.0" - } + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==" }, "dayjs": { "version": "1.11.6", @@ -23403,9 +23400,9 @@ "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" }, "regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "regex-not": { "version": "1.0.2", @@ -24983,6 +24980,14 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "requires": { + "@babel/runtime": "^7.21.0" + } + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", diff --git a/services/API-service/package.json b/services/API-service/package.json index eb169fb6c..c0f6f8214 100644 --- a/services/API-service/package.json +++ b/services/API-service/package.json @@ -37,6 +37,7 @@ "class-transformer": "^0.3.1", "class-validator": "^0.14.0", "csv-parser": "^3.0.0", + "date-fns": "^3.6.0", "ejs": "^3.1.9", "jsonwebtoken": "^8.1.1", "juice": "^10.0.0", diff --git a/services/API-service/src/api/event/event.service.ts b/services/API-service/src/api/event/event.service.ts index a2b615fb3..d536408a6 100644 --- a/services/API-service/src/api/event/event.service.ts +++ b/services/API-service/src/api/event/event.service.ts @@ -38,6 +38,7 @@ import { EventMapImageEntity } from './event-map-image.entity'; import { TyphoonTrackService } from '../typhoon-track/typhoon-track.service'; import { CountryEntity } from '../country/country.entity'; import { CountryDisasterSettingsEntity } from '../country/country-disaster.entity'; +import { subDays } from 'date-fns'; @Injectable() export class EventService { @@ -89,7 +90,7 @@ export class EventService { ): Promise { const adminAreaIds = await this.getCountryAdminAreaIds(countryCodeISO3); - const sixDaysAgo = new Date(Date.now() - 6 * 24 * 60 * 60 * 1000); + const sixDaysAgo = subDays(new Date(), 6); const eventSummaryQueryBuilder = this.createEventSummaryQueryBuilder( countryCodeISO3, ) From 176e2f800c94532e59e18b9bacbb8d5fe4fa91c4 Mon Sep 17 00:00:00 2001 From: Gulfaraz Rahman Date: Mon, 1 Jul 2024 16:50:16 +0200 Subject: [PATCH 44/55] style: use prettier plugin to sort imports --- .prettierrc | 1 - services/API-service/.prettierrc.js | 5 +- services/API-service/package-lock.json | 1012 ++++++++++++++++-------- services/API-service/package.json | 1 + 4 files changed, 692 insertions(+), 327 deletions(-) diff --git a/.prettierrc b/.prettierrc index 283367cde..fa9699b89 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,6 +3,5 @@ "trailingComma": "all", "singleQuote": true, "printWidth": 80, - "no-parameter-properties": true, "tabWidth": 2 } diff --git a/services/API-service/.prettierrc.js b/services/API-service/.prettierrc.js index a183a7df1..d20840619 100644 --- a/services/API-service/.prettierrc.js +++ b/services/API-service/.prettierrc.js @@ -3,6 +3,9 @@ module.exports = { trailingComma: 'all', singleQuote: true, printWidth: 80, - 'no-parameter-properties': true, tabWidth: 2, + plugins: ['@ianvs/prettier-plugin-sort-imports'], + importOrder: ['^@nestjs', '', '', '', '^[.]'], + importOrderParserPlugins: ['typescript', 'jsx', 'decorators-legacy'], + importOrderTypeScriptVersion: '5.0.0', }; diff --git a/services/API-service/package-lock.json b/services/API-service/package-lock.json index a66c8d0d7..592770c77 100644 --- a/services/API-service/package-lock.json +++ b/services/API-service/package-lock.json @@ -37,6 +37,7 @@ "wkt-io-ts": "^1.0.2" }, "devDependencies": { + "@ianvs/prettier-plugin-sort-imports": "^4.3.0", "@types/express": "^4.17.14", "@types/jest": "^26.0.20", "@types/node": "16.x", @@ -71,41 +72,77 @@ "node": ">=0.10.0" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/code-frame": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", - "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", "dev": true, "dependencies": { - "@babel/highlight": "^7.12.13" + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", + "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", + "dev": true, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.13.tgz", - "integrity": "sha512-BQKE9kXkPlXHPeqissfxo0lySWJcYdEP0hdtJOH/iJfDdhOCcgtNCjftCJg3qqauB4h+lz2N6ixM++b9DN1Tcw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@babel/generator": "^7.12.13", - "@babel/helper-module-transforms": "^7.12.13", - "@babel/helpers": "^7.12.13", - "@babel/parser": "^7.12.13", - "@babel/template": "^7.12.13", - "@babel/traverse": "^7.12.13", - "@babel/types": "^7.12.13", - "convert-source-map": "^1.7.0", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", + "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helpers": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7", + "convert-source-map": "^2.0.0", "debug": "^4.1.0", - "gensync": "^1.0.0-beta.1", - "json5": "^2.1.2", - "lodash": "^4.17.19", - "semver": "^5.4.1", - "source-map": "^0.5.0" + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, "node_modules/@babel/core/node_modules/debug": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", @@ -119,13 +156,10 @@ } }, "node_modules/@babel/core/node_modules/json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, - "dependencies": { - "minimist": "^1.2.5" - }, "bin": { "json5": "lib/cli.js" }, @@ -139,106 +173,119 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "node_modules/@babel/core/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "node_modules/@babel/generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", + "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", "dev": true, - "bin": { - "semver": "bin/semver" + "dependencies": { + "@babel/types": "^7.24.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@babel/core/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", + "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", "dev": true, + "dependencies": { + "@babel/compat-data": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, "engines": { - "node": ">=0.10.0" + "node": ">=6.9.0" } }, - "node_modules/@babel/generator": { - "version": "7.12.15", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.15.tgz", - "integrity": "sha512-6F2xHxBiFXWNSGb7vyCUTBF8RCLY66rS0zEPcP8t/nQyXjha5EuK4z7H5o7fWG8B4M7y6mqVWq1J+1PuwRhecQ==", + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, "dependencies": { - "@babel/types": "^7.12.13", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" + "yallist": "^3.0.2" } }, - "node_modules/@babel/generator/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true }, - "node_modules/@babel/helper-function-name": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz", - "integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==", + "node_modules/@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", "dev": true, "dependencies": { - "@babel/helper-get-function-arity": "^7.12.13", - "@babel/template": "^7.12.13", - "@babel/types": "^7.12.13" + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@babel/helper-get-function-arity": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz", - "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==", + "node_modules/@babel/helper-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", "dev": true, "dependencies": { - "@babel/types": "^7.12.13" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.13.tgz", - "integrity": "sha512-B+7nN0gIL8FZ8SvMcF+EPyB21KnCcZHQZFczCxbiNGV/O0rsrSBlWGLzmtBJ3GMjSVMIm4lpFhR+VdVBuIsUcQ==", + "node_modules/@babel/helper-hoist-variables": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", "dev": true, "dependencies": { - "@babel/types": "^7.12.13" + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.13.tgz", - "integrity": "sha512-NGmfvRp9Rqxy0uHSSVP+SRIW1q31a7Ji10cLBcqSDUngGentY4FRiHOFZFE1CLU5eiL0oE8reH7Tg1y99TDM/g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", "dev": true, "dependencies": { - "@babel/types": "^7.12.13" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.12.13.tgz", - "integrity": "sha512-acKF7EjqOR67ASIlDTupwkKM1eUisNAjaSduo5Cz+793ikfnpe7p4Q7B7EWU2PCoSTPWsQkR7hRUWEIZPiVLGA==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.12.13", - "@babel/helper-replace-supers": "^7.12.13", - "@babel/helper-simple-access": "^7.12.13", - "@babel/helper-split-export-declaration": "^7.12.13", - "@babel/helper-validator-identifier": "^7.12.11", - "@babel/template": "^7.12.13", - "@babel/traverse": "^7.12.13", - "@babel/types": "^7.12.13", - "lodash": "^4.17.19" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz", - "integrity": "sha512-BdWQhoVJkp6nVjB7nkFWcn43dkprYauqtk++Py2eaf/GRDFm5BxRqEIZCiHlZUGAVmtwKcsVL1dC68WmzeFmiA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", + "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", "dev": true, "dependencies": { - "@babel/types": "^7.12.13" + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-plugin-utils": { @@ -247,68 +294,90 @@ "integrity": "sha512-C+10MXCXJLiR6IeG9+Wiejt9jmtFpxUc3MQqCmPY8hfCjyUGl9kT+B2okzEZrtykiwrc4dbCPdDoz0A/HQbDaA==", "dev": true }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.13.tgz", - "integrity": "sha512-pctAOIAMVStI2TMLhozPKbf5yTEXc0OJa0eENheb4w09SrgOWEs+P4nTOZYJQCqs8JlErGLDPDJTiGIp3ygbLg==", + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", "dev": true, "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.12.13", - "@babel/helper-optimise-call-expression": "^7.12.13", - "@babel/traverse": "^7.12.13", - "@babel/types": "^7.12.13" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@babel/helper-simple-access": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.12.13.tgz", - "integrity": "sha512-0ski5dyYIHEfwpWGx5GPWhH35j342JaflmCeQmsPWcrOQDtCN6C1zKAVRFVbK53lPW2c9TsuLLSUDf0tIGJ5hA==", + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", "dev": true, "dependencies": { - "@babel/types": "^7.12.13" + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz", - "integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==", + "node_modules/@babel/helper-string-parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", "dev": true, - "dependencies": { - "@babel/types": "^7.12.13" + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", - "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", - "dev": true + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", + "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } }, "node_modules/@babel/helpers": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.12.13.tgz", - "integrity": "sha512-oohVzLRZ3GQEk4Cjhfs9YkJA4TdIDTObdBEZGrd6F/T0GPSnuV6l22eMcxlvcvzVIPH3VTtxbseudM1zIE+rPQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", + "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", "dev": true, "dependencies": { - "@babel/template": "^7.12.13", - "@babel/traverse": "^7.12.13", - "@babel/types": "^7.12.13" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.12.13.tgz", - "integrity": "sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.12.11", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.12.15", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.15.tgz", - "integrity": "sha512-AQBOU2Z9kWwSZMd6lNjCX0GUgFonL1wAM1db8L8PMk9UDaGsRCArBkU4Sc+UCM3AE4hjbXx+h58Lb3QT4oRmrA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -338,31 +407,38 @@ } }, "node_modules/@babel/template": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", - "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.12.13", - "@babel/parser": "^7.12.13", - "@babel/types": "^7.12.13" + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.13.tgz", - "integrity": "sha512-3Zb4w7eE/OslI0fTp8c7b286/cQps3+vdLW3UcwC8VSJC6GbKn55aeVVu2QJNuCDoeKyptLOFrPq8WqZZBodyA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", + "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.12.13", - "@babel/generator": "^7.12.13", - "@babel/helper-function-name": "^7.12.13", - "@babel/helper-split-export-declaration": "^7.12.13", - "@babel/parser": "^7.12.13", - "@babel/types": "^7.12.13", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.19" + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/traverse/node_modules/debug": { @@ -384,14 +460,17 @@ "dev": true }, "node_modules/@babel/types": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.13.tgz", - "integrity": "sha512-oKrdZTld2im1z8bDwTOQvUbxKwE+854zc16qWZQlcTqMN00pWxHQ4ZeOq0yDMnisOpRykH2/5Qqcrk/OlbAjiQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.12.11", - "lodash": "^4.17.19", + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@cnakazawa/watch": { @@ -611,6 +690,41 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@ianvs/prettier-plugin-sort-imports": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@ianvs/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.3.0.tgz", + "integrity": "sha512-OOMtUcO4J3LoL63dOKAe7bn+lSRRPeit2DqNHpx+wvBp3Grejo2PMaK4Mp1mwy8pnat64ccSgk/lBZbsAdLErw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.24.0", + "@babel/generator": "^7.23.6", + "@babel/parser": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0", + "semver": "^7.5.2" + }, + "peerDependencies": { + "@vue/compiler-sfc": "2.7.x || 3.x", + "prettier": "2 || 3" + }, + "peerDependenciesMeta": { + "@vue/compiler-sfc": { + "optional": true + } + } + }, + "node_modules/@ianvs/prettier-plugin-sort-imports/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@jest/console": { "version": "24.9.0", "resolved": "https://registry.npmjs.org/@jest/console/-/console-24.9.0.tgz", @@ -1445,6 +1559,20 @@ "node": ">=8" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", @@ -1454,12 +1582,31 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", "devOptional": true }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@lukeed/csprng": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", @@ -3509,6 +3656,38 @@ "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", "dev": true }, + "node_modules/browserslist": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", + "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.16" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/bs-logger": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", @@ -3643,6 +3822,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001639", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001639.tgz", + "integrity": "sha512-eFHflNTBIlFwP2AIKaYuBQN/apnUoKNhBdza8ZnW/h2di4LCZ4xFqYlxUxo+LQ76KFI1PGcC1QDxMbxTZpSCAg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, "node_modules/capture-exit": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", @@ -4740,6 +4939,12 @@ "node": ">=0.10.0" } }, + "node_modules/electron-to-chromium": { + "version": "1.4.815", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.815.tgz", + "integrity": "sha512-OvpTT2ItpOXJL7IGcYakRjHCt8L5GrrN/wHCQsRB4PQa1X9fe+X9oen245mIId7s14xvArCGSTIq644yPUKKLg==", + "dev": true + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -4822,9 +5027,9 @@ } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "engines": { "node": ">=6" } @@ -10413,6 +10618,12 @@ "semver": "bin/semver" } }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, "node_modules/nodemon": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.7.tgz", @@ -11011,6 +11222,12 @@ "node": ">= 10.x" } }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -12006,9 +12223,9 @@ "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==" }, "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -13743,6 +13960,36 @@ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "dev": true }, + "node_modules/update-browserslist-db": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", + "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/update-notifier": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz", @@ -14383,38 +14630,61 @@ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", "dev": true }, + "@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "@babel/code-frame": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", - "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", "dev": true, "requires": { - "@babel/highlight": "^7.12.13" + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" } }, + "@babel/compat-data": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", + "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", + "dev": true + }, "@babel/core": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.13.tgz", - "integrity": "sha512-BQKE9kXkPlXHPeqissfxo0lySWJcYdEP0hdtJOH/iJfDdhOCcgtNCjftCJg3qqauB4h+lz2N6ixM++b9DN1Tcw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.12.13", - "@babel/generator": "^7.12.13", - "@babel/helper-module-transforms": "^7.12.13", - "@babel/helpers": "^7.12.13", - "@babel/parser": "^7.12.13", - "@babel/template": "^7.12.13", - "@babel/traverse": "^7.12.13", - "@babel/types": "^7.12.13", - "convert-source-map": "^1.7.0", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", + "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helpers": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7", + "convert-source-map": "^2.0.0", "debug": "^4.1.0", - "gensync": "^1.0.0-beta.1", - "json5": "^2.1.2", - "lodash": "^4.17.19", - "semver": "^5.4.1", - "source-map": "^0.5.0" + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" }, "dependencies": { + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, "debug": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", @@ -14425,115 +14695,110 @@ } }, "json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true } } }, "@babel/generator": { - "version": "7.12.15", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.15.tgz", - "integrity": "sha512-6F2xHxBiFXWNSGb7vyCUTBF8RCLY66rS0zEPcP8t/nQyXjha5EuK4z7H5o7fWG8B4M7y6mqVWq1J+1PuwRhecQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", + "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", + "dev": true, + "requires": { + "@babel/types": "^7.24.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", + "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", "dev": true, "requires": { - "@babel/types": "^7.12.13", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" + "@babel/compat-data": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true } } }, - "@babel/helper-function-name": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz", - "integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==", + "@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.12.13", - "@babel/template": "^7.12.13", - "@babel/types": "^7.12.13" + "@babel/types": "^7.24.7" } }, - "@babel/helper-get-function-arity": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz", - "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==", + "@babel/helper-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", "dev": true, "requires": { - "@babel/types": "^7.12.13" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" } }, - "@babel/helper-member-expression-to-functions": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.13.tgz", - "integrity": "sha512-B+7nN0gIL8FZ8SvMcF+EPyB21KnCcZHQZFczCxbiNGV/O0rsrSBlWGLzmtBJ3GMjSVMIm4lpFhR+VdVBuIsUcQ==", + "@babel/helper-hoist-variables": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", "dev": true, "requires": { - "@babel/types": "^7.12.13" + "@babel/types": "^7.24.7" } }, "@babel/helper-module-imports": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.13.tgz", - "integrity": "sha512-NGmfvRp9Rqxy0uHSSVP+SRIW1q31a7Ji10cLBcqSDUngGentY4FRiHOFZFE1CLU5eiL0oE8reH7Tg1y99TDM/g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", "dev": true, "requires": { - "@babel/types": "^7.12.13" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" } }, "@babel/helper-module-transforms": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.12.13.tgz", - "integrity": "sha512-acKF7EjqOR67ASIlDTupwkKM1eUisNAjaSduo5Cz+793ikfnpe7p4Q7B7EWU2PCoSTPWsQkR7hRUWEIZPiVLGA==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.12.13", - "@babel/helper-replace-supers": "^7.12.13", - "@babel/helper-simple-access": "^7.12.13", - "@babel/helper-split-export-declaration": "^7.12.13", - "@babel/helper-validator-identifier": "^7.12.11", - "@babel/template": "^7.12.13", - "@babel/traverse": "^7.12.13", - "@babel/types": "^7.12.13", - "lodash": "^4.17.19" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz", - "integrity": "sha512-BdWQhoVJkp6nVjB7nkFWcn43dkprYauqtk++Py2eaf/GRDFm5BxRqEIZCiHlZUGAVmtwKcsVL1dC68WmzeFmiA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", + "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", "dev": true, "requires": { - "@babel/types": "^7.12.13" + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" } }, "@babel/helper-plugin-utils": { @@ -14542,68 +14807,69 @@ "integrity": "sha512-C+10MXCXJLiR6IeG9+Wiejt9jmtFpxUc3MQqCmPY8hfCjyUGl9kT+B2okzEZrtykiwrc4dbCPdDoz0A/HQbDaA==", "dev": true }, - "@babel/helper-replace-supers": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.13.tgz", - "integrity": "sha512-pctAOIAMVStI2TMLhozPKbf5yTEXc0OJa0eENheb4w09SrgOWEs+P4nTOZYJQCqs8JlErGLDPDJTiGIp3ygbLg==", - "dev": true, - "requires": { - "@babel/helper-member-expression-to-functions": "^7.12.13", - "@babel/helper-optimise-call-expression": "^7.12.13", - "@babel/traverse": "^7.12.13", - "@babel/types": "^7.12.13" - } - }, "@babel/helper-simple-access": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.12.13.tgz", - "integrity": "sha512-0ski5dyYIHEfwpWGx5GPWhH35j342JaflmCeQmsPWcrOQDtCN6C1zKAVRFVbK53lPW2c9TsuLLSUDf0tIGJ5hA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", "dev": true, "requires": { - "@babel/types": "^7.12.13" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" } }, "@babel/helper-split-export-declaration": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz", - "integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", "dev": true, "requires": { - "@babel/types": "^7.12.13" + "@babel/types": "^7.24.7" } }, + "@babel/helper-string-parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "dev": true + }, "@babel/helper-validator-identifier": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", - "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", + "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", "dev": true }, "@babel/helpers": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.12.13.tgz", - "integrity": "sha512-oohVzLRZ3GQEk4Cjhfs9YkJA4TdIDTObdBEZGrd6F/T0GPSnuV6l22eMcxlvcvzVIPH3VTtxbseudM1zIE+rPQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", + "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", "dev": true, "requires": { - "@babel/template": "^7.12.13", - "@babel/traverse": "^7.12.13", - "@babel/types": "^7.12.13" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" } }, "@babel/highlight": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.12.13.tgz", - "integrity": "sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.12.11", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" } }, "@babel/parser": { - "version": "7.12.15", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.15.tgz", - "integrity": "sha512-AQBOU2Z9kWwSZMd6lNjCX0GUgFonL1wAM1db8L8PMk9UDaGsRCArBkU4Sc+UCM3AE4hjbXx+h58Lb3QT4oRmrA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", "dev": true }, "@babel/plugin-syntax-object-rest-spread": { @@ -14624,31 +14890,32 @@ } }, "@babel/template": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", - "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", "dev": true, "requires": { - "@babel/code-frame": "^7.12.13", - "@babel/parser": "^7.12.13", - "@babel/types": "^7.12.13" + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" } }, "@babel/traverse": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.13.tgz", - "integrity": "sha512-3Zb4w7eE/OslI0fTp8c7b286/cQps3+vdLW3UcwC8VSJC6GbKn55aeVVu2QJNuCDoeKyptLOFrPq8WqZZBodyA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", + "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", "dev": true, "requires": { - "@babel/code-frame": "^7.12.13", - "@babel/generator": "^7.12.13", - "@babel/helper-function-name": "^7.12.13", - "@babel/helper-split-export-declaration": "^7.12.13", - "@babel/parser": "^7.12.13", - "@babel/types": "^7.12.13", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.19" + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", + "debug": "^4.3.1", + "globals": "^11.1.0" }, "dependencies": { "debug": { @@ -14669,13 +14936,13 @@ } }, "@babel/types": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.13.tgz", - "integrity": "sha512-oKrdZTld2im1z8bDwTOQvUbxKwE+854zc16qWZQlcTqMN00pWxHQ4ZeOq0yDMnisOpRykH2/5Qqcrk/OlbAjiQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.12.11", - "lodash": "^4.17.19", + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" } }, @@ -14833,6 +15100,28 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "@ianvs/prettier-plugin-sort-imports": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@ianvs/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.3.0.tgz", + "integrity": "sha512-OOMtUcO4J3LoL63dOKAe7bn+lSRRPeit2DqNHpx+wvBp3Grejo2PMaK4Mp1mwy8pnat64ccSgk/lBZbsAdLErw==", + "dev": true, + "requires": { + "@babel/core": "^7.24.0", + "@babel/generator": "^7.23.6", + "@babel/parser": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0", + "semver": "^7.5.2" + }, + "dependencies": { + "semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true + } + } + }, "@jest/console": { "version": "24.9.0", "resolved": "https://registry.npmjs.org/@jest/console/-/console-24.9.0.tgz", @@ -15547,18 +15836,45 @@ } } }, + "@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "@jridgewell/resolve-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", "devOptional": true }, + "@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true + }, "@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", "devOptional": true }, + "@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "@lukeed/csprng": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", @@ -17014,6 +17330,18 @@ } } }, + "browserslist": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", + "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.16" + } + }, "bs-logger": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", @@ -17120,6 +17448,12 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==" }, + "caniuse-lite": { + "version": "1.0.30001639", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001639.tgz", + "integrity": "sha512-eFHflNTBIlFwP2AIKaYuBQN/apnUoKNhBdza8ZnW/h2di4LCZ4xFqYlxUxo+LQ76KFI1PGcC1QDxMbxTZpSCAg==", + "dev": true + }, "capture-exit": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", @@ -17977,6 +18311,12 @@ "jake": "^10.8.5" } }, + "electron-to-chromium": { + "version": "1.4.815", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.815.tgz", + "integrity": "sha512-OvpTT2ItpOXJL7IGcYakRjHCt8L5GrrN/wHCQsRB4PQa1X9fe+X9oen245mIId7s14xvArCGSTIq644yPUKKLg==", + "dev": true + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -18044,9 +18384,9 @@ } }, "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==" }, "escape-goat": { "version": "2.1.1", @@ -22609,6 +22949,12 @@ } } }, + "node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, "nodemon": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.7.tgz", @@ -23082,6 +23428,12 @@ } } }, + "picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true + }, "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -23861,9 +24213,9 @@ "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==" }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true }, "semver-diff": { @@ -25159,6 +25511,16 @@ } } }, + "update-browserslist-db": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", + "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "dev": true, + "requires": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + } + }, "update-notifier": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz", diff --git a/services/API-service/package.json b/services/API-service/package.json index c0f6f8214..479095b3b 100644 --- a/services/API-service/package.json +++ b/services/API-service/package.json @@ -55,6 +55,7 @@ "wkt-io-ts": "^1.0.2" }, "devDependencies": { + "@ianvs/prettier-plugin-sort-imports": "^4.3.0", "@types/express": "^4.17.14", "@types/jest": "^26.0.20", "@types/node": "16.x", From 805e7b14cfc2fef1e47a586be9062dd561df45b4 Mon Sep 17 00:00:00 2001 From: Gulfaraz Rahman Date: Mon, 1 Jul 2024 16:51:30 +0200 Subject: [PATCH 45/55] style: sort imports using prettier plugin --- services/API-service/appdatasource.ts | 2 + .../1710512991479-rename-mock-rasters.ts | 3 +- services/API-service/ormconfig.ts | 1 + .../admin-area-data.controller.ts | 3 +- .../admin-area-data/admin-area-data.entity.ts | 9 ++-- .../admin-area-data/admin-area-data.module.ts | 3 +- .../admin-area-data.service.ts | 10 ++-- .../dto/upload-admin-area-data.dto.ts | 10 ++-- .../admin-area-dynamic-data.controller.ts | 39 ++++++++------ .../admin-area-dynamic-data.entity.ts | 9 ++-- .../admin-area-dynamic-data.module.ts | 19 +++---- .../admin-area-dynamic-data.service.ts | 30 ++++++----- .../dto/dynamic-data-place-code.dto.ts | 3 +- .../dto/upload-admin-area-dynamic-data.dto.ts | 12 +++-- .../api/admin-area/admin-area.controller.ts | 9 ++-- .../src/api/admin-area/admin-area.entity.ts | 12 +++-- .../src/api/admin-area/admin-area.module.ts | 11 ++-- .../src/api/admin-area/admin-area.service.ts | 16 +++--- .../src/api/admin-area/event-area.entity.ts | 8 +-- .../admin-area/services/event-area.service.ts | 4 +- .../api/country/country-disaster.entity.ts | 2 + .../src/api/country/country.controller.ts | 3 +- .../src/api/country/country.entity.ts | 6 ++- .../src/api/country/country.module.ts | 3 +- .../src/api/country/country.service.ts | 6 ++- .../src/api/country/dto/add-countries.dto.ts | 4 +- .../api/country/dto/notification-info.dto.ts | 3 +- .../src/api/disaster/disaster.entity.ts | 8 +-- .../api/eap-actions/area-of-focus.entity.ts | 4 +- .../eap-actions/dto/check-eap-action.dto.ts | 3 +- .../src/api/eap-actions/dto/eap-action.dto.ts | 6 ++- .../eap-actions/eap-action-status.entity.ts | 8 +-- .../src/api/eap-actions/eap-action.entity.ts | 9 ++-- .../api/eap-actions/eap-actions.controller.ts | 19 +++---- .../src/api/eap-actions/eap-actions.module.ts | 15 +++--- .../api/eap-actions/eap-actions.service.ts | 16 +++--- .../src/api/event/dto/event-place-code.dto.ts | 6 ++- .../api/event/dto/trigger-per-leadtime.dto.ts | 4 +- .../dto/upload-trigger-per-leadtime.dto.ts | 10 ++-- .../src/api/event/event-map-image.entity.ts | 8 +-- .../src/api/event/event-place-code.entity.ts | 11 ++-- .../src/api/event/event.controller.ts | 28 +++++----- .../API-service/src/api/event/event.module.ts | 21 ++++---- .../src/api/event/event.service.ts | 47 ++++++++-------- .../api/event/trigger-per-lead-time.entity.ts | 7 +-- .../dto/station-forecast.dto.ts | 4 +- .../glofas-station/dto/upload-station.dto.ts | 3 +- .../dto/upload-trigger-per-station.ts | 8 +-- .../glofas-station.controller.ts | 1 + .../glofas-station/glofas-station.module.ts | 5 +- .../glofas-station/glofas-station.service.ts | 8 +-- .../src/api/lead-time/lead-time.entity.ts | 4 +- .../dto/upload-asset-exposure-status.dto.ts | 4 +- .../lines-data/dto/upload-buildings.dto.ts | 3 +- .../api/lines-data/dto/upload-roads.dto.ts | 3 +- .../lines-data-dynamic-status.entity.ts | 8 +-- .../api/lines-data/lines-data-views.entity.ts | 5 +- .../api/lines-data/lines-data.controller.ts | 5 +- .../src/api/lines-data/lines-data.entity.ts | 6 +-- .../src/api/lines-data/lines-data.module.ts | 5 +- .../src/api/lines-data/lines-data.service.ts | 10 ++-- .../api/metadata/dto/add-indicators.dto.ts | 3 +- .../src/api/metadata/dto/add-layers.dto.ts | 4 +- .../api/metadata/indicator-metadata.entity.ts | 3 +- .../src/api/metadata/layer-metadata.entity.ts | 3 +- .../src/api/metadata/metadata.controller.ts | 1 + .../src/api/metadata/metadata.module.ts | 15 +++--- .../src/api/metadata/metadata.service.ts | 2 + .../notification/dto/send-notification.dto.ts | 6 ++- .../email/email-template.service.ts | 20 +++---- .../api/notification/email/email.service.ts | 5 +- .../api/notification/lookup/lookup.module.ts | 1 + .../api/notification/lookup/lookup.service.ts | 1 + .../notification-content.module.ts | 3 +- .../notification-content.service.ts | 16 +++--- .../notification/notification.controller.ts | 7 +-- .../api/notification/notification.module.ts | 19 +++---- .../api/notification/notification.service.ts | 13 ++--- .../whatsapp/auth.middlewareTwilio.ts | 2 + .../api/notification/whatsapp/twilio.dto.ts | 1 + .../whatsapp/whatsapp.controller.ts | 1 + .../notification/whatsapp/whatsapp.module.ts | 1 + .../notification/whatsapp/whatsapp.service.ts | 4 +- .../dto/upload-asset-exposure-status.dto.ts | 6 ++- .../dto/upload-community-notifications.dto.ts | 3 +- .../point-data/dto/upload-dam-sites.dto.ts | 3 +- .../dto/upload-evacuation-centers.dto.ts | 1 + .../api/point-data/dto/upload-gauge.dto.ts | 3 +- .../dto/upload-glofas-station.dto.ts | 3 +- .../point-data/dto/upload-health-sites.dto.ts | 1 + .../dto/upload-red-cross-branch.dto.ts | 3 +- .../api/point-data/dto/upload-schools.dto.ts | 3 +- .../point-data/dto/upload-waterpoint.dto.ts | 3 +- .../point-data/dynamic-point-data.entity.ts | 11 ++-- .../api/point-data/point-data.controller.ts | 5 +- .../src/api/point-data/point-data.entity.ts | 3 +- .../src/api/point-data/point-data.module.ts | 5 +- .../src/api/point-data/point-data.service.ts | 24 +++++---- .../rainfall-triggers.controller.ts | 1 + .../rainfall-triggers.entity.ts | 8 +-- .../rainfall-triggers.module.ts | 3 +- .../rainfall-triggers.service.ts | 2 + .../typhoon-track/dto/trackpoint-details.ts | 5 +- .../typhoon-track/dto/upload-typhoon-track.ts | 6 ++- .../typhoon-track/typhoon-track.controller.ts | 1 + .../api/typhoon-track/typhoon-track.entity.ts | 8 +-- .../api/typhoon-track/typhoon-track.module.ts | 3 +- .../typhoon-track/typhoon-track.service.ts | 2 + .../src/api/user/dto/create-user.dto.ts | 8 +-- .../src/api/user/dto/delete-user.dto.ts | 3 +- .../src/api/user/dto/login-user.dto.ts | 3 +- .../src/api/user/dto/update-password.dto.ts | 3 +- .../src/api/user/user.controller.ts | 17 +++--- .../src/api/user/user.decorator.ts | 4 +- .../API-service/src/api/user/user.entity.ts | 18 ++++--- .../API-service/src/api/user/user.model.ts | 3 +- .../API-service/src/api/user/user.module.ts | 9 ++-- .../API-service/src/api/user/user.service.ts | 21 ++++---- .../api/waterpoints/waterpoints.controller.ts | 12 +++-- .../src/api/waterpoints/waterpoints.module.ts | 3 +- .../api/waterpoints/waterpoints.service.ts | 8 +-- services/API-service/src/app.controller.ts | 3 +- services/API-service/src/app.module.ts | 33 ++++++------ .../API-service/src/cronjob/cronjob.module.ts | 1 + .../src/cronjob/cronjob.service.ts | 1 + services/API-service/src/main.ts | 12 +++-- services/API-service/src/roles.decorator.ts | 1 + services/API-service/src/roles.guard.ts | 10 ++-- services/API-service/src/scripts.ts | 4 +- .../src/scripts/geoserver-sync.service.ts | 8 +-- .../src/scripts/mock-helper.service.ts | 11 ++-- .../src/scripts/mock.controller.ts | 16 +++--- .../API-service/src/scripts/mock.service.ts | 34 ++++++------ .../src/scripts/scripts.controller.ts | 14 ++--- .../API-service/src/scripts/scripts.module.ts | 54 ++++++++++--------- .../src/scripts/scripts.service.ts | 38 ++++++------- .../src/scripts/seed-admin-area-data.ts | 8 +-- .../src/scripts/seed-admin-area.ts | 7 +-- .../API-service/src/scripts/seed-helper.ts | 3 +- services/API-service/src/scripts/seed-init.ts | 44 +++++++-------- .../API-service/src/scripts/seed-line-data.ts | 10 ++-- .../src/scripts/seed-point-data.ts | 8 +-- services/API-service/src/scripts/seed-prod.ts | 8 +-- .../src/scripts/seed-rainfall-data.ts | 8 +-- services/API-service/src/shared/data.model.ts | 1 + .../API-service/src/shared/helper.service.ts | 12 +++-- .../src/shared/pipes/validation.pipe.ts | 7 +-- services/API-service/src/typeorm.module.ts | 2 + 148 files changed, 744 insertions(+), 525 deletions(-) diff --git a/services/API-service/appdatasource.ts b/services/API-service/appdatasource.ts index e29a3ca46..b2789d5ac 100644 --- a/services/API-service/appdatasource.ts +++ b/services/API-service/appdatasource.ts @@ -1,3 +1,5 @@ import { DataSource, DataSourceOptions } from 'typeorm'; + import { ORMConfig } from './ormconfig'; + export const AppDataSource = new DataSource(ORMConfig as DataSourceOptions); diff --git a/services/API-service/migration/1710512991479-rename-mock-rasters.ts b/services/API-service/migration/1710512991479-rename-mock-rasters.ts index 4ce5f8a52..ff35abeae 100644 --- a/services/API-service/migration/1710512991479-rename-mock-rasters.ts +++ b/services/API-service/migration/1710512991479-rename-mock-rasters.ts @@ -1,7 +1,8 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; import * as fs from 'fs'; import * as path from 'path'; +import { MigrationInterface, QueryRunner } from 'typeorm'; + export class RenameMockRasters1710512991479 implements MigrationInterface { public async up(_queryRunner: QueryRunner): Promise { const directoryPath = './geoserver-volume/raster-files/mock-output/'; diff --git a/services/API-service/ormconfig.ts b/services/API-service/ormconfig.ts index 54a054e9e..c5830ba14 100644 --- a/services/API-service/ormconfig.ts +++ b/services/API-service/ormconfig.ts @@ -1,4 +1,5 @@ import * as fs from 'fs'; + import { DataSourceOptions } from 'typeorm'; export const ORMConfig: DataSourceOptions = { diff --git a/services/API-service/src/api/admin-area-data/admin-area-data.controller.ts b/services/API-service/src/api/admin-area-data/admin-area-data.controller.ts index c6df15115..e7a1d091a 100644 --- a/services/API-service/src/api/admin-area-data/admin-area-data.controller.ts +++ b/services/API-service/src/api/admin-area-data/admin-area-data.controller.ts @@ -18,13 +18,14 @@ import { ApiResponse, ApiTags, } from '@nestjs/swagger'; + import { Roles } from '../../roles.decorator'; import { RolesGuard } from '../../roles.guard'; +import { FILE_UPLOAD_API_FORMAT } from '../../shared/file-upload-api-format'; import { AdminDataReturnDto } from '../admin-area-dynamic-data/dto/admin-data-return.dto'; import { UserRole } from '../user/user-role.enum'; import { AdminAreaDataService } from './admin-area-data.service'; import { UploadAdminAreaDataJsonDto } from './dto/upload-admin-area-data.dto'; -import { FILE_UPLOAD_API_FORMAT } from '../../shared/file-upload-api-format'; @ApiBearerAuth() @UseGuards(RolesGuard) diff --git a/services/API-service/src/api/admin-area-data/admin-area-data.entity.ts b/services/API-service/src/api/admin-area-data/admin-area-data.entity.ts index 8e814fb22..398ab633a 100644 --- a/services/API-service/src/api/admin-area-data/admin-area-data.entity.ts +++ b/services/API-service/src/api/admin-area-data/admin-area-data.entity.ts @@ -1,11 +1,12 @@ import { - Entity, - PrimaryGeneratedColumn, Column, - ManyToOne, - JoinColumn, + Entity, Index, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, } from 'typeorm'; + import { AdminLevel } from '../country/admin-level.enum'; import { CountryEntity } from '../country/country.entity'; diff --git a/services/API-service/src/api/admin-area-data/admin-area-data.module.ts b/services/API-service/src/api/admin-area-data/admin-area-data.module.ts index c2fe9c72f..94204c953 100644 --- a/services/API-service/src/api/admin-area-data/admin-area-data.module.ts +++ b/services/API-service/src/api/admin-area-data/admin-area-data.module.ts @@ -1,11 +1,12 @@ +import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; + import { HelperService } from '../../shared/helper.service'; import { UserModule } from '../user/user.module'; import { AdminAreaDataController } from './admin-area-data.controller'; import { AdminAreaDataEntity } from './admin-area-data.entity'; import { AdminAreaDataService } from './admin-area-data.service'; -import { HttpModule } from '@nestjs/axios'; @Module({ imports: [ diff --git a/services/API-service/src/api/admin-area-data/admin-area-data.service.ts b/services/API-service/src/api/admin-area-data/admin-area-data.service.ts index aaf578c41..3ca40b22c 100644 --- a/services/API-service/src/api/admin-area-data/admin-area-data.service.ts +++ b/services/API-service/src/api/admin-area-data/admin-area-data.service.ts @@ -1,15 +1,17 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; + +import { validate } from 'class-validator'; import { Repository } from 'typeorm'; + +import { HelperService } from '../../shared/helper.service'; +import { AdminDataReturnDto } from '../admin-area-dynamic-data/dto/admin-data-return.dto'; +import { UpdateableStaticIndicator } from '../admin-area-dynamic-data/enum/dynamic-data-unit'; import { AdminAreaDataEntity } from './admin-area-data.entity'; import { UploadAdminAreaDataDto, UploadAdminAreaDataJsonDto, } from './dto/upload-admin-area-data.dto'; -import { validate } from 'class-validator'; -import { AdminDataReturnDto } from '../admin-area-dynamic-data/dto/admin-data-return.dto'; -import { HelperService } from '../../shared/helper.service'; -import { UpdateableStaticIndicator } from '../admin-area-dynamic-data/enum/dynamic-data-unit'; @Injectable() export class AdminAreaDataService { diff --git a/services/API-service/src/api/admin-area-data/dto/upload-admin-area-data.dto.ts b/services/API-service/src/api/admin-area-data/dto/upload-admin-area-data.dto.ts index 7d6b14912..550f86077 100644 --- a/services/API-service/src/api/admin-area-data/dto/upload-admin-area-data.dto.ts +++ b/services/API-service/src/api/admin-area-data/dto/upload-admin-area-data.dto.ts @@ -1,3 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; + +import { Type } from 'class-transformer'; import { IsArray, IsEnum, @@ -7,14 +10,13 @@ import { IsString, ValidateNested, } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { ManyToOne, JoinColumn } from 'typeorm'; +import { JoinColumn, ManyToOne } from 'typeorm'; + import { DynamicDataPlaceCodeDto } from '../../admin-area-dynamic-data/dto/dynamic-data-place-code.dto'; +import indicatorData from '../../admin-area-dynamic-data/dto/example/ETH/malaria/upload-potential_cases-3.json'; import { UpdateableStaticIndicator } from '../../admin-area-dynamic-data/enum/dynamic-data-unit'; import { AdminLevel } from '../../country/admin-level.enum'; import { CountryEntity } from '../../country/country.entity'; -import indicatorData from '../../admin-area-dynamic-data/dto/example/ETH/malaria/upload-potential_cases-3.json'; export class UploadAdminAreaDataDto { @ApiProperty() diff --git a/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.controller.ts b/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.controller.ts index d85aedc5b..689043455 100644 --- a/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.controller.ts +++ b/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.controller.ts @@ -1,26 +1,35 @@ -import { AdminDataReturnDto } from './dto/admin-data-return.dto'; -import { DynamicIndicator } from './enum/dynamic-data-unit'; -import { Body, Get, Param, UploadedFile } from '@nestjs/common'; -import { Controller, Post, UseGuards, UseInterceptors } from '@nestjs/common'; import { - ApiOperation, - ApiConsumes, + Body, + Controller, + Get, + Param, + Post, + UploadedFile, + UseGuards, + UseInterceptors, +} from '@nestjs/common'; +import { Query } from '@nestjs/common/decorators'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { ApiBearerAuth, - ApiTags, - ApiParam, ApiBody, - ApiResponse, + ApiConsumes, + ApiOperation, + ApiParam, ApiQuery, + ApiResponse, + ApiTags, } from '@nestjs/swagger'; + +import { Roles } from '../../roles.decorator'; import { RolesGuard } from '../../roles.guard'; -import { UploadAdminAreaDynamicDataDto } from './dto/upload-admin-area-dynamic-data.dto'; -import { AdminAreaDynamicDataService } from './admin-area-dynamic-data.service'; +import { FILE_UPLOAD_API_FORMAT } from '../../shared/file-upload-api-format'; import { DisasterType } from '../disaster/disaster-type.enum'; -import { FileInterceptor } from '@nestjs/platform-express'; -import { Roles } from '../../roles.decorator'; import { UserRole } from '../user/user-role.enum'; -import { Query } from '@nestjs/common/decorators'; -import { FILE_UPLOAD_API_FORMAT } from '../../shared/file-upload-api-format'; +import { AdminAreaDynamicDataService } from './admin-area-dynamic-data.service'; +import { AdminDataReturnDto } from './dto/admin-data-return.dto'; +import { UploadAdminAreaDynamicDataDto } from './dto/upload-admin-area-dynamic-data.dto'; +import { DynamicIndicator } from './enum/dynamic-data-unit'; @ApiBearerAuth() @UseGuards(RolesGuard) diff --git a/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.entity.ts b/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.entity.ts index 4d1143d6e..de6f17225 100644 --- a/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.entity.ts +++ b/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.entity.ts @@ -1,11 +1,12 @@ import { - Entity, - PrimaryGeneratedColumn, Column, - ManyToOne, - JoinColumn, + Entity, Index, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, } from 'typeorm'; + import { CountryEntity } from '../country/country.entity'; import { DisasterEntity } from '../disaster/disaster.entity'; import { LeadTimeEntity } from '../lead-time/lead-time.entity'; diff --git a/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.module.ts b/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.module.ts index f7b529db9..268de513f 100644 --- a/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.module.ts +++ b/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.module.ts @@ -1,16 +1,17 @@ -import { CountryModule } from './../country/country.module'; -import { UserModule } from '../user/user.module'; -import { AdminAreaDynamicDataService } from './admin-area-dynamic-data.service'; -import { AdminAreaDynamicDataController } from './admin-area-dynamic-data.controller'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { TriggerPerLeadTime } from '../event/trigger-per-lead-time.entity'; -import { AdminAreaDynamicDataEntity } from './admin-area-dynamic-data.entity'; -import { EventModule } from '../event/event.module'; -import { DisasterEntity } from '../disaster/disaster.entity'; -import { CountryEntity } from '../country/country.entity'; + import { HelperService } from '../../shared/helper.service'; import { AdminAreaModule } from '../admin-area/admin-area.module'; +import { CountryEntity } from '../country/country.entity'; +import { DisasterEntity } from '../disaster/disaster.entity'; +import { EventModule } from '../event/event.module'; +import { TriggerPerLeadTime } from '../event/trigger-per-lead-time.entity'; +import { UserModule } from '../user/user.module'; +import { CountryModule } from './../country/country.module'; +import { AdminAreaDynamicDataController } from './admin-area-dynamic-data.controller'; +import { AdminAreaDynamicDataEntity } from './admin-area-dynamic-data.entity'; +import { AdminAreaDynamicDataService } from './admin-area-dynamic-data.service'; @Module({ imports: [ diff --git a/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.service.ts b/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.service.ts index d424227b2..6dbe706f6 100644 --- a/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.service.ts +++ b/services/API-service/src/api/admin-area-dynamic-data/admin-area-dynamic-data.service.ts @@ -1,21 +1,23 @@ -import { LeadTime } from './enum/lead-time.enum'; -import { DynamicDataPlaceCodeDto } from './dto/dynamic-data-place-code.dto'; +import fs from 'fs'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { DataSource, In, IsNull, MoreThanOrEqual, Repository } from 'typeorm'; -import { UploadAdminAreaDynamicDataDto } from './dto/upload-admin-area-dynamic-data.dto'; import { InjectRepository } from '@nestjs/typeorm'; -import { AdminAreaDynamicDataEntity } from './admin-area-dynamic-data.entity'; -import { DynamicIndicator } from './enum/dynamic-data-unit'; -import { AdminDataReturnDto } from './dto/admin-data-return.dto'; -import { UploadTriggerPerLeadTimeDto } from '../event/dto/upload-trigger-per-leadtime.dto'; -import { EventService } from '../event/event.service'; -import { DisasterEntity } from '../disaster/disaster.entity'; -import { DisasterType } from '../disaster/disaster-type.enum'; -import fs from 'fs'; -import { CountryEntity } from '../country/country.entity'; + +import { DataSource, In, IsNull, MoreThanOrEqual, Repository } from 'typeorm'; + +import { DisasterTypeGeoServerMapper } from '../../scripts/disaster-type-geoserver-file.mapper'; import { HelperService } from '../../shared/helper.service'; import { EventAreaService } from '../admin-area/services/event-area.service'; -import { DisasterTypeGeoServerMapper } from '../../scripts/disaster-type-geoserver-file.mapper'; +import { CountryEntity } from '../country/country.entity'; +import { DisasterType } from '../disaster/disaster-type.enum'; +import { DisasterEntity } from '../disaster/disaster.entity'; +import { UploadTriggerPerLeadTimeDto } from '../event/dto/upload-trigger-per-leadtime.dto'; +import { EventService } from '../event/event.service'; +import { AdminAreaDynamicDataEntity } from './admin-area-dynamic-data.entity'; +import { AdminDataReturnDto } from './dto/admin-data-return.dto'; +import { DynamicDataPlaceCodeDto } from './dto/dynamic-data-place-code.dto'; +import { UploadAdminAreaDynamicDataDto } from './dto/upload-admin-area-dynamic-data.dto'; +import { DynamicIndicator } from './enum/dynamic-data-unit'; +import { LeadTime } from './enum/lead-time.enum'; interface RasterData { originalname: string; diff --git a/services/API-service/src/api/admin-area-dynamic-data/dto/dynamic-data-place-code.dto.ts b/services/API-service/src/api/admin-area-dynamic-data/dto/dynamic-data-place-code.dto.ts index 003620ec3..efee267fe 100644 --- a/services/API-service/src/api/admin-area-dynamic-data/dto/dynamic-data-place-code.dto.ts +++ b/services/API-service/src/api/admin-area-dynamic-data/dto/dynamic-data-place-code.dto.ts @@ -1,6 +1,7 @@ -import { IsNotEmpty, IsNumber, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsNumber, IsString } from 'class-validator'; + export class DynamicDataPlaceCodeDto { @ApiProperty() @IsNotEmpty() diff --git a/services/API-service/src/api/admin-area-dynamic-data/dto/upload-admin-area-dynamic-data.dto.ts b/services/API-service/src/api/admin-area-dynamic-data/dto/upload-admin-area-dynamic-data.dto.ts index f93be9656..bd314f830 100644 --- a/services/API-service/src/api/admin-area-dynamic-data/dto/upload-admin-area-dynamic-data.dto.ts +++ b/services/API-service/src/api/admin-area-dynamic-data/dto/upload-admin-area-dynamic-data.dto.ts @@ -1,3 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; + +import { Type } from 'class-transformer'; import { IsArray, IsEnum, @@ -7,13 +10,12 @@ import { IsString, ValidateNested, } from 'class-validator'; -import { Type } from 'class-transformer'; -import { ApiProperty } from '@nestjs/swagger'; + +import { DisasterType } from '../../disaster/disaster-type.enum'; +import { DynamicIndicator } from '../enum/dynamic-data-unit'; +import { LeadTime } from '../enum/lead-time.enum'; import { DynamicDataPlaceCodeDto } from './dynamic-data-place-code.dto'; import exposure from './example/PHL/dengue/upload-potential_cases-2.json'; -import { LeadTime } from '../enum/lead-time.enum'; -import { DynamicIndicator } from '../enum/dynamic-data-unit'; -import { DisasterType } from '../../disaster/disaster-type.enum'; export class UploadAdminAreaDynamicDataDto { @ApiProperty({ example: 'PHL' }) diff --git a/services/API-service/src/api/admin-area/admin-area.controller.ts b/services/API-service/src/api/admin-area/admin-area.controller.ts index bd03cb3e1..c497f97e6 100644 --- a/services/API-service/src/api/admin-area/admin-area.controller.ts +++ b/services/API-service/src/api/admin-area/admin-area.controller.ts @@ -17,13 +17,14 @@ import { ApiResponse, ApiTags, } from '@nestjs/swagger'; -import { GeoJson } from '../../shared/geo.model'; + +import { Roles } from '../../roles.decorator'; import { RolesGuard } from '../../roles.guard'; -import { AdminAreaService } from './admin-area.service'; import { AggregateDataRecord } from '../../shared/data.model'; -import { AdminAreaEntity } from './admin-area.entity'; -import { Roles } from '../../roles.decorator'; +import { GeoJson } from '../../shared/geo.model'; import { UserRole } from '../user/user-role.enum'; +import { AdminAreaEntity } from './admin-area.entity'; +import { AdminAreaService } from './admin-area.service'; @ApiBearerAuth() @UseGuards(RolesGuard) diff --git a/services/API-service/src/api/admin-area/admin-area.entity.ts b/services/API-service/src/api/admin-area/admin-area.entity.ts index f1acf5042..f4af44831 100644 --- a/services/API-service/src/api/admin-area/admin-area.entity.ts +++ b/services/API-service/src/api/admin-area/admin-area.entity.ts @@ -1,14 +1,16 @@ import { ApiProperty } from '@nestjs/swagger'; + import { - Entity, - PrimaryGeneratedColumn, Column, - ManyToOne, - JoinColumn, - OneToMany, + Entity, Index, + JoinColumn, + ManyToOne, MultiPolygon, + OneToMany, + PrimaryGeneratedColumn, } from 'typeorm'; + import { CountryEntity } from '../country/country.entity'; import { EventPlaceCodeEntity } from '../event/event-place-code.entity'; diff --git a/services/API-service/src/api/admin-area/admin-area.module.ts b/services/API-service/src/api/admin-area/admin-area.module.ts index f5e3ca1ff..37ab73e4c 100644 --- a/services/API-service/src/api/admin-area/admin-area.module.ts +++ b/services/API-service/src/api/admin-area/admin-area.module.ts @@ -1,18 +1,19 @@ -import { CountryModule } from './../country/country.module'; +import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; + import { HelperService } from '../../shared/helper.service'; +import { AdminAreaDynamicDataEntity } from '../admin-area-dynamic-data/admin-area-dynamic-data.entity'; import { CountryEntity } from '../country/country.entity'; +import { DisasterEntity } from '../disaster/disaster.entity'; import { EventModule } from '../event/event.module'; import { UserModule } from '../user/user.module'; +import { CountryModule } from './../country/country.module'; import { AdminAreaController } from './admin-area.controller'; import { AdminAreaEntity } from './admin-area.entity'; import { AdminAreaService } from './admin-area.service'; -import { DisasterEntity } from '../disaster/disaster.entity'; -import { AdminAreaDynamicDataEntity } from '../admin-area-dynamic-data/admin-area-dynamic-data.entity'; -import { HttpModule } from '@nestjs/axios'; -import { EventAreaService } from './services/event-area.service'; import { EventAreaEntity } from './event-area.entity'; +import { EventAreaService } from './services/event-area.service'; @Module({ imports: [ diff --git a/services/API-service/src/api/admin-area/admin-area.service.ts b/services/API-service/src/api/admin-area/admin-area.service.ts index f11ca8d4a..43c98c262 100644 --- a/services/API-service/src/api/admin-area/admin-area.service.ts +++ b/services/API-service/src/api/admin-area/admin-area.service.ts @@ -1,17 +1,19 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { GeoJson } from '../../shared/geo.model'; -import { HelperService } from '../../shared/helper.service'; + import { InsertResult, MoreThan, MoreThanOrEqual, Repository } from 'typeorm'; -import { AdminAreaEntity } from './admin-area.entity'; -import { EventService } from '../event/event.service'; + import { AggregateDataRecord } from '../../shared/data.model'; -import { AdminAreaDynamicDataEntity } from '../admin-area-dynamic-data/admin-area-dynamic-data.entity'; +import { GeoJson } from '../../shared/geo.model'; +import { HelperService } from '../../shared/helper.service'; import { AdminAreaDataEntity } from '../admin-area-data/admin-area-data.entity'; -import { DisasterType } from '../disaster/disaster-type.enum'; -import { DisasterEntity } from '../disaster/disaster.entity'; +import { AdminAreaDynamicDataEntity } from '../admin-area-dynamic-data/admin-area-dynamic-data.entity'; import { DynamicIndicator } from '../admin-area-dynamic-data/enum/dynamic-data-unit'; import { LeadTime } from '../admin-area-dynamic-data/enum/lead-time.enum'; +import { DisasterType } from '../disaster/disaster-type.enum'; +import { DisasterEntity } from '../disaster/disaster.entity'; +import { EventService } from '../event/event.service'; +import { AdminAreaEntity } from './admin-area.entity'; import { EventAreaService } from './services/event-area.service'; @Injectable() diff --git a/services/API-service/src/api/admin-area/event-area.entity.ts b/services/API-service/src/api/admin-area/event-area.entity.ts index e79c87e29..085245d30 100644 --- a/services/API-service/src/api/admin-area/event-area.entity.ts +++ b/services/API-service/src/api/admin-area/event-area.entity.ts @@ -1,12 +1,14 @@ import { ApiProperty } from '@nestjs/swagger'; + import { - Entity, - PrimaryGeneratedColumn, Column, - ManyToOne, + Entity, JoinColumn, + ManyToOne, MultiPolygon, + PrimaryGeneratedColumn, } from 'typeorm'; + import { CountryEntity } from '../country/country.entity'; import { DisasterType } from '../disaster/disaster-type.enum'; import { DisasterEntity } from '../disaster/disaster.entity'; diff --git a/services/API-service/src/api/admin-area/services/event-area.service.ts b/services/API-service/src/api/admin-area/services/event-area.service.ts index 42f6f65ec..b292772d3 100644 --- a/services/API-service/src/api/admin-area/services/event-area.service.ts +++ b/services/API-service/src/api/admin-area/services/event-area.service.ts @@ -1,6 +1,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, MoreThanOrEqual, InsertResult } from 'typeorm'; + +import { InsertResult, MoreThanOrEqual, Repository } from 'typeorm'; + import { AggregateDataRecord, EventSummaryCountry, diff --git a/services/API-service/src/api/country/country-disaster.entity.ts b/services/API-service/src/api/country/country-disaster.entity.ts index 8f53d8cf2..68655d202 100644 --- a/services/API-service/src/api/country/country-disaster.entity.ts +++ b/services/API-service/src/api/country/country-disaster.entity.ts @@ -1,4 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; + import { Column, Entity, @@ -8,6 +9,7 @@ import { ManyToOne, PrimaryGeneratedColumn, } from 'typeorm'; + import { DisasterType } from '../disaster/disaster-type.enum'; import { DisasterEntity } from '../disaster/disaster.entity'; import { LeadTimeEntity } from '../lead-time/lead-time.entity'; diff --git a/services/API-service/src/api/country/country.controller.ts b/services/API-service/src/api/country/country.controller.ts index 85dec28eb..f713ea67b 100644 --- a/services/API-service/src/api/country/country.controller.ts +++ b/services/API-service/src/api/country/country.controller.ts @@ -6,12 +6,13 @@ import { ApiResponse, ApiTags, } from '@nestjs/swagger'; + import { Roles } from '../../roles.decorator'; -import { NotificationInfoDto } from './dto/notification-info.dto'; import { UserRole } from '../user/user-role.enum'; import { CountryEntity } from './country.entity'; import { CountryService } from './country.service'; import { AddCountriesDto } from './dto/add-countries.dto'; +import { NotificationInfoDto } from './dto/notification-info.dto'; @ApiBearerAuth() @ApiTags('country') diff --git a/services/API-service/src/api/country/country.entity.ts b/services/API-service/src/api/country/country.entity.ts index 2b42542e6..85e4f75b1 100644 --- a/services/API-service/src/api/country/country.entity.ts +++ b/services/API-service/src/api/country/country.entity.ts @@ -1,3 +1,5 @@ +import { ApiProperty } from '@nestjs/swagger'; + import { Column, Entity, @@ -7,11 +9,11 @@ import { OneToOne, PrimaryGeneratedColumn, } from 'typeorm'; + import { BoundingBox } from '../../shared/geo.model'; -import { UserEntity } from '../user/user.entity'; import { DisasterEntity } from '../disaster/disaster.entity'; import { NotificationInfoEntity } from '../notification/notifcation-info.entity'; -import { ApiProperty } from '@nestjs/swagger'; +import { UserEntity } from '../user/user.entity'; import { CountryDisasterSettingsEntity } from './country-disaster.entity'; @Entity('country') diff --git a/services/API-service/src/api/country/country.module.ts b/services/API-service/src/api/country/country.module.ts index e3e94e2fd..f0850e486 100644 --- a/services/API-service/src/api/country/country.module.ts +++ b/services/API-service/src/api/country/country.module.ts @@ -1,5 +1,7 @@ +import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; + import { DisasterEntity } from '../disaster/disaster.entity'; import { LeadTimeEntity } from '../lead-time/lead-time.entity'; import { NotificationInfoEntity } from '../notification/notifcation-info.entity'; @@ -8,7 +10,6 @@ import { CountryDisasterSettingsEntity } from './country-disaster.entity'; import { CountryController } from './country.controller'; import { CountryEntity } from './country.entity'; import { CountryService } from './country.service'; -import { HttpModule } from '@nestjs/axios'; @Module({ imports: [ diff --git a/services/API-service/src/api/country/country.service.ts b/services/API-service/src/api/country/country.service.ts index ffb6a04a1..8d96324e2 100644 --- a/services/API-service/src/api/country/country.service.ts +++ b/services/API-service/src/api/country/country.service.ts @@ -1,10 +1,12 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; + import { In, Repository } from 'typeorm'; + import { DisasterType } from '../disaster/disaster-type.enum'; import { DisasterEntity } from '../disaster/disaster.entity'; import { LeadTimeEntity } from '../lead-time/lead-time.entity'; -import { NotificationInfoDto } from './dto/notification-info.dto'; +import { NotificationInfoEntity } from '../notification/notifcation-info.entity'; import { AdminLevel } from './admin-level.enum'; import { CountryDisasterSettingsEntity } from './country-disaster.entity'; import { CountryEntity } from './country.entity'; @@ -13,7 +15,7 @@ import { CountryDisasterSettingsDto, CountryDto, } from './dto/add-countries.dto'; -import { NotificationInfoEntity } from '../notification/notifcation-info.entity'; +import { NotificationInfoDto } from './dto/notification-info.dto'; @Injectable() export class CountryService { diff --git a/services/API-service/src/api/country/dto/add-countries.dto.ts b/services/API-service/src/api/country/dto/add-countries.dto.ts index 52adfd06d..5ff0f9879 100644 --- a/services/API-service/src/api/country/dto/add-countries.dto.ts +++ b/services/API-service/src/api/country/dto/add-countries.dto.ts @@ -1,5 +1,7 @@ -import { IsNotEmpty } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; + +import { IsNotEmpty } from 'class-validator'; + import { BoundingBox } from '../../../shared/geo.model'; import { DisasterType } from '../../disaster/disaster-type.enum'; import { AdminLevel } from '../admin-level.enum'; diff --git a/services/API-service/src/api/country/dto/notification-info.dto.ts b/services/API-service/src/api/country/dto/notification-info.dto.ts index bd27c5d29..8374168d6 100644 --- a/services/API-service/src/api/country/dto/notification-info.dto.ts +++ b/services/API-service/src/api/country/dto/notification-info.dto.ts @@ -1,6 +1,7 @@ -import { IsOptional, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsOptional, IsString } from 'class-validator'; + export class NotificationInfoDto { @ApiProperty() @IsString() diff --git a/services/API-service/src/api/disaster/disaster.entity.ts b/services/API-service/src/api/disaster/disaster.entity.ts index cda1131a4..b648e2a53 100644 --- a/services/API-service/src/api/disaster/disaster.entity.ts +++ b/services/API-service/src/api/disaster/disaster.entity.ts @@ -1,4 +1,5 @@ -import { CountryEntity } from './../country/country.entity'; +import { ApiProperty } from '@nestjs/swagger'; + import { Column, Entity, @@ -6,13 +7,14 @@ import { ManyToMany, PrimaryGeneratedColumn, } from 'typeorm'; -import { DisasterType } from './disaster-type.enum'; -import { ApiProperty } from '@nestjs/swagger'; + import { LeadTime, LeadTimeUnit, } from '../admin-area-dynamic-data/enum/lead-time.enum'; import { UserEntity } from '../user/user.entity'; +import { CountryEntity } from './../country/country.entity'; +import { DisasterType } from './disaster-type.enum'; @Entity('disaster') export class DisasterEntity { diff --git a/services/API-service/src/api/eap-actions/area-of-focus.entity.ts b/services/API-service/src/api/eap-actions/area-of-focus.entity.ts index fc097d256..2e1cf58a1 100644 --- a/services/API-service/src/api/eap-actions/area-of-focus.entity.ts +++ b/services/API-service/src/api/eap-actions/area-of-focus.entity.ts @@ -1,5 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Entity, Column, OneToMany, PrimaryColumn } from 'typeorm'; + +import { Column, Entity, OneToMany, PrimaryColumn } from 'typeorm'; + import { EapActionEntity } from './eap-action.entity'; @Entity('area-of-focus') diff --git a/services/API-service/src/api/eap-actions/dto/check-eap-action.dto.ts b/services/API-service/src/api/eap-actions/dto/check-eap-action.dto.ts index 0e65837a1..52afc0877 100644 --- a/services/API-service/src/api/eap-actions/dto/check-eap-action.dto.ts +++ b/services/API-service/src/api/eap-actions/dto/check-eap-action.dto.ts @@ -1,6 +1,7 @@ -import { IsBoolean, IsNotEmpty, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsBoolean, IsNotEmpty, IsString } from 'class-validator'; + export class CheckEapActionDto { @ApiProperty() @IsNotEmpty() diff --git a/services/API-service/src/api/eap-actions/dto/eap-action.dto.ts b/services/API-service/src/api/eap-actions/dto/eap-action.dto.ts index 11f9f4c63..8c8ae807c 100644 --- a/services/API-service/src/api/eap-actions/dto/eap-action.dto.ts +++ b/services/API-service/src/api/eap-actions/dto/eap-action.dto.ts @@ -1,7 +1,9 @@ -import { IsNotEmpty, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; -import { AreaOfFocusEntity } from '../area-of-focus.entity'; + +import { IsNotEmpty, IsString } from 'class-validator'; + import { DisasterType } from '../../disaster/disaster-type.enum'; +import { AreaOfFocusEntity } from '../area-of-focus.entity'; class EapActionDto { @ApiProperty({ example: 'UGA' }) diff --git a/services/API-service/src/api/eap-actions/eap-action-status.entity.ts b/services/API-service/src/api/eap-actions/eap-action-status.entity.ts index 5b7ef3a11..ac934d15d 100644 --- a/services/API-service/src/api/eap-actions/eap-action-status.entity.ts +++ b/services/API-service/src/api/eap-actions/eap-action-status.entity.ts @@ -1,11 +1,13 @@ import { ApiProperty } from '@nestjs/swagger'; + import { - Entity, - PrimaryGeneratedColumn, Column, - ManyToOne, + Entity, Index, + ManyToOne, + PrimaryGeneratedColumn, } from 'typeorm'; + import { EventPlaceCodeEntity } from '../event/event-place-code.entity'; import { UserEntity } from '../user/user.entity'; import { EapActionEntity } from './eap-action.entity'; diff --git a/services/API-service/src/api/eap-actions/eap-action.entity.ts b/services/API-service/src/api/eap-actions/eap-action.entity.ts index ab70d6749..51ac4442b 100644 --- a/services/API-service/src/api/eap-actions/eap-action.entity.ts +++ b/services/API-service/src/api/eap-actions/eap-action.entity.ts @@ -1,11 +1,12 @@ import { - Entity, - PrimaryGeneratedColumn, Column, - OneToMany, - ManyToOne, + Entity, JoinColumn, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, } from 'typeorm'; + import { CountryEntity } from '../country/country.entity'; import { DisasterEntity } from '../disaster/disaster.entity'; import { AreaOfFocusEntity } from './area-of-focus.entity'; diff --git a/services/API-service/src/api/eap-actions/eap-actions.controller.ts b/services/API-service/src/api/eap-actions/eap-actions.controller.ts index ae535b920..155f3348a 100644 --- a/services/API-service/src/api/eap-actions/eap-actions.controller.ts +++ b/services/API-service/src/api/eap-actions/eap-actions.controller.ts @@ -1,6 +1,4 @@ -import { Controller, Post, Body, Get, UseGuards, Param } from '@nestjs/common'; -import { EapAction, EapActionsService } from './eap-actions.service'; -import { UserDecorator } from '../user/user.decorator'; +import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, @@ -8,15 +6,18 @@ import { ApiResponse, ApiTags, } from '@nestjs/swagger'; -import { CheckEapActionDto } from './dto/check-eap-action.dto'; -import { EapActionStatusEntity } from './eap-action-status.entity'; -import { AreaOfFocusEntity } from './area-of-focus.entity'; -import { RolesGuard } from '../../roles.guard'; + import { Roles } from '../../roles.decorator'; +import { RolesGuard } from '../../roles.guard'; +import { DisasterType } from '../disaster/disaster-type.enum'; import { UserRole } from '../user/user-role.enum'; -import { EapActionEntity } from './eap-action.entity'; +import { UserDecorator } from '../user/user.decorator'; +import { AreaOfFocusEntity } from './area-of-focus.entity'; +import { CheckEapActionDto } from './dto/check-eap-action.dto'; import { AddEapActionsDto } from './dto/eap-action.dto'; -import { DisasterType } from '../disaster/disaster-type.enum'; +import { EapActionStatusEntity } from './eap-action-status.entity'; +import { EapActionEntity } from './eap-action.entity'; +import { EapAction, EapActionsService } from './eap-actions.service'; @ApiBearerAuth() @ApiTags('eap-actions') diff --git a/services/API-service/src/api/eap-actions/eap-actions.module.ts b/services/API-service/src/api/eap-actions/eap-actions.module.ts index 74cb74192..b9e19d453 100644 --- a/services/API-service/src/api/eap-actions/eap-actions.module.ts +++ b/services/API-service/src/api/eap-actions/eap-actions.module.ts @@ -1,17 +1,18 @@ -import { CountryEntity } from './../country/country.entity'; -import { TriggerPerLeadTime } from '../event/trigger-per-lead-time.entity'; +import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; + +import { AdminAreaEntity } from '../admin-area/admin-area.entity'; +import { EventPlaceCodeEntity } from '../event/event-place-code.entity'; +import { TriggerPerLeadTime } from '../event/trigger-per-lead-time.entity'; import { UserEntity } from '../user/user.entity'; import { UserModule } from '../user/user.module'; -import { EapActionEntity } from './eap-action.entity'; +import { CountryEntity } from './../country/country.entity'; +import { AreaOfFocusEntity } from './area-of-focus.entity'; import { EapActionStatusEntity } from './eap-action-status.entity'; +import { EapActionEntity } from './eap-action.entity'; import { EapActionsController } from './eap-actions.controller'; import { EapActionsService } from './eap-actions.service'; -import { AreaOfFocusEntity } from './area-of-focus.entity'; -import { EventPlaceCodeEntity } from '../event/event-place-code.entity'; -import { AdminAreaEntity } from '../admin-area/admin-area.entity'; -import { HttpModule } from '@nestjs/axios'; @Module({ imports: [ diff --git a/services/API-service/src/api/eap-actions/eap-actions.service.ts b/services/API-service/src/api/eap-actions/eap-actions.service.ts index f3f43c1ee..79853bd17 100644 --- a/services/API-service/src/api/eap-actions/eap-actions.service.ts +++ b/services/API-service/src/api/eap-actions/eap-actions.service.ts @@ -1,15 +1,17 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { UserEntity } from '../user/user.entity'; + import { In, IsNull, Repository } from 'typeorm'; -import { EapActionEntity } from './eap-action.entity'; -import { EapActionStatusEntity } from './eap-action-status.entity'; -import { CheckEapActionDto } from './dto/check-eap-action.dto'; -import { AreaOfFocusEntity } from './area-of-focus.entity'; -import { EventPlaceCodeEntity } from '../event/event-place-code.entity'; + import { AdminAreaEntity } from '../admin-area/admin-area.entity'; -import { AddEapActionsDto } from './dto/eap-action.dto'; import { DisasterType } from '../disaster/disaster-type.enum'; +import { EventPlaceCodeEntity } from '../event/event-place-code.entity'; +import { UserEntity } from '../user/user.entity'; +import { AreaOfFocusEntity } from './area-of-focus.entity'; +import { CheckEapActionDto } from './dto/check-eap-action.dto'; +import { AddEapActionsDto } from './dto/eap-action.dto'; +import { EapActionStatusEntity } from './eap-action-status.entity'; +import { EapActionEntity } from './eap-action.entity'; export interface EapAction { Early_action: string; diff --git a/services/API-service/src/api/event/dto/event-place-code.dto.ts b/services/API-service/src/api/event/dto/event-place-code.dto.ts index c3b3dd32e..f41b79a73 100644 --- a/services/API-service/src/api/event/dto/event-place-code.dto.ts +++ b/services/API-service/src/api/event/dto/event-place-code.dto.ts @@ -1,7 +1,9 @@ -import { IsNotEmpty, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; -import { DisasterType } from '../../disaster/disaster-type.enum'; + +import { IsNotEmpty, IsString } from 'class-validator'; + import { LeadTime } from '../../admin-area-dynamic-data/enum/lead-time.enum'; +import { DisasterType } from '../../disaster/disaster-type.enum'; export class AffectedAreaDto { public placeCode: string; diff --git a/services/API-service/src/api/event/dto/trigger-per-leadtime.dto.ts b/services/API-service/src/api/event/dto/trigger-per-leadtime.dto.ts index 1553d4901..1aa1fe456 100644 --- a/services/API-service/src/api/event/dto/trigger-per-leadtime.dto.ts +++ b/services/API-service/src/api/event/dto/trigger-per-leadtime.dto.ts @@ -1,5 +1,7 @@ -import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; + +import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator'; + import { LeadTime } from '../../admin-area-dynamic-data/enum/lead-time.enum'; export class TriggerPerLeadTimeDto { diff --git a/services/API-service/src/api/event/dto/upload-trigger-per-leadtime.dto.ts b/services/API-service/src/api/event/dto/upload-trigger-per-leadtime.dto.ts index 93d99aafb..487b0a171 100644 --- a/services/API-service/src/api/event/dto/upload-trigger-per-leadtime.dto.ts +++ b/services/API-service/src/api/event/dto/upload-trigger-per-leadtime.dto.ts @@ -1,3 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; + +import { Type } from 'class-transformer'; import { IsArray, IsEnum, @@ -6,11 +9,10 @@ import { IsString, ValidateNested, } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { TriggerPerLeadTimeDto } from './trigger-per-leadtime.dto'; -import triggers from './example/triggers-per-leadtime-UGA-triggered.json'; + import { DisasterType } from '../../disaster/disaster-type.enum'; +import triggers from './example/triggers-per-leadtime-UGA-triggered.json'; +import { TriggerPerLeadTimeDto } from './trigger-per-leadtime.dto'; export class UploadTriggerPerLeadTimeDto { @ApiProperty({ example: 'UGA' }) diff --git a/services/API-service/src/api/event/event-map-image.entity.ts b/services/API-service/src/api/event/event-map-image.entity.ts index 36c0f1587..1052b0000 100644 --- a/services/API-service/src/api/event/event-map-image.entity.ts +++ b/services/API-service/src/api/event/event-map-image.entity.ts @@ -1,11 +1,13 @@ import { ApiProperty } from '@nestjs/swagger'; + import { - Entity, Column, - PrimaryGeneratedColumn, - ManyToOne, + Entity, JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, } from 'typeorm'; + import { CountryEntity } from '../country/country.entity'; import { DisasterType } from '../disaster/disaster-type.enum'; import { DisasterEntity } from '../disaster/disaster.entity'; diff --git a/services/API-service/src/api/event/event-place-code.entity.ts b/services/API-service/src/api/event/event-place-code.entity.ts index ca3a7561a..bef237b0a 100644 --- a/services/API-service/src/api/event/event-place-code.entity.ts +++ b/services/API-service/src/api/event/event-place-code.entity.ts @@ -1,13 +1,14 @@ import { - Entity, - Column, Check, - PrimaryGeneratedColumn, + Column, + Entity, + JoinColumn, JoinTable, - OneToMany, ManyToOne, - JoinColumn, + OneToMany, + PrimaryGeneratedColumn, } from 'typeorm'; + import { AdminAreaEntity } from '../admin-area/admin-area.entity'; import { DisasterEntity } from '../disaster/disaster.entity'; import { EapActionStatusEntity } from '../eap-actions/eap-action-status.entity'; diff --git a/services/API-service/src/api/event/event.controller.ts b/services/API-service/src/api/event/event.controller.ts index 539107eeb..990cc1cb4 100644 --- a/services/API-service/src/api/event/event.controller.ts +++ b/services/API-service/src/api/event/event.controller.ts @@ -1,8 +1,4 @@ -import { - ActivationLogDto, - EventPlaceCodeDto, -} from './dto/event-place-code.dto'; -import { EventService } from './event.service'; +import stream from 'stream'; import { Body, Controller, @@ -17,6 +13,7 @@ import { UseGuards, UseInterceptors, } from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; import { ApiBearerAuth, ApiBody, @@ -27,18 +24,23 @@ import { ApiResponse, ApiTags, } from '@nestjs/swagger'; + +import { Response } from 'express-serve-static-core'; + +import { Roles } from '../../roles.decorator'; import { RolesGuard } from '../../roles.guard'; -import { UploadTriggerPerLeadTimeDto } from './dto/upload-trigger-per-leadtime.dto'; import { EventSummaryCountry, TriggeredArea } from '../../shared/data.model'; -import { DateDto, TriggerPerLeadTimeExampleDto } from './dto/date.dto'; -import { Roles } from '../../roles.decorator'; -import { UserRole } from '../user/user-role.enum'; -import { UserDecorator } from '../user/user.decorator'; -import { FileInterceptor } from '@nestjs/platform-express'; -import stream from 'stream'; -import { Response } from 'express-serve-static-core'; import { IMAGE_UPLOAD_API_FORMAT } from '../../shared/file-upload-api-format'; import { SendNotificationDto } from '../notification/dto/send-notification.dto'; +import { UserRole } from '../user/user-role.enum'; +import { UserDecorator } from '../user/user.decorator'; +import { DateDto, TriggerPerLeadTimeExampleDto } from './dto/date.dto'; +import { + ActivationLogDto, + EventPlaceCodeDto, +} from './dto/event-place-code.dto'; +import { UploadTriggerPerLeadTimeDto } from './dto/upload-trigger-per-leadtime.dto'; +import { EventService } from './event.service'; @ApiBearerAuth() @ApiTags('event') diff --git a/services/API-service/src/api/event/event.module.ts b/services/API-service/src/api/event/event.module.ts index 52ab2aeb9..633c42cc8 100644 --- a/services/API-service/src/api/event/event.module.ts +++ b/services/API-service/src/api/event/event.module.ts @@ -1,20 +1,21 @@ -import { EapActionsModule } from './../eap-actions/eap-actions.module'; -import { CountryModule } from './../country/country.module'; -import { EventPlaceCodeEntity } from './event-place-code.entity'; -import { UserModule } from './../user/user.module'; import { Module } from '@nestjs/common'; -import { EventController } from './event.controller'; -import { EventService } from './event.service'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { TriggerPerLeadTime } from './trigger-per-lead-time.entity'; + +import { HelperService } from '../../shared/helper.service'; import { AdminAreaDynamicDataEntity } from '../admin-area-dynamic-data/admin-area-dynamic-data.entity'; import { AdminAreaEntity } from '../admin-area/admin-area.entity'; +import { CountryEntity } from '../country/country.entity'; import { DisasterEntity } from '../disaster/disaster.entity'; -import { HelperService } from '../../shared/helper.service'; +import { TyphoonTrackModule } from '../typhoon-track/typhoon-track.module'; import { UserEntity } from '../user/user.entity'; +import { CountryModule } from './../country/country.module'; +import { EapActionsModule } from './../eap-actions/eap-actions.module'; +import { UserModule } from './../user/user.module'; import { EventMapImageEntity } from './event-map-image.entity'; -import { TyphoonTrackModule } from '../typhoon-track/typhoon-track.module'; -import { CountryEntity } from '../country/country.entity'; +import { EventPlaceCodeEntity } from './event-place-code.entity'; +import { EventController } from './event.controller'; +import { EventService } from './event.service'; +import { TriggerPerLeadTime } from './trigger-per-lead-time.entity'; @Module({ imports: [ diff --git a/services/API-service/src/api/event/event.service.ts b/services/API-service/src/api/event/event.service.ts index d536408a6..c016e676b 100644 --- a/services/API-service/src/api/event/event.service.ts +++ b/services/API-service/src/api/event/event.service.ts @@ -1,44 +1,45 @@ -import { EapActionsService } from './../eap-actions/eap-actions.service'; -import { AdminAreaDynamicDataEntity } from './../admin-area-dynamic-data/admin-area-dynamic-data.entity'; -import { EventPlaceCodeEntity } from './event-place-code.entity'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { subDays } from 'date-fns'; import { - ActivationLogDto, - AffectedAreaDto, - EventPlaceCodeDto, -} from './dto/event-place-code.dto'; -import { + DataSource, + In, + IsNull, LessThan, + MoreThan, MoreThanOrEqual, Repository, - In, - MoreThan, - IsNull, - DataSource, SelectQueryBuilder, } from 'typeorm'; -import { InjectRepository } from '@nestjs/typeorm'; -import { LeadTime } from '../admin-area-dynamic-data/enum/lead-time.enum'; -import { UploadTriggerPerLeadTimeDto } from './dto/upload-trigger-per-leadtime.dto'; -import { TriggerPerLeadTime } from './trigger-per-lead-time.entity'; import { DisasterSpecificProperties, EventSummaryCountry, TriggeredArea, } from '../../shared/data.model'; +import { HelperService } from '../../shared/helper.service'; +import { LeadTime } from '../admin-area-dynamic-data/enum/lead-time.enum'; import { AdminAreaEntity } from '../admin-area/admin-area.entity'; -import { DateDto } from './dto/date.dto'; -import { TriggerPerLeadTimeDto } from './dto/trigger-per-leadtime.dto'; +import { CountryDisasterSettingsEntity } from '../country/country-disaster.entity'; +import { CountryEntity } from '../country/country.entity'; import { DisasterType } from '../disaster/disaster-type.enum'; import { DisasterEntity } from '../disaster/disaster.entity'; -import { HelperService } from '../../shared/helper.service'; +import { TyphoonTrackService } from '../typhoon-track/typhoon-track.service'; import { UserEntity } from '../user/user.entity'; +import { AdminAreaDynamicDataEntity } from './../admin-area-dynamic-data/admin-area-dynamic-data.entity'; +import { EapActionsService } from './../eap-actions/eap-actions.service'; +import { DateDto } from './dto/date.dto'; +import { + ActivationLogDto, + AffectedAreaDto, + EventPlaceCodeDto, +} from './dto/event-place-code.dto'; +import { TriggerPerLeadTimeDto } from './dto/trigger-per-leadtime.dto'; +import { UploadTriggerPerLeadTimeDto } from './dto/upload-trigger-per-leadtime.dto'; import { EventMapImageEntity } from './event-map-image.entity'; -import { TyphoonTrackService } from '../typhoon-track/typhoon-track.service'; -import { CountryEntity } from '../country/country.entity'; -import { CountryDisasterSettingsEntity } from '../country/country-disaster.entity'; -import { subDays } from 'date-fns'; +import { EventPlaceCodeEntity } from './event-place-code.entity'; +import { TriggerPerLeadTime } from './trigger-per-lead-time.entity'; @Injectable() export class EventService { diff --git a/services/API-service/src/api/event/trigger-per-lead-time.entity.ts b/services/API-service/src/api/event/trigger-per-lead-time.entity.ts index ef3eeee88..778baaf76 100644 --- a/services/API-service/src/api/event/trigger-per-lead-time.entity.ts +++ b/services/API-service/src/api/event/trigger-per-lead-time.entity.ts @@ -1,10 +1,11 @@ import { - Entity, - PrimaryGeneratedColumn, Column, - ManyToOne, + Entity, JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, } from 'typeorm'; + import { LeadTime } from '../admin-area-dynamic-data/enum/lead-time.enum'; import { CountryEntity } from '../country/country.entity'; import { DisasterEntity } from '../disaster/disaster.entity'; diff --git a/services/API-service/src/api/glofas-station/dto/station-forecast.dto.ts b/services/API-service/src/api/glofas-station/dto/station-forecast.dto.ts index 1cdc7b59c..7c7bceed0 100644 --- a/services/API-service/src/api/glofas-station/dto/station-forecast.dto.ts +++ b/services/API-service/src/api/glofas-station/dto/station-forecast.dto.ts @@ -1,3 +1,5 @@ +import { ApiProperty } from '@nestjs/swagger'; + import { IsIn, IsNotEmpty, @@ -5,7 +7,7 @@ import { IsOptional, IsString, } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; + import { EapAlertClassKeyEnum } from '../../../shared/data.model'; export class GlofasStationForecastDto { diff --git a/services/API-service/src/api/glofas-station/dto/upload-station.dto.ts b/services/API-service/src/api/glofas-station/dto/upload-station.dto.ts index 4e296ad43..41ec3ef93 100644 --- a/services/API-service/src/api/glofas-station/dto/upload-station.dto.ts +++ b/services/API-service/src/api/glofas-station/dto/upload-station.dto.ts @@ -1,6 +1,7 @@ -import { IsNotEmpty, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + export class UploadStationDto { @ApiProperty({ example: 'G1374' }) @IsNotEmpty() diff --git a/services/API-service/src/api/glofas-station/dto/upload-trigger-per-station.ts b/services/API-service/src/api/glofas-station/dto/upload-trigger-per-station.ts index 55f0f244a..6229d388d 100644 --- a/services/API-service/src/api/glofas-station/dto/upload-trigger-per-station.ts +++ b/services/API-service/src/api/glofas-station/dto/upload-trigger-per-station.ts @@ -1,3 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; + +import { Type } from 'class-transformer'; import { IsArray, IsNotEmpty, @@ -5,11 +8,10 @@ import { IsString, ValidateNested, } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; + import { LeadTime } from '../../admin-area-dynamic-data/enum/lead-time.enum'; -import { Type } from 'class-transformer'; -import { GlofasStationForecastDto } from './station-forecast.dto'; import stations from '../../point-data/dto/example/glofas-stations/glofas-stations-UGA-triggered.json'; +import { GlofasStationForecastDto } from './station-forecast.dto'; export class UploadTriggerPerStationDto { @ApiProperty({ example: 'UGA' }) diff --git a/services/API-service/src/api/glofas-station/glofas-station.controller.ts b/services/API-service/src/api/glofas-station/glofas-station.controller.ts index 50c2d3b13..6a54db489 100644 --- a/services/API-service/src/api/glofas-station/glofas-station.controller.ts +++ b/services/API-service/src/api/glofas-station/glofas-station.controller.ts @@ -6,6 +6,7 @@ import { ApiResponse, ApiTags, } from '@nestjs/swagger'; + import { Roles } from '../../roles.decorator'; import { RolesGuard } from '../../roles.guard'; import { UserRole } from '../user/user-role.enum'; diff --git a/services/API-service/src/api/glofas-station/glofas-station.module.ts b/services/API-service/src/api/glofas-station/glofas-station.module.ts index b4e05210f..3b5c76086 100644 --- a/services/API-service/src/api/glofas-station/glofas-station.module.ts +++ b/services/API-service/src/api/glofas-station/glofas-station.module.ts @@ -1,15 +1,16 @@ +import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; + import { HelperService } from '../../shared/helper.service'; import { AdminAreaDynamicDataEntity } from '../admin-area-dynamic-data/admin-area-dynamic-data.entity'; import { AdminAreaEntity } from '../admin-area/admin-area.entity'; import { CountryEntity } from '../country/country.entity'; import { EventModule } from '../event/event.module'; +import { PointDataModule } from '../point-data/point-data.module'; import { UserModule } from '../user/user.module'; import { GlofasStationController } from './glofas-station.controller'; import { GlofasStationService } from './glofas-station.service'; -import { HttpModule } from '@nestjs/axios'; -import { PointDataModule } from '../point-data/point-data.module'; @Module({ imports: [ diff --git a/services/API-service/src/api/glofas-station/glofas-station.service.ts b/services/API-service/src/api/glofas-station/glofas-station.service.ts index dd7f407cf..bfd264f0b 100644 --- a/services/API-service/src/api/glofas-station/glofas-station.service.ts +++ b/services/API-service/src/api/glofas-station/glofas-station.service.ts @@ -1,12 +1,14 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; + import { Repository } from 'typeorm'; -import { UploadTriggerPerStationDto } from './dto/upload-trigger-per-station'; -import { DisasterType } from '../disaster/disaster-type.enum'; + import { CountryEntity } from '../country/country.entity'; -import { PointDataService } from '../point-data/point-data.service'; +import { DisasterType } from '../disaster/disaster-type.enum'; import { UploadDynamicPointDataDto } from '../point-data/dto/upload-asset-exposure-status.dto'; import { PointDataEnum } from '../point-data/point-data.entity'; +import { PointDataService } from '../point-data/point-data.service'; +import { UploadTriggerPerStationDto } from './dto/upload-trigger-per-station'; @Injectable() export class GlofasStationService { diff --git a/services/API-service/src/api/lead-time/lead-time.entity.ts b/services/API-service/src/api/lead-time/lead-time.entity.ts index f374b78ce..a14e29b1b 100644 --- a/services/API-service/src/api/lead-time/lead-time.entity.ts +++ b/services/API-service/src/api/lead-time/lead-time.entity.ts @@ -1,5 +1,7 @@ -import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from 'typeorm'; +import { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from 'typeorm'; + import { CountryDisasterSettingsEntity } from '../country/country-disaster.entity'; + @Entity('lead-time') export class LeadTimeEntity { @PrimaryGeneratedColumn('uuid') diff --git a/services/API-service/src/api/lines-data/dto/upload-asset-exposure-status.dto.ts b/services/API-service/src/api/lines-data/dto/upload-asset-exposure-status.dto.ts index 6d6b352fc..dd809b37b 100644 --- a/services/API-service/src/api/lines-data/dto/upload-asset-exposure-status.dto.ts +++ b/services/API-service/src/api/lines-data/dto/upload-asset-exposure-status.dto.ts @@ -1,3 +1,5 @@ +import { ApiProperty } from '@nestjs/swagger'; + import { IsArray, IsEnum, @@ -5,7 +7,7 @@ import { IsOptional, IsString, } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; + import { LeadTime } from '../../admin-area-dynamic-data/enum/lead-time.enum'; import { DisasterType } from '../../disaster/disaster-type.enum'; import { LinesDataEnum } from '../lines-data.entity'; diff --git a/services/API-service/src/api/lines-data/dto/upload-buildings.dto.ts b/services/API-service/src/api/lines-data/dto/upload-buildings.dto.ts index d9252c7d1..5d6522451 100644 --- a/services/API-service/src/api/lines-data/dto/upload-buildings.dto.ts +++ b/services/API-service/src/api/lines-data/dto/upload-buildings.dto.ts @@ -1,6 +1,7 @@ -import { IsNotEmpty } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty } from 'class-validator'; + export class BuildingDto { @ApiProperty({ example: 1234 }) public fid: number = undefined; diff --git a/services/API-service/src/api/lines-data/dto/upload-roads.dto.ts b/services/API-service/src/api/lines-data/dto/upload-roads.dto.ts index 2c320678c..864510b6a 100644 --- a/services/API-service/src/api/lines-data/dto/upload-roads.dto.ts +++ b/services/API-service/src/api/lines-data/dto/upload-roads.dto.ts @@ -1,6 +1,7 @@ -import { IsNotEmpty, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + export class RoadDto { @ApiProperty({ example: 'highway' }) @IsString() diff --git a/services/API-service/src/api/lines-data/lines-data-dynamic-status.entity.ts b/services/API-service/src/api/lines-data/lines-data-dynamic-status.entity.ts index 7b50870ca..2317dbed4 100644 --- a/services/API-service/src/api/lines-data/lines-data-dynamic-status.entity.ts +++ b/services/API-service/src/api/lines-data/lines-data-dynamic-status.entity.ts @@ -1,12 +1,14 @@ import { ApiProperty } from '@nestjs/swagger'; + import { - Entity, - PrimaryGeneratedColumn, Column, + Entity, + Index, JoinColumn, ManyToOne, - Index, + PrimaryGeneratedColumn, } from 'typeorm'; + import { LeadTime } from '../admin-area-dynamic-data/enum/lead-time.enum'; import { LeadTimeEntity } from '../lead-time/lead-time.entity'; import { LinesDataEntity } from './lines-data.entity'; diff --git a/services/API-service/src/api/lines-data/lines-data-views.entity.ts b/services/API-service/src/api/lines-data/lines-data-views.entity.ts index 7f7bbf506..7b82e0a23 100644 --- a/services/API-service/src/api/lines-data/lines-data-views.entity.ts +++ b/services/API-service/src/api/lines-data/lines-data-views.entity.ts @@ -1,7 +1,8 @@ import { ViewEntity } from 'typeorm'; -import { LinesDataEntity, LinesDataEnum } from './lines-data.entity'; -import { LinesDataDynamicStatusEntity } from './lines-data-dynamic-status.entity'; + import { AppDataSource } from '../../../appdatasource'; +import { LinesDataDynamicStatusEntity } from './lines-data-dynamic-status.entity'; +import { LinesDataEntity, LinesDataEnum } from './lines-data.entity'; const getViewQuery = (type: LinesDataEnum) => { return () => diff --git a/services/API-service/src/api/lines-data/lines-data.controller.ts b/services/API-service/src/api/lines-data/lines-data.controller.ts index e9e2e5bed..bba97c730 100644 --- a/services/API-service/src/api/lines-data/lines-data.controller.ts +++ b/services/API-service/src/api/lines-data/lines-data.controller.ts @@ -17,12 +17,13 @@ import { ApiResponse, ApiTags, } from '@nestjs/swagger'; + import { Roles } from '../../roles.decorator'; import { RolesGuard } from '../../roles.guard'; +import { FILE_UPLOAD_API_FORMAT } from '../../shared/file-upload-api-format'; import { UserRole } from '../user/user-role.enum'; -import { LinesDataService } from './lines-data.service'; import { UploadLinesExposureStatusDto } from './dto/upload-asset-exposure-status.dto'; -import { FILE_UPLOAD_API_FORMAT } from '../../shared/file-upload-api-format'; +import { LinesDataService } from './lines-data.service'; @ApiBearerAuth() @ApiTags('lines-data') diff --git a/services/API-service/src/api/lines-data/lines-data.entity.ts b/services/API-service/src/api/lines-data/lines-data.entity.ts index 416386ce2..66ffbf8ea 100644 --- a/services/API-service/src/api/lines-data/lines-data.entity.ts +++ b/services/API-service/src/api/lines-data/lines-data.entity.ts @@ -1,9 +1,9 @@ import { - Entity, - PrimaryGeneratedColumn, Column, - Index, + Entity, Geometry, + Index, + PrimaryGeneratedColumn, } from 'typeorm'; export enum LinesDataEnum { diff --git a/services/API-service/src/api/lines-data/lines-data.module.ts b/services/API-service/src/api/lines-data/lines-data.module.ts index a16a0e0ec..36c72f55a 100644 --- a/services/API-service/src/api/lines-data/lines-data.module.ts +++ b/services/API-service/src/api/lines-data/lines-data.module.ts @@ -1,12 +1,13 @@ +import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; + import { HelperService } from '../../shared/helper.service'; import { UserModule } from '../user/user.module'; +import { LinesDataDynamicStatusEntity } from './lines-data-dynamic-status.entity'; import { LinesDataController } from './lines-data.controller'; import { LinesDataEntity } from './lines-data.entity'; import { LinesDataService } from './lines-data.service'; -import { LinesDataDynamicStatusEntity } from './lines-data-dynamic-status.entity'; -import { HttpModule } from '@nestjs/axios'; @Module({ imports: [ diff --git a/services/API-service/src/api/lines-data/lines-data.service.ts b/services/API-service/src/api/lines-data/lines-data.service.ts index f52ee6375..d468d0720 100644 --- a/services/API-service/src/api/lines-data/lines-data.service.ts +++ b/services/API-service/src/api/lines-data/lines-data.service.ts @@ -1,12 +1,14 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { HelperService } from '../../shared/helper.service'; + import { MoreThanOrEqual, Repository } from 'typeorm'; -import { LinesDataEntity, LinesDataEnum } from './lines-data.entity'; -import { RoadDto } from './dto/upload-roads.dto'; + +import { HelperService } from '../../shared/helper.service'; import { UploadLinesExposureStatusDto } from './dto/upload-asset-exposure-status.dto'; -import { LinesDataDynamicStatusEntity } from './lines-data-dynamic-status.entity'; import { BuildingDto } from './dto/upload-buildings.dto'; +import { RoadDto } from './dto/upload-roads.dto'; +import { LinesDataDynamicStatusEntity } from './lines-data-dynamic-status.entity'; +import { LinesDataEntity, LinesDataEnum } from './lines-data.entity'; @Injectable() export class LinesDataService { 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 f975c0b9e..e0029e2d2 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 @@ -1,3 +1,5 @@ +import { ApiProperty } from '@nestjs/swagger'; + import { IsBoolean, IsIn, @@ -5,7 +7,6 @@ import { IsNumber, IsString, } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; export class IndicatorDto { @ApiProperty({ 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 72d048578..0abac94a5 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 @@ -1,5 +1,7 @@ -import { IsBoolean, IsEnum, IsIn, IsNotEmpty, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; + +import { IsBoolean, IsEnum, IsIn, IsNotEmpty, IsString } from 'class-validator'; + import { DisasterType } from '../../disaster/disaster-type.enum'; export class LayerDto { diff --git a/services/API-service/src/api/metadata/indicator-metadata.entity.ts b/services/API-service/src/api/metadata/indicator-metadata.entity.ts index 413cea53d..ce8ab7512 100644 --- a/services/API-service/src/api/metadata/indicator-metadata.entity.ts +++ b/services/API-service/src/api/metadata/indicator-metadata.entity.ts @@ -1,5 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; + +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; @Entity('indicator-metadata') export class IndicatorMetadataEntity { diff --git a/services/API-service/src/api/metadata/layer-metadata.entity.ts b/services/API-service/src/api/metadata/layer-metadata.entity.ts index 1f2251e01..4c021f993 100644 --- a/services/API-service/src/api/metadata/layer-metadata.entity.ts +++ b/services/API-service/src/api/metadata/layer-metadata.entity.ts @@ -1,6 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; + import { IsIn } from 'class-validator'; -import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; @Entity('layer-metadata') export class LayerMetadataEntity { diff --git a/services/API-service/src/api/metadata/metadata.controller.ts b/services/API-service/src/api/metadata/metadata.controller.ts index d97333542..42fb49c9d 100644 --- a/services/API-service/src/api/metadata/metadata.controller.ts +++ b/services/API-service/src/api/metadata/metadata.controller.ts @@ -6,6 +6,7 @@ import { ApiResponse, ApiTags, } from '@nestjs/swagger'; + import { Roles } from '../../roles.decorator'; import { RolesGuard } from '../../roles.guard'; import { UserRole } from '../user/user-role.enum'; diff --git a/services/API-service/src/api/metadata/metadata.module.ts b/services/API-service/src/api/metadata/metadata.module.ts index 1d9cfcaad..bba7858ad 100644 --- a/services/API-service/src/api/metadata/metadata.module.ts +++ b/services/API-service/src/api/metadata/metadata.module.ts @@ -1,15 +1,16 @@ -import { CountryModule } from './../country/country.module'; +import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; + +import { HelperService } from '../../shared/helper.service'; +import { DisasterEntity } from '../disaster/disaster.entity'; +import { EventModule } from '../event/event.module'; import { UserModule } from '../user/user.module'; -import { MetadataController } from './metadata.controller'; +import { CountryModule } from './../country/country.module'; import { IndicatorMetadataEntity } from './indicator-metadata.entity'; -import { MetadataService } from './metadata.service'; import { LayerMetadataEntity } from './layer-metadata.entity'; -import { HelperService } from '../../shared/helper.service'; -import { EventModule } from '../event/event.module'; -import { DisasterEntity } from '../disaster/disaster.entity'; -import { HttpModule } from '@nestjs/axios'; +import { MetadataController } from './metadata.controller'; +import { MetadataService } from './metadata.service'; @Module({ imports: [ diff --git a/services/API-service/src/api/metadata/metadata.service.ts b/services/API-service/src/api/metadata/metadata.service.ts index 574f4fb11..536928b3f 100644 --- a/services/API-service/src/api/metadata/metadata.service.ts +++ b/services/API-service/src/api/metadata/metadata.service.ts @@ -1,6 +1,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; + import { Repository } from 'typeorm'; + import { DisasterType } from '../disaster/disaster-type.enum'; import { AddIndicatorsDto, IndicatorDto } from './dto/add-indicators.dto'; import { AddLayersDto, LayerDto } from './dto/add-layers.dto'; diff --git a/services/API-service/src/api/notification/dto/send-notification.dto.ts b/services/API-service/src/api/notification/dto/send-notification.dto.ts index 98431e8d2..2109c565a 100644 --- a/services/API-service/src/api/notification/dto/send-notification.dto.ts +++ b/services/API-service/src/api/notification/dto/send-notification.dto.ts @@ -1,7 +1,9 @@ -import { IsIn, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; -import { DisasterType } from '../../disaster/disaster-type.enum'; + +import { IsIn, IsNotEmpty, IsOptional, IsString } from 'class-validator'; + import countries from '../../../scripts/json/countries.json'; +import { DisasterType } from '../../disaster/disaster-type.enum'; export class SendNotificationDto { @ApiProperty({ example: countries.map((c) => c.countryCodeISO3).join(' | ') }) diff --git a/services/API-service/src/api/notification/email/email-template.service.ts b/services/API-service/src/api/notification/email/email-template.service.ts index e3b0aca78..246736ed1 100644 --- a/services/API-service/src/api/notification/email/email-template.service.ts +++ b/services/API-service/src/api/notification/email/email-template.service.ts @@ -1,19 +1,21 @@ +import * as fs from 'fs'; import { Injectable } from '@nestjs/common'; -import { ContentEventEmail } from '../dto/content-trigger-email.dto'; -import { - NotificationDataPerEventDto, - TriggerStatusLabelEnum, -} from '../dto/notification-date-per-event.dto'; + import * as ejs from 'ejs'; -import * as fs from 'fs'; -import { CountryTimeZoneMapping } from '../../country/country-time-zone-mapping'; -import { DisasterType } from '../../disaster/disaster-type.enum'; +import * as juice from 'juice'; + import { EapAlertClassKeyEnum, EventSummaryCountry, } from '../../../shared/data.model'; +import { CountryTimeZoneMapping } from '../../country/country-time-zone-mapping'; import { CountryEntity } from '../../country/country.entity'; -import * as juice from 'juice'; +import { DisasterType } from '../../disaster/disaster-type.enum'; +import { ContentEventEmail } from '../dto/content-trigger-email.dto'; +import { + NotificationDataPerEventDto, + TriggerStatusLabelEnum, +} from '../dto/notification-date-per-event.dto'; import { formatActionUnitValue } from '../helpers/format-action-unit-value.helper'; const emailFolder = './src/api/notification/email'; 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 2149f6773..cc8a71beb 100644 --- a/services/API-service/src/api/notification/email/email.service.ts +++ b/services/API-service/src/api/notification/email/email.service.ts @@ -1,9 +1,10 @@ -import { CountryEntity } from './../../country/country.entity'; import { Injectable } from '@nestjs/common'; + import Mailchimp from 'mailchimp-api-v3'; -import { DisasterType } from '../../disaster/disaster-type.enum'; import { EventSummaryCountry } from '../../../shared/data.model'; +import { DisasterType } from '../../disaster/disaster-type.enum'; +import { CountryEntity } from './../../country/country.entity'; import { NotificationContentService } from './../notification-content/notification-content.service'; import { EmailTemplateService } from './email-template.service'; diff --git a/services/API-service/src/api/notification/lookup/lookup.module.ts b/services/API-service/src/api/notification/lookup/lookup.module.ts index c69fd6c58..62fcab6e7 100644 --- a/services/API-service/src/api/notification/lookup/lookup.module.ts +++ b/services/API-service/src/api/notification/lookup/lookup.module.ts @@ -1,4 +1,5 @@ import { Module } from '@nestjs/common'; + import { LookupService } from './lookup.service'; @Module({ diff --git a/services/API-service/src/api/notification/lookup/lookup.service.ts b/services/API-service/src/api/notification/lookup/lookup.service.ts index a50a79955..6426c8c06 100644 --- a/services/API-service/src/api/notification/lookup/lookup.service.ts +++ b/services/API-service/src/api/notification/lookup/lookup.service.ts @@ -1,4 +1,5 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; + import { twilioClient } from '../whatsapp/twilio.client'; @Injectable() diff --git a/services/API-service/src/api/notification/notification-content/notification-content.module.ts b/services/API-service/src/api/notification/notification-content/notification-content.module.ts index 6369045e5..585dc94fc 100644 --- a/services/API-service/src/api/notification/notification-content/notification-content.module.ts +++ b/services/API-service/src/api/notification/notification-content/notification-content.module.ts @@ -1,5 +1,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; + +import { HelperService } from '../../../shared/helper.service'; import { AdminAreaDataModule } from '../../admin-area-data/admin-area-data.module'; import { AdminAreaDynamicDataModule } from '../../admin-area-dynamic-data/admin-area-dynamic-data.module'; import { AdminAreaModule } from '../../admin-area/admin-area.module'; @@ -8,7 +10,6 @@ import { DisasterEntity } from '../../disaster/disaster.entity'; import { EventModule } from '../../event/event.module'; import { IndicatorMetadataEntity } from '../../metadata/indicator-metadata.entity'; import { NotificationContentService } from './notification-content.service'; -import { HelperService } from '../../../shared/helper.service'; @Module({ imports: [ 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 5adc44489..c99e0b5b1 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 @@ -1,20 +1,22 @@ -import { CountryEntity } from '../../country/country.entity'; import { Injectable } from '@nestjs/common'; -import { EventService } from '../../event/event.service'; import { InjectRepository } from '@nestjs/typeorm'; + import { Repository } from 'typeorm'; -import { IndicatorMetadataEntity } from '../../metadata/indicator-metadata.entity'; + +import { EventSummaryCountry, TriggeredArea } from '../../../shared/data.model'; +import { HelperService } from '../../../shared/helper.service'; import { LeadTime } from '../../admin-area-dynamic-data/enum/lead-time.enum'; +import { CountryEntity } from '../../country/country.entity'; import { DisasterType } from '../../disaster/disaster-type.enum'; import { DisasterEntity } from '../../disaster/disaster.entity'; -import { EventSummaryCountry, TriggeredArea } from '../../../shared/data.model'; -import { HelperService } from '../../../shared/helper.service'; +import { EventService } from '../../event/event.service'; +import { IndicatorMetadataEntity } from '../../metadata/indicator-metadata.entity'; +import { AdminAreaLabel } from '../dto/admin-area-notification-info.dto'; +import { ContentEventEmail } from '../dto/content-trigger-email.dto'; import { NotificationDataPerEventDto, TriggerStatusLabelEnum, } from '../dto/notification-date-per-event.dto'; -import { AdminAreaLabel } from '../dto/admin-area-notification-info.dto'; -import { ContentEventEmail } from '../dto/content-trigger-email.dto'; @Injectable() export class NotificationContentService { diff --git a/services/API-service/src/api/notification/notification.controller.ts b/services/API-service/src/api/notification/notification.controller.ts index f894c9b45..55e87b1ad 100644 --- a/services/API-service/src/api/notification/notification.controller.ts +++ b/services/API-service/src/api/notification/notification.controller.ts @@ -1,4 +1,3 @@ -import { NotificationService } from './notification.service'; import { Body, Controller, @@ -13,10 +12,12 @@ import { ApiResponse, ApiTags, } from '@nestjs/swagger'; -import { RolesGuard } from '../../roles.guard'; -import { SendNotificationDto } from './dto/send-notification.dto'; + import { Roles } from '../../roles.decorator'; +import { RolesGuard } from '../../roles.guard'; import { UserRole } from '../user/user-role.enum'; +import { SendNotificationDto } from './dto/send-notification.dto'; +import { NotificationService } from './notification.service'; @ApiBearerAuth() @UseGuards(RolesGuard) diff --git a/services/API-service/src/api/notification/notification.module.ts b/services/API-service/src/api/notification/notification.module.ts index 316927954..833c61d7b 100644 --- a/services/API-service/src/api/notification/notification.module.ts +++ b/services/API-service/src/api/notification/notification.module.ts @@ -1,17 +1,18 @@ -import { AdminAreaDynamicDataModule } from './../admin-area-dynamic-data/admin-area-dynamic-data.module'; -import { NotificationInfoEntity } from './notifcation-info.entity'; -import { EventModule } from './../event/event.module'; import { Module } from '@nestjs/common'; -import { UserModule } from '../user/user.module'; -import { NotificationController } from './notification.controller'; -import { NotificationService } from './notification.service'; import { TypeOrmModule } from '@nestjs/typeorm'; + import { IndicatorMetadataEntity } from '../metadata/indicator-metadata.entity'; -import { WhatsappModule } from './whatsapp/whatsapp.module'; -import { NotificationContentModule } from './notification-content/notification-content.module'; -import { EmailService } from './email/email.service'; import { TyphoonTrackModule } from '../typhoon-track/typhoon-track.module'; +import { UserModule } from '../user/user.module'; +import { AdminAreaDynamicDataModule } from './../admin-area-dynamic-data/admin-area-dynamic-data.module'; +import { EventModule } from './../event/event.module'; import { EmailTemplateService } from './email/email-template.service'; +import { EmailService } from './email/email.service'; +import { NotificationInfoEntity } from './notifcation-info.entity'; +import { NotificationContentModule } from './notification-content/notification-content.module'; +import { NotificationController } from './notification.controller'; +import { NotificationService } from './notification.service'; +import { WhatsappModule } from './whatsapp/whatsapp.module'; @Module({ imports: [ diff --git a/services/API-service/src/api/notification/notification.service.ts b/services/API-service/src/api/notification/notification.service.ts index ba08436e3..9ba65ddc4 100644 --- a/services/API-service/src/api/notification/notification.service.ts +++ b/services/API-service/src/api/notification/notification.service.ts @@ -1,12 +1,13 @@ import { Injectable } from '@nestjs/common'; -import { EventService } from '../event/event.service'; -import { DisasterType } from '../disaster/disaster-type.enum'; -import { WhatsappService } from './whatsapp/whatsapp.service'; -import { NotificationContentService } from './notification-content/notification-content.service'; -import { EmailService } from './email/email.service'; -import { TyphoonTrackService } from '../typhoon-track/typhoon-track.service'; + import { EventSummaryCountry } from '../../shared/data.model'; import { LeadTime } from '../admin-area-dynamic-data/enum/lead-time.enum'; +import { DisasterType } from '../disaster/disaster-type.enum'; +import { EventService } from '../event/event.service'; +import { TyphoonTrackService } from '../typhoon-track/typhoon-track.service'; +import { EmailService } from './email/email.service'; +import { NotificationContentService } from './notification-content/notification-content.service'; +import { WhatsappService } from './whatsapp/whatsapp.service'; @Injectable() export class NotificationService { diff --git a/services/API-service/src/api/notification/whatsapp/auth.middlewareTwilio.ts b/services/API-service/src/api/notification/whatsapp/auth.middlewareTwilio.ts index fbd5bdf68..7985f19c0 100644 --- a/services/API-service/src/api/notification/whatsapp/auth.middlewareTwilio.ts +++ b/services/API-service/src/api/notification/whatsapp/auth.middlewareTwilio.ts @@ -1,6 +1,8 @@ import { HttpStatus, Injectable, NestMiddleware } from '@nestjs/common'; import { HttpException } from '@nestjs/common/exceptions/http.exception'; + import { NextFunction, Request, Response } from 'express'; + import { DEBUG, EXTERNAL_API } from '../../../config'; import { twilio } from './twilio.client'; diff --git a/services/API-service/src/api/notification/whatsapp/twilio.dto.ts b/services/API-service/src/api/notification/whatsapp/twilio.dto.ts index 3b6f1bcb1..5e3acc318 100644 --- a/services/API-service/src/api/notification/whatsapp/twilio.dto.ts +++ b/services/API-service/src/api/notification/whatsapp/twilio.dto.ts @@ -1,4 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; + import { IsOptional, IsString } from 'class-validator'; export enum TwilioStatus { diff --git a/services/API-service/src/api/notification/whatsapp/whatsapp.controller.ts b/services/API-service/src/api/notification/whatsapp/whatsapp.controller.ts index 79db69d44..a067fb05e 100644 --- a/services/API-service/src/api/notification/whatsapp/whatsapp.controller.ts +++ b/services/API-service/src/api/notification/whatsapp/whatsapp.controller.ts @@ -1,5 +1,6 @@ import { Body, Controller, Post } from '@nestjs/common'; import { ApiConsumes, ApiTags } from '@nestjs/swagger'; + import { SendTestWhatsappDto, TwilioIncomingCallbackDto, diff --git a/services/API-service/src/api/notification/whatsapp/whatsapp.module.ts b/services/API-service/src/api/notification/whatsapp/whatsapp.module.ts index e79653ba0..653537e2e 100644 --- a/services/API-service/src/api/notification/whatsapp/whatsapp.module.ts +++ b/services/API-service/src/api/notification/whatsapp/whatsapp.module.ts @@ -5,6 +5,7 @@ import { RequestMethod, } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; + import { API_PATHS } from '../../../config'; import { CountryEntity } from '../../country/country.entity'; import { EventMapImageEntity } from '../../event/event-map-image.entity'; diff --git a/services/API-service/src/api/notification/whatsapp/whatsapp.service.ts b/services/API-service/src/api/notification/whatsapp/whatsapp.service.ts index 3359046c2..681ff1699 100644 --- a/services/API-service/src/api/notification/whatsapp/whatsapp.service.ts +++ b/services/API-service/src/api/notification/whatsapp/whatsapp.service.ts @@ -1,6 +1,8 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; + import { IsNull, Not, Repository } from 'typeorm'; + import { EXTERNAL_API } from '../../../config'; import { EventSummaryCountry } from '../../../shared/data.model'; import { CountryEntity } from '../../country/country.entity'; @@ -8,6 +10,7 @@ import { DisasterType } from '../../disaster/disaster-type.enum'; import { EventMapImageEntity } from '../../event/event-map-image.entity'; import { EventService } from '../../event/event.service'; import { UserEntity } from '../../user/user.entity'; +import { formatActionUnitValue } from '../helpers/format-action-unit-value.helper'; import { LookupService } from '../lookup/lookup.service'; import { NotificationContentService } from '../notification-content/notification-content.service'; import { twilioClient } from './twilio.client'; @@ -16,7 +19,6 @@ import { TwilioStatusCallbackDto, } from './twilio.dto'; import { NotificationType, TwilioMessageEntity } from './twilio.entity'; -import { formatActionUnitValue } from '../helpers/format-action-unit-value.helper'; @Injectable() export class WhatsappService { diff --git a/services/API-service/src/api/point-data/dto/upload-asset-exposure-status.dto.ts b/services/API-service/src/api/point-data/dto/upload-asset-exposure-status.dto.ts index 031f95b1b..5f88e3e68 100644 --- a/services/API-service/src/api/point-data/dto/upload-asset-exposure-status.dto.ts +++ b/services/API-service/src/api/point-data/dto/upload-asset-exposure-status.dto.ts @@ -1,3 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; + +import { Type } from 'class-transformer'; import { IsArray, IsEnum, @@ -6,11 +9,10 @@ import { IsString, ValidateNested, } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; + import { LeadTime } from '../../admin-area-dynamic-data/enum/lead-time.enum'; import { DisasterType } from '../../disaster/disaster-type.enum'; import { PointDataEnum } from '../point-data.entity'; -import { Type } from 'class-transformer'; export class UploadAssetExposureStatusDto { @ApiProperty({ example: ['123', '234'] }) diff --git a/services/API-service/src/api/point-data/dto/upload-community-notifications.dto.ts b/services/API-service/src/api/point-data/dto/upload-community-notifications.dto.ts index 4be79b600..d330f62de 100644 --- a/services/API-service/src/api/point-data/dto/upload-community-notifications.dto.ts +++ b/services/API-service/src/api/point-data/dto/upload-community-notifications.dto.ts @@ -1,6 +1,7 @@ -import { IsNotEmpty, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + export class CommunityNotificationDto { @ApiProperty({ example: 'nameVolunteer' }) @IsNotEmpty() diff --git a/services/API-service/src/api/point-data/dto/upload-dam-sites.dto.ts b/services/API-service/src/api/point-data/dto/upload-dam-sites.dto.ts index 884515095..d8880551c 100644 --- a/services/API-service/src/api/point-data/dto/upload-dam-sites.dto.ts +++ b/services/API-service/src/api/point-data/dto/upload-dam-sites.dto.ts @@ -1,6 +1,7 @@ -import { IsNotEmpty, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + export class DamSiteDto { @ApiProperty({ example: 'name' }) @IsNotEmpty() diff --git a/services/API-service/src/api/point-data/dto/upload-evacuation-centers.dto.ts b/services/API-service/src/api/point-data/dto/upload-evacuation-centers.dto.ts index 570099bdf..ae7cf3e0c 100644 --- a/services/API-service/src/api/point-data/dto/upload-evacuation-centers.dto.ts +++ b/services/API-service/src/api/point-data/dto/upload-evacuation-centers.dto.ts @@ -1,4 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; + import { IsNotEmpty, IsString } from 'class-validator'; export class EvacuationCenterDto { diff --git a/services/API-service/src/api/point-data/dto/upload-gauge.dto.ts b/services/API-service/src/api/point-data/dto/upload-gauge.dto.ts index b64eb1c74..5f39ba5a9 100644 --- a/services/API-service/src/api/point-data/dto/upload-gauge.dto.ts +++ b/services/API-service/src/api/point-data/dto/upload-gauge.dto.ts @@ -1,6 +1,7 @@ -import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; + export class GaugeDto { @ApiProperty({ example: 'name' }) @IsString() diff --git a/services/API-service/src/api/point-data/dto/upload-glofas-station.dto.ts b/services/API-service/src/api/point-data/dto/upload-glofas-station.dto.ts index 5df1ce898..337fe39ca 100644 --- a/services/API-service/src/api/point-data/dto/upload-glofas-station.dto.ts +++ b/services/API-service/src/api/point-data/dto/upload-glofas-station.dto.ts @@ -1,6 +1,7 @@ -import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; + export class GlofasStationDto { @ApiProperty({ example: 'G5100' }) @IsString() diff --git a/services/API-service/src/api/point-data/dto/upload-health-sites.dto.ts b/services/API-service/src/api/point-data/dto/upload-health-sites.dto.ts index 0d109f291..45bf22447 100644 --- a/services/API-service/src/api/point-data/dto/upload-health-sites.dto.ts +++ b/services/API-service/src/api/point-data/dto/upload-health-sites.dto.ts @@ -1,4 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; + import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; export class HealthSiteDto { diff --git a/services/API-service/src/api/point-data/dto/upload-red-cross-branch.dto.ts b/services/API-service/src/api/point-data/dto/upload-red-cross-branch.dto.ts index 4b9c1b2f8..cd160a223 100644 --- a/services/API-service/src/api/point-data/dto/upload-red-cross-branch.dto.ts +++ b/services/API-service/src/api/point-data/dto/upload-red-cross-branch.dto.ts @@ -1,6 +1,7 @@ -import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; + export class RedCrossBranchDto { @ApiProperty({ example: 'branch name' }) @IsNotEmpty() diff --git a/services/API-service/src/api/point-data/dto/upload-schools.dto.ts b/services/API-service/src/api/point-data/dto/upload-schools.dto.ts index e2b1419ce..d1e1cd08b 100644 --- a/services/API-service/src/api/point-data/dto/upload-schools.dto.ts +++ b/services/API-service/src/api/point-data/dto/upload-schools.dto.ts @@ -1,6 +1,7 @@ -import { IsNotEmpty, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + export class SchoolDto { @ApiProperty({ example: 'name' }) @IsString() diff --git a/services/API-service/src/api/point-data/dto/upload-waterpoint.dto.ts b/services/API-service/src/api/point-data/dto/upload-waterpoint.dto.ts index a98238e5a..d42a501cb 100644 --- a/services/API-service/src/api/point-data/dto/upload-waterpoint.dto.ts +++ b/services/API-service/src/api/point-data/dto/upload-waterpoint.dto.ts @@ -1,6 +1,7 @@ -import { IsNotEmpty, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + export class WaterpointDto { @ApiProperty({ example: 'name' }) @IsString() diff --git a/services/API-service/src/api/point-data/dynamic-point-data.entity.ts b/services/API-service/src/api/point-data/dynamic-point-data.entity.ts index b8b228b96..6b56bcde9 100644 --- a/services/API-service/src/api/point-data/dynamic-point-data.entity.ts +++ b/services/API-service/src/api/point-data/dynamic-point-data.entity.ts @@ -1,13 +1,14 @@ import { - Entity, - PrimaryGeneratedColumn, Column, - ManyToOne, - JoinColumn, + Entity, Index, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, } from 'typeorm'; -import { PointDataEntity } from './point-data.entity'; + import { LeadTimeEntity } from '../lead-time/lead-time.entity'; +import { PointDataEntity } from './point-data.entity'; @Entity('dynamic-point-data') export class DynamicPointDataEntity { diff --git a/services/API-service/src/api/point-data/point-data.controller.ts b/services/API-service/src/api/point-data/point-data.controller.ts index aa51421b1..884734e4d 100644 --- a/services/API-service/src/api/point-data/point-data.controller.ts +++ b/services/API-service/src/api/point-data/point-data.controller.ts @@ -21,16 +21,17 @@ import { ApiResponse, ApiTags, } from '@nestjs/swagger'; + import { Roles } from '../../roles.decorator'; import { RolesGuard } from '../../roles.guard'; +import { FILE_UPLOAD_API_FORMAT } from '../../shared/file-upload-api-format'; import { GeoJson } from '../../shared/geo.model'; import { UserRole } from '../user/user-role.enum'; -import { CommunityNotification, PointDataService } from './point-data.service'; import { UploadAssetExposureStatusDto, UploadDynamicPointDataDto, } from './dto/upload-asset-exposure-status.dto'; -import { FILE_UPLOAD_API_FORMAT } from '../../shared/file-upload-api-format'; +import { CommunityNotification, PointDataService } from './point-data.service'; @ApiBearerAuth() @ApiTags('point-data') diff --git a/services/API-service/src/api/point-data/point-data.entity.ts b/services/API-service/src/api/point-data/point-data.entity.ts index 27c87058c..cf0bb940c 100644 --- a/services/API-service/src/api/point-data/point-data.entity.ts +++ b/services/API-service/src/api/point-data/point-data.entity.ts @@ -1,4 +1,5 @@ -import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'; +import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; + import { DynamicPointDataEntity } from './dynamic-point-data.entity'; export enum PointDataEnum { diff --git a/services/API-service/src/api/point-data/point-data.module.ts b/services/API-service/src/api/point-data/point-data.module.ts index d615a9994..eb97f1758 100644 --- a/services/API-service/src/api/point-data/point-data.module.ts +++ b/services/API-service/src/api/point-data/point-data.module.ts @@ -1,13 +1,14 @@ +import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; + import { HelperService } from '../../shared/helper.service'; import { WhatsappModule } from '../notification/whatsapp/whatsapp.module'; import { UserModule } from '../user/user.module'; +import { DynamicPointDataEntity } from './dynamic-point-data.entity'; import { PointDataController } from './point-data.controller'; import { PointDataEntity } from './point-data.entity'; import { PointDataService } from './point-data.service'; -import { HttpModule } from '@nestjs/axios'; -import { DynamicPointDataEntity } from './dynamic-point-data.entity'; @Module({ imports: [ diff --git a/services/API-service/src/api/point-data/point-data.service.ts b/services/API-service/src/api/point-data/point-data.service.ts index d08c16421..d00d8fd32 100644 --- a/services/API-service/src/api/point-data/point-data.service.ts +++ b/services/API-service/src/api/point-data/point-data.service.ts @@ -1,26 +1,28 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; + import { validate } from 'class-validator'; +import { IsNull, MoreThanOrEqual, Repository } from 'typeorm'; + import { GeoJson } from '../../shared/geo.model'; import { HelperService } from '../../shared/helper.service'; -import { IsNull, MoreThanOrEqual, Repository } from 'typeorm'; -import { EvacuationCenterDto } from './dto/upload-evacuation-centers.dto'; -import { PointDataEntity, PointDataEnum } from './point-data.entity'; -import { DamSiteDto } from './dto/upload-dam-sites.dto'; -import { HealthSiteDto } from './dto/upload-health-sites.dto'; -import { RedCrossBranchDto } from './dto/upload-red-cross-branch.dto'; -import { CommunityNotificationDto } from './dto/upload-community-notifications.dto'; +import { DisasterType } from '../disaster/disaster-type.enum'; import { WhatsappService } from '../notification/whatsapp/whatsapp.service'; -import { SchoolDto } from './dto/upload-schools.dto'; -import { WaterpointDto } from './dto/upload-waterpoint.dto'; import { UploadAssetExposureStatusDto, UploadDynamicPointDataDto, } from './dto/upload-asset-exposure-status.dto'; -import { DisasterType } from '../disaster/disaster-type.enum'; +import { CommunityNotificationDto } from './dto/upload-community-notifications.dto'; +import { DamSiteDto } from './dto/upload-dam-sites.dto'; +import { EvacuationCenterDto } from './dto/upload-evacuation-centers.dto'; import { GaugeDto } from './dto/upload-gauge.dto'; -import { DynamicPointDataEntity } from './dynamic-point-data.entity'; import { GlofasStationDto } from './dto/upload-glofas-station.dto'; +import { HealthSiteDto } from './dto/upload-health-sites.dto'; +import { RedCrossBranchDto } from './dto/upload-red-cross-branch.dto'; +import { SchoolDto } from './dto/upload-schools.dto'; +import { WaterpointDto } from './dto/upload-waterpoint.dto'; +import { DynamicPointDataEntity } from './dynamic-point-data.entity'; +import { PointDataEntity, PointDataEnum } from './point-data.entity'; export interface CommunityNotification { nameVolunteer: string; diff --git a/services/API-service/src/api/rainfall-triggers/rainfall-triggers.controller.ts b/services/API-service/src/api/rainfall-triggers/rainfall-triggers.controller.ts index aaf39d3fc..a26a065f2 100644 --- a/services/API-service/src/api/rainfall-triggers/rainfall-triggers.controller.ts +++ b/services/API-service/src/api/rainfall-triggers/rainfall-triggers.controller.ts @@ -6,6 +6,7 @@ import { ApiResponse, ApiTags, } from '@nestjs/swagger'; + import { RolesGuard } from '../../roles.guard'; import { RainfallTriggersEntity } from './rainfall-triggers.entity'; import { RainfallTriggersService } from './rainfall-triggers.service'; diff --git a/services/API-service/src/api/rainfall-triggers/rainfall-triggers.entity.ts b/services/API-service/src/api/rainfall-triggers/rainfall-triggers.entity.ts index 4a37bbc1d..a65c862a8 100644 --- a/services/API-service/src/api/rainfall-triggers/rainfall-triggers.entity.ts +++ b/services/API-service/src/api/rainfall-triggers/rainfall-triggers.entity.ts @@ -1,11 +1,13 @@ import { ApiProperty } from '@nestjs/swagger'; + import { - Entity, - PrimaryGeneratedColumn, Column, - ManyToOne, + Entity, JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, } from 'typeorm'; + import { LeadTime } from '../admin-area-dynamic-data/enum/lead-time.enum'; import { CountryEntity } from '../country/country.entity'; diff --git a/services/API-service/src/api/rainfall-triggers/rainfall-triggers.module.ts b/services/API-service/src/api/rainfall-triggers/rainfall-triggers.module.ts index 8417dd78e..ab54500e2 100644 --- a/services/API-service/src/api/rainfall-triggers/rainfall-triggers.module.ts +++ b/services/API-service/src/api/rainfall-triggers/rainfall-triggers.module.ts @@ -1,10 +1,11 @@ +import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; + import { UserModule } from '../user/user.module'; import { RainfallTriggersController } from './rainfall-triggers.controller'; import { RainfallTriggersEntity } from './rainfall-triggers.entity'; import { RainfallTriggersService } from './rainfall-triggers.service'; -import { HttpModule } from '@nestjs/axios'; @Module({ imports: [ diff --git a/services/API-service/src/api/rainfall-triggers/rainfall-triggers.service.ts b/services/API-service/src/api/rainfall-triggers/rainfall-triggers.service.ts index 49caa6e4a..532d08dec 100644 --- a/services/API-service/src/api/rainfall-triggers/rainfall-triggers.service.ts +++ b/services/API-service/src/api/rainfall-triggers/rainfall-triggers.service.ts @@ -1,6 +1,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; + import { Repository } from 'typeorm'; + import { RainfallTriggersEntity } from './rainfall-triggers.entity'; @Injectable() diff --git a/services/API-service/src/api/typhoon-track/dto/trackpoint-details.ts b/services/API-service/src/api/typhoon-track/dto/trackpoint-details.ts index cd6519f34..d1ea98640 100644 --- a/services/API-service/src/api/typhoon-track/dto/trackpoint-details.ts +++ b/services/API-service/src/api/typhoon-track/dto/trackpoint-details.ts @@ -1,3 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; + +import { Type } from 'class-transformer'; import { IsBoolean, IsDate, @@ -5,8 +8,6 @@ import { IsNotEmpty, IsNumber, } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; export enum TyphoonCategory { TD = 'TD', diff --git a/services/API-service/src/api/typhoon-track/dto/upload-typhoon-track.ts b/services/API-service/src/api/typhoon-track/dto/upload-typhoon-track.ts index 2534316c9..2b0bda241 100644 --- a/services/API-service/src/api/typhoon-track/dto/upload-typhoon-track.ts +++ b/services/API-service/src/api/typhoon-track/dto/upload-typhoon-track.ts @@ -1,3 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; + +import { Type } from 'class-transformer'; import { IsArray, IsNotEmpty, @@ -5,9 +8,8 @@ import { IsString, ValidateNested, } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; + import { LeadTime } from '../../admin-area-dynamic-data/enum/lead-time.enum'; -import { Type } from 'class-transformer'; import { TrackpointDetailsDto } from './trackpoint-details'; export class UploadTyphoonTrackDto { diff --git a/services/API-service/src/api/typhoon-track/typhoon-track.controller.ts b/services/API-service/src/api/typhoon-track/typhoon-track.controller.ts index ed02b04c3..93382493a 100644 --- a/services/API-service/src/api/typhoon-track/typhoon-track.controller.ts +++ b/services/API-service/src/api/typhoon-track/typhoon-track.controller.ts @@ -15,6 +15,7 @@ import { ApiResponse, ApiTags, } from '@nestjs/swagger'; + import { Roles } from '../../roles.decorator'; import { RolesGuard } from '../../roles.guard'; import { GeoJson } from '../../shared/geo.model'; diff --git a/services/API-service/src/api/typhoon-track/typhoon-track.entity.ts b/services/API-service/src/api/typhoon-track/typhoon-track.entity.ts index a8511b8ac..34180fe60 100644 --- a/services/API-service/src/api/typhoon-track/typhoon-track.entity.ts +++ b/services/API-service/src/api/typhoon-track/typhoon-track.entity.ts @@ -1,11 +1,13 @@ import { ApiProperty } from '@nestjs/swagger'; + import { - Entity, - PrimaryGeneratedColumn, Column, - ManyToOne, + Entity, JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, } from 'typeorm'; + import { GeoJson } from '../../shared/geo.model'; import { CountryEntity } from '../country/country.entity'; import { LeadTimeEntity } from '../lead-time/lead-time.entity'; diff --git a/services/API-service/src/api/typhoon-track/typhoon-track.module.ts b/services/API-service/src/api/typhoon-track/typhoon-track.module.ts index ed01ec32c..d21bf08a1 100644 --- a/services/API-service/src/api/typhoon-track/typhoon-track.module.ts +++ b/services/API-service/src/api/typhoon-track/typhoon-track.module.ts @@ -1,12 +1,13 @@ +import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; + import { HelperService } from '../../shared/helper.service'; import { TriggerPerLeadTime } from '../event/trigger-per-lead-time.entity'; import { UserModule } from '../user/user.module'; import { TyphoonTrackController } from './typhoon-track.controller'; import { TyphoonTrackEntity } from './typhoon-track.entity'; import { TyphoonTrackService } from './typhoon-track.service'; -import { HttpModule } from '@nestjs/axios'; @Module({ imports: [ diff --git a/services/API-service/src/api/typhoon-track/typhoon-track.service.ts b/services/API-service/src/api/typhoon-track/typhoon-track.service.ts index f44eff507..cd244a4f4 100644 --- a/services/API-service/src/api/typhoon-track/typhoon-track.service.ts +++ b/services/API-service/src/api/typhoon-track/typhoon-track.service.ts @@ -1,6 +1,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; + import { InsertResult, MoreThanOrEqual, Repository } from 'typeorm'; + import { DisasterSpecificProperties } from '../../shared/data.model'; import { GeoJson } from '../../shared/geo.model'; import { HelperService } from '../../shared/helper.service'; diff --git a/services/API-service/src/api/user/dto/create-user.dto.ts b/services/API-service/src/api/user/dto/create-user.dto.ts index 658e79d3c..cdc70f851 100644 --- a/services/API-service/src/api/user/dto/create-user.dto.ts +++ b/services/API-service/src/api/user/dto/create-user.dto.ts @@ -1,3 +1,5 @@ +import { ApiProperty } from '@nestjs/swagger'; + import { ArrayNotEmpty, IsArray, @@ -9,11 +11,11 @@ import { IsString, MinLength, } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; -import { UserRole } from '../user-role.enum'; -import { UserStatus } from '../user-status.enum'; + import countries from '../../../scripts/json/countries.json'; import disasterTypes from '../../../scripts/json/disasters.json'; +import { UserRole } from '../user-role.enum'; +import { UserStatus } from '../user-status.enum'; const userRoleArray = Object.values(UserRole).map((item) => String(item)); diff --git a/services/API-service/src/api/user/dto/delete-user.dto.ts b/services/API-service/src/api/user/dto/delete-user.dto.ts index 156605bc9..6607f952f 100644 --- a/services/API-service/src/api/user/dto/delete-user.dto.ts +++ b/services/API-service/src/api/user/dto/delete-user.dto.ts @@ -1,6 +1,7 @@ -import { IsNotEmpty } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty } from 'class-validator'; + export class DeleteUserDto { @ApiProperty() @IsNotEmpty() diff --git a/services/API-service/src/api/user/dto/login-user.dto.ts b/services/API-service/src/api/user/dto/login-user.dto.ts index 389bf55ce..2e0128650 100644 --- a/services/API-service/src/api/user/dto/login-user.dto.ts +++ b/services/API-service/src/api/user/dto/login-user.dto.ts @@ -1,6 +1,7 @@ -import { IsEmail, IsNotEmpty, MinLength } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsNotEmpty, MinLength } from 'class-validator'; + export class LoginUserDto { @ApiProperty({ example: 'dunant@redcross.nl' }) @IsEmail() diff --git a/services/API-service/src/api/user/dto/update-password.dto.ts b/services/API-service/src/api/user/dto/update-password.dto.ts index 6bed76aec..508b255c5 100644 --- a/services/API-service/src/api/user/dto/update-password.dto.ts +++ b/services/API-service/src/api/user/dto/update-password.dto.ts @@ -1,6 +1,7 @@ -import { IsNotEmpty, IsOptional, IsString, MinLength } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsOptional, IsString, MinLength } from 'class-validator'; + export class UpdatePasswordDto { @ApiProperty({ example: 'abcd' }) @IsNotEmpty() diff --git a/services/API-service/src/api/user/user.controller.ts b/services/API-service/src/api/user/user.controller.ts index cb29a5bf6..afebd1343 100644 --- a/services/API-service/src/api/user/user.controller.ts +++ b/services/API-service/src/api/user/user.controller.ts @@ -1,26 +1,27 @@ import { - Post, Body, Controller, - UsePipes, HttpStatus, + Post, UseGuards, + UsePipes, } from '@nestjs/common'; -import { UserService } from './user.service'; -import { UserResponseObject } from './user.model'; -import { CreateUserDto, LoginUserDto, UpdatePasswordDto } from './dto'; import { HttpException } from '@nestjs/common/exceptions/http.exception'; -import { ValidationPipe } from '../../shared/pipes/validation.pipe'; import { - ApiTags, ApiBearerAuth, ApiOperation, ApiResponse, + ApiTags, } from '@nestjs/swagger'; + +import { Roles } from '../../roles.decorator'; import { RolesGuard } from '../../roles.guard'; +import { ValidationPipe } from '../../shared/pipes/validation.pipe'; +import { CreateUserDto, LoginUserDto, UpdatePasswordDto } from './dto'; import { UserRole } from './user-role.enum'; -import { Roles } from '../../roles.decorator'; import { UserDecorator } from './user.decorator'; +import { UserResponseObject } from './user.model'; +import { UserService } from './user.service'; @ApiTags('-- user --') @Controller('user') diff --git a/services/API-service/src/api/user/user.decorator.ts b/services/API-service/src/api/user/user.decorator.ts index 26da13080..0e0beadf3 100644 --- a/services/API-service/src/api/user/user.decorator.ts +++ b/services/API-service/src/api/user/user.decorator.ts @@ -1,5 +1,7 @@ -import { ExecutionContext, createParamDecorator } from '@nestjs/common'; +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + import * as jwt from 'jsonwebtoken'; + import { User } from './user.model'; export const UserDecorator = createParamDecorator( diff --git a/services/API-service/src/api/user/user.entity.ts b/services/API-service/src/api/user/user.entity.ts index 78689fb18..e7de74318 100644 --- a/services/API-service/src/api/user/user.entity.ts +++ b/services/API-service/src/api/user/user.entity.ts @@ -1,20 +1,22 @@ +import crypto from 'crypto'; + import { IsEmail } from 'class-validator'; import { - Entity, - PrimaryGeneratedColumn, - Column, BeforeInsert, - OneToMany, - ManyToMany, + Column, + Entity, JoinTable, + ManyToMany, + OneToMany, + PrimaryGeneratedColumn, } from 'typeorm'; -import crypto from 'crypto'; + import { CountryEntity } from '../country/country.entity'; +import { DisasterEntity } from '../disaster/disaster.entity'; import { EapActionStatusEntity } from '../eap-actions/eap-action-status.entity'; +import { EventPlaceCodeEntity } from '../event/event-place-code.entity'; import { UserRole } from './user-role.enum'; import { UserStatus } from './user-status.enum'; -import { EventPlaceCodeEntity } from '../event/event-place-code.entity'; -import { DisasterEntity } from '../disaster/disaster.entity'; @Entity('user') export class UserEntity { diff --git a/services/API-service/src/api/user/user.model.ts b/services/API-service/src/api/user/user.model.ts index a57c2b084..e0822aed5 100644 --- a/services/API-service/src/api/user/user.model.ts +++ b/services/API-service/src/api/user/user.model.ts @@ -1,6 +1,7 @@ +import { ApiProperty } from '@nestjs/swagger'; + import { UserRole } from './user-role.enum'; import { UserStatus } from './user-status.enum'; -import { ApiProperty } from '@nestjs/swagger'; export class User { public userId: string; diff --git a/services/API-service/src/api/user/user.module.ts b/services/API-service/src/api/user/user.module.ts index f5784cd3a..8c5018ba5 100644 --- a/services/API-service/src/api/user/user.module.ts +++ b/services/API-service/src/api/user/user.module.ts @@ -1,11 +1,12 @@ import { Module } from '@nestjs/common'; -import { UserController } from './user.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { UserEntity } from './user.entity'; -import { UserService } from './user.service'; + import { CountryEntity } from '../country/country.entity'; -import { LookupModule } from '../notification/lookup/lookup.module'; import { DisasterEntity } from '../disaster/disaster.entity'; +import { LookupModule } from '../notification/lookup/lookup.module'; +import { UserController } from './user.controller'; +import { UserEntity } from './user.entity'; +import { UserService } from './user.service'; @Module({ imports: [ diff --git a/services/API-service/src/api/user/user.service.ts b/services/API-service/src/api/user/user.service.ts index 52dd269bd..3d3e6f5fd 100644 --- a/services/API-service/src/api/user/user.service.ts +++ b/services/API-service/src/api/user/user.service.ts @@ -1,18 +1,19 @@ -import { Injectable } from '@nestjs/common'; +import crypto from 'crypto'; +import { HttpStatus, Injectable } from '@nestjs/common'; +import { HttpException } from '@nestjs/common/exceptions/http.exception'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, In } from 'typeorm'; -import { UserEntity } from './user.entity'; -import { CreateUserDto, LoginUserDto, UpdatePasswordDto } from './dto'; -import { UserResponseObject } from './user.model'; + import { validate } from 'class-validator'; -import { HttpException } from '@nestjs/common/exceptions/http.exception'; -import { HttpStatus } from '@nestjs/common'; -import crypto from 'crypto'; import jwt from 'jsonwebtoken'; +import { In, Repository } from 'typeorm'; + import { CountryEntity } from '../country/country.entity'; -import { UserRole } from './user-role.enum'; -import { LookupService } from '../notification/lookup/lookup.service'; import { DisasterEntity } from '../disaster/disaster.entity'; +import { LookupService } from '../notification/lookup/lookup.service'; +import { CreateUserDto, LoginUserDto, UpdatePasswordDto } from './dto'; +import { UserRole } from './user-role.enum'; +import { UserEntity } from './user.entity'; +import { UserResponseObject } from './user.model'; @Injectable() export class UserService { diff --git a/services/API-service/src/api/waterpoints/waterpoints.controller.ts b/services/API-service/src/api/waterpoints/waterpoints.controller.ts index 8220f5ec3..3764446ef 100644 --- a/services/API-service/src/api/waterpoints/waterpoints.controller.ts +++ b/services/API-service/src/api/waterpoints/waterpoints.controller.ts @@ -1,15 +1,17 @@ -import { Get, Param, Controller, UseGuards } from '@nestjs/common'; -import { AxiosResponse } from 'axios'; +import { Controller, Get, Param, UseGuards } from '@nestjs/common'; import { - ApiTags, + ApiBearerAuth, ApiOperation, ApiParam, - ApiBearerAuth, ApiResponse, + ApiTags, } from '@nestjs/swagger'; + +import { AxiosResponse } from 'axios'; + +import { RolesGuard } from '../../roles.guard'; import { GeoJson } from '../../shared/geo.model'; import { WaterpointsService } from './waterpoints.service'; -import { RolesGuard } from '../../roles.guard'; @ApiBearerAuth() @UseGuards(RolesGuard) diff --git a/services/API-service/src/api/waterpoints/waterpoints.module.ts b/services/API-service/src/api/waterpoints/waterpoints.module.ts index e20253c41..56720635e 100644 --- a/services/API-service/src/api/waterpoints/waterpoints.module.ts +++ b/services/API-service/src/api/waterpoints/waterpoints.module.ts @@ -1,9 +1,10 @@ +import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; + import { CountryModule } from '../country/country.module'; import { UserModule } from '../user/user.module'; import { WaterpointsController } from './waterpoints.controller'; import { WaterpointsService } from './waterpoints.service'; -import { HttpModule } from '@nestjs/axios'; @Module({ imports: [HttpModule, UserModule, CountryModule], diff --git a/services/API-service/src/api/waterpoints/waterpoints.service.ts b/services/API-service/src/api/waterpoints/waterpoints.service.ts index c4ca8d656..ace8a43e5 100644 --- a/services/API-service/src/api/waterpoints/waterpoints.service.ts +++ b/services/API-service/src/api/waterpoints/waterpoints.service.ts @@ -1,10 +1,12 @@ +import { HttpService } from '@nestjs/axios'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; + import { AxiosResponse } from 'axios'; -import { WKTStringFromGeometry } from 'wkt-io-ts'; import { isRight } from 'fp-ts/lib/Either'; -import { CountryService } from '../country/country.service'; +import { WKTStringFromGeometry } from 'wkt-io-ts'; + import { GeoJson } from '../../shared/geo.model'; -import { HttpService } from '@nestjs/axios'; +import { CountryService } from '../country/country.service'; @Injectable() export class WaterpointsService { diff --git a/services/API-service/src/app.controller.ts b/services/API-service/src/app.controller.ts index a3589bd93..10c750934 100644 --- a/services/API-service/src/app.controller.ts +++ b/services/API-service/src/app.controller.ts @@ -1,10 +1,11 @@ -import { Get, Controller, UseGuards } from '@nestjs/common'; +import { Controller, Get, UseGuards } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags, } from '@nestjs/swagger'; + import { RolesGuard } from './roles.guard'; @ApiTags('-- check API --') diff --git a/services/API-service/src/app.module.ts b/services/API-service/src/app.module.ts index e927b7518..c5ee48b02 100644 --- a/services/API-service/src/app.module.ts +++ b/services/API-service/src/app.module.ts @@ -1,26 +1,27 @@ import { Module } from '@nestjs/common'; -import { AppController } from './app.controller'; -import { HealthModule } from './health.module'; -import { EapActionsModule } from './api/eap-actions/eap-actions.module'; -import { ScriptsModule } from './scripts/scripts.module'; +import { ScheduleModule } from '@nestjs/schedule'; + +import { AdminAreaDataModule } from './api/admin-area-data/admin-area-data.module'; +import { AdminAreaDynamicDataModule } from './api/admin-area-dynamic-data/admin-area-dynamic-data.module'; +import { AdminAreaModule } from './api/admin-area/admin-area.module'; import { CountryModule } from './api/country/country.module'; -import { WaterpointsModule } from './api/waterpoints/waterpoints.module'; +import { DisasterModule } from './api/disaster/disaster.module'; +import { EapActionsModule } from './api/eap-actions/eap-actions.module'; import { EventModule } from './api/event/event.module'; -import { MetadataModule } from './api/metadata/metadata.module'; -import { AdminAreaModule } from './api/admin-area/admin-area.module'; import { GlofasStationModule } from './api/glofas-station/glofas-station.module'; -import { AdminAreaDynamicDataModule } from './api/admin-area-dynamic-data/admin-area-dynamic-data.module'; -import { DisasterModule } from './api/disaster/disaster.module'; -import { AdminAreaDataModule } from './api/admin-area-data/admin-area-data.module'; -import { RainfallTriggersModule } from './api/rainfall-triggers/rainfall-triggers.module'; +import { LinesDataModule } from './api/lines-data/lines-data.module'; +import { MetadataModule } from './api/metadata/metadata.module'; import { NotificationModule } from './api/notification/notification.module'; -import { UserModule } from './api/user/user.module'; -import { TyphoonTrackModule } from './api/typhoon-track/typhoon-track.module'; import { WhatsappModule } from './api/notification/whatsapp/whatsapp.module'; -import { CronjobModule } from './cronjob/cronjob.module'; -import { ScheduleModule } from '@nestjs/schedule'; import { PointDataModule } from './api/point-data/point-data.module'; -import { LinesDataModule } from './api/lines-data/lines-data.module'; +import { RainfallTriggersModule } from './api/rainfall-triggers/rainfall-triggers.module'; +import { TyphoonTrackModule } from './api/typhoon-track/typhoon-track.module'; +import { UserModule } from './api/user/user.module'; +import { WaterpointsModule } from './api/waterpoints/waterpoints.module'; +import { AppController } from './app.controller'; +import { CronjobModule } from './cronjob/cronjob.module'; +import { HealthModule } from './health.module'; +import { ScriptsModule } from './scripts/scripts.module'; import { TypeOrmModule } from './typeorm.module'; @Module({ diff --git a/services/API-service/src/cronjob/cronjob.module.ts b/services/API-service/src/cronjob/cronjob.module.ts index 8f74ea67c..487b014ba 100644 --- a/services/API-service/src/cronjob/cronjob.module.ts +++ b/services/API-service/src/cronjob/cronjob.module.ts @@ -1,4 +1,5 @@ import { Module } from '@nestjs/common'; + import { AdminAreaDynamicDataModule } from '../api/admin-area-dynamic-data/admin-area-dynamic-data.module'; import { CronjobService } from './cronjob.service'; diff --git a/services/API-service/src/cronjob/cronjob.service.ts b/services/API-service/src/cronjob/cronjob.service.ts index 9be1806b7..ec5d59843 100644 --- a/services/API-service/src/cronjob/cronjob.service.ts +++ b/services/API-service/src/cronjob/cronjob.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; import { Cron, CronExpression } from '@nestjs/schedule'; + import { AdminAreaDynamicDataService } from '../api/admin-area-dynamic-data/admin-area-dynamic-data.service'; @Injectable() diff --git a/services/API-service/src/main.ts b/services/API-service/src/main.ts index 17e95f170..5e6bbc7b9 100644 --- a/services/API-service/src/main.ts +++ b/services/API-service/src/main.ts @@ -1,15 +1,17 @@ -import { EXTERNAL_API, PORT } from './config'; +import { BadRequestException, ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; -import { ApplicationModule } from './app.module'; import { - SwaggerModule, DocumentBuilder, - SwaggerDocumentOptions, SwaggerCustomOptions, + SwaggerDocumentOptions, + SwaggerModule, } from '@nestjs/swagger'; -import { BadRequestException, ValidationPipe } from '@nestjs/common'; + import * as bodyParser from 'body-parser'; +import { ApplicationModule } from './app.module'; +import { EXTERNAL_API, PORT } from './config'; + async function bootstrap(): Promise { const appOptions = { cors: true }; const app = await NestFactory.create(ApplicationModule, appOptions); diff --git a/services/API-service/src/roles.decorator.ts b/services/API-service/src/roles.decorator.ts index ea6255c28..a4eb3aa1b 100644 --- a/services/API-service/src/roles.decorator.ts +++ b/services/API-service/src/roles.decorator.ts @@ -1,4 +1,5 @@ import { SetMetadata } from '@nestjs/common'; + import { UserRole } from './api/user/user-role.enum'; export const Roles = (...roles: UserRole[]) => SetMetadata('roles', roles); diff --git a/services/API-service/src/roles.guard.ts b/services/API-service/src/roles.guard.ts index cd6d426e4..2be104149 100644 --- a/services/API-service/src/roles.guard.ts +++ b/services/API-service/src/roles.guard.ts @@ -1,9 +1,11 @@ -import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; -import * as jwt from 'jsonwebtoken'; -import { UserService } from './api/user/user.service'; -import { User } from './api/user/user.model'; +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; + +import * as jwt from 'jsonwebtoken'; + import { UserRole } from './api/user/user-role.enum'; +import { User } from './api/user/user.model'; +import { UserService } from './api/user/user.service'; @Injectable() export class RolesGuard implements CanActivate { diff --git a/services/API-service/src/scripts.ts b/services/API-service/src/scripts.ts index 8bf6fed4b..514255fda 100644 --- a/services/API-service/src/scripts.ts +++ b/services/API-service/src/scripts.ts @@ -1,5 +1,7 @@ import { NestFactory } from '@nestjs/core'; -import { ScriptsModule, InterfaceScript } from './scripts/scripts.module'; + +import { InterfaceScript, ScriptsModule } from './scripts/scripts.module'; + import yargs = require('yargs'); async function main(): Promise { diff --git a/services/API-service/src/scripts/geoserver-sync.service.ts b/services/API-service/src/scripts/geoserver-sync.service.ts index e032d52f1..465aba245 100644 --- a/services/API-service/src/scripts/geoserver-sync.service.ts +++ b/services/API-service/src/scripts/geoserver-sync.service.ts @@ -1,11 +1,13 @@ +import fs from 'fs'; import { HttpService } from '@nestjs/axios'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; + import { firstValueFrom } from 'rxjs'; -import { INTERNAL_GEOSERVER_API_URL } from '../config'; -import countries from './json/countries.json'; + import { DisasterType } from '../api/disaster/disaster-type.enum'; +import { INTERNAL_GEOSERVER_API_URL } from '../config'; import { DisasterTypeGeoServerMapper } from './disaster-type-geoserver-file.mapper'; -import fs from 'fs'; +import countries from './json/countries.json'; const workspaceName = 'ibf-system'; diff --git a/services/API-service/src/scripts/mock-helper.service.ts b/services/API-service/src/scripts/mock-helper.service.ts index a51a174f5..b75b305ea 100644 --- a/services/API-service/src/scripts/mock-helper.service.ts +++ b/services/API-service/src/scripts/mock-helper.service.ts @@ -1,16 +1,17 @@ +import fs from 'fs'; import { Injectable } from '@nestjs/common'; + +import { AdminAreaDynamicDataService } from '../api/admin-area-dynamic-data/admin-area-dynamic-data.service'; import { LeadTime } from '../api/admin-area-dynamic-data/enum/lead-time.enum'; import { DisasterType } from '../api/disaster/disaster-type.enum'; +import { EventService } from '../api/event/event.service'; import { UploadLinesExposureStatusDto } from '../api/lines-data/dto/upload-asset-exposure-status.dto'; import { LinesDataEnum } from '../api/lines-data/lines-data.entity'; +import { LinesDataService } from '../api/lines-data/lines-data.service'; import { UploadDynamicPointDataDto } from '../api/point-data/dto/upload-asset-exposure-status.dto'; import { PointDataEnum } from '../api/point-data/point-data.entity'; -import { DisasterTypeGeoServerMapper } from './disaster-type-geoserver-file.mapper'; -import { AdminAreaDynamicDataService } from '../api/admin-area-dynamic-data/admin-area-dynamic-data.service'; -import { EventService } from '../api/event/event.service'; -import { LinesDataService } from '../api/lines-data/lines-data.service'; import { PointDataService } from '../api/point-data/point-data.service'; -import fs from 'fs'; +import { DisasterTypeGeoServerMapper } from './disaster-type-geoserver-file.mapper'; @Injectable() export class MockHelperService { diff --git a/services/API-service/src/scripts/mock.controller.ts b/services/API-service/src/scripts/mock.controller.ts index 090ec78ed..3b56212a0 100644 --- a/services/API-service/src/scripts/mock.controller.ts +++ b/services/API-service/src/scripts/mock.controller.ts @@ -1,9 +1,9 @@ import { + Body, Controller, + HttpStatus, Post, - Body, Res, - HttpStatus, UseGuards, } from '@nestjs/common'; import { @@ -13,17 +13,19 @@ import { ApiResponse, ApiTags, } from '@nestjs/swagger'; + import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; -import { RolesGuard } from '../roles.guard'; + import { DisasterType } from '../api/disaster/disaster-type.enum'; -import { Roles } from '../roles.decorator'; import { UserRole } from '../api/user/user-role.enum'; -import { MockService } from './mock.service'; +import { Roles } from '../roles.decorator'; +import { RolesGuard } from '../roles.guard'; import { - FloodsScenario, - FlashFloodsScenario, EpidemicsScenario, + FlashFloodsScenario, + FloodsScenario, } from './enum/mock-scenario.enum'; +import { MockService } from './mock.service'; export class MockBaseScenario { @ApiProperty({ example: 'fill_in_secret' }) diff --git a/services/API-service/src/scripts/mock.service.ts b/services/API-service/src/scripts/mock.service.ts index dd6795844..dda8bfde1 100644 --- a/services/API-service/src/scripts/mock.service.ts +++ b/services/API-service/src/scripts/mock.service.ts @@ -1,29 +1,31 @@ -import { Injectable } from '@nestjs/common'; -import { DisasterType } from '../api/disaster/disaster-type.enum'; import fs from 'fs'; -import { LeadTime } from '../api/admin-area-dynamic-data/enum/lead-time.enum'; -import { MetadataService } from '../api/metadata/metadata.service'; -import { DynamicIndicator } from '../api/admin-area-dynamic-data/enum/dynamic-data-unit'; +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { In, Repository } from 'typeorm'; + +import { AdminAreaDynamicDataEntity } from '../api/admin-area-dynamic-data/admin-area-dynamic-data.entity'; import { AdminAreaDynamicDataService } from '../api/admin-area-dynamic-data/admin-area-dynamic-data.service'; +import { DynamicIndicator } from '../api/admin-area-dynamic-data/enum/dynamic-data-unit'; +import { LeadTime } from '../api/admin-area-dynamic-data/enum/lead-time.enum'; +import { AdminAreaService } from '../api/admin-area/admin-area.service'; import { AdminLevel } from '../api/country/admin-level.enum'; +import { DisasterType } from '../api/disaster/disaster-type.enum'; +import { EapActionStatusEntity } from '../api/eap-actions/eap-action-status.entity'; +import { EventPlaceCodeEntity } from '../api/event/event-place-code.entity'; import { EventService } from '../api/event/event.service'; -import countries from './json/countries.json'; +import { TriggerPerLeadTime } from '../api/event/trigger-per-lead-time.entity'; import { GlofasStationService } from '../api/glofas-station/glofas-station.service'; +import { MetadataService } from '../api/metadata/metadata.service'; +import { DEBUG } from '../config'; +import { GeoserverSyncService } from './geoserver-sync.service'; +import countries from './json/countries.json'; +import { MockHelperService } from './mock-helper.service'; import { MockEpidemicsScenario, MockFlashFloodsScenario, MockFloodsScenario, } from './mock.controller'; -import { In, Repository } from 'typeorm'; -import { EventPlaceCodeEntity } from '../api/event/event-place-code.entity'; -import { InjectRepository } from '@nestjs/typeorm'; -import { AdminAreaDynamicDataEntity } from '../api/admin-area-dynamic-data/admin-area-dynamic-data.entity'; -import { EapActionStatusEntity } from '../api/eap-actions/eap-action-status.entity'; -import { TriggerPerLeadTime } from '../api/event/trigger-per-lead-time.entity'; -import { AdminAreaService } from '../api/admin-area/admin-area.service'; -import { MockHelperService } from './mock-helper.service'; -import { DEBUG } from '../config'; -import { GeoserverSyncService } from './geoserver-sync.service'; class Scenario { scenarioName: string; diff --git a/services/API-service/src/scripts/scripts.controller.ts b/services/API-service/src/scripts/scripts.controller.ts index d5b6f694c..4aded00b4 100644 --- a/services/API-service/src/scripts/scripts.controller.ts +++ b/services/API-service/src/scripts/scripts.controller.ts @@ -1,9 +1,9 @@ import { + Body, Controller, + HttpStatus, Post, - Body, Res, - HttpStatus, UseGuards, } from '@nestjs/common'; import { @@ -13,6 +13,7 @@ import { ApiResponse, ApiTags, } from '@nestjs/swagger'; + import { IsEnum, IsIn, @@ -20,12 +21,13 @@ import { IsOptional, IsString, } from 'class-validator'; -import { SeedInit } from './seed-init'; -import { ScriptsService } from './scripts.service'; -import { RolesGuard } from '../roles.guard'; + import { DisasterType } from '../api/disaster/disaster-type.enum'; -import { Roles } from '../roles.decorator'; import { UserRole } from '../api/user/user-role.enum'; +import { Roles } from '../roles.decorator'; +import { RolesGuard } from '../roles.guard'; +import { ScriptsService } from './scripts.service'; +import { SeedInit } from './seed-init'; class ResetDto { @ApiProperty({ example: 'fill_in_secret' }) diff --git a/services/API-service/src/scripts/scripts.module.ts b/services/API-service/src/scripts/scripts.module.ts index e1c38e464..e28a4b26f 100644 --- a/services/API-service/src/scripts/scripts.module.ts +++ b/services/API-service/src/scripts/scripts.module.ts @@ -1,39 +1,41 @@ -import { EapActionStatusEntity } from './../api/eap-actions/eap-action-status.entity'; -import { EventPlaceCodeEntity } from './../api/event/event-place-code.entity'; -import { AdminAreaDynamicDataModule } from './../api/admin-area-dynamic-data/admin-area-dynamic-data.module'; +import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'; + import { Arguments } from 'yargs'; -import { ScriptsController } from './scripts.controller'; -import { SeedInit } from './seed-init'; -import { GlofasStationModule } from '../api/glofas-station/glofas-station.module'; -import { ScriptsService } from './scripts.service'; -import { EventModule } from '../api/event/event.module'; -import { UserModule } from '../api/user/user.module'; + +import { ORMConfig } from '../../ormconfig'; +import { AdminAreaDataModule } from '../api/admin-area-data/admin-area-data.module'; +import { AdminAreaDynamicDataEntity } from '../api/admin-area-dynamic-data/admin-area-dynamic-data.entity'; import { AdminAreaEntity } from '../api/admin-area/admin-area.entity'; -import { LeadTimeEntity } from '../api/lead-time/lead-time.entity'; -import { CountryEntity } from '../api/country/country.entity'; -import { TyphoonTrackModule } from '../api/typhoon-track/typhoon-track.module'; -import SeedProd from './seed-prod'; -import { MetadataModule } from '../api/metadata/metadata.module'; -import SeedAdminArea from './seed-admin-area'; import { AdminAreaModule } from '../api/admin-area/admin-area.module'; +import { CountryEntity } from '../api/country/country.entity'; import { CountryModule } from '../api/country/country.module'; -import SeedAdminAreaData from './seed-admin-area-data'; -import SeedPointData from './seed-point-data'; -import SeedRainfallData from './seed-rainfall-data'; -import { PointDataModule } from '../api/point-data/point-data.module'; -import { AdminAreaDataModule } from '../api/admin-area-data/admin-area-data.module'; +import { EventModule } from '../api/event/event.module'; import { TriggerPerLeadTime } from '../api/event/trigger-per-lead-time.entity'; -import { AdminAreaDynamicDataEntity } from '../api/admin-area-dynamic-data/admin-area-dynamic-data.entity'; +import { GlofasStationModule } from '../api/glofas-station/glofas-station.module'; +import { LeadTimeEntity } from '../api/lead-time/lead-time.entity'; import { LinesDataModule } from '../api/lines-data/lines-data.module'; -import SeedLineData from './seed-line-data'; -import { ORMConfig } from '../../ormconfig'; -import { MockService } from './mock.service'; -import { MockController } from './mock.controller'; +import { MetadataModule } from '../api/metadata/metadata.module'; +import { PointDataModule } from '../api/point-data/point-data.module'; +import { TyphoonTrackModule } from '../api/typhoon-track/typhoon-track.module'; +import { UserModule } from '../api/user/user.module'; +import { AdminAreaDynamicDataModule } from './../api/admin-area-dynamic-data/admin-area-dynamic-data.module'; +import { EapActionStatusEntity } from './../api/eap-actions/eap-action-status.entity'; +import { EventPlaceCodeEntity } from './../api/event/event-place-code.entity'; import { GeoserverSyncService } from './geoserver-sync.service'; -import { HttpModule } from '@nestjs/axios'; import { MockHelperService } from './mock-helper.service'; +import { MockController } from './mock.controller'; +import { MockService } from './mock.service'; +import { ScriptsController } from './scripts.controller'; +import { ScriptsService } from './scripts.service'; +import SeedAdminArea from './seed-admin-area'; +import SeedAdminAreaData from './seed-admin-area-data'; +import { SeedInit } from './seed-init'; +import SeedLineData from './seed-line-data'; +import SeedPointData from './seed-point-data'; +import SeedProd from './seed-prod'; +import SeedRainfallData from './seed-rainfall-data'; @Module({ imports: [ diff --git a/services/API-service/src/scripts/scripts.service.ts b/services/API-service/src/scripts/scripts.service.ts index daee18356..c067bf3b1 100644 --- a/services/API-service/src/scripts/scripts.service.ts +++ b/services/API-service/src/scripts/scripts.service.ts @@ -1,31 +1,33 @@ +import fs from 'fs'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { In, Repository } from 'typeorm'; + +import { AdminAreaDynamicDataEntity } from '../api/admin-area-dynamic-data/admin-area-dynamic-data.entity'; import { AdminAreaDynamicDataService } from '../api/admin-area-dynamic-data/admin-area-dynamic-data.service'; +import { DynamicIndicator } from '../api/admin-area-dynamic-data/enum/dynamic-data-unit'; +import { LeadTime } from '../api/admin-area-dynamic-data/enum/lead-time.enum'; +import { AdminAreaEntity } from '../api/admin-area/admin-area.entity'; +import { AdminLevel } from '../api/country/admin-level.enum'; +import { CountryEntity } from '../api/country/country.entity'; import { DisasterType } from '../api/disaster/disaster-type.enum'; +import { EapActionStatusEntity } from '../api/eap-actions/eap-action-status.entity'; +import { EventPlaceCodeEntity } from '../api/event/event-place-code.entity'; +import { EventService } from '../api/event/event.service'; +import { TriggerPerLeadTime } from '../api/event/trigger-per-lead-time.entity'; import { GlofasStationService } from '../api/glofas-station/glofas-station.service'; +import { MetadataService } from '../api/metadata/metadata.service'; +import { TyphoonTrackService } from '../api/typhoon-track/typhoon-track.service'; +import countries from './json/countries.json'; +import { MockHelperService } from './mock-helper.service'; +import { MockService } from './mock.service'; import { MockAll, MockDynamic, MockTyphoonScenario, TyphoonScenario, } from './scripts.controller'; -import countries from './json/countries.json'; -import fs from 'fs'; -import { DynamicIndicator } from '../api/admin-area-dynamic-data/enum/dynamic-data-unit'; -import { LeadTime } from '../api/admin-area-dynamic-data/enum/lead-time.enum'; -import { EventService } from '../api/event/event.service'; -import { InjectRepository } from '@nestjs/typeorm'; -import { EventPlaceCodeEntity } from '../api/event/event-place-code.entity'; -import { In, Repository } from 'typeorm'; -import { EapActionStatusEntity } from '../api/eap-actions/eap-action-status.entity'; -import { CountryEntity } from '../api/country/country.entity'; -import { TyphoonTrackService } from '../api/typhoon-track/typhoon-track.service'; -import { MetadataService } from '../api/metadata/metadata.service'; -import { AdminLevel } from '../api/country/admin-level.enum'; -import { TriggerPerLeadTime } from '../api/event/trigger-per-lead-time.entity'; -import { AdminAreaDynamicDataEntity } from '../api/admin-area-dynamic-data/admin-area-dynamic-data.entity'; -import { AdminAreaEntity } from '../api/admin-area/admin-area.entity'; -import { MockHelperService } from './mock-helper.service'; -import { MockService } from './mock.service'; @Injectable() export class ScriptsService { diff --git a/services/API-service/src/scripts/seed-admin-area-data.ts b/services/API-service/src/scripts/seed-admin-area-data.ts index e6f063450..d10f90f41 100644 --- a/services/API-service/src/scripts/seed-admin-area-data.ts +++ b/services/API-service/src/scripts/seed-admin-area-data.ts @@ -1,9 +1,11 @@ import { Injectable } from '@nestjs/common'; -import { InterfaceScript } from './scripts.module'; -import { DataSource } from 'typeorm'; -import { SeedHelper } from './seed-helper'; + import { AdminLevel } from 'src/api/country/admin-level.enum'; +import { DataSource } from 'typeorm'; + import { AdminAreaDataService } from '../api/admin-area-data/admin-area-data.service'; +import { InterfaceScript } from './scripts.module'; +import { SeedHelper } from './seed-helper'; interface AdminAreaDataRecord { placeCode: string; diff --git a/services/API-service/src/scripts/seed-admin-area.ts b/services/API-service/src/scripts/seed-admin-area.ts index 3ace5a711..2248f1d8f 100644 --- a/services/API-service/src/scripts/seed-admin-area.ts +++ b/services/API-service/src/scripts/seed-admin-area.ts @@ -1,9 +1,10 @@ -import { Injectable } from '@nestjs/common'; -import { InterfaceScript } from './scripts.module'; -import countries from './json/countries.json'; import fs from 'fs'; +import { Injectable } from '@nestjs/common'; + import { AdminAreaService } from '../api/admin-area/admin-area.service'; import { EventAreaService } from '../api/admin-area/services/event-area.service'; +import countries from './json/countries.json'; +import { InterfaceScript } from './scripts.module'; @Injectable() export class SeedAdminArea implements InterfaceScript { diff --git a/services/API-service/src/scripts/seed-helper.ts b/services/API-service/src/scripts/seed-helper.ts index 8d4094b31..c6aaf0c8f 100644 --- a/services/API-service/src/scripts/seed-helper.ts +++ b/services/API-service/src/scripts/seed-helper.ts @@ -1,6 +1,7 @@ import fs from 'fs'; -import csv from 'csv-parser'; import { Readable } from 'stream'; + +import csv from 'csv-parser'; import { DataSource } from 'typeorm'; export class SeedHelper { diff --git a/services/API-service/src/scripts/seed-init.ts b/services/API-service/src/scripts/seed-init.ts index 35ce840b6..fd7cdfbb3 100644 --- a/services/API-service/src/scripts/seed-init.ts +++ b/services/API-service/src/scripts/seed-init.ts @@ -1,41 +1,41 @@ import { Injectable } from '@nestjs/common'; -import { InterfaceScript } from './scripts.module'; + import { DataSource } from 'typeorm'; + +import { + LeadTime, + LeadTimeUnit, +} from '../api/admin-area-dynamic-data/enum/lead-time.enum'; import { CountryEntity } from '../api/country/country.entity'; +import { CountryService } from '../api/country/country.service'; +import { NotificationInfoDto } from '../api/country/dto/notification-info.dto'; +import { DisasterType } from '../api/disaster/disaster-type.enum'; +import { DisasterEntity } from '../api/disaster/disaster.entity'; import { AreaOfFocusEntity } from '../api/eap-actions/area-of-focus.entity'; import { EapActionEntity } from '../api/eap-actions/eap-action.entity'; -import { IndicatorMetadataEntity } from '../api/metadata/indicator-metadata.entity'; import { LeadTimeEntity } from '../api/lead-time/lead-time.entity'; +import { IndicatorMetadataEntity } from '../api/metadata/indicator-metadata.entity'; +import { LayerMetadataEntity } from '../api/metadata/layer-metadata.entity'; +import { NotificationInfoEntity } from '../api/notification/notifcation-info.entity'; import { UserRole } from '../api/user/user-role.enum'; import { UserStatus } from '../api/user/user-status.enum'; import { UserEntity } from '../api/user/user.entity'; -import { LayerMetadataEntity } from '../api/metadata/layer-metadata.entity'; -import { DisasterType } from '../api/disaster/disaster-type.enum'; -import { DisasterEntity } from '../api/disaster/disaster.entity'; -import { NotificationInfoEntity } from '../api/notification/notifcation-info.entity'; - -import leadTimes from './json/lead-times.json'; -import notificationInfo from './json/notification-info.json'; -import countries from './json/countries.json'; -import users from './json/users.json'; import areasOfFocus from './json/areas-of-focus.json'; +import countries from './json/countries.json'; +import disasters from './json/disasters.json'; import eapActions from './json/EAP-actions.json'; import indicatorMetadata from './json/indicator-metadata.json'; import layerMetadata from './json/layer-metadata.json'; -import disasters from './json/disasters.json'; - +import leadTimes from './json/lead-times.json'; +import notificationInfo from './json/notification-info.json'; +import users from './json/users.json'; +import { InterfaceScript } from './scripts.module'; import SeedAdminArea from './seed-admin-area'; -import { SeedHelper } from './seed-helper'; import SeedAdminAreaData from './seed-admin-area-data'; -import SeedRainfallData from './seed-rainfall-data'; -import SeedPointData from './seed-point-data'; -import { CountryService } from '../api/country/country.service'; -import { NotificationInfoDto } from '../api/country/dto/notification-info.dto'; +import { SeedHelper } from './seed-helper'; import SeedLineData from './seed-line-data'; -import { - LeadTime, - LeadTimeUnit, -} from '../api/admin-area-dynamic-data/enum/lead-time.enum'; +import SeedPointData from './seed-point-data'; +import SeedRainfallData from './seed-rainfall-data'; @Injectable() export class SeedInit implements InterfaceScript { diff --git a/services/API-service/src/scripts/seed-line-data.ts b/services/API-service/src/scripts/seed-line-data.ts index 0d5776a46..87c6931f0 100644 --- a/services/API-service/src/scripts/seed-line-data.ts +++ b/services/API-service/src/scripts/seed-line-data.ts @@ -1,10 +1,12 @@ import { Injectable } from '@nestjs/common'; -import { InterfaceScript } from './scripts.module'; + import { DataSource } from 'typeorm'; -import { SeedHelper } from './seed-helper'; -import countries from './json/countries.json'; -import { LinesDataService } from '../api/lines-data/lines-data.service'; + import { LinesDataEnum } from '../api/lines-data/lines-data.entity'; +import { LinesDataService } from '../api/lines-data/lines-data.service'; +import countries from './json/countries.json'; +import { InterfaceScript } from './scripts.module'; +import { SeedHelper } from './seed-helper'; @Injectable() export class SeedLineData implements InterfaceScript { diff --git a/services/API-service/src/scripts/seed-point-data.ts b/services/API-service/src/scripts/seed-point-data.ts index c5e462c41..61d85688c 100644 --- a/services/API-service/src/scripts/seed-point-data.ts +++ b/services/API-service/src/scripts/seed-point-data.ts @@ -1,10 +1,12 @@ import { Injectable } from '@nestjs/common'; -import { InterfaceScript } from './scripts.module'; + import { DataSource } from 'typeorm'; -import { SeedHelper } from './seed-helper'; -import countries from './json/countries.json'; + import { PointDataEnum } from '../api/point-data/point-data.entity'; import { PointDataService } from '../api/point-data/point-data.service'; +import countries from './json/countries.json'; +import { InterfaceScript } from './scripts.module'; +import { SeedHelper } from './seed-helper'; @Injectable() export class SeedPointData implements InterfaceScript { diff --git a/services/API-service/src/scripts/seed-prod.ts b/services/API-service/src/scripts/seed-prod.ts index 34b6a9057..f4a598d38 100644 --- a/services/API-service/src/scripts/seed-prod.ts +++ b/services/API-service/src/scripts/seed-prod.ts @@ -1,10 +1,12 @@ import { Injectable } from '@nestjs/common'; -import { InterfaceScript } from './scripts.module'; + import { DataSource } from 'typeorm'; -import { UserEntity } from '../api/user/user.entity'; -import users from './json/users.json'; + import { UserRole } from '../api/user/user-role.enum'; import { UserStatus } from '../api/user/user-status.enum'; +import { UserEntity } from '../api/user/user.entity'; +import users from './json/users.json'; +import { InterfaceScript } from './scripts.module'; @Injectable() export class SeedProd implements InterfaceScript { diff --git a/services/API-service/src/scripts/seed-rainfall-data.ts b/services/API-service/src/scripts/seed-rainfall-data.ts index a08d48d1a..f46a78da6 100644 --- a/services/API-service/src/scripts/seed-rainfall-data.ts +++ b/services/API-service/src/scripts/seed-rainfall-data.ts @@ -1,10 +1,12 @@ -import { DisasterType } from './../api/disaster/disaster-type.enum'; import { Injectable } from '@nestjs/common'; -import { InterfaceScript } from './scripts.module'; + import { DataSource } from 'typeorm'; -import { SeedHelper } from './seed-helper'; + import { RainfallTriggersEntity } from '../api/rainfall-triggers/rainfall-triggers.entity'; +import { DisasterType } from './../api/disaster/disaster-type.enum'; import countries from './json/countries.json'; +import { InterfaceScript } from './scripts.module'; +import { SeedHelper } from './seed-helper'; @Injectable() export class SeedRainfallData implements InterfaceScript { diff --git a/services/API-service/src/shared/data.model.ts b/services/API-service/src/shared/data.model.ts index 81a259349..0d2be4e18 100644 --- a/services/API-service/src/shared/data.model.ts +++ b/services/API-service/src/shared/data.model.ts @@ -1,4 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; + import { LeadTime } from '../api/admin-area-dynamic-data/enum/lead-time.enum'; import { Geometry } from './geo.model'; diff --git a/services/API-service/src/shared/helper.service.ts b/services/API-service/src/shared/helper.service.ts index ea607c9df..a10e2c406 100644 --- a/services/API-service/src/shared/helper.service.ts +++ b/services/API-service/src/shared/helper.service.ts @@ -1,15 +1,17 @@ import { Injectable } from '@nestjs/common'; -import { Readable } from 'typeorm/platform/PlatformTools'; -import { DisasterType } from '../api/disaster/disaster-type.enum'; -import { GeoJson, GeoJsonFeature } from './geo.model'; + import csv from 'csv-parser'; -import { DateDto } from '../api/event/dto/date.dto'; import { DataSource } from 'typeorm'; -import { TriggerPerLeadTime } from '../api/event/trigger-per-lead-time.entity'; +import { Readable } from 'typeorm/platform/PlatformTools'; + import { LeadTime, LeadTimeUnit, } from '../api/admin-area-dynamic-data/enum/lead-time.enum'; +import { DisasterType } from '../api/disaster/disaster-type.enum'; +import { DateDto } from '../api/event/dto/date.dto'; +import { TriggerPerLeadTime } from '../api/event/trigger-per-lead-time.entity'; +import { GeoJson, GeoJsonFeature } from './geo.model'; @Injectable() export class HelperService { diff --git a/services/API-service/src/shared/pipes/validation.pipe.ts b/services/API-service/src/shared/pipes/validation.pipe.ts index eacc9a7bf..6420e979a 100644 --- a/services/API-service/src/shared/pipes/validation.pipe.ts +++ b/services/API-service/src/shared/pipes/validation.pipe.ts @@ -1,14 +1,15 @@ import { - PipeTransform, ArgumentMetadata, BadRequestException, HttpStatus, Injectable, + PipeTransform, } from '@nestjs/common'; -import { validate } from 'class-validator'; -import { plainToClass } from 'class-transformer'; import { HttpException } from '@nestjs/common/exceptions/http.exception'; +import { plainToClass } from 'class-transformer'; +import { validate } from 'class-validator'; + @Injectable() export class ValidationPipe implements PipeTransform { public async transform( diff --git a/services/API-service/src/typeorm.module.ts b/services/API-service/src/typeorm.module.ts index c92e0ba7d..451d21c9f 100644 --- a/services/API-service/src/typeorm.module.ts +++ b/services/API-service/src/typeorm.module.ts @@ -1,5 +1,7 @@ import { Global, Module } from '@nestjs/common'; + import { DataSource } from 'typeorm'; + import { AppDataSource } from '../appdatasource'; @Global() From f42558d6075c1aebc2dcd977dcae7cfe074d3539 Mon Sep 17 00:00:00 2001 From: Gulfaraz Rahman Date: Thu, 4 Jul 2024 18:15:08 +0200 Subject: [PATCH 46/55] style: typo --- .../notification/email/email-template.service.ts | 13 ++++++------- .../notification/email/html/table-trigger-row.html | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/services/API-service/src/api/notification/email/email-template.service.ts b/services/API-service/src/api/notification/email/email-template.service.ts index a3a7f1925..0b5b110f0 100644 --- a/services/API-service/src/api/notification/email/email-template.service.ts +++ b/services/API-service/src/api/notification/email/email-template.service.ts @@ -14,7 +14,6 @@ import { } from '../../../shared/data.model'; import { CountryEntity } from '../../country/country.entity'; import * as juice from 'juice'; -import { formatActionUnitValue } from '../helpers/format-action-unit-value.helper'; const emailFolder = './src/api/notification/email'; const emailTemplateFolder = `${emailFolder}/html`; @@ -120,7 +119,7 @@ export class EmailTemplateService { const template = this.readHtmlFile('event-finished.html'); for (const event of events) { - const eventFinshedHtml = ejs.render(template, { + const eventFinishedHtml = ejs.render(template, { disasterTypeLabel: disasterTypeLabel, eventName: event.eventName, issuedDate: this.dateObjectToDateTimeString( @@ -129,7 +128,7 @@ export class EmailTemplateService { ), timezone: CountryTimeZoneMapping[country.countryCodeISO3], }); - html += eventFinshedHtml; + html += eventFinishedHtml; } return html; } @@ -221,7 +220,7 @@ export class EmailTemplateService { private async formatEmail( emailKeyValueReplaceObject: Record, ): Promise { - // TODO REFACTOR: Apply styles in a septerate file also for the base.html + // TODO REFACTOR: Apply styles in a separate file also for the base.html const template = this.readHtmlFile('base.html'); const styles = this.readHtmlFile('styles.ejs'); const templateWithStyle = styles + template; @@ -304,13 +303,13 @@ export class EmailTemplateService { private getTablesRows(event: NotificationDataPerEventDto) { return event.triggeredAreas .map((area) => { - const tableRowHmltFileName = + const tableRowHtmlFileName = TriggerStatusLabelEnum.Trigger === event.triggerStatusLabel ? 'table-trigger-row.html' : 'table-warning-row.html'; - const areaTemplate = this.readHtmlFile(tableRowHmltFileName); + const areaTemplate = this.readHtmlFile(tableRowHtmlFileName); const areaData = { - affectectedOfIndicator: area.actionsValue, + affectedOfIndicator: area.actionsValue, adminBoundary: area.displayName ? area.displayName : area.name, higherAdminBoundary: area.nameParent, }; diff --git a/services/API-service/src/api/notification/email/html/table-trigger-row.html b/services/API-service/src/api/notification/email/html/table-trigger-row.html index 8f942e29e..5dfe41cd0 100644 --- a/services/API-service/src/api/notification/email/html/table-trigger-row.html +++ b/services/API-service/src/api/notification/email/html/table-trigger-row.html @@ -1,6 +1,6 @@
+ Find more information about the potentially exposed areas, view the map and manage anticipatory actions.
- <%= affectectedOfIndicator %> + <%= affectedOfIndicator %> <%= adminBoundary %> (<%= higherAdminBoundary %>) From c841b6efdbdfbc20da173c99fa320e3c641a3b32 Mon Sep 17 00:00:00 2001 From: Gulfaraz Rahman Date: Thu, 4 Jul 2024 18:57:39 +0200 Subject: [PATCH 47/55] refactor: readability issues and minor refactors --- .../email/email-template.service.ts | 169 ++++++------------ 1 file changed, 56 insertions(+), 113 deletions(-) diff --git a/services/API-service/src/api/notification/email/email-template.service.ts b/services/API-service/src/api/notification/email/email-template.service.ts index 0b5b110f0..01aad41f5 100644 --- a/services/API-service/src/api/notification/email/email-template.service.ts +++ b/services/API-service/src/api/notification/email/email-template.service.ts @@ -57,7 +57,7 @@ export class EmailTemplateService { const disasterType = emailContent.disasterType; const keyValueReplaceObject = { - emailBody: this.getEmailBody(false), + emailBody: this.readHtmlFile('trigger-notification.html'), headerEventOverview: this.getHeaderEventStarted(emailContent), notificationActions: this.getNotificationActionsHtml( country, @@ -84,9 +84,9 @@ export class EmailTemplateService { disasterTypeLabel: string, ): Record { const keyValueReplaceObject = { - emailBody: this.getEmailBody(true), + emailBody: this.readHtmlFile('trigger-finished.html'), headerEventOverview: '', - eventOverview: this.geEventsFinishedOverview( + eventOverview: this.getEventsFinishedOverview( country, events, disasterTypeLabel, @@ -102,35 +102,18 @@ export class EmailTemplateService { return keyValueReplaceObject; } - private getEmailBody(triggerFinished: boolean): string { - if (triggerFinished) { - return this.readHtmlFile('trigger-finished.html'); - } else { - return this.readHtmlFile('trigger-notification.html'); - } - } - - private geEventsFinishedOverview( + private getEventsFinishedOverview( country: CountryEntity, events: EventSummaryCountry[], disasterTypeLabel: string, ): string { - let html = ''; const template = this.readHtmlFile('event-finished.html'); - - for (const event of events) { - const eventFinishedHtml = ejs.render(template, { - disasterTypeLabel: disasterTypeLabel, - eventName: event.eventName, - issuedDate: this.dateObjectToDateTimeString( - new Date(event.startDate), - country.countryCodeISO3, - ), - timezone: CountryTimeZoneMapping[country.countryCodeISO3], - }); - html += eventFinishedHtml; - } - return html; + return events.map(event => ejs.render(template, { + disasterTypeLabel, + eventName: event.eventName, + issuedDate: this.dateObjectToDateTimeString(new Date(event.startDate), country.countryCodeISO3), + timezone: CountryTimeZoneMapping[country.countryCodeISO3], + })).join(''); } private getHeaderEventStarted(emailContent: ContentEventEmail): string { @@ -162,59 +145,47 @@ export class EmailTemplateService { return html; } - private getSocialMediaHtml(country: CountryEntity): string { - if (country.notificationInfo.linkSocialMediaType) { - this.readHtmlFile('social-media-link.html'); - } else { - return ''; - } + private getSocialMediaHtml(country: CountryEntity) { + return country.notificationInfo.linkSocialMediaType ? this.readHtmlFile('social-media-link.html') : ''; } - private getMapImageHtml(emailContent: ContentEventEmail): string { - let html = ''; - for (const event of emailContent.dataPerEvent) { - const mapImage = event.mapImage; - if (mapImage) { - let eventHtml = this.readHtmlFile('map-image.html'); + private getMapImageHtml(emailContent: ContentEventEmail) { + return emailContent.dataPerEvent + .filter(event => event.mapImage) + .map(event => { + const eventHtmlTemplate = this.readHtmlFile('map-image.html'); const replacements = { mapImgSrc: this.getMapImgSrc( emailContent.country.countryCodeISO3, emailContent.disasterType, event.eventName, ), - mapImgDescription: this.getMapImageDescription( - emailContent.disasterType, - ), + mapImgDescription: this.getMapImageDescription(emailContent.disasterType), eventName: event.eventName ? `(for ${event.eventName})` : '', }; - eventHtml = ejs.render(eventHtml, replacements); - html += eventHtml; - } - } - return html; + return ejs.render(eventHtmlTemplate, replacements); + }) + .join(''); } private getMapImgSrc( countryCodeISO3: string, disasterType: DisasterType, eventName: string, - ): string { - const src = `${ + ) { + return `${ process.env.NG_API_URL }/event/event-map-image/${countryCodeISO3}/${disasterType}/${ eventName || 'no-name' }`; - - return src; } private getMapImageDescription(disasterType: DisasterType): string { - switch (disasterType) { - case DisasterType.Floods: - return 'The triggered areas are outlined in purple. The potential flood extent is shown in red.
'; - default: - return ''; - } + const descriptions = { + [DisasterType.Floods]: 'The triggered areas are outlined in purple. The potential flood extent is shown in red.
', + }; + + return descriptions[disasterType] || ''; } private async formatEmail( @@ -288,16 +259,13 @@ export class EmailTemplateService { .join(''); } - private getEventSeverityLabel( - eapAlertClassKey: EapAlertClassKeyEnum, - ): string { - if (eapAlertClassKey === EapAlertClassKeyEnum.med) { - return 'Medium'; - } else if (eapAlertClassKey === EapAlertClassKeyEnum.min) { - return 'Low'; - } else { - return ''; - } + private getEventSeverityLabel(eapAlertClassKey: EapAlertClassKeyEnum): string { + const severityLabels = { + [EapAlertClassKeyEnum.med]: 'Medium', + [EapAlertClassKeyEnum.min]: 'Low', + }; + + return severityLabels[eapAlertClassKey] || ''; } private getTablesRows(event: NotificationDataPerEventDto) { @@ -370,39 +338,20 @@ export class EmailTemplateService { .join(''); } - private getDisasterIssuedLabel( - eapLabel: string, - triggerStatusLabel: TriggerStatusLabelEnum, - ) { - if (eapLabel) { - return eapLabel; - } else { - return triggerStatusLabel; - } + private getDisasterIssuedLabel(eapLabel: string, triggerStatusLabel: TriggerStatusLabelEnum) { + return eapLabel || triggerStatusLabel; } - private getAdvisoryHtml( - triggerStatusLabel: TriggerStatusLabelEnum, - eapLink: string, - ): string { - const advisoryHtml = - triggerStatusLabel === TriggerStatusLabelEnum.Trigger - ? this.readHtmlFile('advisory-trigger.html') - : this.readHtmlFile('advisory-warning.html'); - return ejs.render(advisoryHtml, { eapLink: eapLink }); + private getAdvisoryHtml(triggerStatusLabel: TriggerStatusLabelEnum, eapLink: string) { + const fileName = triggerStatusLabel === TriggerStatusLabelEnum.Trigger ? 'advisory-trigger.html' : 'advisory-warning.html'; + const advisoryHtml = this.readHtmlFile(fileName); + return ejs.render(advisoryHtml, { eapLink }); } - private getTotalAffectedHtml( - event: NotificationDataPerEventDto, - indicatorUnit: string, - ): string { - let html = ''; - if (event.triggerStatusLabel === TriggerStatusLabelEnum.Warning) { - html = this.readHtmlFile('body-total-affected-warning.html'); - } else { - html = this.readHtmlFile('body-total-affected-trigger.html'); - } - return ejs.render(html, { + private getTotalAffectedHtml(event: NotificationDataPerEventDto, indicatorUnit: string): string { + const fileName = event.triggerStatusLabel === TriggerStatusLabelEnum.Warning ? 'body-total-affected-warning.html' : 'body-total-affected-trigger.html'; + const htmlTemplate = this.readHtmlFile(fileName); + return ejs.render(htmlTemplate, { totalAffectedOfIndicator: event.totalAffectedOfIndicator, indicatorUnit: indicatorUnit, }); @@ -471,25 +420,19 @@ export class EmailTemplateService { eapAlertClassKey: EapAlertClassKeyEnum, triggerStatusLabel: TriggerStatusLabelEnum, ) { - let fileName = ''; - // Still need implement the difference between medium and low warning - if (eapAlertClassKey) { - if (eapAlertClassKey === EapAlertClassKeyEnum.med) { - fileName = 'warning-medium.png'; - } else if (eapAlertClassKey === EapAlertClassKeyEnum.min) { - fileName = 'warning-low.png'; - } else { - fileName = 'trigger.png'; - } - } else { - fileName = - triggerStatusLabel === TriggerStatusLabelEnum.Trigger - ? 'trigger.png' - : 'warning-medium.png'; + const fileNameMap = { + [EapAlertClassKeyEnum.med]: 'warning-medium.png', + [EapAlertClassKeyEnum.min]: 'warning-low.png', + default: 'trigger.png', + }; + + let fileName = eapAlertClassKey ? fileNameMap[eapAlertClassKey] : fileNameMap.default; + if (!eapAlertClassKey && triggerStatusLabel !== TriggerStatusLabelEnum.Trigger) { + fileName = 'warning-medium.png'; } + const filePath = `${emailIconFolder}/${fileName}`; - const imageDataURL = this.getPngImageAsDataURL(filePath); - return imageDataURL; + return this.getPngImageAsDataURL(filePath); } private getLogoImageAsDataURL() { From 435a2f51aac9d0bf6a079975dbcc687dde3ffa63 Mon Sep 17 00:00:00 2001 From: Gulfaraz Rahman Date: Thu, 4 Jul 2024 19:10:01 +0200 Subject: [PATCH 48/55] style: format using prettier --- .../email/email-template.service.ts | 92 +++++++++++++------ 1 file changed, 63 insertions(+), 29 deletions(-) diff --git a/services/API-service/src/api/notification/email/email-template.service.ts b/services/API-service/src/api/notification/email/email-template.service.ts index 01aad41f5..d01d5a1eb 100644 --- a/services/API-service/src/api/notification/email/email-template.service.ts +++ b/services/API-service/src/api/notification/email/email-template.service.ts @@ -1,19 +1,19 @@ -import { Injectable } from '@nestjs/common'; -import { ContentEventEmail } from '../dto/content-trigger-email.dto'; -import { - NotificationDataPerEventDto, - TriggerStatusLabelEnum, -} from '../dto/notification-date-per-event.dto'; -import * as ejs from 'ejs'; import * as fs from 'fs'; -import { CountryTimeZoneMapping } from '../../country/country-time-zone-mapping'; -import { DisasterType } from '../../disaster/disaster-type.enum'; +import * as ejs from 'ejs'; +import * as juice from 'juice'; import { EapAlertClassKeyEnum, EventSummaryCountry, } from '../../../shared/data.model'; +import { CountryTimeZoneMapping } from '../../country/country-time-zone-mapping'; import { CountryEntity } from '../../country/country.entity'; -import * as juice from 'juice'; +import { DisasterType } from '../../disaster/disaster-type.enum'; +import { ContentEventEmail } from '../dto/content-trigger-email.dto'; +import { + NotificationDataPerEventDto, + TriggerStatusLabelEnum, +} from '../dto/notification-date-per-event.dto'; +import { Injectable } from '@nestjs/common'; const emailFolder = './src/api/notification/email'; const emailTemplateFolder = `${emailFolder}/html`; @@ -108,12 +108,19 @@ export class EmailTemplateService { disasterTypeLabel: string, ): string { const template = this.readHtmlFile('event-finished.html'); - return events.map(event => ejs.render(template, { - disasterTypeLabel, - eventName: event.eventName, - issuedDate: this.dateObjectToDateTimeString(new Date(event.startDate), country.countryCodeISO3), - timezone: CountryTimeZoneMapping[country.countryCodeISO3], - })).join(''); + return events + .map((event) => + ejs.render(template, { + disasterTypeLabel, + eventName: event.eventName, + issuedDate: this.dateObjectToDateTimeString( + new Date(event.startDate), + country.countryCodeISO3, + ), + timezone: CountryTimeZoneMapping[country.countryCodeISO3], + }), + ) + .join(''); } private getHeaderEventStarted(emailContent: ContentEventEmail): string { @@ -146,13 +153,15 @@ export class EmailTemplateService { } private getSocialMediaHtml(country: CountryEntity) { - return country.notificationInfo.linkSocialMediaType ? this.readHtmlFile('social-media-link.html') : ''; + return country.notificationInfo.linkSocialMediaType + ? this.readHtmlFile('social-media-link.html') + : ''; } private getMapImageHtml(emailContent: ContentEventEmail) { return emailContent.dataPerEvent - .filter(event => event.mapImage) - .map(event => { + .filter((event) => event.mapImage) + .map((event) => { const eventHtmlTemplate = this.readHtmlFile('map-image.html'); const replacements = { mapImgSrc: this.getMapImgSrc( @@ -160,7 +169,9 @@ export class EmailTemplateService { emailContent.disasterType, event.eventName, ), - mapImgDescription: this.getMapImageDescription(emailContent.disasterType), + mapImgDescription: this.getMapImageDescription( + emailContent.disasterType, + ), eventName: event.eventName ? `(for ${event.eventName})` : '', }; return ejs.render(eventHtmlTemplate, replacements); @@ -182,7 +193,8 @@ export class EmailTemplateService { private getMapImageDescription(disasterType: DisasterType): string { const descriptions = { - [DisasterType.Floods]: 'The triggered areas are outlined in purple. The potential flood extent is shown in red.
', + [DisasterType.Floods]: + 'The triggered areas are outlined in purple. The potential flood extent is shown in red.
', }; return descriptions[disasterType] || ''; @@ -259,7 +271,9 @@ export class EmailTemplateService { .join(''); } - private getEventSeverityLabel(eapAlertClassKey: EapAlertClassKeyEnum): string { + private getEventSeverityLabel( + eapAlertClassKey: EapAlertClassKeyEnum, + ): string { const severityLabels = { [EapAlertClassKeyEnum.med]: 'Medium', [EapAlertClassKeyEnum.min]: 'Low', @@ -338,18 +352,33 @@ export class EmailTemplateService { .join(''); } - private getDisasterIssuedLabel(eapLabel: string, triggerStatusLabel: TriggerStatusLabelEnum) { + private getDisasterIssuedLabel( + eapLabel: string, + triggerStatusLabel: TriggerStatusLabelEnum, + ) { return eapLabel || triggerStatusLabel; } - private getAdvisoryHtml(triggerStatusLabel: TriggerStatusLabelEnum, eapLink: string) { - const fileName = triggerStatusLabel === TriggerStatusLabelEnum.Trigger ? 'advisory-trigger.html' : 'advisory-warning.html'; + private getAdvisoryHtml( + triggerStatusLabel: TriggerStatusLabelEnum, + eapLink: string, + ) { + const fileName = + triggerStatusLabel === TriggerStatusLabelEnum.Trigger + ? 'advisory-trigger.html' + : 'advisory-warning.html'; const advisoryHtml = this.readHtmlFile(fileName); return ejs.render(advisoryHtml, { eapLink }); } - private getTotalAffectedHtml(event: NotificationDataPerEventDto, indicatorUnit: string): string { - const fileName = event.triggerStatusLabel === TriggerStatusLabelEnum.Warning ? 'body-total-affected-warning.html' : 'body-total-affected-trigger.html'; + private getTotalAffectedHtml( + event: NotificationDataPerEventDto, + indicatorUnit: string, + ): string { + const fileName = + event.triggerStatusLabel === TriggerStatusLabelEnum.Warning + ? 'body-total-affected-warning.html' + : 'body-total-affected-trigger.html'; const htmlTemplate = this.readHtmlFile(fileName); return ejs.render(htmlTemplate, { totalAffectedOfIndicator: event.totalAffectedOfIndicator, @@ -426,8 +455,13 @@ export class EmailTemplateService { default: 'trigger.png', }; - let fileName = eapAlertClassKey ? fileNameMap[eapAlertClassKey] : fileNameMap.default; - if (!eapAlertClassKey && triggerStatusLabel !== TriggerStatusLabelEnum.Trigger) { + let fileName = eapAlertClassKey + ? fileNameMap[eapAlertClassKey] + : fileNameMap.default; + if ( + !eapAlertClassKey && + triggerStatusLabel !== TriggerStatusLabelEnum.Trigger + ) { fileName = 'warning-medium.png'; } From 29ef92c61e1113071e21cd1e41b051f49b601326 Mon Sep 17 00:00:00 2001 From: Gulfaraz Rahman Date: Thu, 4 Jul 2024 19:27:06 +0200 Subject: [PATCH 49/55] style: format fix --- .../test/email/typhoon/test-typhoon-scenario.helper.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/API-service/test/email/typhoon/test-typhoon-scenario.helper.ts b/services/API-service/test/email/typhoon/test-typhoon-scenario.helper.ts index 38fbb23d4..e6289e900 100644 --- a/services/API-service/test/email/typhoon/test-typhoon-scenario.helper.ts +++ b/services/API-service/test/email/typhoon/test-typhoon-scenario.helper.ts @@ -9,8 +9,8 @@ export async function testTyphoonScenario( accessToken: string, ): Promise { const nrOfEvents = 2; - const eventName = 'Mock typhoon' - const disasterTypeLabel = DisasterType.Typhoon + const eventName = 'Mock typhoon'; + const disasterTypeLabel = DisasterType.Typhoon; // const disasterType = DisasterType.Typhoon; // const disasterTypeLabel = disasters.find( From 6ff16e765e6d110576c710b83b59e485b27f8638 Mon Sep 17 00:00:00 2001 From: Gulfaraz Rahman Date: Fri, 12 Jul 2024 15:20:53 +0200 Subject: [PATCH 50/55] style: fix lint --- services/API-service/src/scripts/mock.controller.ts | 4 ++-- .../API-service/test/email/floods/email-ssd-floods.test.ts | 4 +--- .../API-service/test/email/floods/email-uga-floods.test.ts | 4 +--- .../test/email/floods/test-flood-scenario.helper.ts | 5 +++-- .../test/email/typhoon/test-typhoon-scenario.helper.ts | 3 ++- services/API-service/test/helpers/utility.helper.ts | 5 +++-- 6 files changed, 12 insertions(+), 13 deletions(-) diff --git a/services/API-service/src/scripts/mock.controller.ts b/services/API-service/src/scripts/mock.controller.ts index d02971e37..d36611af8 100644 --- a/services/API-service/src/scripts/mock.controller.ts +++ b/services/API-service/src/scripts/mock.controller.ts @@ -2,11 +2,11 @@ import { Body, Controller, HttpStatus, + ParseBoolPipe, Post, + Query, Res, UseGuards, - Query, - ParseBoolPipe, } from '@nestjs/common'; import { ApiBearerAuth, diff --git a/services/API-service/test/email/floods/email-ssd-floods.test.ts b/services/API-service/test/email/floods/email-ssd-floods.test.ts index 531d61f9a..50e0ffb95 100644 --- a/services/API-service/test/email/floods/email-ssd-floods.test.ts +++ b/services/API-service/test/email/floods/email-ssd-floods.test.ts @@ -1,8 +1,6 @@ -import { getAccessToken, resetDB } from '../../helpers/utility.helper'; import { FloodsScenario } from '../../../src/scripts/enum/mock-scenario.enum'; - import scenarios from '../../../src/scripts/mock-data/floods/ssd/scenarios.json'; - +import { getAccessToken, resetDB } from '../../helpers/utility.helper'; import { testFloodScenario } from './test-flood-scenario.helper'; const countryCodeISO3 = 'SSD'; diff --git a/services/API-service/test/email/floods/email-uga-floods.test.ts b/services/API-service/test/email/floods/email-uga-floods.test.ts index 2b1788a8a..e0ea2e6c3 100644 --- a/services/API-service/test/email/floods/email-uga-floods.test.ts +++ b/services/API-service/test/email/floods/email-uga-floods.test.ts @@ -1,8 +1,6 @@ -import { getAccessToken, resetDB } from '../../helpers/utility.helper'; import { FloodsScenario } from '../../../src/scripts/enum/mock-scenario.enum'; - import scenarios from '../../../src/scripts/mock-data/floods/uga/scenarios.json'; - +import { getAccessToken, resetDB } from '../../helpers/utility.helper'; import { testFloodScenario } from './test-flood-scenario.helper'; const countryCodeISO3 = 'UGA'; diff --git a/services/API-service/test/email/floods/test-flood-scenario.helper.ts b/services/API-service/test/email/floods/test-flood-scenario.helper.ts index 91d7746d7..30b67375b 100644 --- a/services/API-service/test/email/floods/test-flood-scenario.helper.ts +++ b/services/API-service/test/email/floods/test-flood-scenario.helper.ts @@ -1,8 +1,9 @@ +import { JSDOM } from 'jsdom'; + import { DisasterType } from '../../../src/api/disaster/disaster-type.enum'; import { FloodsScenario } from '../../../src/scripts/enum/mock-scenario.enum'; -import { mockFloods, sendNotification } from '../../helpers/utility.helper'; import disasters from '../../../src/scripts/json/disasters.json'; -import { JSDOM } from 'jsdom'; +import { mockFloods, sendNotification } from '../../helpers/utility.helper'; export interface TestFloodScenarioDto { scenarios: any[]; diff --git a/services/API-service/test/email/typhoon/test-typhoon-scenario.helper.ts b/services/API-service/test/email/typhoon/test-typhoon-scenario.helper.ts index e6289e900..80ca33af3 100644 --- a/services/API-service/test/email/typhoon/test-typhoon-scenario.helper.ts +++ b/services/API-service/test/email/typhoon/test-typhoon-scenario.helper.ts @@ -1,7 +1,8 @@ +import { JSDOM } from 'jsdom'; + import { DisasterType } from '../../../src/api/disaster/disaster-type.enum'; import { TyphoonScenario } from '../../../src/scripts/enum/mock-scenario.enum'; import { mockTyphoon, sendNotification } from '../../helpers/utility.helper'; -import { JSDOM } from 'jsdom'; export async function testTyphoonScenario( scenario: TyphoonScenario, diff --git a/services/API-service/test/helpers/utility.helper.ts b/services/API-service/test/helpers/utility.helper.ts index 7632558e4..feeeb60ee 100644 --- a/services/API-service/test/helpers/utility.helper.ts +++ b/services/API-service/test/helpers/utility.helper.ts @@ -1,11 +1,12 @@ import * as request from 'supertest'; import TestAgent from 'supertest/lib/agent'; -import users from '../../src/scripts/json/users.json'; + +import { DisasterType } from '../../src/api/disaster/disaster-type.enum'; import { FloodsScenario, TyphoonScenario, } from '../../src/scripts/enum/mock-scenario.enum'; -import { DisasterType } from '../../src/api/disaster/disaster-type.enum'; +import users from '../../src/scripts/json/users.json'; export async function getAccessToken(): Promise { const admin = users.find((user) => user.userRole === 'admin'); From 2a0564ce440543c3b88a75d447e4eb8f3df55124 Mon Sep 17 00:00:00 2001 From: Gulfaraz Rahman Date: Fri, 12 Jul 2024 15:25:21 +0200 Subject: [PATCH 51/55] style: format workflow yml --- .github/workflows/workflow.yml | 208 ++++++++++++++++----------------- 1 file changed, 104 insertions(+), 104 deletions(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 6f3cb6cae..3ae1843cd 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -1,109 +1,109 @@ -name: "Continuous Integration for IBF" +name: 'Continuous Integration for IBF' on: - push: - branches: [master] - paths-ignore: - - "./package.json" - - "./COMMITLOG.md" - pull_request: - branches: [master] + push: + branches: [master] + paths-ignore: + - './package.json' + - './COMMITLOG.md' + pull_request: + branches: [master] jobs: - detect-changes: - runs-on: ubuntu-latest - - outputs: - ibf-api-service: ${{ steps.filter.outputs.ibf-api-service }} - ibf-dashboard: ${{ steps.filter.outputs.ibf-dashboard }} - - steps: - - uses: actions/checkout@v3 - - uses: dorny/paths-filter@v2 - id: filter - with: - filters: | - ibf-api-service: - - "services/API-service/**" - ibf-dashboard: - - "interfaces/IBF-dashboard/**" - - ibf-api-service: - needs: detect-changes - if: ${{ needs.detect-changes.outputs.ibf-api-service == 'true' }} - - runs-on: ubuntu-latest - + detect-changes: + runs-on: ubuntu-latest + + outputs: + ibf-api-service: ${{ steps.filter.outputs.ibf-api-service }} + ibf-dashboard: ${{ steps.filter.outputs.ibf-dashboard }} + + steps: + - uses: actions/checkout@v3 + - uses: dorny/paths-filter@v2 + id: filter + with: + filters: | + ibf-api-service: + - "services/API-service/**" + ibf-dashboard: + - "interfaces/IBF-dashboard/**" + + ibf-api-service: + needs: detect-changes + if: ${{ needs.detect-changes.outputs.ibf-api-service == 'true' }} + + runs-on: ubuntu-latest + + env: + SECRET: ${{ secrets.SECRET }} + MC_API: ${{ secrets.MC_API }} + + strategy: + matrix: + node-version: [12.x] + + defaults: + run: + working-directory: 'services/API-service' + + steps: + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3.4.1 + with: + node-version: ${{ matrix.node-version }} + - run: npm ci --no-audit + - run: npm run lint + - run: npm test + - run: docker build . --file Dockerfile --tag + rodekruis/ibf-api-service:$(date +%s) + + ibf-dashboard: + needs: detect-changes + if: ${{ needs.detect-changes.outputs.ibf-dashboard == 'true' }} + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [14.x] + + defaults: + run: + working-directory: 'interfaces/IBF-dashboard' + + steps: + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3.4.1 + with: + node-version: ${{ matrix.node-version }} + - run: npm ci --no-audit + - run: npm test + - run: docker build . --file Dockerfile --tag + rodekruis/ibf-dashboard:$(date +%s) + + bump-version: + needs: [ibf-api-service, ibf-dashboard] + if: | + always() && + github.event_name == 'push' + + runs-on: ubuntu-latest + + steps: + - name: Wait for previous workflow to complete + uses: softprops/turnstyle@v1 + with: + abort-after-seconds: 1800 env: - SECRET: ${{ secrets.SECRET }} - MC_API: ${{ secrets.MC_API }} - - strategy: - matrix: - node-version: [12.x] - - defaults: - run: - working-directory: "services/API-service" - - steps: - - uses: actions/checkout@v3 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3.4.1 - with: - node-version: ${{ matrix.node-version }} - - run: npm ci --no-audit - - run: npm run lint - - run: npm test - - run: docker build . --file Dockerfile --tag - rodekruis/ibf-api-service:$(date +%s) - - ibf-dashboard: - needs: detect-changes - if: ${{ needs.detect-changes.outputs.ibf-dashboard == 'true' }} - - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [14.x] - - defaults: - run: - working-directory: "interfaces/IBF-dashboard" - - steps: - - uses: actions/checkout@v3 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3.4.1 - with: - node-version: ${{ matrix.node-version }} - - run: npm ci --no-audit - - run: npm test - - run: docker build . --file Dockerfile --tag - rodekruis/ibf-dashboard:$(date +%s) - - bump-version: - needs: [ibf-api-service, ibf-dashboard] - if: | - always() && - github.event_name == 'push' - - runs-on: ubuntu-latest - - steps: - - name: Wait for previous workflow to complete - uses: softprops/turnstyle@v1 - with: - abort-after-seconds: 1800 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - uses: actions/checkout@v3 - - - name: Bump version and push tag - uses: TriPSs/conventional-changelog-action@v3 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - git-message: "chore(release): {version}" - release-count: 10 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - uses: actions/checkout@v3 + + - name: Bump version and push tag + uses: TriPSs/conventional-changelog-action@v3 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + git-message: 'chore(release): {version}' + release-count: 10 From a296bb4f1578704bff4ab29440ab8a9ba2351754 Mon Sep 17 00:00:00 2001 From: Gulfaraz Rahman Date: Fri, 12 Jul 2024 15:25:56 +0200 Subject: [PATCH 52/55] chore: set workflow node version to match docker image --- .github/workflows/workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 3ae1843cd..815aae13d 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -40,7 +40,7 @@ jobs: strategy: matrix: - node-version: [12.x] + node-version: [17.x] defaults: run: From 092874c6f316b78e317dcb393a67635614eaa409 Mon Sep 17 00:00:00 2001 From: Gulfaraz Rahman Date: Fri, 12 Jul 2024 15:40:12 +0200 Subject: [PATCH 53/55] chore: fix AB#23107 --- docker-compose.override.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 8e8fa7ac2..96e26a218 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -1,8 +1,6 @@ -version: '3.8' - services: ibf-api-service: - command: [ 'npm', 'run', 'start:dev' ] + command: ['npm', 'run', 'start:dev'] environment: - NODE_ENV=development - LOCAL_PORT_IBF_SERVICE=${LOCAL_PORT_IBF_SERVICE} @@ -16,7 +14,7 @@ services: - api-network ibf-dashboard: - entrypoint: [ 'echo', 'Service ibf-dashboard disabled' ] + entrypoint: ['echo', 'Service ibf-dashboard disabled'] ibf-geoserver: ports: From 1b0b5c2d94ea4aad819ff8cc4cc0f6dcced64e8c Mon Sep 17 00:00:00 2001 From: Gulfaraz Rahman Date: Fri, 12 Jul 2024 17:11:25 +0200 Subject: [PATCH 54/55] chore: bump node version --- services/API-service/.node-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/API-service/.node-version b/services/API-service/.node-version index cc5875fab..b26a23938 100644 --- a/services/API-service/.node-version +++ b/services/API-service/.node-version @@ -1 +1 @@ -v10.15.3 +v17.9.1 From 230d09ea839c4533242fa4aed733ed455921abd4 Mon Sep 17 00:00:00 2001 From: Gulfaraz Rahman Date: Fri, 12 Jul 2024 17:35:37 +0200 Subject: [PATCH 55/55] style: prettier --- services/API-service/.prettierignore | 17 +++++++++++++++++ .../email/drought/email-uga-drought.test.ts | 5 +++-- 2 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 services/API-service/.prettierignore diff --git a/services/API-service/.prettierignore b/services/API-service/.prettierignore new file mode 100644 index 000000000..10ff87a63 --- /dev/null +++ b/services/API-service/.prettierignore @@ -0,0 +1,17 @@ +# Generated files +dist +coverage +www + + +# External code +node_modules + + +# Raster-files +geoserver-volume/raster-files/* +!geoserver-volume/raster-files/README.md + + +# certificates +cert/ diff --git a/services/API-service/test/email/drought/email-uga-drought.test.ts b/services/API-service/test/email/drought/email-uga-drought.test.ts index be9795664..ad781c13b 100644 --- a/services/API-service/test/email/drought/email-uga-drought.test.ts +++ b/services/API-service/test/email/drought/email-uga-drought.test.ts @@ -1,11 +1,12 @@ +import { JSDOM } from 'jsdom'; + +import { DisasterType } from '../../../src/api/disaster/disaster-type.enum'; import { getAccessToken, mockDynamicData, resetDB, sendNotification, } from '../../helpers/utility.helper'; -import { DisasterType } from '../../../src/api/disaster/disaster-type.enum'; -import { JSDOM } from 'jsdom'; const countryCodeISO3 = 'UGA'; const disasterType = DisasterType.Drought;