diff --git a/FabApprovalWorkerService/Models/Approval.cs b/FabApprovalWorkerService/Models/Approval.cs new file mode 100644 index 0000000..df12e54 --- /dev/null +++ b/FabApprovalWorkerService/Models/Approval.cs @@ -0,0 +1,22 @@ +using FabApprovalWorkerService.Utilities; + +namespace FabApprovalWorkerService.Models; + +public class Approval { + public int ApprovalID { get; set; } + public required int IssueID { get; set; } + public required string RoleName { get; set; } + public required string SubRole { get; set; } + public required int UserID { get; set; } + public User? User { get; set; } + public required int SubRoleID { get; set; } + public int ItemStatus { get; set; } = 0; + public string StatusMessage = "Assigned"; + public DateTime NotifyDate { get; set; } = DateTimeUtilities.MIN_DT; + public required DateTime AssignedDate { get; set; } + public DateTime CompletedDate { get; set; } = DateTimeUtilities.MAX_DT; + public string Comments { get; set; } = ""; + public int Step { get; set; } = 1; + public string SubRoleCategoryItem { get; set; } = ""; + public int TaskID { get; set; } +} diff --git a/FabApprovalWorkerService/Models/MRB.cs b/FabApprovalWorkerService/Models/MRB.cs new file mode 100644 index 0000000..7b07761 --- /dev/null +++ b/FabApprovalWorkerService/Models/MRB.cs @@ -0,0 +1,29 @@ +using FabApprovalWorkerService.Utilities; + +namespace FabApprovalWorkerService.Models; + +public class MRB { + public int MRBNumber { get; set; } + public int OriginatorID { get; set; } + public string Title { get; set; } = ""; + public DateTime SubmittedDate { get; set; } = DateTimeUtilities.MIN_DT; + public DateTime CloseDate { get; set; } = DateTimeUtilities.MAX_DT; + public DateTime CancelDate { get; set; } = DateTimeUtilities.MAX_DT; + public DateTime ApprovalDate { get; set; } = DateTimeUtilities.MAX_DT; + public string IssueDescription { get; set; } = ""; + public int NumberOfLotsAffected { get; set; } + public int Val { get; set; } + public bool CustomerImpacted { get; set; } = false; + public string CustomerImpactedName { get; set; } = ""; + public string Department { get; set; } = ""; + public string Process { get; set; } = ""; + public int RMANo { get; set; } + public string PCRBNo { get; set; } = ""; + public bool SpecsImpacted { get; set; } = false; + public int ProcessECNNumber { get; set; } + public bool TrainingRequired { get; set; } = false; + public required int StageNo { get; set; } + public required string Status { get; set; } + public string Tool { get; set; } = ""; + public string Category { get; set; } = string.Empty; +} diff --git a/FabApprovalWorkerService/Models/PCRB.cs b/FabApprovalWorkerService/Models/PCRB.cs new file mode 100644 index 0000000..6b7df52 --- /dev/null +++ b/FabApprovalWorkerService/Models/PCRB.cs @@ -0,0 +1,18 @@ +using FabApprovalWorkerService.Utilities; + +namespace FabApprovalWorkerService.Models; + +public class PCRB { + public int PlanNumber { get; set; } + public int OwnerID { get; set; } + public string OwnerName { get; set; } = ""; + public string Title { get; set; } = ""; + public string ChangeLevel { get; set; } = "Mesa"; + public bool IsITAR { get; set; } = false; + public int CurrentStep { get; set; } = 0; + public string ReasonForChange { get; set; } = ""; + public string ChangeDescription { get; set; } = ""; + public DateTime InsertTimeStamp { get; set; } = DateTimeUtilities.MIN_DT; + public DateTime LastUpdateDate { get; set; } = DateTimeUtilities.MIN_DT; + public DateTime ClosedDate { get; set; } = DateTimeUtilities.MAX_DT; +} diff --git a/FabApprovalWorkerService/Program.cs b/FabApprovalWorkerService/Program.cs index c901f1d..44aaff6 100644 --- a/FabApprovalWorkerService/Program.cs +++ b/FabApprovalWorkerService/Program.cs @@ -29,6 +29,9 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddQuartz(q => { JobKey pendingOOOStatusJob = new JobKey("Pending OOO status job"); @@ -100,6 +103,16 @@ builder.Services.AddQuartz(q => { .WithIdentity("Certification training group trigger") .WithCronSchedule(CronScheduleBuilder.DailyAtHourAndMinute(1, 0)) ); + + JobKey approvalNotificationJob = new JobKey("Approval notification job"); + q.AddJob(opts => opts + .WithIdentity(approvalNotificationJob) + ); + q.AddTrigger(opts => opts + .ForJob(approvalNotificationJob) + .WithIdentity("Approval notification trigger") + .WithCronSchedule(CronScheduleBuilder.DailyAtHourAndMinute(6, 0)) + ); }); builder.Services.AddQuartzHostedService(opt => { diff --git a/FabApprovalWorkerService/Services/ApprovalService.cs b/FabApprovalWorkerService/Services/ApprovalService.cs new file mode 100644 index 0000000..41e08d4 --- /dev/null +++ b/FabApprovalWorkerService/Services/ApprovalService.cs @@ -0,0 +1,65 @@ +using FabApprovalWorkerService.Models; +using FabApprovalWorkerService.Utilities; + +using System.Text; + +namespace FabApprovalWorkerService.Services; + +public interface IApprovalService { + Task> GetActiveApprovalsWithNotificationOlderThanFiveDays(); + Task UpdateApproval(Approval approval); +} + +public class ApprovalService : IApprovalService { + private readonly ILogger _logger; + private readonly IDalService _dalService; + + public ApprovalService(ILogger logger, IDalService dalService) { + _logger = logger ?? throw new ArgumentNullException("ILogger not injected"); + _dalService = dalService ?? throw new ArgumentNullException("IDalService not injected"); + } + + public async Task> GetActiveApprovalsWithNotificationOlderThanFiveDays() { + try { + _logger.LogInformation($"Attempting to get active approvals with notifications older than five days"); + + StringBuilder queryBuilder = new(); + queryBuilder.Append("select * from Approval where "); + queryBuilder.Append($"(CompletedDate is null or CompletedDate >= '{DateTimeUtilities.MAX_DT.ToString("yyyy-MM-dd HH:mm")}') and "); + queryBuilder.Append($"(NotifyDate is null or NotifyDate < '{DateTimeOffset.Now.AddDays(-5).ToString("yyyy-MM-dd HH:mm")}')"); + + IEnumerable approvals = await _dalService.QueryAsync(queryBuilder.ToString()); + + return approvals; + } catch (Exception ex) { + _logger.LogError($"Unable to get active approvals with notifications older than five days, because {ex.Message}"); + throw; + } + } + + public async Task UpdateApproval(Approval approval) { + try { + _logger.LogInformation("Attempting to update an approval"); + + if (approval is null) throw new ArgumentNullException("Approval cannot be null"); + + StringBuilder queryBuilder = new(); + queryBuilder.Append($"update Approval set IssueID={approval.IssueID}, RoleName='{approval.RoleName}', "); + queryBuilder.Append($"SubRole='{approval.SubRole}', UserID={approval.UserID}, SubRoleID={approval.SubRoleID}, "); + queryBuilder.Append($"ItemStatus={Convert.ToInt32(approval.ItemStatus)}, Step={approval.Step}, "); + queryBuilder.Append($"NotifyDate='{approval.NotifyDate.ToString("yyyy-MM-dd HH:mm:ss")}', "); + queryBuilder.Append($"AssignedDate='{approval.AssignedDate.ToString("yyyy-MM-dd HH:mm:ss")}', "); + queryBuilder.Append($"CompletedDate='{approval.CompletedDate.ToString("yyyy-MM-dd HH:mm:ss")}', "); + queryBuilder.Append($"Comments='{approval.Comments.Replace("'", "''")}', "); + queryBuilder.Append($"TaskID={approval.TaskID} "); + queryBuilder.Append($"where ApprovalID={approval.ApprovalID};"); + + int rowsUpdated = await _dalService.ExecuteAsync(queryBuilder.ToString()); + + if (rowsUpdated <= 0) throw new Exception("Unable to update approval in database"); + } catch (Exception ex) { + _logger.LogError($"Approval update failed, because {ex.Message}"); + throw; + } + } +} diff --git a/FabApprovalWorkerService/Services/CorrectiveActionService.cs b/FabApprovalWorkerService/Services/CorrectiveActionService.cs index 435f975..626f9ee 100644 --- a/FabApprovalWorkerService/Services/CorrectiveActionService.cs +++ b/FabApprovalWorkerService/Services/CorrectiveActionService.cs @@ -1,4 +1,5 @@ using FabApprovalWorkerService.Models; +using FabApprovalWorkerService.Utilities; using System.Text; @@ -7,6 +8,8 @@ namespace FabApprovalWorkerService.Services; public interface ICorrectiveActionService { Task> GetCorrectiveActionsWithFollowUpInFiveDays(); Task CreateCorrectiveActionFollowUpApproval(int caNo, int qaId); + Task CANumberIsActive(int number); + Task GetCorrectiveActionById(int id); } public class CorrectiveActionService : ICorrectiveActionService { @@ -64,4 +67,43 @@ public class CorrectiveActionService : ICorrectiveActionService { throw; } } + + public async Task CANumberIsActive(int number) { + try { + _logger.LogInformation($"Attempting to determine if {number} is an active CA#"); + + if (number <= 0) return false; + + StringBuilder queryBuilder = new(); + queryBuilder.Append($"select count(CANo) as count from _8DCorrectiveAction "); + queryBuilder.Append($"where CANo={number} and "); + queryBuilder.Append($"(ClosedDate is null or ClosedDate >= '{DateTimeUtilities.MAX_DT.ToString("yyyy-MM-dd HH:mm")}')"); + + int rowsReturned = (await _dalService.QueryAsync(queryBuilder.ToString())).FirstOrDefault(); + + return rowsReturned > 0; + } catch (Exception ex) { + _logger.LogError($"Unable to determine if {number} is an active CA#, because {ex.Message}"); + throw; + } + } + + public async Task GetCorrectiveActionById(int id) { + try { + _logger.LogInformation($"Attempting to get CA# {id}"); + + if (id <= 0) throw new ArgumentException($"{id} is not a valid CA#"); + + string sql = $"select * from _8DCorrectiveAction where CANo={id}"; + + CorrectiveAction? ca = (await _dalService.QueryAsync(sql)).FirstOrDefault(); + + if (ca is null) throw new Exception($"no CA found with CA# {id}"); + + return ca; + } catch (Exception ex) { + _logger.LogError($"Unable to get CA# {id}, because {ex.Message}"); + throw; + } + } } diff --git a/FabApprovalWorkerService/Services/DbConnectionService.cs b/FabApprovalWorkerService/Services/DbConnectionService.cs index 2024e80..b5e1ba5 100644 --- a/FabApprovalWorkerService/Services/DbConnectionService.cs +++ b/FabApprovalWorkerService/Services/DbConnectionService.cs @@ -21,10 +21,6 @@ public class DbConnectionService : IDbConnectionService { } public IDbConnection GetConnection() { - if (_envName.ToLower().Equals("development")) { - return new SqliteConnection(_dbConnectionString); - } else { - return new SqlConnection(_dbConnectionString); - } + return new SqlConnection(_dbConnectionString); } } diff --git a/FabApprovalWorkerService/Services/ECNService.cs b/FabApprovalWorkerService/Services/ECNService.cs index 93b32ae..6a70042 100644 --- a/FabApprovalWorkerService/Services/ECNService.cs +++ b/FabApprovalWorkerService/Services/ECNService.cs @@ -1,4 +1,5 @@ using FabApprovalWorkerService.Models; +using FabApprovalWorkerService.Utilities; using System.Text; @@ -10,6 +11,7 @@ public interface IECNService { Task> GetTECNNotificationUserEmails(); Task GetEcnByNumber(int ecnNumber); bool EcnIsExpired(ECN ecn); + Task ECNNumberIsActive(int number); } public class ECNService : IECNService { @@ -163,4 +165,23 @@ public class ECNService : IECNService { throw; } } + + public async Task ECNNumberIsActive(int number) { + try { + _logger.LogInformation($"Attempting to determine if {number} is a valid ECN#"); + + if (number <= 0) return false; + + StringBuilder queryBuilder = new(); + queryBuilder.Append($"select count(ECNNumber) as count from ECN where ECNNumber={number} and "); + queryBuilder.Append($"(CloseDate is null or CloseDate >= '{DateTimeUtilities.MAX_DT.ToString("yyyy-MM-dd HH:mm")}')"); + + int rowsReturned = (await _dalService.QueryAsync(queryBuilder.ToString())).FirstOrDefault(); + + return rowsReturned > 0; + } catch (Exception ex) { + _logger.LogError($"Unable to determine if {number} is a valid ECN#, because {ex.Message}"); + throw; + } + } } diff --git a/FabApprovalWorkerService/Services/MRBService.cs b/FabApprovalWorkerService/Services/MRBService.cs new file mode 100644 index 0000000..3b1dfeb --- /dev/null +++ b/FabApprovalWorkerService/Services/MRBService.cs @@ -0,0 +1,59 @@ +using FabApprovalWorkerService.Models; +using FabApprovalWorkerService.Utilities; + +using System.Text; + +namespace FabApprovalWorkerService.Services; + +public interface IMRBService { + Task MRBNumberIsActive(int number); + Task GetMRBById(int id); +} + +public class MRBService : IMRBService { + private readonly ILogger _logger; + private readonly IDalService _dalService; + + public MRBService(ILogger logger, IDalService dalService) { + _logger = logger ?? throw new ArgumentNullException("ILogger not injected"); + _dalService = dalService ?? throw new ArgumentNullException("IDalService not injected"); + } + + public async Task MRBNumberIsActive(int number) { + try { + _logger.LogInformation($"Attempting to determine if {number} is an active MRB#"); + + if (number <= 0) return false; + + StringBuilder queryBuilder = new(); + queryBuilder.Append($"select count(MRBNumber) as count from MRB where MRBNumber={number} and "); + queryBuilder.Append($"(CloseDate is null or CloseDate >= '{DateTimeUtilities.MAX_DT.ToString("yyyy-MM-dd HH:mm")}')"); + + int rowsReturned = (await _dalService.QueryAsync(queryBuilder.ToString())).FirstOrDefault(); + + return rowsReturned > 0; + } catch (Exception ex) { + _logger.LogError($"Unable to determine if {number} is an active MRB#, because {ex.Message}"); + throw; + } + } + + public async Task GetMRBById(int id) { + try { + _logger.LogInformation("Attempting to get an MRB by ID"); + + if (id < 0) throw new ArgumentException("Invalid MRB number"); + + string sql = $"select * from MRB where MRBNumber={id}"; + + MRB? mrb = (await _dalService.QueryAsync(sql)).FirstOrDefault(); + + if (mrb is null) throw new Exception($"Unable to get MRB {id}"); + + return mrb; + } catch (Exception ex) { + _logger.LogError($"Unable to get MRB# {id}, because {ex.Message}"); + throw; + } + } +} diff --git a/FabApprovalWorkerService/Services/PCRBService.cs b/FabApprovalWorkerService/Services/PCRBService.cs new file mode 100644 index 0000000..acf9503 --- /dev/null +++ b/FabApprovalWorkerService/Services/PCRBService.cs @@ -0,0 +1,60 @@ +using FabApprovalWorkerService.Models; +using FabApprovalWorkerService.Utilities; + +using System.Text; + +namespace FabApprovalWorkerService.Services; + +public interface IPCRBService { + Task PCRBNumberIsActive(int number); + public Task GetPCRBByPlanNumber(int planNumber); +} + +public class PCRBService : IPCRBService { + private readonly ILogger _logger; + private readonly IDalService _dalService; + + public PCRBService(ILogger logger, IDalService dalService) { + _logger = logger ?? throw new ArgumentNullException("ILogger not injected"); + _dalService = dalService ?? throw new ArgumentNullException("IDalService not injected"); + } + + public async Task PCRBNumberIsActive(int number) { + try { + _logger.LogInformation($"Attempting to determine if {number} is an active PCRB#"); + + if (number <= 0) return false; + + StringBuilder queryBuilder = new(); + queryBuilder.Append("select count(PlanNumber) as count from CCChangeControl "); + queryBuilder.Append($"where PlanNumber={number} and "); + queryBuilder.Append($"(ClosedDate is null or ClosedDate >= '{DateTimeUtilities.MAX_DT.ToString("yyyy-MM-dd HH:mm")}')"); + + int rowsReturned = (await _dalService.QueryAsync(queryBuilder.ToString())).FirstOrDefault(); + + return rowsReturned > 0; + } catch (Exception ex) { + _logger.LogError($"Unable to determine if {number} is an active PCRB#, because {ex.Message}"); + throw; + } + } + + public async Task GetPCRBByPlanNumber(int planNumber) { + try { + _logger.LogInformation("Attempting to get a PCRB by plan#"); + + if (planNumber <= 0) throw new ArgumentException($"{planNumber} is not a valid PCRB#"); + + string sql = $"select * from CCChangeControl where PlanNumber={planNumber}"; + + PCRB? pcrb = (await _dalService.QueryAsync(sql)).FirstOrDefault(); + + if (pcrb is null) throw new Exception($"unable to find PCRB {planNumber}"); + + return pcrb; + } catch (Exception ex) { + _logger.LogError($"Unable to get PCRB# {planNumber}, because {ex.Message}"); + throw; + } + } +} diff --git a/FabApprovalWorkerService/Services/SmtpService.cs b/FabApprovalWorkerService/Services/SmtpService.cs index 6dac67c..f03f8d1 100644 --- a/FabApprovalWorkerService/Services/SmtpService.cs +++ b/FabApprovalWorkerService/Services/SmtpService.cs @@ -66,6 +66,7 @@ public class SmtpService : ISmtpService { } } } else { + _logger.LogInformation("Not sending email per configuration"); messageWasSent = true; } } catch (Exception ex) { diff --git a/FabApprovalWorkerService/Utilities/DateTimeUtilities.cs b/FabApprovalWorkerService/Utilities/DateTimeUtilities.cs new file mode 100644 index 0000000..38b5edb --- /dev/null +++ b/FabApprovalWorkerService/Utilities/DateTimeUtilities.cs @@ -0,0 +1,14 @@ +namespace FabApprovalWorkerService.Utilities; + +public class DateTimeUtilities { + public static DateTime MIN_DT = new DateTime(1753, 1, 1, 0, 0, 0); + public static DateTime MAX_DT = new DateTime(9999, 12, 31, 11, 59, 59); + + public static string GetDateAsStringMinDefault(DateTime dt) { + return dt > MIN_DT ? dt.ToString("yyyy-MM-dd HH:mm") : ""; + } + + public static string GetDateAsStringMaxDefault(DateTime dt) { + return dt < MAX_DT ? dt.ToString("yyyy-MM-dd HH:mm") : ""; + } +} \ No newline at end of file diff --git a/FabApprovalWorkerService/Workers/ApprovalNotificationWorker.cs b/FabApprovalWorkerService/Workers/ApprovalNotificationWorker.cs new file mode 100644 index 0000000..139b35f --- /dev/null +++ b/FabApprovalWorkerService/Workers/ApprovalNotificationWorker.cs @@ -0,0 +1,162 @@ +using FabApprovalWorkerService.Models; +using FabApprovalWorkerService.Services; + +using Infineon.Monitoring.MonA; + +using Quartz; + +using System.Net.Mail; +using System.Text; + +namespace FabApprovalWorkerService.Workers; + +public class ApprovalNotificationWorker : IJob { + private readonly ILogger _logger; + private readonly IApprovalService _approvalService; + private readonly IECNService _ecnService; + private readonly ICorrectiveActionService _caService; + private readonly IMRBService _mrbService; + private readonly IPCRBService _pcrbService; + private readonly ISmtpService _smtpService; + private readonly IMonInClient _monInClient; + private readonly IUserService _userService; + private readonly string _oldFabApprovalBaseUrl; + private readonly string _newFabApprovalBaseUrl; + + public ApprovalNotificationWorker(ILogger logger, + IApprovalService approvalService, + IECNService ecnService, + ICorrectiveActionService caService, + IMRBService mrbService, + IPCRBService pcrbService, + ISmtpService smtpService, + IMonInClient monInClient, + IUserService userService) { + _logger = logger ?? throw new ArgumentNullException("ILogger not injected"); + _approvalService = approvalService ?? throw new ArgumentNullException("IApprovalService not injected"); + _ecnService = ecnService ?? throw new ArgumentNullException("IECNService not injected"); + _caService = caService ?? throw new ArgumentNullException("ICorrectiveActionService not injected"); + _mrbService = mrbService ?? throw new ArgumentNullException("IMRBService not injected"); + _pcrbService = pcrbService ?? throw new ArgumentNullException("IPCRBService not injected"); + _smtpService = smtpService ?? throw new ArgumentNullException("ISmtpService not injected"); + _monInClient = monInClient ?? throw new ArgumentNullException("IMonInClient not injected"); + _userService = userService ?? throw new ArgumentNullException("IUserService not injected"); + _oldFabApprovalBaseUrl = Environment.GetEnvironmentVariable("OldFabApprovalUrl") ?? + throw new ArgumentNullException("OldFabApprovalUrl environment variable not found"); + _newFabApprovalBaseUrl = Environment.GetEnvironmentVariable("NewFabApprovalBaseUrl") ?? + throw new ArgumentNullException("NewFabApprovalBaseUrl environment variable not found"); + } + + public async Task Execute(IJobExecutionContext context) { + DateTime start = DateTime.Now; + bool isInternalError = false; + StringBuilder errorMessage = new(); + string metricName = "ApprovalNotificationWorker"; + + try { + _logger.LogInformation("Attempting to notify approvers of outstanding approvals"); + + IEnumerable approvals = await _approvalService.GetActiveApprovalsWithNotificationOlderThanFiveDays(); + + foreach (Approval approval in approvals) { + bool isEcn = false; + bool isCa = false; + bool isMrb = false; + bool isPcrb = false; + + string approvalType = string.Empty; + + string title = string.Empty; + + string url = string.Empty; + + if (await _ecnService.ECNNumberIsActive(approval.IssueID)) { + isEcn = true; + approvalType = "ECN"; + + ECN ecn = await _ecnService.GetEcnByNumber(approval.IssueID); + title = ecn.Title; + + url = $"{_oldFabApprovalBaseUrl}/ECN/Edit?IssueID={approval.IssueID}"; + } else if (await _caService.CANumberIsActive(approval.IssueID)) { + isCa = true; + approvalType = "Corrective Action"; + + CorrectiveAction ca = await _caService.GetCorrectiveActionById(approval.IssueID); + title = ca.CATitle; + + url = $"{_oldFabApprovalBaseUrl}/CorrectiveAction/Edit?IssueID={approval.IssueID}"; + } else if (await _mrbService.MRBNumberIsActive(approval.IssueID)) { + isMrb = true; + approvalType = "MRB"; + + MRB mrb = await _mrbService.GetMRBById(approval.IssueID); + title = mrb.Title; + + url = $"{_newFabApprovalBaseUrl}/redirect?redirectPath=mrb/{approval.IssueID}"; + } else if (await _pcrbService.PCRBNumberIsActive(approval.IssueID)) { + isPcrb = true; + approvalType = "PCRB"; + + PCRB pcrb = await _pcrbService.GetPCRBByPlanNumber(approval.IssueID); + title = pcrb.Title; + + url = $"{_newFabApprovalBaseUrl}/redirect?redirectPath=pcrb/{approval.IssueID}"; + } + + /*if (approval.AssignedDate < DateTimeOffset.Now.AddDays(-150)) { + approval.CompletedDate = DateTime.Now; + approval.ItemStatus = 1; + + await _approvalService.UpdateApproval(approval); + }*/ + + if ((isEcn || isCa || isMrb || isPcrb) && + !string.IsNullOrWhiteSpace(title) && + !string.IsNullOrWhiteSpace(url)) { + + User user = await _userService.GetUserById(approval.UserID); + + IEnumerable recipients = new List() { + new MailAddress(user.Email) + }; + + IEnumerable ccRecipients = new List(); + + StringBuilder bodyBuilder = new(); + bodyBuilder.Append($"*** Please DO NOT reply to this email ***"); + bodyBuilder.Append("

This is a reminder that you have an approval waiting for you to review."); + bodyBuilder.Append("

Please log on to the Mesa Approval website to view the assignment and act accordingly. "); + bodyBuilder.Append($"

Document Type: {approvalType}"); + bodyBuilder.Append($"
Title: {title}"); + bodyBuilder.Append($"

{url}"); + bodyBuilder.Append($"

If you have any questions or trouble logging on please contact a site administrator."); + bodyBuilder.Append($"

Thank you!"); + + string subject = $"{approvalType} Approval Reminder: {title}"; + + await _smtpService.SendEmail(recipients, ccRecipients, subject, bodyBuilder.ToString()); + + approval.NotifyDate = DateTime.Now; + await _approvalService.UpdateApproval(approval); + } + } + } catch (Exception ex) { + StringBuilder errMsgBuilder = new(); + errMsgBuilder.Append("An exception occurred when attempting to create follow up approvals for "); + errMsgBuilder.Append($"CAs needing it in five days. Exception: {ex.Message}"); + _logger.LogError(errMsgBuilder.ToString()); + isInternalError = true; + } finally { + DateTime end = DateTime.Now; + double latencyInMS = (end - start).TotalMilliseconds; + _monInClient.PostMetric(metricName + "Latency", latencyInMS); + + if (isInternalError) { + _monInClient.PostStatus(metricName, State.Critical); + } else { + _monInClient.PostStatus(metricName, State.Ok); + } + } + } +}