From 794b6162936b7776ef4ab2d8edfcce498f725342 Mon Sep 17 00:00:00 2001 From: Chase Tucker Date: Wed, 3 Apr 2024 12:05:44 -0700 Subject: [PATCH] Created Windows Service --- .gitignore | 3 +- .../FabApprovalWorkerService.csproj | 8 +- .../FabApprovalWorkerService.csproj.user | 1 + FabApprovalWorkerService/Models/ECN.cs | 8 +- .../Models/TrainingAssignment.cs | 12 +++ FabApprovalWorkerService/Program.cs | 25 +++-- .../Services/ECNService.cs | 26 ++--- .../Services/TrainingService.cs | 102 ++++++++++++++++++ .../Services/WindowsService.cs | 40 +++++++ .../SetupScripts/CreateECNTable.sql | 13 ++- .../CreateTECNNotificationUsersTable.sql | 6 +- .../Workers/ExpiringTECNWorker.cs | 7 +- .../SmtpServiceTests.cs | 5 +- .../TrainingServiceTests.cs | 29 +++++ 14 files changed, 247 insertions(+), 38 deletions(-) create mode 100644 FabApprovalWorkerService/Models/TrainingAssignment.cs create mode 100644 FabApprovalWorkerService/Services/TrainingService.cs create mode 100644 FabApprovalWorkerService/Services/WindowsService.cs create mode 100644 FabApprovalWorkerServiceTests/TrainingServiceTests.cs diff --git a/.gitignore b/.gitignore index 3e96316..df733cb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .vs bin obj -TestResults \ No newline at end of file +TestResults +Properties \ No newline at end of file diff --git a/FabApprovalWorkerService/FabApprovalWorkerService.csproj b/FabApprovalWorkerService/FabApprovalWorkerService.csproj index 1598a3e..07e9db7 100644 --- a/FabApprovalWorkerService/FabApprovalWorkerService.csproj +++ b/FabApprovalWorkerService/FabApprovalWorkerService.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -8,6 +8,9 @@ true True Debug;Release;Staging + exe + win-x64 + x64 @@ -26,12 +29,15 @@ + + + diff --git a/FabApprovalWorkerService/FabApprovalWorkerService.csproj.user b/FabApprovalWorkerService/FabApprovalWorkerService.csproj.user index dca6fd5..0e45b1b 100644 --- a/FabApprovalWorkerService/FabApprovalWorkerService.csproj.user +++ b/FabApprovalWorkerService/FabApprovalWorkerService.csproj.user @@ -4,5 +4,6 @@ ApiControllerEmptyScaffolder root/Common/Api C:\Users\tuckerc\FabApprovalWorkerService\FabApprovalWorkerService\Properties\PublishProfiles\Staging.pubxml + <_LastSelectedProfileId>C:\Users\tuckerc\FabApprovalWorkerService\FabApprovalWorkerService\Properties\PublishProfiles\FolderProfile.pubxml \ No newline at end of file diff --git a/FabApprovalWorkerService/Models/ECN.cs b/FabApprovalWorkerService/Models/ECN.cs index 1b1d313..6996cd5 100644 --- a/FabApprovalWorkerService/Models/ECN.cs +++ b/FabApprovalWorkerService/Models/ECN.cs @@ -1,10 +1,14 @@ -namespace FabApprovalWorkerService.Models; +using Dapper.Contrib.Extensions; +namespace FabApprovalWorkerService.Models; + +[Table("ECN")] 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; } + public DateTime ExtensionDate { get; set; } = DateTime.MinValue; public required int OriginatorID { get; set; } public required string Title { get; set; } } diff --git a/FabApprovalWorkerService/Models/TrainingAssignment.cs b/FabApprovalWorkerService/Models/TrainingAssignment.cs new file mode 100644 index 0000000..32a1ee9 --- /dev/null +++ b/FabApprovalWorkerService/Models/TrainingAssignment.cs @@ -0,0 +1,12 @@ +using Dapper.Contrib.Extensions; + +namespace FabApprovalWorkerService.Models; +[Table("TrainingAssignment")] +public class TrainingAssignment { + [Key] + public int ID { get; set; } + public int TrainingID { get; set; } + public bool Status { get; set; } = false; + public bool Deleted { get; set; } = false; + public DateTime DeletedDate { get; set; } +} diff --git a/FabApprovalWorkerService/Program.cs b/FabApprovalWorkerService/Program.cs index 57c4dda..e94eb71 100644 --- a/FabApprovalWorkerService/Program.cs +++ b/FabApprovalWorkerService/Program.cs @@ -8,7 +8,7 @@ using Quartz; using System.Net.Mail; -WebApplicationBuilder builder = WebApplication.CreateBuilder(args); +HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); builder.Logging.ClearProviders(); builder.Logging.SetMinimumLevel(LogLevel.Trace); @@ -36,11 +36,7 @@ builder.Services.AddQuartz(q => { q.AddTrigger(opts => opts .ForJob(pendingOOOStatusJob) .WithIdentity("Pending OOO status trigger") - .WithSimpleSchedule(x => x - .WithIntervalInMinutes(10) - .RepeatForever() - ) - .StartNow() + .WithCronSchedule(CronScheduleBuilder.DailyAtHourAndMinute(0, 0)) ); JobKey expiredOOOStatusJob = new JobKey("Expired OOO status job"); @@ -50,6 +46,16 @@ builder.Services.AddQuartz(q => { q.AddTrigger(opts => opts .ForJob(expiredOOOStatusJob) .WithIdentity("Expired OOO status trigger") + .WithCronSchedule(CronScheduleBuilder.DailyAtHourAndMinute(0, 0)) + ); + + JobKey expiringTECNJob = new JobKey("Expiring TECN job"); + q.AddJob(opts => opts + .WithIdentity(expiringTECNJob) + ); + q.AddTrigger(opts => opts + .ForJob(expiringTECNJob) + .WithIdentity("Expiring TECN trigger") .WithSimpleSchedule(x => x .WithIntervalInMinutes(10) .RepeatForever() @@ -62,6 +68,11 @@ builder.Services.AddQuartzHostedService(opt => { opt.WaitForJobsToComplete = true; }); -WebApplication app = builder.Build(); +builder.Services.AddWindowsService(options => { + options.ServiceName = "Fab Approval Worker Service"; +}); +builder.Services.AddHostedService(); + +IHost app = builder.Build(); app.Run(); diff --git a/FabApprovalWorkerService/Services/ECNService.cs b/FabApprovalWorkerService/Services/ECNService.cs index be814b3..e4f9586 100644 --- a/FabApprovalWorkerService/Services/ECNService.cs +++ b/FabApprovalWorkerService/Services/ECNService.cs @@ -32,13 +32,13 @@ public class ECNService : IECNService { queryBuilder.Append($"ExpirationDate between '{yesterday}' "); queryBuilder.Append($"and '{today}'"); - IEnumerable expiredTecns = (await _dalService.QueryAsync(queryBuilder.ToString())) - .Where(e => e.ExtensionDate is null || e.ExtensionDate < DateTime.Now) - .ToList(); + IEnumerable expiredTecns = (await _dalService.QueryAsync(queryBuilder.ToString())); - _logger.LogInformation($"Found {expiredTecns.Count()} expired TECNs"); + IEnumerable expiredTecnsNotExtended = expiredTecns + .Where(e => e.ExtensionDate < DateTime.Now) + .ToList(); - return expiredTecns; + return expiredTecnsNotExtended; } catch (Exception ex) { StringBuilder errMsgBuilder = new(); errMsgBuilder.Append("An exception occurred when attempting to get all TECNs expired in the last day. "); @@ -56,18 +56,18 @@ public class ECNService : IECNService { string fiveDaysFromToday = DateTime.Now.AddDays(5).ToString("yyyy-MM-dd HH:mm:ss"); StringBuilder queryBuilder = new StringBuilder(); - queryBuilder.Append("select ECNNumber, IsTECN, ExpirationDate, ExtensionDate, OriginatorID, Title "); - queryBuilder.Append($"from ECN where IsTECN = 1 and "); + queryBuilder.Append("select * from ECN "); + queryBuilder.Append($"where IsTECN = 1 and "); queryBuilder.Append($"ExpirationDate between '{today}' "); - queryBuilder.Append($"and '{fiveDaysFromToday}'"); + queryBuilder.Append($"and '{fiveDaysFromToday}';"); - IEnumerable expiringTecns = (await _dalService.QueryAsync(queryBuilder.ToString())) - .Where(e => e.ExtensionDate is null || e.ExtensionDate <= DateTime.Now.AddDays(5)) + IEnumerable expiringTecns = (await _dalService.QueryAsync(queryBuilder.ToString())); + + IEnumerable expiringTecnsNotExtended = expiringTecns + .Where(e => e.ExtensionDate <= DateTime.Now.AddDays(5)) .ToList(); - _logger.LogInformation($"Found {expiringTecns.Count()} expiring TECNs"); - - return expiringTecns; + return expiringTecnsNotExtended; } catch (Exception ex) { StringBuilder errMsgBuilder = new(); errMsgBuilder.Append("An exception occurred when attempting to get all TECNs expiring in the next five days. "); diff --git a/FabApprovalWorkerService/Services/TrainingService.cs b/FabApprovalWorkerService/Services/TrainingService.cs new file mode 100644 index 0000000..1ebab9b --- /dev/null +++ b/FabApprovalWorkerService/Services/TrainingService.cs @@ -0,0 +1,102 @@ +using FabApprovalWorkerService.Models; + +using System.Text; + +namespace FabApprovalWorkerService.Services; + +public interface ITrainingService { + Task> GetTrainingIdsForECN(int ecnNumber); + Task DeleteTrainingAssignment(int trainingId); + Task> GetTrainingAssignmentIdsForTraining(int trainingId); + Task DeleteDocAssignment(int trainingAssignmentId); +} + +public class TrainingService : ITrainingService { + private ILogger _logger; + private IDalService _dalService; + + public TrainingService(ILogger logger, IDalService dalService) { + _logger = logger ?? + throw new ArgumentNullException("ILogger not injected"); + _dalService = dalService ?? + throw new ArgumentNullException("IDalService not injected"); + } + + public async Task DeleteDocAssignment(int trainingAssignmentId) { + if (trainingAssignmentId <= 0) throw new ArgumentException($"Invalid training assignment id: {trainingAssignmentId}"); + + try { + _logger.LogInformation($"Attempting to delete training doc assignments for training assignment {trainingAssignmentId}"); + + StringBuilder queryBuilder = new(); + queryBuilder.Append($"update TrainingDocAcks set Deleted = 1, "); + queryBuilder.Append($"DeletedDate = {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} "); + queryBuilder.Append($"where TrainingAssignmentID = {trainingAssignmentId} and Reviewed = 0;"); + + await _dalService.ExecuteAsync(queryBuilder.ToString()); + } catch (Exception ex) { + StringBuilder errMsgBuilder = new(); + errMsgBuilder.Append($"An exception occurred when attempting to delete training doc assignment "); + errMsgBuilder.Append($"{trainingAssignmentId}. Exception: {ex.Message}"); + _logger.LogError(errMsgBuilder.ToString()); + throw; + } + } + + public async Task DeleteTrainingAssignment(int trainingId) { + if (trainingId <= 0) throw new ArgumentException($"Invalid training id: {trainingId}"); + + try { + _logger.LogInformation($"Attempting to delete training assignment {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 assignment "); + errMsgBuilder.Append($"{trainingId}. Exception: {ex.Message}"); + _logger.LogError(errMsgBuilder.ToString()); + throw; + } + } + + public async Task> GetTrainingAssignmentIdsForTraining(int trainingId) { + if (trainingId <= 0) throw new ArgumentException($"Invalid trainingID: {trainingId}"); + + try { + _logger.LogInformation($"Attempting to get training assignment ids for training id {trainingId}"); + + string sql = $"select ID from TrainingAssignments where TrainingID = {trainingId};"; + + return await _dalService.QueryAsync(sql); + } catch (Exception ex) { + StringBuilder errMsgBuilder = new(); + errMsgBuilder.Append($"An exception occurred when attempting to get training assignment ids "); + errMsgBuilder.Append($"for training id {trainingId}. Exception: {ex.Message}"); + _logger.LogError(errMsgBuilder.ToString()); + throw; + } + } + + public async Task> GetTrainingIdsForECN(int ecnNumber) { + if (ecnNumber <= 0) throw new ArgumentException($"Invalid ecnNumber: {ecnNumber}"); + + try { + _logger.LogInformation($"Attempting to get training ids for ecn {ecnNumber}"); + + string sql = $"select TrainingID from Training where ECN = {ecnNumber};"; + + return await _dalService.QueryAsync(sql); + } catch (Exception ex) { + StringBuilder errMsgBuilder = new(); + errMsgBuilder.Append($"An exception occurred when attempting to get training ids "); + errMsgBuilder.Append($"for ECN {ecnNumber}. Exception: {ex.Message}"); + _logger.LogError(errMsgBuilder.ToString()); + throw; + } + } +} diff --git a/FabApprovalWorkerService/Services/WindowsService.cs b/FabApprovalWorkerService/Services/WindowsService.cs new file mode 100644 index 0000000..0f8b4b3 --- /dev/null +++ b/FabApprovalWorkerService/Services/WindowsService.cs @@ -0,0 +1,40 @@ +using FabApprovalWorkerService.Models; + +namespace FabApprovalWorkerService.Services; + +public class WindowsService : BackgroundService { + private readonly ILogger _logger; + private readonly IMonInWorkerClient _monInClient; + + public WindowsService(ILogger logger, + IServiceProvider serviceProvider) { + _logger = logger ?? + throw new ArgumentNullException("ILogger not injected"); + using (IServiceScope scope = serviceProvider.CreateScope()) { + _monInClient = scope.ServiceProvider.GetService() ?? + throw new ArgumentNullException("IMonInWorkerClient not injected"); + } + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) { + _logger.LogInformation("Starting Windows service"); + + try { + while (!stoppingToken.IsCancellationRequested) { + await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); + + _monInClient.PostStatus("WindowsService", StatusValue.Ok); + } + } catch (OperationCanceledException) { + _logger.LogError("The Windows service has been stopped"); + + _monInClient.PostStatus("WindowsService", StatusValue.Critical); + } catch (Exception ex) { + _logger.LogError($"An exception occurred when running Windows Service. Exception: {ex.Message}"); + + _monInClient.PostStatus("WindowsService", StatusValue.Critical); + + Environment.Exit(1); + } + } +} diff --git a/FabApprovalWorkerService/SetupScripts/CreateECNTable.sql b/FabApprovalWorkerService/SetupScripts/CreateECNTable.sql index 304da20..a627c62 100644 --- a/FabApprovalWorkerService/SetupScripts/CreateECNTable.sql +++ b/FabApprovalWorkerService/SetupScripts/CreateECNTable.sql @@ -1,17 +1,16 @@ drop table if exists ECN; create table ECN ( - ECNID integer primary key, - ECNNumber integer not null, + ECNNumber integer primary key not null, IsTECN integer default 0, - ExpirationDate text not null, + ExpirationDate text, ExtensionDate text, OriginatorID integer not null, Title text not null ); insert into ECN (ECNNumber, IsTECN, ExpirationDate, ExtensionDate, OriginatorID, Title) -values (1, 0, '2024-03-30 00:00:00', '', 1, 'title1'), -(2, 1, '2024-04-01 00:00:00', '', 6, 'title2'), -(3, 1, '2024-06-01 00:00:00', '', 4, 'title3'), -(4, 1, '2024-04-01 00:00:00', '2024-06-01 00:00:00', 3, 'title4') \ No newline at end of file +values (1, 0, '2024-04-06 00:00:00', null, 1, 'title1'), +(2, 1, '2024-04-04 00:00:00', null, 6, 'title2'), +(3, 1, '2024-06-01 00:00:00', null, 4, 'title3'), +(4, 1, '2024-04-03 00:00:00', '2024-06-01 00:00:00', 3, 'title4') \ No newline at end of file diff --git a/FabApprovalWorkerService/SetupScripts/CreateTECNNotificationUsersTable.sql b/FabApprovalWorkerService/SetupScripts/CreateTECNNotificationUsersTable.sql index a1005bc..ea7c2ee 100644 --- a/FabApprovalWorkerService/SetupScripts/CreateTECNNotificationUsersTable.sql +++ b/FabApprovalWorkerService/SetupScripts/CreateTECNNotificationUsersTable.sql @@ -1,9 +1,9 @@ -drop table if exists TECNNotificationUsers; +drop table if exists TECNNotificationsUsers; -create table TECNNotificationUsers ( +create table TECNNotificationsUsers ( Id integer primary key, UserId integer not null ); -insert into TECNNotificationUsers (UserId) +insert into TECNNotificationsUsers (UserId) values (1), (2), (3); \ No newline at end of file diff --git a/FabApprovalWorkerService/Workers/ExpiringTECNWorker.cs b/FabApprovalWorkerService/Workers/ExpiringTECNWorker.cs index 1140e83..3b8e975 100644 --- a/FabApprovalWorkerService/Workers/ExpiringTECNWorker.cs +++ b/FabApprovalWorkerService/Workers/ExpiringTECNWorker.cs @@ -43,6 +43,8 @@ public class ExpiringTECNWorker : IJob { IEnumerable expiringTECNs = await _ecnService.GetExpiringTECNs(); + _logger.LogInformation($"There are {expiringTECNs.Count()} TECNs expiring in the next 5 days"); + foreach (ECN eCN in expiringTECNs) { string recipientEmail = await _userService.GetUserEmail(eCN.OriginatorID); MailAddress recipientAddress = new MailAddress(recipientEmail); @@ -55,9 +57,8 @@ public class ExpiringTECNWorker : IJob { } StringBuilder bodyBuilder = new(); - bodyBuilder.Append($"Good day, TECN# {eCN.ECNNumber} will be expiring in "); - bodyBuilder.Append($"{(eCN.ExpirationDate - DateTime.Now).Days} "); - bodyBuilder.Append($"on {eCN.ExpirationDate.ToString("MMMM dd, yyyy")}. "); + bodyBuilder.Append($"Good day, TECN# {eCN.ECNNumber} will be expire on "); + bodyBuilder.Append($"{eCN.ExpirationDate.ToString("MMMM dd, yyyy")}. "); bodyBuilder.Append($"
Review TECN "); bodyBuilder.Append("here "); diff --git a/FabApprovalWorkerServiceTests/SmtpServiceTests.cs b/FabApprovalWorkerServiceTests/SmtpServiceTests.cs index 191ca69..80a3a9f 100644 --- a/FabApprovalWorkerServiceTests/SmtpServiceTests.cs +++ b/FabApprovalWorkerServiceTests/SmtpServiceTests.cs @@ -112,6 +112,9 @@ internal class SmtpServiceTests { Assert.True(await _smtpService.SendEmail(ADDRESS_LIST, ADDRESS_LIST, "subject", "body")); - _mockSmtpClient.Verify(s => s.Send(It.IsAny())); + string? env = Environment.GetEnvironmentVariable("FabApprovalEnvironmentName"); + + if (env is not null && !env.ToLower().Equals("development")) + _mockSmtpClient.Verify(s => s.Send(It.IsAny())); } } diff --git a/FabApprovalWorkerServiceTests/TrainingServiceTests.cs b/FabApprovalWorkerServiceTests/TrainingServiceTests.cs new file mode 100644 index 0000000..8f75830 --- /dev/null +++ b/FabApprovalWorkerServiceTests/TrainingServiceTests.cs @@ -0,0 +1,29 @@ +using FabApprovalWorkerService.Services; + +using Microsoft.Extensions.Logging; + +using Moq; + +namespace FabApprovalWorkerServiceTests; +public class TrainingServiceTests { + private Mock> _mockLogger; + private Mock _mockDalService; + + private TrainingService _trainingService; + + [SetUp] + public void Setup() { + _mockLogger = new Mock>(); + _mockDalService = new Mock(); + } + + [Test] + public void TrainingServiceWithNullLoggerShouldThrowException() { + Assert.Throws(() => new TrainingService(null, _mockDalService.Object)); + } + + [Test] + public void TrainingServiceWithNullDalServiceShouldThrowException() { + Assert.Throws(() => new TrainingService(_mockLogger.Object, null)); + } +}