diff --git a/Apps/JobScheduler/src/Jobs/AssignBetaFeatureAccessJob.cs b/Apps/JobScheduler/src/Jobs/AssignBetaFeatureAccessJob.cs new file mode 100644 index 0000000000..1c5c849884 --- /dev/null +++ b/Apps/JobScheduler/src/Jobs/AssignBetaFeatureAccessJob.cs @@ -0,0 +1,135 @@ +// ------------------------------------------------------------------------- +// Copyright © 2019 Province of British Columbia +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------- +namespace HealthGateway.JobScheduler.Jobs +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Hangfire; + using HealthGateway.Common.Data.Utils; + using HealthGateway.Database.Constants; + using HealthGateway.Database.Context; + using HealthGateway.Database.Models; + using Microsoft.EntityFrameworkCore; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.Logging; + + /// + /// Assigns users access to a beta feature. + /// + /// The configuration to use. + /// The logger to use. + /// The database context to use. + public class AssignBetaFeatureAccessJob(IConfiguration configuration, ILogger logger, GatewayDbContext dbContext) + { + /// + /// Key used to identify this job in the config. + /// + public const string JobKey = "AssignBetaFeatureAccess"; + + private const string EnabledTemplateKey = "Enabled"; + private const string MaxBatchSizeKey = "MaxBatchSize"; + private const string UserCountKey = "UserCount"; + private const string BetaFeatureKey = "BetaFeature"; + private const int ConcurrencyTimeout = 5 * 60; // 5 Minutes + + private readonly bool enabled = configuration.GetValue($"{JobKey}:{EnabledTemplateKey}", false); + private readonly int maxBatchSize = configuration.GetValue($"{JobKey}:{MaxBatchSizeKey}", 0); + private readonly int userCount = configuration.GetValue($"{JobKey}:{UserCountKey}", 0); + private readonly BetaFeature betaFeature = configuration.GetValue($"{JobKey}:{BetaFeatureKey}", null) + ?? throw new ArgumentNullException(nameof(configuration), $"{JobKey}:{BetaFeatureKey} is null"); + + /// + /// Assigns users access to a beta feature. + /// + /// to manage the async request. + /// A representing the result of the asynchronous operation. + [DisableConcurrentExecution(ConcurrencyTimeout)] + public async Task ProcessAsync(CancellationToken ct = default) + { + if (!this.enabled) + { + logger.LogInformation("{JobKey} is disabled", JobKey); + return; + } + + if (this.userCount < 1) + { + logger.LogInformation("{JobKey}:{UserCountKey} must be at least 1", JobKey, UserCountKey); + return; + } + + if (this.maxBatchSize < 1) + { + logger.LogInformation("{JobKey}:{MaxBatchSizeKey} must be at least 1", JobKey, MaxBatchSizeKey); + return; + } + + logger.LogInformation("Starting process of assigning access to {BetaFeature} beta feature for up to {UserCount} user(s)", EnumUtility.ToEnumString(this.betaFeature), this.userCount); + int processedUserCount = 0; + while (processedUserCount < this.userCount) + { + int batchSize = Math.Min(this.maxBatchSize, this.userCount - processedUserCount); + + logger.LogInformation("Retrieving up to {BatchSize} user(s) with verified email addresses who have recently logged in", batchSize); + IList userProfiles = await this.GetRecentUserProfiles(processedUserCount, batchSize, ct); + + int retrievedCount = userProfiles.Count; + logger.LogInformation("Retrieved {RetrievedCount} user(s)", retrievedCount); + if (retrievedCount > 0) + { + logger.LogInformation("Adding beta feature access for retrieved user(s)"); + IEnumerable associations = userProfiles.Select(this.GenerateBetaFeatureAccess); + dbContext.BetaFeatureAccess.AddRange(associations); + await dbContext.SaveChangesAsync(ct); // commit after each iteration + } + + processedUserCount += retrievedCount; + if (retrievedCount < batchSize) + { + break; + } + } + + logger.LogInformation("Completed processing, having assigned beta feature access to {ProcessedUserCount} user(s)", processedUserCount); + } + + private async Task> GetRecentUserProfiles(int skip, int take, CancellationToken ct) + { + return await dbContext.UserProfile + .Where(u => !string.IsNullOrWhiteSpace(u.Email)) + .Where(u => u.BetaFeatureCodes.All(c => c.Code != this.betaFeature)) + .OrderByDescending(o => o.LastLoginDateTime) + .Skip(skip) + .Take(take) + .ToListAsync(ct); + } + + private BetaFeatureAccess GenerateBetaFeatureAccess(UserProfile userProfile) + { + DateTime now = DateTime.UtcNow; + return new BetaFeatureAccess + { + Hdid = userProfile.HdId, + BetaFeatureCode = this.betaFeature, + CreatedDateTime = now, + UpdatedDateTime = now, + }; + } + } +} diff --git a/Apps/JobScheduler/src/Startup.cs b/Apps/JobScheduler/src/Startup.cs index 7f88927b8e..8cb8b47f21 100644 --- a/Apps/JobScheduler/src/Startup.cs +++ b/Apps/JobScheduler/src/Startup.cs @@ -190,6 +190,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) SchedulerHelper.ScheduleJobAsync(this.configuration, "CloseAccounts", j => j.ProcessAsync(CancellationToken.None)); SchedulerHelper.ScheduleJobAsync(this.configuration, "OneTime", j => j.ProcessAsync(CancellationToken.None)); SchedulerHelper.ScheduleJobAsync(this.configuration, "DeleteEmailJob", j => j.DeleteOldEmailsAsync(CancellationToken.None)); + SchedulerHelper.ScheduleJobAsync(this.configuration, AssignBetaFeatureAccessJob.JobKey, j => j.ProcessAsync(CancellationToken.None)); } } } diff --git a/Apps/JobScheduler/src/appsettings.json b/Apps/JobScheduler/src/appsettings.json index 95ab18e7f4..4eda2e12d8 100644 --- a/Apps/JobScheduler/src/appsettings.json +++ b/Apps/JobScheduler/src/appsettings.json @@ -26,10 +26,7 @@ ] }, "RequestLogging": { - "ExcludedPaths": [ - "/stats", - "/health" - ] + "ExcludedPaths": ["/stats", "/health"] }, "OpenIdConnect": { "Authority": "https://loginproxy.gov.bc.ca/auth/realms/health-gateway-gold", @@ -72,9 +69,7 @@ "TraceConsoleExporterEnabled": false, "ZipkinEnabled": false, "ZipkinUri": "", - "IgnorePathPrefixes": [ - "/health" - ] + "IgnorePathPrefixes": ["/health"] }, "AllowOrigins": "*", "ConnectionStrings": { @@ -197,6 +192,16 @@ "TargetFolder": "Resources", "AppName": "PROV-DRUG" }, + "AssignBetaFeatureAccess": { + "Id": "Assign Beta Feature Access", + "Schedule": "30 16 * * *", + "Immediate": "false", + "Delay": 60, + "Enabled": true, + "MaxBatchSize": 500, + "UserCount": 100, + "BetaFeature": "Salesforce" + }, "PharmacyAssessmentFile": { "Url": "https://raw.githubusercontent.com/bcgov/pharmacy-assessment/main/Pharmacy%20Assessment%20PIN.csv", "TargetFolder": "Resources",