Skip to content
Merged

Dev #2577

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
cc9ead7
AB#30134: Use ApproxNumberOfEmployees Instead Of OrganizationSize
aurelio-aot Jun 16, 2026
931726a
Merge branch 'dev' into feature/AB#30134-Replace-OrganizationSize-Wit…
aurelio-aot Jun 16, 2026
f60cba7
Merge branch 'dev' into feature/AB#30134-Replace-OrganizationSize-Wit…
aurelio-aot Jun 16, 2026
83ca4e6
Merge pull request #2567 from bcgov/feature/AB#30134-Replace-Organiza…
aurelio-aot Jun 16, 2026
e053a09
[AB#32708] Fix preview attachment menu item alignment
plavoie-BC Jun 17, 2026
5059805
[AB#32438] Improve width of Applicant Portal Configuration table
plavoie-BC Jun 17, 2026
bdcffaf
Merge pull request #2570 from bcgov/bugfix/AB#32708-css-bugfixes
JamesPasta Jun 17, 2026
0d10791
[AB#33225] Update EmailConsumer to log sent date on success
plavoie-BC Jun 17, 2026
2267598
[AB#33225] Add datafix for SentDateTime in EmailLogs table
plavoie-BC Jun 17, 2026
a08d342
[AB#33225] Add Sent Date and adjust EmailHistoryWidget layout
plavoie-BC Jun 17, 2026
8ff7a16
[AB#33225] Refine EmailLogs update to set SentDateTime only if null
plavoie-BC Jun 17, 2026
eb2e14d
[AB#33225] SonarQube and quality fixes
plavoie-BC Jun 17, 2026
3cc285c
AB#33332: Additional Columns In Application/Applicant Payment List
aurelio-aot Jun 17, 2026
98369cd
AB#33332: Open COLUMNS Menu Upward If No Space Below
aurelio-aot Jun 17, 2026
1799585
AB#33332: Fix Sonarqube Issues
aurelio-aot Jun 17, 2026
50dc1b6
AB#29931 - Added Recipient to EmailLog
hasanpour Jun 18, 2026
c2a6c07
AB#33471 - Added View Notification List permission
hasanpour Jun 18, 2026
6efa19c
AB#29928 - Populate Recipient and EmailType on sent emails
hasanpour Jun 18, 2026
877001e
AB#29928 - Added NotificationList app service
hasanpour Jun 18, 2026
2b4148f
AB#33470: Bugfix: Dates Off By 1 Day
aurelio-aot Jun 18, 2026
ccb4064
AB#29928 - Added Notification List page and menu
hasanpour Jun 18, 2026
a584b32
AB#29928 - Validate notification list sort input
hasanpour Jun 18, 2026
ae0c8f5
AB#29928 - Fixed Copilot mentioned issues.
hasanpour Jun 18, 2026
0cf118a
AB#31529 update length check for radio length worksheet
AndreGAot Jun 19, 2026
66f990f
Merge pull request #2576 from bcgov/bugfix/AB#31529-ws-radio-validation
AndreGAot Jun 19, 2026
c462a44
Merge pull request #2571 from bcgov/feature/AB#33225-email-history-se…
JamesPasta Jun 19, 2026
e0b16a1
Merge pull request #2572 from bcgov/feature/AB#33332-Payment-List-Inv…
JamesPasta Jun 19, 2026
ef3dc20
Merge pull request #2574 from bcgov/feature/AB#29928-Notification-List
JamesPasta Jun 19, 2026
a36f1ca
Merge pull request #2573 from bcgov/bugfix/AB#33470-DecisionDate-Dont…
JamesPasta Jun 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
{
<div class="option-container" id="option@(Model.Options.IndexOf(option) + 1)Container">
<input type="radio" name="radio_input" />
<input type="text" id="option@(Model.Options.IndexOf(option) + 1)" name="Options" class="form-control" value="@option" />
<input type="text" id="option@(Model.Options.IndexOf(option) + 1)" name="Options" class="form-control" value="@option" minlength="1" maxlength="60" />
<button type="button" class="btn btn-danger btn-sm remove-option-btn" data-option-id="option@(Model.Options.IndexOf(option) + 1)Container">Delete Option</button>
</div>
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Unity.Notifications.Localization;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.Features;
using Volo.Abp.Localization;
using Volo.Abp.SettingManagement;

Expand All @@ -19,6 +20,15 @@ public override void Define(IPermissionDefinitionContext context)
NotificationsPermissions.Email.Send,
L($"Permission:{NotificationsPermissions.Email.Send}"));

var notificationListPermission = notificationsPermissionsGroup.AddPermission(
NotificationsPermissions.NotificationList.Default,
L($"Permission:{NotificationsPermissions.NotificationList.Default}"))
.RequireFeatures("Unity.Notifications");

notificationListPermission.AddChild(
NotificationsPermissions.NotificationList.View,
L($"Permission:{NotificationsPermissions.NotificationList.View}"));

var settingManagement = context.GetGroup(SettingManagementPermissions.GroupName);
settingManagement.AddPermission(NotificationsPermissions.Settings, L("Permission:NotificationsPermissions.Settings"));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ public static class Email
public const string Send = "Notifications.Email.Send";
}

public static class NotificationList
{
public const string Default = "Notifications.NotificationList";
public const string View = "Notifications.NotificationList.View";
}

public static string[] GetAll()
{
return ReflectionHelper.GetPublicConstantsRecursively(typeof(NotificationsPermissions));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<PackageReference Include="System.Text.Json" Version="10.0.3" />
<PackageReference Include="Volo.Abp.Ddd.Application.Contracts" Version="10.3.0" />
<PackageReference Include="Volo.Abp.Authorization" Version="10.3.0" />
<PackageReference Include="Volo.Abp.Features" Version="10.3.0" />
<PackageReference Include="Volo.Abp.SettingManagement.Application.Contracts" Version="10.3.0" />
<ProjectReference Include="..\Unity.Notifications.Domain.Shared\Unity.Notifications.Domain.Shared.csproj" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public class EmailNotificationEvent
[JsonConverter(typeof(JsonStringEnumConverter))]
public EmailAction Action { get; set; }
public string? EmailTemplateName { get; set; } = string.Empty;
public RecipientType? Recipient { get; set; }
public EmailType? EmailType { get; set; }
public List<EmailAttachmentData>? EmailAttachments { get; set; }
public List<Guid>? PaymentRequestIds { get; set; }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,15 @@ private async Task<EmailLog> InitializeEmail(string emailTo, string body, string
{
string emailToAddress = String.Join(",", eventData.EmailAddressList);

return await InitializeEmailAndUploadAttachments(
var emailLog = await InitializeEmailAndUploadAttachments(
emailToAddress,
eventData.Body,
FAILED_PAYMENTS_SUBJECT,
eventData.ApplicationId,
eventData.EmailFrom,
eventData.EmailTemplateName);

return await StampClassificationAsync(emailLog, EmailType.EventBased, RecipientType.Internal);
}
case EmailAction.SendCustom:
return await HandleSendCustomEmail(eventData);
Expand All @@ -151,10 +153,9 @@ private async Task<EmailLog> InitializeEmail(string emailTo, string body, string
if (eventData.PaymentRequestIds != null && eventData.PaymentRequestIds.Count != 0)
{
emailLog.PaymentRequestIds = string.Join(",", eventData.PaymentRequestIds);
await emailLogsRepository.UpdateAsync(emailLog, autoSave: true);
}

return emailLog;
return await StampClassificationAsync(emailLog, EmailType.EventBased, RecipientType.Internal);
}
case EmailAction.Retry:
default:
Expand All @@ -167,10 +168,13 @@ private async Task<EmailLog> InitializeEmail(string emailTo, string body, string
string emailToAddress = String.Join(",", eventData.EmailAddressList);
string? emailCC = eventData.Cc?.Any() == true ? String.Join(",", eventData.Cc) : null;
string? emailBCC = eventData.Bcc?.Any() == true ? String.Join(",", eventData.Bcc) : null;


var emailType = eventData.EmailType ?? EmailType.Manual;
var recipient = eventData.Recipient ?? RecipientType.External;

if (eventData.Id == Guid.Empty)
{
return await InitializeEmailAndUploadAttachments(
var newEmailLog = await InitializeEmailAndUploadAttachments(
emailToAddress,
eventData.Body,
eventData.Subject,
Expand All @@ -180,6 +184,8 @@ private async Task<EmailLog> InitializeEmail(string emailTo, string body, string
emailCC,
emailBCC,
eventData.EmailAttachments);

return await StampClassificationAsync(newEmailLog, emailType, recipient);
}

EmailLog? emailLog = await emailNotificationService.UpdateEmailLog(
Expand All @@ -196,12 +202,20 @@ private async Task<EmailLog> InitializeEmail(string emailTo, string body, string

if (emailLog != null)
{
return emailLog;
return await StampClassificationAsync(emailLog, emailType, recipient);
}

throw new UserFriendlyException("Unable to update Email Log");
}

private async Task<EmailLog> StampClassificationAsync(EmailLog emailLog, EmailType emailType, RecipientType recipient)
{
emailLog.EmailType = emailType;
emailLog.Recipient = recipient;
await emailLogsRepository.UpdateAsync(emailLog, autoSave: true);
return emailLog;
}

private async Task HandleSaveDraftEmail(EmailNotificationEvent eventData)
{
string emailToAddress = String.Join(",", eventData.EmailAddressList);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
using Amazon.S3;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Amazon.S3;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Unity.Modules.Shared.MessageBrokers.RabbitMQ.Interfaces;
using Unity.Notifications.EmailNotifications;
using Unity.Notifications.Emails;
using Unity.Notifications.Events;
using Unity.Notifications.Integrations.RabbitMQ.QueueMessages;
using Volo.Abp;
using Volo.Abp.Data;
using Volo.Abp.EventBus.Local;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Uow;
using Unity.Notifications.Emails;
using Volo.Abp.EventBus.Local;

namespace Unity.Notifications.Integrations.RabbitMQ;

Expand Down Expand Up @@ -193,24 +193,47 @@ private async Task UpdateEmailLogStatus(EmailLog log, HttpResponseMessage respon
? EmailStatus.Sent
: EmailStatus.Failed;

// Publish local event for successful FSB payment notifications
if (response.IsSuccessStatusCode && !string.IsNullOrEmpty(log.PaymentRequestIds))
// Log SentDateTime on success
if (response.IsSuccessStatusCode)
{
var paymentIds = log.PaymentRequestIds
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(id => Guid.Parse(id))
.ToList();
// Use CHES response Date header (response time) or current time as fallback
var sentDateTime = SanitizeResponseDateTime(response.Headers?.Date?.UtcDateTime);
log.SentDateTime = sentDateTime;

await localEventBus.PublishAsync(new FsbEmailSentEto
// Publish local event for successful FSB payment notifications
if (!string.IsNullOrEmpty(log.PaymentRequestIds))
{
EmailLogId = log.Id,
PaymentRequestIds = paymentIds,
SentDate = DateTime.UtcNow,
TenantId = log.TenantId
});
var paymentIds = log.PaymentRequestIds
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(id => Guid.Parse(id))
.ToList();

await localEventBus.PublishAsync(new FsbEmailSentEto
{
EmailLogId = log.Id,
PaymentRequestIds = paymentIds,
SentDate = sentDateTime,
TenantId = log.TenantId
});
}
}
}

private static readonly TimeSpan _maxFutureSkew = TimeSpan.FromMinutes(5);
private static readonly TimeSpan _maxPastAge = TimeSpan.FromMinutes(15);

// CHES may return a Date header that is incorrect due to clock skew or environment misconfiguration.
// We will sanitize it as an external value by ensuring it's not in the future or past beyond a reasonable threshold.
private static DateTime SanitizeResponseDateTime(DateTime? headerDate)
{
if (headerDate is null) return DateTime.UtcNow;

var now = DateTime.UtcNow;
return headerDate.Value >= now - _maxPastAge && headerDate.Value <= now + _maxFutureSkew
? headerDate.Value
: now;
}

// -----------------------------
// SAFE-CONCURRENCY SAVE
// -----------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;

namespace Unity.Notifications.Emails;

[JsonConverter(typeof(JsonStringEnumConverter))]
public enum EmailType
{
Manual,
Scheduled,
EventBased,
Delayed
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Text.Json.Serialization;

namespace Unity.Notifications.Emails;

[JsonConverter(typeof(JsonStringEnumConverter))]
public enum RecipientType
{
Internal,
External
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@
"Setting:Notifications.Mailing.DefaultFromAddress.Description": "The default From sender address",
"Permission:Notifications": "Notifications",
"Permission:Notifications.Email": "Email",
"Permission:Notifications.Email.Send": "Send Email"
"Permission:Notifications.Email.Send": "Send Email",
"Permission:Notifications.NotificationList": "Notification List",
"Permission:Notifications.NotificationList.View": "View Notification List",
"Menu:Notifications": "Notifications",
"Search": "Search",
"Filter": "Filter",
"Export": "Export",
"NotificationList:NotificationId": "Notification Id",
"NotificationList:SubmissionId": "Submission Id",
"NotificationList:ApplicantName": "Applicant Name",
"NotificationList:SentDate": "Sent Date",
"NotificationList:Status": "Status",
"NotificationList:From": "From",
"NotificationList:To": "To",
"NotificationList:Subject": "Subject",
"NotificationList:Recipient": "Recipient",
"NotificationList:EmailType": "Email Type"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public class EmailLog : AuditedAggregateRoot<Guid>, IMultiTenant
public string ChesStatus { get; set; } = string.Empty;
public string? ChesHttpStatusCode { get; set; }
public string Status { get; set; } = string.Empty;
public RecipientType? Recipient { get; set; }
public EmailType? EmailType { get; set; }
public DateTime? SendOnDateTime { get; set; }
public DateTime? SentDateTime { get; set; }
public string TemplateName { get; set; } = string.Empty;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@

namespace Unity.Notifications.Emails
{
public interface IEmailLogsRepository : IBasicRepository<EmailLog, Guid>
public interface IEmailLogsRepository : IRepository<EmailLog, Guid>
{
Task<EmailLog?> GetByIdAsync(Guid id, bool includeDetails = false);
Task<List<EmailLog>> GetByApplicationIdAsync(Guid applicationId);
Task<EmailLog?> GetByIdAsync(Guid id, bool includeDetails = false);
Task<List<EmailLog>> GetByApplicationIdAsync(Guid applicationId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ public static void ConfigureNotifications(
NotificationsDbProperties.DbSchema);

b.ConfigureByConvention();

b.Property(x => x.Recipient).HasConversion<string>().HasMaxLength(32);
b.Property(x => x.EmailType).HasConversion<string>().HasMaxLength(32);
});

modelBuilder.Entity<EmailLogAttachment>(b =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
using System.Threading.Tasks;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Unity.Notifications.Localization;
using Unity.Notifications.Permissions;
using Volo.Abp.Features;
using Volo.Abp.UI.Navigation;

namespace Unity.Notifications.Web.Menus;
Expand All @@ -7,19 +11,27 @@ public class NotificationsMenuContributor : IMenuContributor
{
public async Task ConfigureMenuAsync(MenuConfigurationContext context)
{
if (context.Menu.Name == StandardMenus.Main)
var featureChecker = context.ServiceProvider.GetRequiredService<IFeatureChecker>();

if (await featureChecker.IsEnabledAsync("Unity.Notifications") && context.Menu.Name == StandardMenus.Main)
{
await ConfigureMainMenuAsync(context);
ConfigureMainMenu(context);
}
}

#pragma warning disable S1172 // Unused method parameters should be removed
#pragma warning disable IDE0060 // Remove unused parameter
private static Task ConfigureMainMenuAsync(MenuConfigurationContext context)
#pragma warning restore IDE0060 // Remove unused parameter
#pragma warning restore S1172 // Unused method parameters should be removed
private static void ConfigureMainMenu(MenuConfigurationContext context)
{
// Add main menu items.
return Task.CompletedTask;
var l = context.GetLocalizer<NotificationsResource>();

context.Menu.AddItem(
new ApplicationMenuItem(
NotificationsMenus.NotificationList,
l["Menu:Notifications"],
"~/Notifications",
icon: "fl fl-mail",
order: 9,
requiredPermissionName: NotificationsPermissions.NotificationList.View
)
);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
namespace Unity.Notifications.Web.Menus;
namespace Unity.Notifications.Web.Menus;

public static class NotificationsMenus
{
public const string Prefix = "Notifications";

//Add your menu items here...
public const string NotificationList = Prefix + ".NotificationList";
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
@page
@page
@using Microsoft.Extensions.Localization
@using Unity.Notifications.Localization
@using Unity.Notifications.Web.Pages.Notifications
@model Unity.Notifications.Web.Pages.Notifications.IndexModel
@inject IStringLocalizer<NotificationsResource> L

@{
@section scripts {
<abp-script-bundle>
<abp-script src="/Pages/Notifications/Index.js" />
</abp-script-bundle>
}
<h1>Notifications</h1>
<p>@L["SamplePageMessage"]</p>

<div class="container-fluid px-2">
<div class="action-bar p-2 search-action-bar d-flex align-items-center gap-2">
<abp-card-title class="mb-0">@L["Menu:Notifications"]</abp-card-title>
<label for="search" class="visually-hidden">@L["Search"]</label>
<input type="search" id="search" class="tbl-search" placeholder="@L["Search"]" aria-label="@L["Search"]" />
<span id="dynamicButtonContainerId" class="ms-auto"></span>
</div>
<abp-table id="NotificationListTable"></abp-table>
</div>
Loading
Loading