From efcac18969541315c5ddce844b7659356ee83c6b Mon Sep 17 00:00:00 2001 From: mgtennant <100305096+mgtennant@users.noreply.github.com> Date: Thu, 26 Sep 2024 13:58:29 -0700 Subject: [PATCH 1/8] wip, need more info --- .../file_parse_and_validation.module.ts | 20 +++++--- .../file_parse_and_validation.service.ts | 16 +++++- .../notifications/notifications.controller.ts | 10 ++-- .../notifications/notifications.service.ts | 50 +++++++++++++------ 4 files changed, 66 insertions(+), 30 deletions(-) diff --git a/backend/src/file_parse_and_validation/file_parse_and_validation.module.ts b/backend/src/file_parse_and_validation/file_parse_and_validation.module.ts index 65ec219f..eb474f77 100644 --- a/backend/src/file_parse_and_validation/file_parse_and_validation.module.ts +++ b/backend/src/file_parse_and_validation/file_parse_and_validation.module.ts @@ -1,12 +1,18 @@ -import { Module } from '@nestjs/common'; -import { HttpModule } from '@nestjs/axios'; -import { FileParseValidateService } from './file_parse_and_validation.service'; -import { AqiApiService } from 'src/aqi_api/aqi_api.service'; -import { FileSubmissionsService } from 'src/file_submissions/file_submissions.service'; +import { Module } from "@nestjs/common"; +import { HttpModule } from "@nestjs/axios"; +import { FileParseValidateService } from "./file_parse_and_validation.service"; +import { AqiApiService } from "src/aqi_api/aqi_api.service"; +import { FileSubmissionsService } from "src/file_submissions/file_submissions.service"; +import { NotificationsService } from "src/notifications/notifications.service"; @Module({ - providers: [FileParseValidateService, FileSubmissionsService, AqiApiService], + providers: [ + FileParseValidateService, + FileSubmissionsService, + AqiApiService, + NotificationsService, + ], exports: [FileParseValidateService], - imports: [HttpModule] + imports: [HttpModule], }) export class FileParseValidateModule {} diff --git a/backend/src/file_parse_and_validation/file_parse_and_validation.service.ts b/backend/src/file_parse_and_validation/file_parse_and_validation.service.ts index 4f8d3f86..9f13ceb1 100644 --- a/backend/src/file_parse_and_validation/file_parse_and_validation.service.ts +++ b/backend/src/file_parse_and_validation/file_parse_and_validation.service.ts @@ -14,6 +14,7 @@ import * as path from "path"; import * as csvWriter from "csv-writer"; import fs from "fs"; import { PrismaService } from "nestjs-prisma"; +import { NotificationsService } from "src/notifications/notifications.service"; const visits: FieldVisits = { MinistryContact: "", @@ -146,6 +147,7 @@ export class FileParseValidateService { private prisma: PrismaService, private readonly fileSubmissionsService: FileSubmissionsService, private readonly aqiService: AqiApiService, + private readonly notificationsService: NotificationsService, ) {} async getQueuedFiles() { @@ -599,7 +601,7 @@ export class FileParseValidateService { return dupeCount; } - async localValidation(allRecords, observaionFilePath) { + async localValidation(allRecords, observationFilePath) { let errorLogs = []; for (const [index, record] of allRecords.entries()) { const isoDateTimeRegex = @@ -861,7 +863,7 @@ export class FileParseValidateService { // Do a dry run of the observations const observationsErrors = await this.aqiService.importObservations( - observaionFilePath, + observationFilePath, "dryrun", ); @@ -978,6 +980,16 @@ export class FileParseValidateService { data: file_error_log_data, }); + const email = "mtennant@salussystems.com"; + await this.notificationsService.sendDataSubmitterNotification(email, { + file_name: fileName, + user_account_name: "???", + location_ids: [], + file_status: "REJECTED", + errors: localValidationResults, + warnings: null, + }); + return; } else if (!(await localValidationResults).includes("ERROR")) { await this.fileSubmissionsService.updateFileStatus( diff --git a/backend/src/notifications/notifications.controller.ts b/backend/src/notifications/notifications.controller.ts index 8a7b473f..2504cd1d 100644 --- a/backend/src/notifications/notifications.controller.ts +++ b/backend/src/notifications/notifications.controller.ts @@ -32,20 +32,20 @@ export class NotificationsController { file_name: "test_file.csv", user_account_name: "MTENNANT", file_status: "Failed", - errors: "Something went wrong.", - warnings: "", + errors: ["Something went wrong."], + warnings: [], }; return this.notificationsService.sendContactNotification(email, variables); } @Post("update-notification") updateNotification( - @Body() userData: { email: string; username: string; enabled: boolean } + @Body() userData: { email: string; username: string; enabled: boolean }, ) { return this.notificationsService.updateNotificationEntry( userData.email, userData.username, - userData.enabled + userData.enabled, ); } @@ -53,7 +53,7 @@ export class NotificationsController { getNotificationStatus(@Body() userData: { email: string; username: string }) { return this.notificationsService.getNotificationStatus( userData.email, - userData.username + userData.username, ); } diff --git a/backend/src/notifications/notifications.service.ts b/backend/src/notifications/notifications.service.ts index f79b6f48..533b2d15 100644 --- a/backend/src/notifications/notifications.service.ts +++ b/backend/src/notifications/notifications.service.ts @@ -160,8 +160,8 @@ export class NotificationsService { user_account_name: string; location_ids: string[]; file_status: string; - errors: string; - warnings: string; + errors: string[]; + warnings: string[]; }, ): Promise { let body = ` @@ -170,10 +170,15 @@ export class NotificationsService {

Date and Time of Upload: {{sys_time}}

Locations ID(s): ${variables.location_ids.join(", ")}

`; - if (variables.warnings !== "") { + + const warningString = + variables.warnings.length > 0 ? variables.warnings.join("\n") : ""; + const errorString = + variables.errors.length > 0 ? variables.errors.join("\n") : ""; + if (warningString !== "") { body += `

Warnings: {{warnings}}

`; } - if (variables.errors !== "") { + if (errorString !== "") { body += `

Errors: {{errors}}

`; } @@ -194,13 +199,17 @@ export class NotificationsService { }; const sys_time = date.toLocaleString("en-US", options); let status_string = "Imported"; - if (variables.errors !== "") { + if (errorString !== "") { status_string = "Failed"; - } else if (variables.warnings !== "") { + } else if (warningString !== "") { status_string = "Imported with Warnings"; } return this.sendEmail([email], emailTemplate, { - ...variables, + file_name: variables.file_name, + user_account_name: variables.user_account_name, + file_status: variables.file_status, + errors: errorString, + warnings: warningString, sys_time, status_string, }); @@ -220,8 +229,8 @@ export class NotificationsService { file_name: string; user_account_name: string; file_status: string; - warnings: string; - errors: string; + warnings: string[]; + errors: string[]; }, ): Promise { const notificationInfo = await this.getNotificationStatus( @@ -240,10 +249,15 @@ export class NotificationsService {

Date and Time of Upload: {{sys_time}}

Locations ID(s): E123445, E464353, E232524

`; - if (variables.warnings !== "") { + + const warningString = + variables.warnings.length > 0 ? variables.warnings.join("\n") : ""; + const errorString = + variables.errors.length > 0 ? variables.errors.join("\n") : ""; + if (warningString !== "") { body += `

Warnings: {{warnings}}

`; } - if (variables.errors !== "") { + if (errorString !== "") { body += `

Errors: {{errors}}

`; } @@ -266,13 +280,17 @@ export class NotificationsService { }; const sys_time = date.toLocaleString("en-US", options); let status_string = "Imported"; - if (variables.errors !== "") { + if (errorString !== "") { status_string = "Failed"; - } else if (variables.warnings !== "") { + } else if (warningString !== "") { status_string = "Imported with Warnings"; } return this.sendEmail([email], emailTemplate, { - ...variables, + file_name: variables.file_name, + user_account_name: variables.user_account_name, + file_status: variables.file_status, + errors: errorString, + warnings: warningString, sys_time, status_string, }); @@ -380,8 +398,8 @@ export class NotificationsService { user_account_name: username, location_ids: [], file_status: "FAILED", - errors: errors.join(","), - warnings: "", + errors: errors, + warnings: [], }; // Notify the Data Submitter From a2c74a9c37f699eea4c017640eddf17c038ffe1b Mon Sep 17 00:00:00 2001 From: mgtennant <100305096+mgtennant@users.noreply.github.com> Date: Fri, 27 Sep 2024 13:04:31 -0700 Subject: [PATCH 2/8] add grantBucketAccess function, fix typos --- .../file_submissions.service.ts | 43 ++++++++++++++++++- .../src/objectStore/objectStore.service.ts | 36 ++++++++-------- 2 files changed, 60 insertions(+), 19 deletions(-) diff --git a/backend/src/file_submissions/file_submissions.service.ts b/backend/src/file_submissions/file_submissions.service.ts index 99e8bef8..000aa131 100644 --- a/backend/src/file_submissions/file_submissions.service.ts +++ b/backend/src/file_submissions/file_submissions.service.ts @@ -51,7 +51,11 @@ export class FileSubmissionsService { submission_date: createFileSubmissionDto.submission_date, submitter_user_id: createFileSubmissionDto.submitter_user_id, submission_status: { connect: { submission_status_code: "QUEUED" } }, - file_operation_codes: { connect: { file_operation_code: createFileSubmissionDto.file_operation_code } }, + file_operation_codes: { + connect: { + file_operation_code: createFileSubmissionDto.file_operation_code, + }, + }, submitter_agency_name: createFileSubmissionDto.submitter_agency_name, sample_count: createFileSubmissionDto.sample_count, results_count: createFileSubmissionDto.result_count, @@ -229,6 +233,41 @@ export class FileSubmissionsService { } } +/** + * Grants the current IDIR user the ability to upload files to the S3 bucket + * @param token + */ +async function grantBucketAccess(token: string) { + const axios = require("axios"); + + let config = { + method: "put", + url: `${process.env.COMS_URI}/v1/bucket`, + headers: { + Authorization: "Bearer " + token, + "Content-Type": "application/json", + }, + data: { + accessKeyId: process.env.OBJECTSTORE_ACCESS_KEY, + bucket: process.env.OBJECTSTORE_BUCKET, + bucketName: process.env.OBJECTSTORE_BUCKET_NAME, + endpoint: process.env.OBJECTSTORE_URL, + secretAccessKey: process.env.OBJECTSTORE_SECRET_KEY, + active: true, + key: "/", + permCodes: ["CREATE"], + }, + }; + + await axios + .request(config) + .then((res) => console.log(res)) + .catch((err) => { + console.log("create bucket failed"); + console.log(err); + }); +} + async function saveToS3(token: any, file: Express.Multer.File) { const path = require("path"); let fileGUID = null; @@ -240,6 +279,8 @@ async function saveToS3(token: any, file: Express.Multer.File) { const axios = require("axios"); + await grantBucketAccess(token); + let config = { method: "put", maxBodyLength: Infinity, diff --git a/backend/src/objectStore/objectStore.service.ts b/backend/src/objectStore/objectStore.service.ts index 94a636a0..f516f649 100644 --- a/backend/src/objectStore/objectStore.service.ts +++ b/backend/src/objectStore/objectStore.service.ts @@ -7,45 +7,45 @@ import * as fs from "fs"; export class ObjectStoreService { private readonly logger = new Logger(ObjectStoreService.name); - private readonly objectsotreEndpoint = process.env.OBJECTSTORE_URL; + private readonly objectStoreEndpoint = process.env.OBJECTSTORE_URL; private readonly accessKey = process.env.OBJECTSTORE_ACCESS_KEY; private readonly secretKey = process.env.OBJECTSTORE_SECRET_KEY; private readonly bucketName = process.env.OBJECTSTORE_BUCKET; private readonly backupDirectory = process.env.OBJECTSTORE_BUCKET_NAME; async getFileData(fileName: string) { - try{ - if (!this.objectsotreEndpoint) { + try { + if (!this.objectStoreEndpoint) { throw new Error("Object store endpoint not defined."); } - + const dateValue = new Date().toUTCString(); - + const stringToSign = `GET\n\n\n${dateValue}\n/${this.bucketName}/${fileName}`; - + const signature = crypto .createHmac("sha1", this.secretKey) .update(stringToSign) .digest("base64"); - - const requestUrl = `${this.objectsotreEndpoint}/${this.bucketName}/${fileName}` - + + const requestUrl = `${this.objectStoreEndpoint}/${this.bucketName}/${fileName}`; + const headers = { - 'Authorization': `AWS ${this.accessKey}:${signature}`, - 'Date': dateValue, + Authorization: `AWS ${this.accessKey}:${signature}`, + Date: dateValue, }; - + const response = await axios({ - method: 'get', + method: "get", url: requestUrl, headers: headers, - responseType: 'arraybuffer', // This is important for binary data - }) - + responseType: "arraybuffer", // This is important for binary data + }); + return response.data; } catch (error) { - console.error('Error fetching the file:', error.message); - throw error; + console.error("Error fetching the file:", error.message); + // throw error; } } } From 060d597bbf778b73552033c00433d829299484ad Mon Sep 17 00:00:00 2001 From: mgtennant <100305096+mgtennant@users.noreply.github.com> Date: Fri, 27 Sep 2024 15:39:42 -0700 Subject: [PATCH 3/8] Update notifications.service.ts --- backend/src/notifications/notifications.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/notifications/notifications.service.ts b/backend/src/notifications/notifications.service.ts index 533b2d15..60cd5ece 100644 --- a/backend/src/notifications/notifications.service.ts +++ b/backend/src/notifications/notifications.service.ts @@ -172,9 +172,9 @@ export class NotificationsService { `; const warningString = - variables.warnings.length > 0 ? variables.warnings.join("\n") : ""; + variables.warnings?.length > 0 ? variables.warnings.join("\n") : ""; const errorString = - variables.errors.length > 0 ? variables.errors.join("\n") : ""; + variables.errors?.length > 0 ? variables.errors.join("\n") : ""; if (warningString !== "") { body += `

Warnings: {{warnings}}

`; } From f80d548c4b12e3c8a425d3804d2c112c23d248f9 Mon Sep 17 00:00:00 2001 From: mgtennant <100305096+mgtennant@users.noreply.github.com> Date: Tue, 8 Oct 2024 11:25:04 -0700 Subject: [PATCH 4/8] wip --- .../file_error_logs/file_error_logs.module.ts | 7 ++- .../file_parse_and_validation.service.ts | 12 +--- .../file_submissions.service.ts | 11 ++++ .../src/notifications/notifications.module.ts | 4 +- .../notifications/notifications.service.ts | 59 +++++++++++++++++++ 5 files changed, 80 insertions(+), 13 deletions(-) diff --git a/backend/src/file_error_logs/file_error_logs.module.ts b/backend/src/file_error_logs/file_error_logs.module.ts index 510f3f47..17508a2d 100644 --- a/backend/src/file_error_logs/file_error_logs.module.ts +++ b/backend/src/file_error_logs/file_error_logs.module.ts @@ -1,8 +1,9 @@ -import { Module } from '@nestjs/common'; -import { FileErrorLogsService } from './file_error_logs.service'; -import { FileErrorLogsController } from './file_error_logs.controller'; +import { Module } from "@nestjs/common"; +import { FileErrorLogsService } from "./file_error_logs.service"; +import { FileErrorLogsController } from "./file_error_logs.controller"; @Module({ + exports: [FileErrorLogsService], controllers: [FileErrorLogsController], providers: [FileErrorLogsService], }) diff --git a/backend/src/file_parse_and_validation/file_parse_and_validation.service.ts b/backend/src/file_parse_and_validation/file_parse_and_validation.service.ts index 3aeeed9a..8591e24a 100644 --- a/backend/src/file_parse_and_validation/file_parse_and_validation.service.ts +++ b/backend/src/file_parse_and_validation/file_parse_and_validation.service.ts @@ -988,15 +988,9 @@ export class FileParseValidateService { data: file_error_log_data, }); - const email = "mtennant@salussystems.com"; - await this.notificationsService.sendDataSubmitterNotification(email, { - file_name: fileName, - user_account_name: "???", - location_ids: [], - file_status: "REJECTED", - errors: localValidationResults, - warnings: null, - }); + await this.notificationsService.sendDataSubmitterNotification( + file_submission_id, + ); return; } else if (!(await localValidationResults).includes("ERROR")) { diff --git a/backend/src/file_submissions/file_submissions.service.ts b/backend/src/file_submissions/file_submissions.service.ts index a9d685a2..f1152fc1 100644 --- a/backend/src/file_submissions/file_submissions.service.ts +++ b/backend/src/file_submissions/file_submissions.service.ts @@ -184,6 +184,17 @@ export class FileSubmissionsService { return records; } + async findBySubmissionId(submission_id: string) { + return await this.prisma.file_submission.findUnique({ + where: { + submission_id: submission_id, + }, + include: { + file_error_logs: true, + }, + }); + } + async updateFileStatus(submission_id: string, status: string) { await this.prisma.file_submission.update({ where: { diff --git a/backend/src/notifications/notifications.module.ts b/backend/src/notifications/notifications.module.ts index c80e3860..7af9b840 100644 --- a/backend/src/notifications/notifications.module.ts +++ b/backend/src/notifications/notifications.module.ts @@ -2,9 +2,11 @@ import { Module } from "@nestjs/common"; import { NotificationsController } from "./notifications.controller"; import { NotificationsService } from "./notifications.service"; import { HttpModule } from "@nestjs/axios"; +import { FileErrorLogsService } from "src/file_error_logs/file_error_logs.service"; +import { FileSubmissionsService } from "src/file_submissions/file_submissions.service"; @Module({ - imports: [HttpModule], + imports: [HttpModule, FileErrorLogsService, FileSubmissionsService], controllers: [NotificationsController], providers: [NotificationsService], exports: [NotificationsService], diff --git a/backend/src/notifications/notifications.service.ts b/backend/src/notifications/notifications.service.ts index 60cd5ece..4bd56c5a 100644 --- a/backend/src/notifications/notifications.service.ts +++ b/backend/src/notifications/notifications.service.ts @@ -6,6 +6,8 @@ import { Prisma } from "@prisma/client"; import { PrismaService } from "nestjs-prisma"; import { UpdateNotificationEntryDto } from "./dto/update-notification_entry.dto"; import { EmailTemplate } from "src/types/types"; +import { FileErrorLogsService } from "src/file_error_logs/file_error_logs.service"; +import { FileSubmissionsService } from "src/file_submissions/file_submissions.service"; @Injectable() export class NotificationsService { @@ -13,6 +15,8 @@ export class NotificationsService { constructor( private readonly httpService: HttpService, + private readonly fileErrorLogsService: FileErrorLogsService, + private readonly fileSubmissionsService: FileSubmissionsService, private prisma: PrismaService, ) {} @@ -154,6 +158,61 @@ export class NotificationsService { * @returns */ async sendDataSubmitterNotification( + file_submission_id: string, + ): Promise { + // let body = ` + //

Status: {{file_status}}

+ //

Files Original Name: {{file_name}}

+ //

Date and Time of Upload: {{sys_time}}

+ //

Locations ID(s): ${variables.location_ids.join(", ")}

+ // `; + + // const warningString = + // variables.warnings?.length > 0 ? variables.warnings.join("\n") : ""; + // const errorString = + // variables.errors?.length > 0 ? variables.errors.join("\n") : ""; + // if (warningString !== "") { + // body += `

Warnings: {{warnings}}

`; + // } + // if (errorString !== "") { + // body += `

Errors: {{errors}}

`; + // } + const file_submission = + await this.fileSubmissionsService.findBySubmissionId(file_submission_id); + const body = await this.fileErrorLogsService.findOne(file_submission_id); + const {original_file_name, submitter_user_id,submission_status_code} + await this.findEmailByUserId + + const emailTemplate = { + from: "enmodshelp@gov.bc.ca", + subject: "EnMoDS Data {{submission_status_code}} from {{submitter_user_id}}", + body: body, + }; + const date = new Date(); + const options: Intl.DateTimeFormatOptions = { + timeZone: "America/Los_Angeles", + year: "numeric", + month: "long", + day: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + }; + const sys_time = date.toLocaleString("en-US", options); + // let status_string = "Imported"; + // if (errorString !== "") { + // status_string = "Failed"; + // } else if (warningString !== "") { + // status_string = "Imported with Warnings"; + // } + return this.sendEmail([email], emailTemplate, { + submitter_user_id: submitter_user_id, + submission_status_code: submission_status_code, + sys_time, + }); + } + + async sendDataSubmitterNotification2( email: string, variables: { file_name: string; From 1b6e7799a640e15dbe0646873cc9d3e07a807583 Mon Sep 17 00:00:00 2001 From: m Date: Wed, 9 Oct 2024 17:13:35 -0700 Subject: [PATCH 5/8] data submitter email added --- backend/prisma/schema.prisma | 1 + .../file_parse_and_validation.module.ts | 3 +- .../dto/create-file_submission.dto.ts | 1 + .../dto/file_submission.dto.ts | 5 + .../file_submissions.service.ts | 2 + .../notifications/notifications.controller.ts | 26 +- .../src/notifications/notifications.module.ts | 6 +- .../notifications/notifications.service.ts | 222 ++++-------------- frontend/src/pages/FileUpload.tsx | 4 + .../sql/V1.1.1__notifications_updates.sql | 2 + 10 files changed, 79 insertions(+), 193 deletions(-) create mode 100644 migrations/sql/V1.1.1__notifications_updates.sql diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index bfec1cc1..24b21c7e 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -39,6 +39,7 @@ model file_submission { create_utc_timestamp DateTime @db.Timestamp(6) update_user_id String @db.VarChar(200) update_utc_timestamp DateTime @db.Timestamp(6) + data_submitter_email String? @db.VarChar(200) file_error_logs file_error_logs[] file_operation_codes file_operation_codes @relation(fields: [file_operation_code], references: [file_operation_code], onDelete: NoAction, onUpdate: NoAction, map: "file_operation_code_fk") submission_status submission_status_code @relation(fields: [submission_status_code], references: [submission_status_code], onDelete: NoAction, onUpdate: NoAction, map: "submission_status_code_fk") diff --git a/backend/src/file_parse_and_validation/file_parse_and_validation.module.ts b/backend/src/file_parse_and_validation/file_parse_and_validation.module.ts index ac344995..079c03d7 100644 --- a/backend/src/file_parse_and_validation/file_parse_and_validation.module.ts +++ b/backend/src/file_parse_and_validation/file_parse_and_validation.module.ts @@ -5,6 +5,7 @@ import { AqiApiService } from "src/aqi_api/aqi_api.service"; import { FileSubmissionsService } from "src/file_submissions/file_submissions.service"; import { NotificationsService } from "src/notifications/notifications.service"; import { ObjectStoreModule } from "src/objectStore/objectStore.module"; +import { FileErrorLogsModule } from "src/file_error_logs/file_error_logs.module"; @Module({ providers: [ @@ -14,6 +15,6 @@ import { ObjectStoreModule } from "src/objectStore/objectStore.module"; NotificationsService, ], exports: [FileParseValidateService], - imports: [HttpModule, ObjectStoreModule], + imports: [HttpModule, ObjectStoreModule, FileErrorLogsModule], }) export class FileParseValidateModule {} diff --git a/backend/src/file_submissions/dto/create-file_submission.dto.ts b/backend/src/file_submissions/dto/create-file_submission.dto.ts index 423a0814..bb5d04dc 100644 --- a/backend/src/file_submissions/dto/create-file_submission.dto.ts +++ b/backend/src/file_submissions/dto/create-file_submission.dto.ts @@ -15,6 +15,7 @@ export class CreateFileSubmissionDto extends PickType(FileSubmissionDto, [ 'active_ind', 'error_log', 'organization_guid', + 'data_submitter_email', 'create_user_id', 'create_utc_timestamp', 'update_user_id', diff --git a/backend/src/file_submissions/dto/file_submission.dto.ts b/backend/src/file_submissions/dto/file_submission.dto.ts index 6a466264..bb528082 100644 --- a/backend/src/file_submissions/dto/file_submission.dto.ts +++ b/backend/src/file_submissions/dto/file_submission.dto.ts @@ -73,6 +73,11 @@ export class FileSubmissionDto { }) organization_guid: string; + @ApiProperty({ + description: 'The data submitter\'s email' + }) + data_submitter_email: string; + @ApiProperty({ description: 'The id of the user that created the record', }) diff --git a/backend/src/file_submissions/file_submissions.service.ts b/backend/src/file_submissions/file_submissions.service.ts index f1152fc1..2991640e 100644 --- a/backend/src/file_submissions/file_submissions.service.ts +++ b/backend/src/file_submissions/file_submissions.service.ts @@ -45,6 +45,7 @@ export class FileSubmissionsService { createFileSubmissionDto.sample_count = 0; createFileSubmissionDto.result_count = 0; createFileSubmissionDto.organization_guid = body.orgGUID; // TODO: change this once BCeID is set up + createFileSubmissionDto.data_submitter_email = body.dataSubmitterEmail; createFileSubmissionDto.create_user_id = body.userID; createFileSubmissionDto.create_utc_timestamp = new Date(); createFileSubmissionDto.update_user_id = body.userID; @@ -68,6 +69,7 @@ export class FileSubmissionsService { active_ind: createFileSubmissionDto.active_ind, error_log: createFileSubmissionDto.error_log, organization_guid: createFileSubmissionDto.organization_guid, + data_submitter_email: createFileSubmissionDto.data_submitter_email, create_user_id: createFileSubmissionDto.create_user_id, create_utc_timestamp: createFileSubmissionDto.create_utc_timestamp, update_user_id: createFileSubmissionDto.update_user_id, diff --git a/backend/src/notifications/notifications.controller.ts b/backend/src/notifications/notifications.controller.ts index 2504cd1d..cd0e6e3a 100644 --- a/backend/src/notifications/notifications.controller.ts +++ b/backend/src/notifications/notifications.controller.ts @@ -22,20 +22,20 @@ export class NotificationsController { // test route TODO: delete this @Get("send-email/:email") sendEmail(@Param("email") email: string) { - const re = - /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; - if (!re.test(String(email).toLowerCase())) { - email = "mtennant@salussystems.com"; - } + // const re = + // /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + // if (!re.test(String(email).toLowerCase())) { + // email = "mtennant@salussystems.com"; + // } - const variables = { - file_name: "test_file.csv", - user_account_name: "MTENNANT", - file_status: "Failed", - errors: ["Something went wrong."], - warnings: [], - }; - return this.notificationsService.sendContactNotification(email, variables); + // const variables = { + // file_name: "test_file.csv", + // user_account_name: "MTENNANT", + // file_status: "Failed", + // errors: ["Something went wrong."], + // warnings: [], + // }; + // return this.notificationsService.sendContactNotification(email, variables); } @Post("update-notification") diff --git a/backend/src/notifications/notifications.module.ts b/backend/src/notifications/notifications.module.ts index 7af9b840..b2929c2c 100644 --- a/backend/src/notifications/notifications.module.ts +++ b/backend/src/notifications/notifications.module.ts @@ -2,11 +2,11 @@ import { Module } from "@nestjs/common"; import { NotificationsController } from "./notifications.controller"; import { NotificationsService } from "./notifications.service"; import { HttpModule } from "@nestjs/axios"; -import { FileErrorLogsService } from "src/file_error_logs/file_error_logs.service"; -import { FileSubmissionsService } from "src/file_submissions/file_submissions.service"; +import { FileErrorLogsModule } from "src/file_error_logs/file_error_logs.module"; +import { FileSubmissionsModule } from "src/file_submissions/file_submissions.module"; @Module({ - imports: [HttpModule, FileErrorLogsService, FileSubmissionsService], + imports: [HttpModule, FileErrorLogsModule, FileSubmissionsModule], controllers: [NotificationsController], providers: [NotificationsService], exports: [NotificationsService], diff --git a/backend/src/notifications/notifications.service.ts b/backend/src/notifications/notifications.service.ts index 4bd56c5a..d8681c8b 100644 --- a/backend/src/notifications/notifications.service.ts +++ b/backend/src/notifications/notifications.service.ts @@ -150,40 +150,23 @@ export class NotificationsService { } /** - * Sends an email to the data submitter. Does not check if notifications are filtered. + * Uses a file submission id to gather information on the completed/rejected submission + * and sends an email to the data submitter. Does not check if notifications are filtered. * - * @param email - * @param emailTemplate - * @param variables + * @param file_submission_id * @returns */ async sendDataSubmitterNotification( file_submission_id: string, ): Promise { - // let body = ` - //

Status: {{file_status}}

- //

Files Original Name: {{file_name}}

- //

Date and Time of Upload: {{sys_time}}

- //

Locations ID(s): ${variables.location_ids.join(", ")}

- // `; - - // const warningString = - // variables.warnings?.length > 0 ? variables.warnings.join("\n") : ""; - // const errorString = - // variables.errors?.length > 0 ? variables.errors.join("\n") : ""; - // if (warningString !== "") { - // body += `

Warnings: {{warnings}}

`; - // } - // if (errorString !== "") { - // body += `

Errors: {{errors}}

`; - // } const file_submission = await this.fileSubmissionsService.findBySubmissionId(file_submission_id); const body = await this.fileErrorLogsService.findOne(file_submission_id); - const {original_file_name, submitter_user_id,submission_status_code} - await this.findEmailByUserId + const email = file_submission.data_submitter_email; + if (!this.isValidEmail(email)) { return 'Invalid email'} + const {submitter_user_id,submission_status_code} = file_submission; - const emailTemplate = { + const emailTemplate: EmailTemplate = { from: "enmodshelp@gov.bc.ca", subject: "EnMoDS Data {{submission_status_code}} from {{submitter_user_id}}", body: body, @@ -199,12 +182,7 @@ export class NotificationsService { second: "numeric", }; const sys_time = date.toLocaleString("en-US", options); - // let status_string = "Imported"; - // if (errorString !== "") { - // status_string = "Failed"; - // } else if (warningString !== "") { - // status_string = "Imported with Warnings"; - // } + return this.sendEmail([email], emailTemplate, { submitter_user_id: submitter_user_id, submission_status_code: submission_status_code, @@ -212,68 +190,6 @@ export class NotificationsService { }); } - async sendDataSubmitterNotification2( - email: string, - variables: { - file_name: string; - user_account_name: string; - location_ids: string[]; - file_status: string; - errors: string[]; - warnings: string[]; - }, - ): Promise { - let body = ` -

Status: {{file_status}}

-

Files Original Name: {{file_name}}

-

Date and Time of Upload: {{sys_time}}

-

Locations ID(s): ${variables.location_ids.join(", ")}

- `; - - const warningString = - variables.warnings?.length > 0 ? variables.warnings.join("\n") : ""; - const errorString = - variables.errors?.length > 0 ? variables.errors.join("\n") : ""; - if (warningString !== "") { - body += `

Warnings: {{warnings}}

`; - } - if (errorString !== "") { - body += `

Errors: {{errors}}

`; - } - - const emailTemplate = { - from: "enmodshelp@gov.bc.ca", - subject: "EnMoDS Data {{status_string}} from {{user_account_name}}", - body: body, - }; - const date = new Date(); - const options: Intl.DateTimeFormatOptions = { - timeZone: "America/Los_Angeles", - year: "numeric", - month: "long", - day: "numeric", - hour: "numeric", - minute: "numeric", - second: "numeric", - }; - const sys_time = date.toLocaleString("en-US", options); - let status_string = "Imported"; - if (errorString !== "") { - status_string = "Failed"; - } else if (warningString !== "") { - status_string = "Imported with Warnings"; - } - return this.sendEmail([email], emailTemplate, { - file_name: variables.file_name, - user_account_name: variables.user_account_name, - file_status: variables.file_status, - errors: errorString, - warnings: warningString, - sys_time, - status_string, - }); - } - /** * Sends an email to the contact(s) specified inside of a submitted file. * @@ -283,48 +199,28 @@ export class NotificationsService { * @returns */ async sendContactNotification( - email: string, - variables: { - file_name: string; - user_account_name: string; - file_status: string; - warnings: string[]; - errors: string[]; - }, + file_submission_id: string ): Promise { + const file_submission = + await this.fileSubmissionsService.findBySubmissionId(file_submission_id); + const email = file_submission.data_submitter_email; + if (!this.isValidEmail(email)) { return 'Invalid email'} + const {submitter_user_id,submission_status_code} = file_submission; + + const errorLogs = await this.fileErrorLogsService.findOne(file_submission_id); + const notificationInfo = await this.getNotificationStatus( email, - variables.user_account_name, + submitter_user_id, ); - // check that notifications are enabled before continuing - if (notificationInfo.enabled === false) { - return; - } const unsubscribeLink = - process.env.WEBAPP_URL + `/unsubscribe/${notificationInfo.id}`; - let body = ` -

Status: {{file_status}}

-

Files Original Name: {{file_name}}

-

Date and Time of Upload: {{sys_time}}

-

Locations ID(s): E123445, E464353, E232524

- `; - - const warningString = - variables.warnings.length > 0 ? variables.warnings.join("\n") : ""; - const errorString = - variables.errors.length > 0 ? variables.errors.join("\n") : ""; - if (warningString !== "") { - body += `

Warnings: {{warnings}}

`; - } - if (errorString !== "") { - body += `

Errors: {{errors}}

`; - } + process.env.WEBAPP_URL + `/unsubscribe/${notificationInfo.id}`; - body += `

Unsubscribe

`; + let body = errorLogs.concat(`

Unsubscribe

`) const emailTemplate = { from: "enmodshelp@gov.bc.ca", - subject: "EnMoDS Data {{status_string}} from {{user_account_name}}", + subject: "EnMoDS Data {{submission_status_code}} from {{submitter_user_id}}", body: body, }; const date = new Date(); @@ -338,20 +234,11 @@ export class NotificationsService { second: "numeric", }; const sys_time = date.toLocaleString("en-US", options); - let status_string = "Imported"; - if (errorString !== "") { - status_string = "Failed"; - } else if (warningString !== "") { - status_string = "Imported with Warnings"; - } + return this.sendEmail([email], emailTemplate, { - file_name: variables.file_name, - user_account_name: variables.user_account_name, - file_status: variables.file_status, - errors: errorString, - warnings: warningString, + submitter_user_id: submitter_user_id, + submission_status_code: submission_status_code, sys_time, - status_string, }); } @@ -369,16 +256,13 @@ export class NotificationsService { emails: string[], emailTemplate: EmailTemplate, variables: { - file_name: string; - user_account_name: string; - file_status: string; - errors: string; - warnings: string; + submitter_user_id: string; + submission_status_code: string; sys_time: string; - status_string: string; }, ): Promise { const chesToken = await this.getChesToken(); + console.log('sending email') const data = JSON.stringify({ attachments: [], @@ -402,7 +286,7 @@ export class NotificationsService { const config = { method: "post", - url: `${process.env.ches_email_url}`, + url: `${process.env.CHES_EMAIL_URL}`, headers: { "Content-Type": "application/json", Authorization: `Bearer ${chesToken}`, @@ -446,29 +330,13 @@ export class NotificationsService { * @param ministryContact */ async notifyUserOfError( - email: string, - username: string, - fileName: string, - errors: string[], - ministryContact: string, + file_submission_id: string ) { - const notificationVars = { - file_name: fileName, - user_account_name: username, - location_ids: [], - file_status: "FAILED", - errors: errors, - warnings: [], - }; - // Notify the Data Submitter - if (this.isValidEmail(email)) { - await this.sendDataSubmitterNotification(email, notificationVars); - } + await this.sendDataSubmitterNotification(file_submission_id); + // Notify the Ministry Contact (if they have not disabled notifications) - if (this.isValidEmail(ministryContact)) { - await this.sendContactNotification(ministryContact, notificationVars); - } + await this.sendContactNotification(file_submission_id); } /** @@ -484,16 +352,18 @@ export class NotificationsService { errors: string[], ministryContact: string, ) { - const ftpUser = await this.prisma.ftp_users.findUnique({ - where: { username: username }, - }); - await this.notifyUserOfError( - ftpUser.email, - username, - fileName, - errors, - ministryContact, - ); + // const ftpUser = await this.prisma.ftp_users.findUnique({ + // where: { username: username }, + // }); + // await this.notifyUserOfError( + // ftpUser.email, + // username, + // fileName, + // errors, + // ministryContact, + // ); + + } isValidEmail(email: string): boolean { @@ -536,9 +406,9 @@ export class NotificationsService { * @returns */ async getChesToken(): Promise { - const url = process.env.ches_token_url; + const url = process.env.CHES_TOKEN_URL; const encodedToken = Buffer.from( - `${process.env.ches_client_id}:${process.env.ches_client_secret}`, + `${process.env.CHES_CLIENT_ID}:${process.env.CHES_CLIENT_SECRET}`, ).toString("base64"); const headers = { diff --git a/frontend/src/pages/FileUpload.tsx b/frontend/src/pages/FileUpload.tsx index b748cce7..f48bdf85 100644 --- a/frontend/src/pages/FileUpload.tsx +++ b/frontend/src/pages/FileUpload.tsx @@ -75,6 +75,7 @@ function FileUpload() { formData.append("operation", "VALIDATE"); formData.append("userID", JWT.idir_username); // TODO: This will need to be updated based on BCeID formData.append("orgGUID", JWT.idir_user_guid); // TODO: This will need to be updated based on BCeID and company GUID + formData.append('dataSubmitterEmail', JWT.email); formData.append("token", UserService.getToken()?.toString()); await insertFile(formData).then((response) => { @@ -96,6 +97,7 @@ function FileUpload() { formData.append("operation", "VALIDATE"); formData.append("userID", JWT.idir_username); // TODO: This will need to be updated based on BCeID formData.append("orgGUID", JWT.idir_user_guid); // TODO: This will need to be updated based on BCeID and company GUID + formData.append('dataSubmitterEmail', JWT.email); formData.append("token", UserService.getToken()?.toString()); await insertFile(formData).then(async (response) => { @@ -117,6 +119,7 @@ function FileUpload() { formData.append("operation", "IMPORT"); formData.append("userID", JWT.idir_username); // TODO: This will need to be updated based on BCeID formData.append("orgGUID", JWT.idir_user_guid); // TODO: This will need to be updated based on BCeID and company GUID + formData.append('dataSubmitterEmail', JWT.email); formData.append("token", UserService.getToken()?.toString()); await insertFile(formData).then((response) => { @@ -138,6 +141,7 @@ function FileUpload() { formData.append("operation", "IMPORT"); formData.append("userID", JWT.idir_username); // TODO: This will need to be updated based on BCeID formData.append("orgGUID", JWT.idir_user_guid); // TODO: This will need to be updated based on BCeID and company GUID + formData.append('dataSubmitterEmail', JWT.email); formData.append("token", UserService.getToken()?.toString()); await insertFile(formData).then(async (response) => { diff --git a/migrations/sql/V1.1.1__notifications_updates.sql b/migrations/sql/V1.1.1__notifications_updates.sql new file mode 100644 index 00000000..742f9c27 --- /dev/null +++ b/migrations/sql/V1.1.1__notifications_updates.sql @@ -0,0 +1,2 @@ +ALTER TABLE enmods.file_submission +ADD COLUMN data_submitter_email varchar(200); From b740914992d4f89c05a31dabf8392dd8f3367536 Mon Sep 17 00:00:00 2001 From: mgtennant Date: Wed, 9 Oct 2024 17:21:09 -0700 Subject: [PATCH 6/8] Delete v1.0.8__aqi_data_classifications.sql --- .../sql/v1.0.8__aqi_data_classifications.sql | 66 ------------------- 1 file changed, 66 deletions(-) delete mode 100644 migrations/sql/v1.0.8__aqi_data_classifications.sql diff --git a/migrations/sql/v1.0.8__aqi_data_classifications.sql b/migrations/sql/v1.0.8__aqi_data_classifications.sql deleted file mode 100644 index 2888512b..00000000 --- a/migrations/sql/v1.0.8__aqi_data_classifications.sql +++ /dev/null @@ -1,66 +0,0 @@ -CREATE TABLE IF NOT EXISTS enmods.aqi_data_classifications ( - aqi_data_classifications_id UUID PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(), - custom_id varchar(200) NOT NULL, - description varchar(2000) NULL, - create_user_id varchar(200) NOT NULL, - create_utc_timestamp timestamp NOT NULL, - update_user_id varchar(200) NOT NULL, - update_utc_timestamp timestamp NOT NULL -); -INSERT INTO enmods.aqi_data_classifications( - custom_id, - description, - create_user_id, - create_utc_timestamp, - update_user_id, - update_utc_timestamp -) -VALUES -( - 'LAB', - 'Lab', - 'VMANAWAT', - (now() at time zone 'utc'), - 'VMANAWAT', - (now() at time zone 'utc') -), -( - 'FIELD_RESULT', - 'Field Result', - 'VMANAWAT', - (now() at time zone 'utc'), - 'VMANAWAT', - (now() at time zone 'utc') -), -( - 'FIELD_SURVEY', - 'Field Survey', - 'VMANAWAT', - (now() at time zone 'utc'), - 'VMANAWAT', - (now() at time zone 'utc') -), -( - 'VERTICAL_PROFILE', - 'Vertical Profile', - 'VMANAWAT', - (now() at time zone 'utc'), - 'VMANAWAT', - (now() at time zone 'utc') -), -( - 'ACTIVITY_RESULT', - 'Activity Result', - 'VMANAWAT', - (now() at time zone 'utc'), - 'VMANAWAT', - (now() at time zone 'utc') -), -( - 'SURROGATE_RESULT', - 'Surrogate Result', - 'VMANAWAT', - (now() at time zone 'utc'), - 'VMANAWAT', - (now() at time zone 'utc') -); \ No newline at end of file From 131a8314c3de4a17dc91b952678eb69196fac363 Mon Sep 17 00:00:00 2001 From: mgtennant Date: Wed, 9 Oct 2024 17:21:15 -0700 Subject: [PATCH 7/8] Create V1.0.8__aqi_data_classifications.sql --- .../sql/V1.0.8__aqi_data_classifications.sql | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 migrations/sql/V1.0.8__aqi_data_classifications.sql diff --git a/migrations/sql/V1.0.8__aqi_data_classifications.sql b/migrations/sql/V1.0.8__aqi_data_classifications.sql new file mode 100644 index 00000000..2888512b --- /dev/null +++ b/migrations/sql/V1.0.8__aqi_data_classifications.sql @@ -0,0 +1,66 @@ +CREATE TABLE IF NOT EXISTS enmods.aqi_data_classifications ( + aqi_data_classifications_id UUID PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(), + custom_id varchar(200) NOT NULL, + description varchar(2000) NULL, + create_user_id varchar(200) NOT NULL, + create_utc_timestamp timestamp NOT NULL, + update_user_id varchar(200) NOT NULL, + update_utc_timestamp timestamp NOT NULL +); +INSERT INTO enmods.aqi_data_classifications( + custom_id, + description, + create_user_id, + create_utc_timestamp, + update_user_id, + update_utc_timestamp +) +VALUES +( + 'LAB', + 'Lab', + 'VMANAWAT', + (now() at time zone 'utc'), + 'VMANAWAT', + (now() at time zone 'utc') +), +( + 'FIELD_RESULT', + 'Field Result', + 'VMANAWAT', + (now() at time zone 'utc'), + 'VMANAWAT', + (now() at time zone 'utc') +), +( + 'FIELD_SURVEY', + 'Field Survey', + 'VMANAWAT', + (now() at time zone 'utc'), + 'VMANAWAT', + (now() at time zone 'utc') +), +( + 'VERTICAL_PROFILE', + 'Vertical Profile', + 'VMANAWAT', + (now() at time zone 'utc'), + 'VMANAWAT', + (now() at time zone 'utc') +), +( + 'ACTIVITY_RESULT', + 'Activity Result', + 'VMANAWAT', + (now() at time zone 'utc'), + 'VMANAWAT', + (now() at time zone 'utc') +), +( + 'SURROGATE_RESULT', + 'Surrogate Result', + 'VMANAWAT', + (now() at time zone 'utc'), + 'VMANAWAT', + (now() at time zone 'utc') +); \ No newline at end of file From 8366d1756daae179754aa83370a3ad1458be55f4 Mon Sep 17 00:00:00 2001 From: mgtennant Date: Wed, 9 Oct 2024 17:39:57 -0700 Subject: [PATCH 8/8] attach error log file instead --- .../notifications/notifications.service.ts | 68 ++++++++++++------- 1 file changed, 45 insertions(+), 23 deletions(-) diff --git a/backend/src/notifications/notifications.service.ts b/backend/src/notifications/notifications.service.ts index d8681c8b..ef20a500 100644 --- a/backend/src/notifications/notifications.service.ts +++ b/backend/src/notifications/notifications.service.ts @@ -150,7 +150,7 @@ export class NotificationsService { } /** - * Uses a file submission id to gather information on the completed/rejected submission + * Uses a file submission id to gather information on the completed/rejected submission * and sends an email to the data submitter. Does not check if notifications are filtered. * * @param file_submission_id @@ -161,15 +161,21 @@ export class NotificationsService { ): Promise { const file_submission = await this.fileSubmissionsService.findBySubmissionId(file_submission_id); - const body = await this.fileErrorLogsService.findOne(file_submission_id); + const errorLogs = + await this.fileErrorLogsService.findOne(file_submission_id); + const fileName = `${file_submission.original_file_name}-error_log.txt`; + const email = file_submission.data_submitter_email; - if (!this.isValidEmail(email)) { return 'Invalid email'} - const {submitter_user_id,submission_status_code} = file_submission; + if (!this.isValidEmail(email)) { + return "Invalid email"; + } + const { submitter_user_id, submission_status_code } = file_submission; const emailTemplate: EmailTemplate = { from: "enmodshelp@gov.bc.ca", - subject: "EnMoDS Data {{submission_status_code}} from {{submitter_user_id}}", - body: body, + subject: + "EnMoDS Data {{submission_status_code}} from {{submitter_user_id}}", + body: "

Error Notification

", }; const date = new Date(); const options: Intl.DateTimeFormatOptions = { @@ -186,6 +192,8 @@ export class NotificationsService { return this.sendEmail([email], emailTemplate, { submitter_user_id: submitter_user_id, submission_status_code: submission_status_code, + file_error_log: errorLogs, + file_name: fileName, sys_time, }); } @@ -198,29 +206,34 @@ export class NotificationsService { * @param variables * @returns */ - async sendContactNotification( - file_submission_id: string - ): Promise { + async sendContactNotification(file_submission_id: string): Promise { const file_submission = await this.fileSubmissionsService.findBySubmissionId(file_submission_id); - const email = file_submission.data_submitter_email; - if (!this.isValidEmail(email)) { return 'Invalid email'} - const {submitter_user_id,submission_status_code} = file_submission; + const email = file_submission.data_submitter_email; + if (!this.isValidEmail(email)) { + return "Invalid email"; + } + const { submitter_user_id, submission_status_code } = file_submission; - const errorLogs = await this.fileErrorLogsService.findOne(file_submission_id); + const errorLogs = + await this.fileErrorLogsService.findOne(file_submission_id); + const fileName = `${file_submission.original_file_name}-error_log.txt`; const notificationInfo = await this.getNotificationStatus( email, submitter_user_id, ); const unsubscribeLink = - process.env.WEBAPP_URL + `/unsubscribe/${notificationInfo.id}`; + process.env.WEBAPP_URL + `/unsubscribe/${notificationInfo.id}`; - let body = errorLogs.concat(`

Unsubscribe

`) + let body = errorLogs.concat( + `

Error Notification

Unsubscribe

`, + ); const emailTemplate = { from: "enmodshelp@gov.bc.ca", - subject: "EnMoDS Data {{submission_status_code}} from {{submitter_user_id}}", + subject: + "EnMoDS Data {{submission_status_code}} from {{submitter_user_id}}", body: body, }; const date = new Date(); @@ -238,6 +251,8 @@ export class NotificationsService { return this.sendEmail([email], emailTemplate, { submitter_user_id: submitter_user_id, submission_status_code: submission_status_code, + file_error_log: errorLogs, + file_name: fileName, sys_time, }); } @@ -258,14 +273,25 @@ export class NotificationsService { variables: { submitter_user_id: string; submission_status_code: string; + file_error_log: string; + file_name: string; sys_time: string; }, ): Promise { const chesToken = await this.getChesToken(); - console.log('sending email') + console.log("sending email"); + // file_error_log is a string, convert it to base64 + const base64ErrorLog = btoa(variables.file_error_log); const data = JSON.stringify({ - attachments: [], + attachments: [ + { + content: base64ErrorLog, + contentType: "string", + encoding: "base64", + filename: variables.file_name, + }, + ], bodyType: "html", body: emailTemplate.body, contexts: [ @@ -329,9 +355,7 @@ export class NotificationsService { * @param errors * @param ministryContact */ - async notifyUserOfError( - file_submission_id: string - ) { + async notifyUserOfError(file_submission_id: string) { // Notify the Data Submitter await this.sendDataSubmitterNotification(file_submission_id); @@ -362,8 +386,6 @@ export class NotificationsService { // errors, // ministryContact, // ); - - } isValidEmail(email: string): boolean {