Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Linq.Expressions;
using Unity.Notifications.Emails;
using Unity.Notifications.Events;
using Unity.Notifications.Integrations.Ches;
Expand Down Expand Up @@ -60,51 +61,35 @@ IHttpContextAccessor httpContextAccessor
_httpContextAccessor = httpContextAccessor;
}

private const string approvalBody =
@"Hello,<br>
<br>
Thank you for your grant application. We are pleased to inform you that your project has been approved for funding.<br>
A representative from our Program Area will be reaching out to you shortly with more information on next steps.<br>
<br>
Kind regards.<br>
<br>
*ATTENTION - Please do not reply to this email as it is an automated notification which is unable to receive replies.<br>";

private const string declineBody =
@"Hello,<br>
<br>
Thank you for your application. We would like to advise you that after careful consideration, your project was not selected to receive funding from our Program.<br>
<br>
We know that a lot of effort goes into developing a proposed project and we appreciate the time you took to prepare your application.<br>
<br>
If you have any questions or concerns, please reach out to program team members who will provide further details regarding the funding decision.<br>
<br>
Thank you again for your application.<br>
<br>
*ATTENTION - Please do not reply to this email as it is an automated notification which is unable to receive replies.<br>";

public string GetApprovalBody()
public async Task DeleteEmail(Guid id)
{
return approvalBody;
await _emailLogsRepository.DeleteAsync(id);
}

public string GetDeclineBody()
public async Task<int> GetEmailsChesWithNoResponseCountAsync()
{
return declineBody;
}
var dbNow = DateTime.UtcNow;

public async Task DeleteEmail(Guid id)
{
await _emailLogsRepository.DeleteAsync(id);
// Create the expression to filter the email logs
Expression<Func<EmailLog, bool>> filter = x =>
(x.Status == EmailStatus.Sent && x.ChesResponse == null) ||
(x.Status == EmailStatus.Initialized && x.CreationTime.AddMinutes(10) < dbNow);

// Fetch all email logs and apply the filter using LINQ
var allEmailLogs = await _emailLogsRepository.GetListAsync();
var emailLogs = allEmailLogs.Where(filter.Compile()).ToList();

// Ensure we're returning 0 if no logs are found
return emailLogs?.Count ?? 0;
}

public async Task<EmailLog?> UpdateEmailLog(Guid emailId, string emailTo, string body, string subject, Guid applicationId, string? emailFrom, string? status,string? emailTemplateName)
public async Task<EmailLog?> UpdateEmailLog(Guid emailId, string emailTo, string body, string subject, Guid applicationId, string? emailFrom, string? status, string? emailTemplateName)
{
if (string.IsNullOrEmpty(emailTo))
{
return null;
}

var emailObject = await GetEmailObjectAsync(emailTo, body, subject, emailFrom, "html", emailTemplateName);
EmailLog emailLog = await _emailLogsRepository.GetAsync(emailId);
emailLog = UpdateMappedEmailLog(emailLog, emailObject);
Expand Down Expand Up @@ -170,7 +155,6 @@ public async Task<HttpResponseMessage> SendCommentNotification(EmailCommentDto i
var pathBase = "/GrantApplications/Details?ApplicationId=";
var baseUrl = $"{scheme}://{host}{pathBase}";
var commentLink = $"{baseUrl}{input.ApplicationId}";

var subject = $"Unity-Comment: {input.Subject}";
var fromEmail = defaultFromAddress ?? "NoReply@gov.bc.ca";
string htmlBody = $@"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,9 @@ public interface IEmailNotificationService : IApplicationService
Task<HttpResponseMessage> SendCommentNotification(EmailCommentDto input);
Task<HttpResponseMessage> SendEmailNotification(string emailTo, string body, string subject, string? emailFrom, string? emailBodyType, string? emailTemplateName);
Task SendEmailToQueue(EmailLog emailLog);
string GetApprovalBody();
string GetDeclineBody();
Task<List<EmailHistoryDto>> GetHistoryByApplicationId(Guid applicationId);
Task UpdateSettings(NotificationsSettingsDto settingsDto);
Task DeleteEmail(Guid id);
Task<int> GetEmailsChesWithNoResponseCountAsync();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ public async Task<decimal> GetTotalPaymentRequestAmountByCorrelationIdAsync(Guid
var dbSet = await GetDbSetAsync();
decimal applicationPaymentRequestsTotal = dbSet
.Where(p => p.CorrelationId.Equals(correlationId))
.Where(p => p.Status != PaymentRequestStatus.L1Declined && p.Status != PaymentRequestStatus.L2Declined && p.Status != PaymentRequestStatus.L3Declined)
.Where(p => p.Status != PaymentRequestStatus.L1Declined
&& p.Status != PaymentRequestStatus.L2Declined
&& p.Status != PaymentRequestStatus.L3Declined
&& p.InvoiceStatus != CasPaymentRequestStatus.ErrorFromCas)
.GroupBy(p => p.CorrelationId)
.Select(p => p.Sum(q => q.Amount))
.FirstOrDefault();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using Microsoft.Extensions.Logging;
using Quartz;
using System;
using System.Threading.Tasks;
using Unity.GrantManager.Settings;
using Unity.Modules.Shared.Utils;
using Unity.Notifications.EmailNotifications;
using Unity.Payments.Domain.PaymentRequests;
using Unity.Payments.Enums;
using Volo.Abp.BackgroundWorkers.Quartz;
using Volo.Abp.MultiTenancy;
using Volo.Abp.SettingManagement;
using Volo.Abp.TenantManagement;

namespace Unity.GrantManager.HealthChecks.BackgroundWorkers
{
[DisallowConcurrentExecution]
public class DataHealthCheckWorker : QuartzBackgroundWorkerBase
{
private readonly ICurrentTenant _currentTenant;
private readonly ITenantRepository _tenantRepository;
private readonly IEmailNotificationService _emailNotificationService;
private readonly IPaymentRequestRepository _paymentRequestsRepository;

public DataHealthCheckWorker(ICurrentTenant currentTenant,
ITenantRepository tenantRepository,
ISettingManager settingManager,
IEmailNotificationService emailNotificationService,
IPaymentRequestRepository paymentRequestsRepository)
{
_currentTenant = currentTenant;
_tenantRepository = tenantRepository;
_emailNotificationService = emailNotificationService;
_paymentRequestsRepository = paymentRequestsRepository;

string? envInfo = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");

if (string.Equals(envInfo, "Production", StringComparison.OrdinalIgnoreCase))
{
string cronExpression = SettingDefinitions.GetSettingsValue(settingManager, SettingsConstants.BackgroundJobs.DataHealthCheckMonitor_Expression);

JobDetail = JobBuilder
.Create<DataHealthCheckWorker>()
.WithIdentity(nameof(DataHealthCheckWorker))
.Build();

Trigger = TriggerBuilder
.Create()
.WithIdentity(nameof(DataHealthCheckWorker))
.WithSchedule(CronScheduleBuilder.CronSchedule(cronExpression)
.WithMisfireHandlingInstructionIgnoreMisfires())
.Build();
}
}

public override async Task Execute(IJobExecutionContext context)
{
Logger.LogInformation("Executing DataHealthCheckWorker...");
var tenants = await _tenantRepository.GetListAsync();
bool sendEmail = false;
var emailBodyBuilder = new System.Text.StringBuilder();

foreach (var tenant in tenants)
{
using (_currentTenant.Change(tenant.Id, tenant.Name))
{
// Lookup the missing emails
var missingEmailsCount = await _emailNotificationService.GetEmailsChesWithNoResponseCountAsync();
if (missingEmailsCount > 0)
{
Logger.LogWarning("Tenant {TenantName} has {MissingEmailsCount} missing email(s) with a status of Initialized or Sent but no CHES Response.", tenant.Name, missingEmailsCount);
string missingEmailBody = $"Unity tenant {tenant.Name} has {missingEmailsCount} email(s) that were sent but have no CHES Response.";
sendEmail = true;
emailBodyBuilder.AppendLine($"{missingEmailBody}<br />");
}
// Lookup the missing payments
var missingPayments = await GetPaymentsSentWithoutResponseCountAsync();
if (missingPayments > 0)
{
Logger.LogWarning("Tenant {TenantName} has {MissingPaymentsCount} payments sent without a response.", tenant.Name, missingPayments);
string missingPaymentBody = $"Unity tenant {tenant.Name} has {missingPayments} payment(s) that are in Submitted status but have no CAS Response.";
sendEmail = true;
emailBodyBuilder.AppendLine($"{missingPaymentBody}<br />");
}
}
}

if (sendEmail)
{
string emailBody = emailBodyBuilder.ToString();
await SendEmailAlert(emailBody, "Data Health Check Alert - Emails/Payments Missing Responses");
}

Logger.LogInformation("DataHealthCheckWorker Executed...");
await Task.CompletedTask;
}

private async Task<int> GetPaymentsSentWithoutResponseCountAsync()
{
var payments = await _paymentRequestsRepository.GetListAsync(x => x.Status == PaymentRequestStatus.Submitted && x.CasHttpStatusCode == null);
return payments.Count;
}

private async Task SendEmailAlert(string emailBody, string subject)
{

string htmlBody = $@"
<html>
<body style='font-family: Arial, sans-serif;'>
<p>{emailBody}</p>
<br />
<p style='font-size: 12px; color: #999;'>*Note - Please do not reply to this email as it is an automated notification.</p>
</body>
</html>";

await _emailNotificationService.SendEmailNotification(
"grantmanagementsupport@gov.bc.ca",
htmlBody,
subject,
"NoReply@gov.bc.ca", "html",
"");

Logger.LogInformation("Missing Alerts Sent...");

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public static class BackgroundJobs
{
public const string IntakeResync_Expression = "GrantManager.BackgroundJobs.IntakeResync_Expression";
public const string IntakeResync_NumDaysToCheck = "GrantManager.BackgroundJobs.IntakeResync_NumDaysToCheck";
public const string DataHealthCheckMonitor_Expression = "GrantManager.BackgroundJobs.DataHealthCheckMonitor_Expression";
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using Unity.GrantManager.Localization;
using Unity.Payments.Settings;
using Unity.Payments.Settings;
using Volo.Abp.Localization;
using Volo.Abp.Settings;

Expand All @@ -11,13 +11,13 @@ public class GrantManagerSettingDefinitionProvider : SettingDefinitionProvider
public override void Define(ISettingDefinitionContext context)
{
context.Add(
new SettingDefinition(
SettingsConstants.SectorFilterName,
string.Empty,
L("Setting:GrantManager.Locality.SectorFilter.DisplayName"),
L("Setting:GrantManager.Locality.SectorFilter.Description"),
isVisibleToClients: true,
isInherited: false,
new SettingDefinition(
SettingsConstants.SectorFilterName,
string.Empty,
L("Setting:GrantManager.Locality.SectorFilter.DisplayName"),
L("Setting:GrantManager.Locality.SectorFilter.Description"),
isVisibleToClients: true,
isInherited: false,
isEncrypted: false).WithProviders(TenantSettingValueProvider.ProviderName)
);

Expand All @@ -37,40 +37,42 @@ public override void Define(ISettingDefinitionContext context)
}

context.Add(
new SettingDefinition(
SettingsConstants.UI.Zones,
string.Empty,
L($"Setting:{SettingsConstants.UI.Zones}.DisplayName"),
L($"Setting:{SettingsConstants.UI.Zones}.Description"),
isVisibleToClients: false,
isInherited: false,
new SettingDefinition(
SettingsConstants.UI.Zones,
string.Empty,
L($"Setting:{SettingsConstants.UI.Zones}.DisplayName"),
L($"Setting:{SettingsConstants.UI.Zones}.Description"),
isVisibleToClients: false,
isInherited: false,
isEncrypted: false)
.WithProviders(
TenantSettingValueProvider.ProviderName,
FormSettingValueProvider.ProviderName)
);

AddBackgroundJobSettingDefinition(context);
}

private static void AddBackgroundJobSettingDefinition(ISettingDefinitionContext currentContext)
{
var backGroundSchedules = new Dictionary<string, string>
{
{ SettingsConstants.BackgroundJobs.IntakeResync_NumDaysToCheck, "-4" },
// 23 = 11 pm So 23 + 8 UTC = 7 also at 19 = 11 am
{ SettingsConstants.BackgroundJobs.IntakeResync_Expression, "0 0 7,19 1/1 * ? *" },
// 24 = 12 am So 24 + 8 UTC = 8
{ PaymentSettingsConstants.BackgroundJobs.CasPaymentsReconciliation_ProducerExpression, "0 0 8 1/1 * ? *" },
// 24 = 1 am So 24 + 8 UTC = 9
{ PaymentSettingsConstants.BackgroundJobs.CasFinancialNotificationSummary_ProducerExpression, "0 0 9 1/1 * ? *" }
};

foreach (var setting in backGroundSchedules)
{
AddSettingDefinition(currentContext, setting.Key, setting.Value.ToString());
}


AddBackgroundJobSettingDefinition(context);
}

private static void AddBackgroundJobSettingDefinition(ISettingDefinitionContext currentContext)
{
var backGroundSchedules = new Dictionary<string, string>
{
{ SettingsConstants.BackgroundJobs.IntakeResync_NumDaysToCheck, "-4" },
// 23 = 11 pm So 23 + 8 UTC = 7 also at 19 = 11 am
{ SettingsConstants.BackgroundJobs.IntakeResync_Expression, "0 0 7,19 1/1 * ? *" },
// 24 = 12 am So 24 + 8 UTC = 8
{ PaymentSettingsConstants.BackgroundJobs.CasPaymentsReconciliation_ProducerExpression, "0 0 8 1/1 * ? *" },
// 24 = 1 am So 24 + 8 UTC = 9
{ PaymentSettingsConstants.BackgroundJobs.CasFinancialNotificationSummary_ProducerExpression, "0 0 9 1/1 * ? *" },
// Run hourly
{ SettingsConstants.BackgroundJobs.DataHealthCheckMonitor_Expression, "0 0 * 1/1 * ? *" }
};

foreach (var setting in backGroundSchedules)
{
AddSettingDefinition(currentContext, setting.Key, setting.Value.ToString());
}

}

private static void AddSettingDefinition(ISettingDefinitionContext currentContext, string settingName, string defaultValue = "True")
Expand All @@ -88,8 +90,8 @@ private static void AddSettingDefinition(ISettingDefinitionContext currentContex
isInherited: false,
isEncrypted: false).WithProviders(TenantSettingValueProvider.ProviderName)
);
}
}

private static LocalizableString L(string name)
{
return LocalizableString.Create<GrantManagerResource>(name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ public class GrantManagerDbContext :
public DbSet<RegionalDistrict> RegionalDistricts { get; set; }
public DbSet<TenantToken> TenantTokens { get; set; }
public DbSet<Community> Communities { get; set; }
public DbSet<ChefsMissedSubmission> ChefsMissedSubmissions { get; set; }


#region Entities from the modules
Expand Down Expand Up @@ -148,14 +147,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
b.ConfigureByConvention();
});

modelBuilder.Entity<ChefsMissedSubmission>(b =>
{
b.ToTable(GrantManagerConsts.DbTablePrefix + "ChefsMissedSubmissions",
GrantManagerConsts.DbSchema);

b.ConfigureByConvention();
});

var allEntityTypes = modelBuilder.Model.GetEntityTypes();
foreach (var type in allEntityTypes.Where(t => t.ClrType != typeof(ExtraPropertyDictionary)).Select(t => t.ClrType))
{
Expand Down
Loading