diff --git a/FabApprovalWorkerService/Models/CorrectiveAction.cs b/FabApprovalWorkerService/Models/CorrectiveAction.cs new file mode 100644 index 0000000..1edd596 --- /dev/null +++ b/FabApprovalWorkerService/Models/CorrectiveAction.cs @@ -0,0 +1,10 @@ +namespace FabApprovalWorkerService.Models; + +public class CorrectiveAction { + public required int CANo { get; set; } + public bool ApprovalStatus { get; set; } + public DateTime FollowUpDate { get; set; } + public DateTime ClosedDate { get; set; } + public int QAID { get; set; } + public required string CATitle { get; set; } +} diff --git a/FabApprovalWorkerService/Models/ECN.cs b/FabApprovalWorkerService/Models/ECN.cs index 6996cd5..78b4926 100644 --- a/FabApprovalWorkerService/Models/ECN.cs +++ b/FabApprovalWorkerService/Models/ECN.cs @@ -7,8 +7,9 @@ public class ECN { [Key] public required int ECNNumber { get; set; } public bool IsTECN { get; set; } = false; - public DateTime ExpirationDate { get; set; } - public DateTime ExtensionDate { get; set; } = DateTime.MinValue; + public required DateTime ExpirationDate { get; set; } + public DateTime ExtensionDate { get; set; } public required int OriginatorID { get; set; } public required string Title { get; set; } + public DateTime CloseDate { get; set; } = DateTime.MaxValue; } diff --git a/FabApprovalWorkerService/Models/TrainingAssignment.cs b/FabApprovalWorkerService/Models/TrainingAssignment.cs index 32a1ee9..e01cbf5 100644 --- a/FabApprovalWorkerService/Models/TrainingAssignment.cs +++ b/FabApprovalWorkerService/Models/TrainingAssignment.cs @@ -1,12 +1,16 @@ using Dapper.Contrib.Extensions; namespace FabApprovalWorkerService.Models; + [Table("TrainingAssignment")] public class TrainingAssignment { [Key] public int ID { get; set; } + public required int UserID { get; set; } + public required DateTime DateAssigned { get; set; } public int TrainingID { get; set; } - public bool Status { get; set; } = false; + public bool status { get; set; } = false; public bool Deleted { get; set; } = false; public DateTime DeletedDate { get; set; } + public DateTime LastNotification { get; set; } } diff --git a/FabApprovalWorkerService/Program.cs b/FabApprovalWorkerService/Program.cs index c9e7829..a606639 100644 --- a/FabApprovalWorkerService/Program.cs +++ b/FabApprovalWorkerService/Program.cs @@ -28,6 +28,7 @@ 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"); @@ -69,6 +70,26 @@ builder.Services.AddQuartz(q => { .WithIdentity("Expired TECN trigger") .WithCronSchedule(CronScheduleBuilder.DailyAtHourAndMinute(6, 0)) ); + + JobKey trainingReminderJob = new JobKey("Training reminder job"); + q.AddJob(opts => opts + .WithIdentity(trainingReminderJob) + ); + q.AddTrigger(opts => opts + .ForJob(trainingReminderJob) + .WithIdentity("Training reminder trigger") + .WithCronSchedule(CronScheduleBuilder.DailyAtHourAndMinute(6, 0)) + ); + + JobKey caFollowUpJob = new JobKey("CA follow up job"); + q.AddJob(opts => opts + .WithIdentity(caFollowUpJob) + ); + q.AddTrigger(opts => opts + .ForJob(caFollowUpJob) + .WithIdentity("CA follow up trigger") + .WithCronSchedule(CronScheduleBuilder.DailyAtHourAndMinute(6, 0)) + ); }); builder.Services.AddQuartzHostedService(opt => { diff --git a/FabApprovalWorkerService/Services/CorrectiveActionService.cs b/FabApprovalWorkerService/Services/CorrectiveActionService.cs new file mode 100644 index 0000000..8e20864 --- /dev/null +++ b/FabApprovalWorkerService/Services/CorrectiveActionService.cs @@ -0,0 +1,67 @@ +using FabApprovalWorkerService.Models; + +using System.Text; + +namespace FabApprovalWorkerService.Services; + +public interface ICorrectiveActionService { + Task> GetCorrectiveActionsWithFollowUpInFiveDays(); + Task CreateCorrectiveActionFollowUpApproval(int caNo, int qaId); +} + +public class CorrectiveActionService : ICorrectiveActionService { + private readonly ILogger _logger; + private readonly IDalService _dalService; + + public CorrectiveActionService(ILogger logger, IDalService dalService) { + _logger = logger ?? throw new ArgumentNullException("ILogger not injected"); + _dalService = dalService ?? throw new ArgumentNullException("IDalService not injected"); + } + + public async Task CreateCorrectiveActionFollowUpApproval(int caNo, int qaId) { + try { + _logger.LogInformation($"Attempting to create a follow up approval for CA {caNo} by QA {qaId}"); + + if (caNo <= 0) throw new ArgumentException($"{caNo} is not a valid CA number"); + if (qaId <= 0) throw new ArgumentException($"{qaId} is not a valid User Id"); + + string today = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); + + StringBuilder queryBuilder = new(); + queryBuilder.Append("insert into Approval (IssueID, RoleName, SubRole, UserID, SubRoleID, ItemStatus, Step, "); + queryBuilder.Append("AssignedDate, NotifyDate,RoleAssignedDate, ApprovalType, DocumentTypeID) "); + queryBuilder.Append($"values ({caNo}, '8DQAFollowUp', '8DQAFollowUp', {qaId}, 335, 0, 2, "); + queryBuilder.Append($"{today}, {today}, {today}, 1, 9);"); + + await _dalService.ExecuteAsync(queryBuilder.ToString()); + } catch (Exception ex) { + StringBuilder errMsgBuilder = new(); + errMsgBuilder.Append($"An exception occurred when attempting to create a follow up approval for CA {caNo} by QA {qaId}. "); + errMsgBuilder.Append($"Exception: {ex.Message}"); + _logger.LogError(errMsgBuilder.ToString()); + throw; + } + } + + public async Task> GetCorrectiveActionsWithFollowUpInFiveDays() { + try { + _logger.LogInformation("Attempting to get all CAs needing follow up in five days"); + + DateTime fiveDaysFromToday = DateTime.Now.Date.AddDays(5); + DateTime sixDaysFromToday = DateTime.Now.Date.AddDays(6); + + StringBuilder queryBuilder = new(); + queryBuilder.Append("select * from _8DCorrectiveAction where ApprovalStatus = 1 and FollowUpDate is not null "); + queryBuilder.Append($"and FollowUpDate < '{sixDaysFromToday.ToString("yyyy-MM-dd HH:mm:ss")}' "); + queryBuilder.Append($"and FollowUpDate >= '{fiveDaysFromToday.ToString("yyyy-MM-dd HH:mm:ss")}';"); + + return (await _dalService.QueryAsync(queryBuilder.ToString())).ToList(); + } catch (Exception ex) { + StringBuilder errMsgBuilder = new(); + errMsgBuilder.Append($"An exception occurred when attempting to get all CAs needing follow up in five days. "); + errMsgBuilder.Append($"Exception: {ex.Message}"); + _logger.LogError(errMsgBuilder.ToString()); + throw; + } + } +} diff --git a/FabApprovalWorkerService/Services/ECNService.cs b/FabApprovalWorkerService/Services/ECNService.cs index defdaa9..49f250b 100644 --- a/FabApprovalWorkerService/Services/ECNService.cs +++ b/FabApprovalWorkerService/Services/ECNService.cs @@ -6,9 +6,10 @@ namespace FabApprovalWorkerService.Services; public interface IECNService { Task> GetExpiringTECNs(); - Task> GetExpiredTECNs(); + Task> GetExpiredTECNsInPastDay(); Task> GetTECNNotificationUserEmails(); Task GetEcnByNumber(int ecnNumber); + bool EcnIsExpired(ECN ecn); } public class ECNService : IECNService { @@ -20,6 +21,31 @@ public class ECNService : IECNService { _dalService = dalService ?? throw new ArgumentNullException("IDalService not injected"); } + public bool EcnIsExpired(ECN ecn) { + try { + _logger.LogInformation("Attempting to determine if ECN is expired"); + + if (ecn is null) throw new ArgumentNullException("ECN cannot be null"); + + if (!ecn.IsTECN) return false; + if (ecn.CloseDate <= DateTime.Now) return false; + + DateTime tomorrow = DateTime.Now.Date.AddDays(1); + + bool isExpired = (ecn.ExpirationDate < tomorrow) && (ecn.ExtensionDate < tomorrow); + + _logger.LogInformation($"ECN {ecn.ECNNumber} expired: {isExpired}"); + + return isExpired; + } catch (Exception ex) { + StringBuilder errMsgBuilder = new(); + errMsgBuilder.Append($"An exception occurred when attempting to determine if ECN is expired. "); + errMsgBuilder.Append($"Exception: {ex.Message}"); + _logger.LogError(errMsgBuilder.ToString()); + throw; + } + } + public async Task GetEcnByNumber(int ecnNumber) { try { _logger.LogInformation($"Attempting to get ECN {ecnNumber}"); @@ -42,7 +68,7 @@ public class ECNService : IECNService { } } - public async Task> GetExpiredTECNs() { + public async Task> GetExpiredTECNsInPastDay() { try { _logger.LogInformation("Attempting to get all TECNs expired in the last day"); diff --git a/FabApprovalWorkerService/Services/SmtpService.cs b/FabApprovalWorkerService/Services/SmtpService.cs index 37f2ef1..04b0d38 100644 --- a/FabApprovalWorkerService/Services/SmtpService.cs +++ b/FabApprovalWorkerService/Services/SmtpService.cs @@ -29,7 +29,7 @@ public class SmtpService : ISmtpService { string subject, string body) { if (recipients.IsNullOrEmpty()) throw new ArgumentNullException("recipients cannot be null or empty!"); - if (ccRecipients.IsNullOrEmpty()) throw new ArgumentNullException("ccRecipients cannot be null or empty!"); + if (ccRecipients is null) throw new ArgumentNullException("ccRecipients cannot be null!"); if (subject.IsNullOrEmpty()) throw new ArgumentNullException("subject cannot be null or empty!"); if (body.IsNullOrEmpty()) throw new ArgumentNullException("body cannot be null or empty!"); diff --git a/FabApprovalWorkerService/Services/TrainingService.cs b/FabApprovalWorkerService/Services/TrainingService.cs index f343cba..0852c8e 100644 --- a/FabApprovalWorkerService/Services/TrainingService.cs +++ b/FabApprovalWorkerService/Services/TrainingService.cs @@ -7,12 +7,14 @@ namespace FabApprovalWorkerService.Services; public interface ITrainingService { Task> GetTrainingIdsForECN(int ecnNumber); Task MarkTrainingAsComplete(int trainingId); - Task DeleteTrainingAssignment(int trainingId); + Task DeleteTrainingAssignmentsByTrainingId(int trainingId); + Task DeleteTrainingAssignmentById(int trainingAssignmentId); Task> GetTrainingAssignmentIdsForTraining(int trainingId); Task DeleteDocAssignment(int trainingAssignmentId); Task> GetActiveTrainingAssignments(); Task UpdateTrainingAssignmentLastNotification(int trainingAssignmentId); Task GetEcnNumberByTrainingId(int trainingId); + Task> GetTrainingAssignmentIdsByUserId(int userId); } public class TrainingService : ITrainingService { @@ -47,22 +49,44 @@ public class TrainingService : ITrainingService { } } - public async Task DeleteTrainingAssignment(int trainingId) { + public async Task DeleteTrainingAssignmentsByTrainingId(int trainingId) { if (trainingId <= 0) throw new ArgumentException($"Invalid training id: {trainingId}"); try { - _logger.LogInformation($"Attempting to delete training assignment {trainingId}"); + _logger.LogInformation($"Attempting to delete training assignments for training ID {trainingId}"); StringBuilder queryBuilder = new(); queryBuilder.Append($"update TrainingAssignments set Deleted = 1, "); queryBuilder.Append($"DeletedDate = '{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}' "); queryBuilder.Append($"where TrainingID = {trainingId} and status = 0;"); + await _dalService.ExecuteAsync(queryBuilder.ToString()); + } catch (Exception ex) { + StringBuilder errMsgBuilder = new(); + errMsgBuilder.Append($"An exception occurred when attempting to delete training assignments "); + errMsgBuilder.Append($"for training id {trainingId}. Exception: {ex.Message}"); + _logger.LogError(errMsgBuilder.ToString()); + throw; + } + } + + public async Task DeleteTrainingAssignmentById(int trainingAssignmentId) { + if (trainingAssignmentId <= 0) + throw new ArgumentException($"Invalid training assignment id: {trainingAssignmentId}"); + + try { + _logger.LogInformation($"Attempting to delete training assignment {trainingAssignmentId}"); + + StringBuilder queryBuilder = new(); + queryBuilder.Append($"update TrainingAssignments set Deleted = 1, "); + queryBuilder.Append($"DeletedDate = '{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}' "); + queryBuilder.Append($"where ID = {trainingAssignmentId};"); + await _dalService.ExecuteAsync(queryBuilder.ToString()); } catch (Exception ex) { StringBuilder errMsgBuilder = new(); errMsgBuilder.Append($"An exception occurred when attempting to delete training assignment "); - errMsgBuilder.Append($"{trainingId}. Exception: {ex.Message}"); + errMsgBuilder.Append($"{trainingAssignmentId}. Exception: {ex.Message}"); _logger.LogError(errMsgBuilder.ToString()); throw; } @@ -151,7 +175,7 @@ public class TrainingService : ITrainingService { throw new ArgumentException($"{trainingAssignmentId} is not a valid training assignment Id"); StringBuilder queryBuilder = new(); - queryBuilder.Append($"update TrainingAssignments set LastNotification = {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}"); + queryBuilder.Append($"update TrainingAssignments set LastNotification = '{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}'"); queryBuilder.Append($"where ID = {trainingAssignmentId};"); await _dalService.ExecuteAsync(queryBuilder.ToString()); @@ -186,4 +210,22 @@ public class TrainingService : ITrainingService { throw; } } + + public async Task> GetTrainingAssignmentIdsByUserId(int userId) { + try { + _logger.LogInformation($"Attempting to get all training assignment Ids for user {userId}"); + + if (userId <= 0) throw new ArgumentException($"{userId} is not a valid User ID"); + + string sql = $"select ID from TrainingAssignments where UserID = {userId};"; + + return (await _dalService.QueryAsync(sql)).ToList(); + } catch (Exception ex) { + StringBuilder errMsgBuilder = new(); + errMsgBuilder.Append($"An exception occurred when attempting to get all training assignment Ids for user {userId}. "); + errMsgBuilder.Append($"Exception: {ex.Message}"); + _logger.LogError(errMsgBuilder.ToString()); + throw; + } + } } diff --git a/FabApprovalWorkerService/Workers/CAFollowUpWorker.cs b/FabApprovalWorkerService/Workers/CAFollowUpWorker.cs new file mode 100644 index 0000000..c1a7fbc --- /dev/null +++ b/FabApprovalWorkerService/Workers/CAFollowUpWorker.cs @@ -0,0 +1,87 @@ +using FabApprovalWorkerService.Models; +using FabApprovalWorkerService.Services; + +using Infineon.Monitoring.MonA; + +using Quartz; + +using System.Net.Mail; +using System.Text; + +namespace FabApprovalWorkerService.Workers; + +public class CAFollowUpWorker : IJob { + private readonly ILogger _logger; + private readonly ICorrectiveActionService _caService; + private readonly IUserService _userService; + private readonly ISmtpService _smtpService; + private readonly IMonInClient _monInClient; + private readonly string _baseUrl; + + public CAFollowUpWorker(ILogger logger, + ICorrectiveActionService caService, + IUserService userService, + ISmtpService smtpService, + IMonInClient monInClient) { + _logger = logger ?? throw new ArgumentNullException("ILogger not injected"); + _caService = caService ?? throw new ArgumentNullException("ICorrectiveActionService not injected"); + _userService = userService ?? throw new ArgumentNullException("IUserService not injected"); + _smtpService = smtpService ?? throw new ArgumentNullException("ISmtpService not injected"); + _monInClient = monInClient ?? throw new ArgumentNullException("IMonInClient not injected"); + _baseUrl = Environment.GetEnvironmentVariable("FabApprovalBaseUrl") ?? + throw new ArgumentNullException("FabApprovalBaseUrl environment variable not found"); + } + + public async Task Execute(IJobExecutionContext context) { + DateTime start = DateTime.Now; + bool isInternalError = false; + StringBuilder errorMessage = new(); + string metricName = "CAFollowUpWorker"; + + try { + _logger.LogInformation("Attempting to create follow up approvals for CAs needing it in five days"); + + IEnumerable followUpCAs = (await _caService.GetCorrectiveActionsWithFollowUpInFiveDays()) + .ToList(); + foreach (CorrectiveAction ca in followUpCAs) { + await _caService.CreateCorrectiveActionFollowUpApproval(ca.CANo, ca.QAID); + + string qaEmail = await _userService.GetUserEmail(ca.QAID); + IEnumerable recipients = new List() { + new MailAddress(qaEmail) + }; + + IEnumerable ccRecipients = new List(); + + string subject = $"Fab Approval CA Follow Up - CA# {ca.CANo} - {ca.CATitle}"; + + StringBuilder bodyBuilder = new(); + bodyBuilder.Append($"Corrective Action# {ca.CANo} is ready for follow up. Please log on to Fab Approval "); + bodyBuilder.Append("and review this item.

"); + bodyBuilder.Append($"{_baseUrl}/CorrectiveAction/Edit?issueID={ca.CANo}

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

Thank You!"); + + await _smtpService.SendEmail(recipients, ccRecipients, subject, bodyBuilder.ToString()); + } + + _logger.LogInformation("Successfully created follow up approvals for CAs needing it in five days"); + } 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); + } + } + } +} diff --git a/FabApprovalWorkerService/Workers/ExpiredTECNWorker.cs b/FabApprovalWorkerService/Workers/ExpiredTECNWorker.cs index 96679fe..e0cd5a6 100644 --- a/FabApprovalWorkerService/Workers/ExpiredTECNWorker.cs +++ b/FabApprovalWorkerService/Workers/ExpiredTECNWorker.cs @@ -50,7 +50,7 @@ public class ExpiredTECNWorker : IJob { try { _logger.LogInformation("Attempting to process expired TECNs"); - List expiredEcns = (await _ecnService.GetExpiredTECNs()).ToList(); + List expiredEcns = (await _ecnService.GetExpiredTECNsInPastDay()).ToList(); List tecnNotificationUserEmails = new(); @@ -63,7 +63,7 @@ public class ExpiredTECNWorker : IJob { List trainingIds = (await _trainingService.GetTrainingIdsForECN(ecn.ECNNumber)).ToList(); foreach (int trainingId in trainingIds) { - await _trainingService.DeleteTrainingAssignment(trainingId); + await _trainingService.DeleteTrainingAssignmentsByTrainingId(trainingId); List trainingAssignmentIds = (await _trainingService.GetTrainingAssignmentIdsForTraining(trainingId)).ToList(); diff --git a/FabApprovalWorkerService/Workers/TrainingNotificationWorker.cs b/FabApprovalWorkerService/Workers/TrainingNotificationWorker.cs index 58fbb9c..151b4cb 100644 --- a/FabApprovalWorkerService/Workers/TrainingNotificationWorker.cs +++ b/FabApprovalWorkerService/Workers/TrainingNotificationWorker.cs @@ -5,6 +5,7 @@ using Infineon.Monitoring.MonA; using Quartz; +using System.Net.Mail; using System.Text; namespace FabApprovalWorkerService.Workers; @@ -16,6 +17,7 @@ public class TrainingNotificationWorker : IJob { private readonly IECNService _ecnService; private readonly ISmtpService _smtpService; private readonly IMonInClient _monInClient; + private readonly string _baseUrl; public TrainingNotificationWorker(ILogger logger, ITrainingService trainingService, @@ -29,6 +31,8 @@ public class TrainingNotificationWorker : IJob { _ecnService = ecnService ?? throw new ArgumentNullException("IECNService not injected"); _smtpService = smtpService ?? throw new ArgumentNullException("ISmtpService not injected"); _monInClient = monInClient ?? throw new ArgumentNullException("IMonInClient not injected"); + _baseUrl = Environment.GetEnvironmentVariable("FabApprovalBaseUrl") ?? + throw new ArgumentNullException("FabApprovalBaseUrl environment variable not found"); } public async Task Execute(IJobExecutionContext context) { @@ -42,11 +46,54 @@ public class TrainingNotificationWorker : IJob { IEnumerable trainingAssignments = await _trainingService.GetActiveTrainingAssignments(); + _logger.LogInformation($"There are {trainingAssignments.Count()} active training assignments"); + foreach (TrainingAssignment trainingAssignment in trainingAssignments) { - ECN ecn = await _ecnService.GetEcnByNumber + int ecnNumber = await _trainingService.GetEcnNumberByTrainingId(trainingAssignment.TrainingID); + ECN ecn = await _ecnService.GetEcnByNumber(ecnNumber); + + bool ecnIsExpired = _ecnService.EcnIsExpired(ecn); + if (ecnIsExpired) { + _logger.LogInformation($"ECN {ecn.ECNNumber} is expired. Cancelling all training."); + + await _trainingService.DeleteTrainingAssignmentsByTrainingId(trainingAssignment.TrainingID); + await _trainingService.DeleteDocAssignment(trainingAssignment.ID); + await _trainingService.MarkTrainingAsComplete(trainingAssignment.TrainingID); + } User user = await _userService.GetUserById(trainingAssignment.UserID); - + bool userIsActive = user.IsActive; + if (!userIsActive) { + _logger.LogInformation($"User {user.UserID} is inactive. Cancelling all training."); + + IEnumerable userTrainingAssignmentIds = await _trainingService.GetTrainingAssignmentIdsByUserId(user.UserID); + + foreach (int trainingAssignmentId in userTrainingAssignmentIds) { + await _trainingService.DeleteTrainingAssignmentById(trainingAssignmentId); + await _trainingService.DeleteDocAssignment(trainingAssignmentId); + } + } + + if (!ecnIsExpired && userIsActive && !user.OOO) { + bool lastNotificationMoreThanFourDaysAgo = (DateTime.Now.Date - trainingAssignment.LastNotification).Days >= 5; + bool dateAssignedMoreThanFourteenDaysAgo = (DateTime.Now.Date - trainingAssignment.DateAssigned).Days >= 15; + + bool notificationSent = false; + if (lastNotificationMoreThanFourDaysAgo && dateAssignedMoreThanFourteenDaysAgo) { + await SendTrainingReminder(ecn, user); + notificationSent = true; + await _trainingService.UpdateTrainingAssignmentLastNotification(trainingAssignment.ID); + } + + if (!notificationSent) { + DateTime latestExpirationDate = (ecn.ExtensionDate > ecn.ExpirationDate ? ecn.ExtensionDate : ecn.ExpirationDate); + int daysTillExpiration = (latestExpirationDate - DateTime.Now.Date).Days; + if (daysTillExpiration > 0 && daysTillExpiration <= 5) { + await SendTrainingReminder(ecn, user); + await _trainingService.UpdateTrainingAssignmentLastNotification(trainingAssignment.ID); + } + } + } } _logger.LogInformation("Successfully sent training notifications"); @@ -68,4 +115,23 @@ public class TrainingNotificationWorker : IJob { } } } + + private async Task SendTrainingReminder(ECN ecn, User user) { + _logger.LogInformation($"Attempting to send training reminder for ECN {ecn.ECNNumber} to user {user.UserID}"); + + IEnumerable recipients = new List() { + new MailAddress(user.Email) + }; + + IEnumerable ccRecipients = new List(); + + StringBuilder bodyBuilder = new(); + bodyBuilder.Append("Hello, you have open training assignments in Fab Approval. This is a reminder to "); + bodyBuilder.Append("finish your training assignments.
View your open training assignments "); + bodyBuilder.Append($"here. "); + + string subject = $"Fab Approval Training Reminder - ECN# {ecn.ECNNumber} - {ecn.Title}"; + + await _smtpService.SendEmail(recipients, ccRecipients, subject, bodyBuilder.ToString()); + } } diff --git a/FabApprovalWorkerServiceTests/CorrectiveActionServiceTests.cs b/FabApprovalWorkerServiceTests/CorrectiveActionServiceTests.cs new file mode 100644 index 0000000..8d5de80 --- /dev/null +++ b/FabApprovalWorkerServiceTests/CorrectiveActionServiceTests.cs @@ -0,0 +1,101 @@ +using FabApprovalWorkerService.Models; +using FabApprovalWorkerService.Services; + +using Microsoft.Extensions.Logging; + +using Moq; + +namespace FabApprovalWorkerServiceTests; + +public class CorrectiveActionServiceTests { + private Mock> _mockLogger; + private Mock _mockDalService; + + private CorrectiveActionService _caService; + + [SetUp] + public void Setup() { + _mockLogger = new Mock>(); + _mockDalService = new Mock(); + } + + [Test] + public void CAServiceWithNullLoggerShouldThrowException() { + Assert.Throws(() => new CorrectiveActionService(null, _mockDalService.Object)); + } + + [Test] + public void CAServiceWithNullDalServiceShouldThrowException() { + Assert.Throws(() => new CorrectiveActionService(_mockLogger.Object, null)); + } + + [Test] + public void GetCorrectiveActionsWithFollowUpInFiveDaysDbErrorShouldThrowError() { + _mockDalService.Setup(d => d.QueryAsync(It.IsAny())).Throws(new Exception()); + + _caService = new CorrectiveActionService(_mockLogger.Object, _mockDalService.Object); + + Assert.ThrowsAsync(async Task () => await _caService.GetCorrectiveActionsWithFollowUpInFiveDays()); + } + + [Test] + public async Task GetCorrectiveActionsWithFollowUpInFiveDaysShouldReturnExpectedCAs() { + IEnumerable expectedCAs = new List() { + new CorrectiveAction() { + CANo = 1, + CATitle = "title" + }, + new CorrectiveAction() { + CANo = 2, + CATitle = "title" + }, + new CorrectiveAction() { + CANo = 3, + CATitle = "title" + } + }; + + _mockDalService.Setup(d => d.QueryAsync(It.IsAny())).Returns(Task.FromResult(expectedCAs)); + + _caService = new CorrectiveActionService(_mockLogger.Object, _mockDalService.Object); + + IEnumerable actualCAs = await _caService.GetCorrectiveActionsWithFollowUpInFiveDays(); + + Assert.That(expectedCAs, Is.EquivalentTo(actualCAs)); + } + + //CreateCorrectiveActionFollowUpApproval + [Test] + public void CreateCorrectiveActionFollowUpApprovalWithInvalidCaNoShouldThrowException() { + _caService = new CorrectiveActionService(_mockLogger.Object, _mockDalService.Object); + + Assert.ThrowsAsync(async Task () => await _caService.CreateCorrectiveActionFollowUpApproval(0, 5)); + Assert.ThrowsAsync(async Task () => await _caService.CreateCorrectiveActionFollowUpApproval(-5, 5)); + } + + [Test] + public void CreateCorrectiveActionFollowUpApprovalWithInvalidQaIdShouldThrowException() { + _caService = new CorrectiveActionService(_mockLogger.Object, _mockDalService.Object); + + Assert.ThrowsAsync(async Task () => await _caService.CreateCorrectiveActionFollowUpApproval(5, 0)); + Assert.ThrowsAsync(async Task () => await _caService.CreateCorrectiveActionFollowUpApproval(5, -5)); + } + + [Test] + public void CreateCorrectiveActionFollowUpApprovalDbErrorShouldThrowException() { + _mockDalService.Setup(d => d.ExecuteAsync(It.IsAny())).Throws(new Exception()); + + _caService = new CorrectiveActionService(_mockLogger.Object, _mockDalService.Object); + + Assert.ThrowsAsync(async Task () => await _caService.CreateCorrectiveActionFollowUpApproval(5, 5)); + } + + [Test] + public async Task CreateCorrectiveActionFollowUpApprovalShouldExecuteSql() { + _caService = new CorrectiveActionService(_mockLogger.Object, _mockDalService.Object); + + await _caService.CreateCorrectiveActionFollowUpApproval(5, 5); + + _mockDalService.Verify(d => d.ExecuteAsync(It.IsAny()), Times.Once()); + } +} diff --git a/FabApprovalWorkerServiceTests/ECNServiceTests.cs b/FabApprovalWorkerServiceTests/ECNServiceTests.cs index 54c8c0b..9d83f41 100644 --- a/FabApprovalWorkerServiceTests/ECNServiceTests.cs +++ b/FabApprovalWorkerServiceTests/ECNServiceTests.cs @@ -43,7 +43,7 @@ internal class ECNServiceTests { _ecnService = new ECNService(_mockLogger.Object, _mockDalService.Object); - Assert.ThrowsAsync(async Task () => await _ecnService.GetExpiredTECNs()); + Assert.ThrowsAsync(async Task () => await _ecnService.GetExpiredTECNsInPastDay()); } [Test] @@ -67,7 +67,7 @@ internal class ECNServiceTests { _ecnService = new ECNService(_mockLogger.Object, _mockDalService.Object); - IEnumerable actual = await _ecnService.GetExpiredTECNs(); + IEnumerable actual = await _ecnService.GetExpiredTECNsInPastDay(); Assert.That(actual, Is.Empty); } @@ -137,7 +137,7 @@ internal class ECNServiceTests { _ecnService = new ECNService(_mockLogger.Object, _mockDalService.Object); - IEnumerable actual = await _ecnService.GetExpiredTECNs(); + IEnumerable actual = await _ecnService.GetExpiredTECNsInPastDay(); Assert.That(actual.Count(), Is.EqualTo(3)); } @@ -209,7 +209,7 @@ internal class ECNServiceTests { _ecnService = new ECNService(_mockLogger.Object, _mockDalService.Object); - IEnumerable actual = await _ecnService.GetExpiredTECNs(); + IEnumerable actual = await _ecnService.GetExpiredTECNsInPastDay(); Assert.That(actual.Count(), Is.EqualTo(2)); } @@ -274,7 +274,8 @@ internal class ECNServiceTests { new ECN() { ECNNumber = 1, OriginatorID = 1, - Title = "title" + Title = "title", + ExpirationDate = DateTime.Now } }; @@ -286,4 +287,150 @@ internal class ECNServiceTests { Assert.That(expectedEcns.First(), Is.EqualTo(actualEcn)); } + + [Test] + public void EcnIsExpiredNullEcnShouldThrowException() { + _ecnService = new ECNService(_mockLogger.Object, _mockDalService.Object); + + Assert.Throws(() => _ecnService.EcnIsExpired(null)); + } + + [Test] + public void EcnIsExpiredShouldReturnTrueWhenBeforeToday() { + ECN ecn = new ECN() { + ECNNumber = 1, + OriginatorID = 1, + Title = "title", + ExpirationDate = DateTime.Now.Date.AddDays(-2), + IsTECN = true + }; + + _ecnService = new ECNService(_mockLogger.Object, _mockDalService.Object); + + bool isExpired = _ecnService.EcnIsExpired(ecn); + + Assert.True(isExpired); + } + + [Test] + public void EcnIsExpiredShouldReturnTrueWhenToday() { + ECN ecn = new ECN() { + ECNNumber = 1, + OriginatorID = 1, + Title = "title", + ExpirationDate = DateTime.Now.Date, + IsTECN = true + }; + + _ecnService = new ECNService(_mockLogger.Object, _mockDalService.Object); + + bool isExpired = _ecnService.EcnIsExpired(ecn); + + Assert.True(isExpired); + } + + [Test] + public void EcnIsExpiredShouldReturnFalseWhenAfterToday() { + ECN ecn = new ECN() { + ECNNumber = 1, + OriginatorID = 1, + Title = "title", + ExpirationDate = DateTime.Now.Date.AddDays(2), + IsTECN = true + }; + + _ecnService = new ECNService(_mockLogger.Object, _mockDalService.Object); + + bool isExpired = _ecnService.EcnIsExpired(ecn); + + Assert.False(isExpired); + } + + [Test] + public void EcnIsExpiredShouldReturnFalseWhenExtended() { + ECN ecn = new ECN() { + ECNNumber = 1, + OriginatorID = 1, + Title = "title", + ExpirationDate = DateTime.Now.Date.AddDays(-2), + ExtensionDate = DateTime.Now.Date.AddDays(5), + IsTECN = true + }; + + _ecnService = new ECNService(_mockLogger.Object, _mockDalService.Object); + + bool isExpired = _ecnService.EcnIsExpired(ecn); + + Assert.False(isExpired); + } + + [Test] + public void EcnIsExpiredShouldReturnTrueWhenExtensionIsBeforeToday() { + ECN ecn = new ECN() { + ECNNumber = 1, + OriginatorID = 1, + Title = "title", + ExpirationDate = DateTime.Now.Date.AddDays(-10), + ExtensionDate = DateTime.Now.Date.AddDays(-5), + IsTECN = true + }; + + _ecnService = new ECNService(_mockLogger.Object, _mockDalService.Object); + + bool isExpired = _ecnService.EcnIsExpired(ecn); + + Assert.True(isExpired); + } + + [Test] + public void EcnIsExpiredShouldReturnFalseWhenNotTecn() { + ECN ecn = new ECN() { + ECNNumber = 1, + OriginatorID = 1, + Title = "title", + ExpirationDate = DateTime.Now.Date.AddDays(-2), + IsTECN = false + }; + + _ecnService = new ECNService(_mockLogger.Object, _mockDalService.Object); + + bool isExpired = _ecnService.EcnIsExpired(ecn); + + Assert.False(isExpired); + } + + [Test] + public void EcnIsExpiredShouldReturnFalseWhenClosed() { + ECN ecn = new ECN() { + ECNNumber = 1, + OriginatorID = 1, + Title = "title", + ExpirationDate = DateTime.Now.Date.AddDays(-2), + IsTECN = false, + CloseDate = DateTime.Now.AddDays(-2) + }; + + _ecnService = new ECNService(_mockLogger.Object, _mockDalService.Object); + + bool isExpired = _ecnService.EcnIsExpired(ecn); + + Assert.False(isExpired); + } + + [Test] + public void EcnIsExpiredShouldReturnTrueWhenNotClosed() { + ECN ecn = new ECN() { + ECNNumber = 1, + OriginatorID = 1, + Title = "title", + ExpirationDate = DateTime.Now.Date.AddDays(-2), + IsTECN = true + }; + + _ecnService = new ECNService(_mockLogger.Object, _mockDalService.Object); + + bool isExpired = _ecnService.EcnIsExpired(ecn); + + Assert.True(isExpired); + } } diff --git a/FabApprovalWorkerServiceTests/SmtpServiceTests.cs b/FabApprovalWorkerServiceTests/SmtpServiceTests.cs index 80a3a9f..fff04c9 100644 --- a/FabApprovalWorkerServiceTests/SmtpServiceTests.cs +++ b/FabApprovalWorkerServiceTests/SmtpServiceTests.cs @@ -61,15 +61,6 @@ internal class SmtpServiceTests { }); } - [Test] - public void SendMailWithEmptyccRecipientsShouldThrowException() { - _smtpService = new SmtpService(_mockLogger.Object, _mockSmtpClient.Object); - - Assert.ThrowsAsync(async Task () => { - await _smtpService.SendEmail(ADDRESS_LIST, new List(), "subject", "body"); - }); - } - [Test] public void SendMailWithNullSubjectShouldThrowException() { _smtpService = new SmtpService(_mockLogger.Object, _mockSmtpClient.Object); diff --git a/FabApprovalWorkerServiceTests/TrainingServiceTests.cs b/FabApprovalWorkerServiceTests/TrainingServiceTests.cs index 4ffc309..2fcb6be 100644 --- a/FabApprovalWorkerServiceTests/TrainingServiceTests.cs +++ b/FabApprovalWorkerServiceTests/TrainingServiceTests.cs @@ -2,6 +2,7 @@ using FabApprovalWorkerService.Services; using Microsoft.Extensions.Logging; +using Microsoft.Identity.Client; using Moq; @@ -27,8 +28,6 @@ public class TrainingServiceTests { public void TrainingServiceWithNullDalServiceShouldThrowException() { Assert.Throws(() => new TrainingService(_mockLogger.Object, null)); } -<<<<<<< Updated upstream -======= [Test] public void DeleteDocAssignmentWithInvalidIdShouldThrowException() { @@ -61,7 +60,7 @@ public class TrainingServiceTests { public void DeleteTrainingAssignmentWithInvalidIdShouldThrowException() { _trainingService = new TrainingService(_mockLogger.Object, _mockDalService.Object); - Assert.ThrowsAsync(async Task () => await _trainingService.DeleteTrainingAssignment(-1)); + Assert.ThrowsAsync(async Task () => await _trainingService.DeleteTrainingAssignmentsByTrainingId(-1)); } [Test] @@ -70,7 +69,7 @@ public class TrainingServiceTests { _trainingService = new TrainingService(_mockLogger.Object, _mockDalService.Object); - Assert.ThrowsAsync(async Task () => await _trainingService.DeleteTrainingAssignment(1)); + Assert.ThrowsAsync(async Task () => await _trainingService.DeleteTrainingAssignmentsByTrainingId(1)); } [Test] @@ -79,7 +78,7 @@ public class TrainingServiceTests { _trainingService = new TrainingService(_mockLogger.Object, _mockDalService.Object); - await _trainingService.DeleteTrainingAssignment(3); + await _trainingService.DeleteTrainingAssignmentsByTrainingId(3); _mockDalService.Verify(d => d.ExecuteAsync(It.IsAny())); } @@ -238,5 +237,101 @@ public class TrainingServiceTests { _mockDalService.Verify(d => d.ExecuteAsync(It.IsAny()), Times.Once()); } ->>>>>>> Stashed changes + + [Test] + public void GetEcnNumberByTrainingIdInvalidIdShouldThrowException() { + _trainingService = new TrainingService(_mockLogger.Object, _mockDalService.Object); + + Assert.ThrowsAsync(async Task () => await _trainingService.GetEcnNumberByTrainingId(0)); + Assert.ThrowsAsync(async Task () => await _trainingService.GetEcnNumberByTrainingId(-1)); + } + + [Test] + public void GetEcnNumberByTrainingIdDbErrorShouldThrowException() { + _mockDalService.Setup(d => d.QueryAsync(It.IsAny())).Throws(new Exception()); + + _trainingService = new TrainingService(_mockLogger.Object, _mockDalService.Object); + + Assert.ThrowsAsync(async Task () => await _trainingService.GetEcnNumberByTrainingId(9)); + } + + [Test] + public void GetEcnNumberByTrainingIdEcnNumberNotFoundShouldThrowException() { + IEnumerable expectedIds = new List(); + + _mockDalService.Setup(d => d.QueryAsync(It.IsAny())).Returns(Task.FromResult(expectedIds)); + + _trainingService = new TrainingService(_mockLogger.Object, _mockDalService.Object); + + Assert.ThrowsAsync(async Task () => await _trainingService.GetEcnNumberByTrainingId(9)); + } + + [Test] + public async Task GetEcnNumberByTrainingIdShouldReturnExpectedId() { + IEnumerable expectedIds = new List() { 1 }; + + _mockDalService.Setup(d => d.QueryAsync(It.IsAny())).Returns(Task.FromResult(expectedIds)); + + _trainingService = new TrainingService(_mockLogger.Object, _mockDalService.Object); + + int actualId = await _trainingService.GetEcnNumberByTrainingId(9); + + Assert.That(expectedIds.First(), Is.EqualTo(actualId)); + } + + [Test] + public void DeleteTrainingAssignmentByIdWithInvalidIdShouldThrowException() { + _trainingService = new TrainingService(_mockLogger.Object, _mockDalService.Object); + + Assert.ThrowsAsync(async Task () => await _trainingService.DeleteTrainingAssignmentById(0)); + Assert.ThrowsAsync(async Task () => await _trainingService.DeleteTrainingAssignmentById(-7)); + } + + [Test] + public void DeleteTrainingAssignmentByIdWithDbErrorShouldThrowException() { + _mockDalService.Setup(d => d.ExecuteAsync(It.IsAny())).Throws(new Exception()); + + _trainingService = new TrainingService(_mockLogger.Object, _mockDalService.Object); + + Assert.ThrowsAsync(async Task () => await _trainingService.DeleteTrainingAssignmentById(9)); + } + + [Test] + public async Task DeleteTrainingAssignmentByIdShouldExecuteSql() { + _trainingService = new TrainingService(_mockLogger.Object, _mockDalService.Object); + + await _trainingService.DeleteTrainingAssignmentById(9); + + _mockDalService.Verify(d => d.ExecuteAsync(It.IsAny()), Times.Once()); + } + + [Test] + public void GetTrainingAssignmentIdsByUserIdWithInvalidIdShouldThrowException() { + _trainingService = new TrainingService(_mockLogger.Object, _mockDalService.Object); + + Assert.ThrowsAsync(async Task () => await _trainingService.GetTrainingAssignmentIdsByUserId(0)); + Assert.ThrowsAsync(async Task () => await _trainingService.GetTrainingAssignmentIdsByUserId(-9)); + } + + [Test] + public void GetTrainingAssignmentIdsByUserIdWithDbErrorShouldThrowException() { + _mockDalService.Setup(d => d.QueryAsync(It.IsAny())).Throws(new Exception()); + + _trainingService = new TrainingService(_mockLogger.Object, _mockDalService.Object); + + Assert.ThrowsAsync(async Task () => await _trainingService.GetTrainingAssignmentIdsByUserId(9)); + } + + [Test] + public async Task GetTrainingAssignmentIdsByUserIdShouldReturnExpectedIds() { + IEnumerable expectedIds = new List { 1, 2, 3 }; + + _mockDalService.Setup(d => d.QueryAsync(It.IsAny())).Returns(Task.FromResult(expectedIds)); + + _trainingService = new TrainingService(_mockLogger.Object, _mockDalService.Object); + + IEnumerable actualIds = await _trainingService.GetTrainingAssignmentIdsByUserId(9); + + Assert.That(expectedIds, Is.EquivalentTo(actualIds)); + } } diff --git a/FabApprovalWorkerServiceTests/UserServiceTests.cs b/FabApprovalWorkerServiceTests/UserServiceTests.cs index 9a14634..7954a6a 100644 --- a/FabApprovalWorkerServiceTests/UserServiceTests.cs +++ b/FabApprovalWorkerServiceTests/UserServiceTests.cs @@ -2,7 +2,6 @@ using FabApprovalWorkerService.Models; using FabApprovalWorkerService.Services; using Microsoft.Extensions.Logging; -using Microsoft.Identity.Client; using Moq; @@ -73,7 +72,7 @@ internal class UserServiceTests { } [Test] - public async Task GetAllExpiredOOOUsersDbErrorShouldThrowException() { + public void GetAllExpiredOOOUsersDbErrorShouldThrowException() { _mockDalService.Setup(d => d.QueryAsync(It.IsAny())).ThrowsAsync(new Exception("whoops!")); _userService = new UserService(MOCK_LOGGER, _mockDalService.Object); @@ -93,7 +92,7 @@ internal class UserServiceTests { } [Test] - public async Task GetAllPendingOOOUsersDbErrorShouldThrowException() { + public void GetAllPendingOOOUsersDbErrorShouldThrowException() { _mockDalService.Setup(d => d.QueryAsync(It.IsAny())).ThrowsAsync(new Exception("whoops!")); _userService = new UserService(MOCK_LOGGER, _mockDalService.Object); @@ -113,7 +112,7 @@ internal class UserServiceTests { } [Test] - public async Task IsUserAlreadyOOOWithInvalidUserIdShouldThrowException() { + public void IsUserAlreadyOOOWithInvalidUserIdShouldThrowException() { _userService = new UserService(MOCK_LOGGER, _mockDalService.Object); Assert.ThrowsAsync(async Task () => await _userService.IsUserAlreadyOOO(0)); @@ -153,7 +152,7 @@ internal class UserServiceTests { } [Test] - public async Task IsDelegatorAlreadyDelegatedToDbErrorShouldThrowException() { + public void IsDelegatorAlreadyDelegatedToDbErrorShouldThrowException() { _mockDalService.Setup(d => d.QueryAsync(It.IsAny())).ThrowsAsync(new Exception("whoops!")); _userService = new UserService(MOCK_LOGGER, _mockDalService.Object); @@ -188,7 +187,7 @@ internal class UserServiceTests { } [Test] - public async Task InsertDelegatedRolesWithInvalidUserIdShouldThrowException() { + public void InsertDelegatedRolesWithInvalidUserIdShouldThrowException() { _userService = new UserService(MOCK_LOGGER, _mockDalService.Object); Assert.ThrowsAsync(async Task () => await _userService.InsertDelegatedRoles(0)); @@ -330,7 +329,7 @@ internal class UserServiceTests { } [Test] - public async Task FlagUserAsOOOWithNullOOOTempShouldThrowException() { + public void FlagUserAsOOOWithNullOOOTempShouldThrowException() { _userService = new UserService(MOCK_LOGGER, _mockDalService.Object); Assert.ThrowsAsync(async Task () => await _userService.FlagUserAsOOO(null)); @@ -373,7 +372,7 @@ internal class UserServiceTests { } [Test] - public async Task RemoveOOOFlagForUserWithInvalidIdShouldThrowException() { + public void RemoveOOOFlagForUserWithInvalidIdShouldThrowException() { _userService = new UserService(MOCK_LOGGER, _mockDalService.Object); Assert.ThrowsAsync(async Task () => await _userService.RemoveOOOFlagForUser(0)); @@ -389,7 +388,7 @@ internal class UserServiceTests { } [Test] - public async Task SetOOOTempProcessedWithNullOOOTempShouldThrowException() { + public void SetOOOTempProcessedWithNullOOOTempShouldThrowException() { _userService = new UserService(MOCK_LOGGER, _mockDalService.Object); Assert.ThrowsAsync(async Task () => await _userService.SetOOOTempProcessed(null));