diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/FlexTypeExtensions.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/FlexTypeExtensions.cs index 22b68bdb8..e96a7ce99 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/FlexTypeExtensions.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/FlexTypeExtensions.cs @@ -57,15 +57,27 @@ public static string ConvertInputType(this CustomFieldType type) public static CustomFieldDefinition? ConvertDefinition(this string definition, QuestionType type) { - return type switch + if (string.IsNullOrWhiteSpace(definition)) { - QuestionType.Text => JsonSerializer.Deserialize(definition), - QuestionType.Number => JsonSerializer.Deserialize(definition), - QuestionType.YesNo => JsonSerializer.Deserialize(definition), - QuestionType.SelectList => JsonSerializer.Deserialize(definition), - QuestionType.TextArea => JsonSerializer.Deserialize(definition), - _ => null, - }; + return null; + } + + try + { + return type switch + { + QuestionType.Text => JsonSerializer.Deserialize(definition), + QuestionType.Number => JsonSerializer.Deserialize(definition), + QuestionType.YesNo => JsonSerializer.Deserialize(definition), + QuestionType.SelectList => JsonSerializer.Deserialize(definition), + QuestionType.TextArea => JsonSerializer.Deserialize(definition), + _ => null, + }; + } + catch (JsonException) + { + return null; + } } public static string[] GetCheckedOptions(this string value) diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/NumericDefinitionWidget/NumericDefinitionWidget.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/NumericDefinitionWidget/NumericDefinitionWidget.cs index b8a1e9d89..f0d4745c1 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/NumericDefinitionWidget/NumericDefinitionWidget.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/NumericDefinitionWidget/NumericDefinitionWidget.cs @@ -29,23 +29,36 @@ public class NumericDefinitionWidget : AbpViewComponent .ApplyRequired(form); } + // Cache JsonSerializerOptions instance + private static readonly JsonSerializerOptions CachedJsonOptions = new JsonSerializerOptions + { + NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString + }; + public async Task InvokeAsync(string? definition) { - if (definition != null) + NumericDefinitionViewModel viewModel = new(); + + if (!string.IsNullOrWhiteSpace(definition)) { - NumericDefinition? numericDefinition = JsonSerializer.Deserialize(definition); - if (numericDefinition != null) + try { - return View(await Task.FromResult(new NumericDefinitionViewModel() + var numericDefinition = JsonSerializer.Deserialize(definition, CachedJsonOptions); + + if (numericDefinition != null) { - Min = numericDefinition.Min, - Max = numericDefinition.Max, - Required = numericDefinition.Required - })); + viewModel.Min = numericDefinition.Min; + viewModel.Max = numericDefinition.Max; + viewModel.Required = numericDefinition.Required; + } + } + catch (JsonException) + { + // Optionally log the error } } - return View(await Task.FromResult(new NumericDefinitionViewModel())); + return View(await Task.FromResult(viewModel)); } } diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/Emails/EmailHistoryDto.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/Emails/EmailHistoryDto.cs index e885a6fdb..be4828939 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/Emails/EmailHistoryDto.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/Emails/EmailHistoryDto.cs @@ -10,6 +10,8 @@ public class EmailHistoryDto : ExtensibleAuditedEntityDto public string Status { get; set; } = string.Empty; public string FromAddress { get; set; } = string.Empty; public string ToAddress { get; set; } = string.Empty; + public string Cc { get; set; } = string.Empty; + public string Bcc { get; set; } = string.Empty; public DateTime? SentDateTime { get; set; } public string Body { get; set; } = string.Empty; public EmailHistoryUserDto? SentBy { get; set; } diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationService.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationService.cs index 27b7283d9..5c084550a 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationService.cs @@ -1,28 +1,29 @@ using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Net; using System.Net.Http; using System.Threading.Tasks; -using System.Linq.Expressions; +using Unity.Modules.Shared.Utils; using Unity.Notifications.Emails; using Unity.Notifications.Events; using Unity.Notifications.Integrations.Ches; using Unity.Notifications.Integrations.RabbitMQ; +using Unity.Notifications.Permissions; +using Unity.Notifications.Settings; using Unity.Notifications.TeamsNotifications; +using Volo.Abp; using Volo.Abp.Application.Services; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Entities; -using Volo.Abp.Users; -using Volo.Abp.SettingManagement; -using Unity.Notifications.Settings; -using Unity.Notifications.Permissions; -using Volo.Abp; using Volo.Abp.Features; -using Microsoft.AspNetCore.Http; +using Volo.Abp.SettingManagement; +using Volo.Abp.Users; namespace Unity.Notifications.EmailNotifications; @@ -83,14 +84,14 @@ public async Task GetEmailsChesWithNoResponseCountAsync() return emailLogs?.Count ?? 0; } - public async Task UpdateEmailLog(Guid emailId, string emailTo, string body, string subject, Guid applicationId, string? emailFrom, string? status, string? emailTemplateName) + public async Task UpdateEmailLog(Guid emailId, string emailTo, string body, string subject, Guid applicationId, string? emailFrom, string? status, string? emailTemplateName, string? emailCC = null, string? emailBCC = null) { if (string.IsNullOrEmpty(emailTo)) { return null; } - var emailObject = await GetEmailObjectAsync(emailTo, body, subject, emailFrom, "html", emailTemplateName); + var emailObject = await GetEmailObjectAsync(emailTo, body, subject, emailFrom, "html", emailTemplateName, emailCC, emailBCC); EmailLog emailLog = await _emailLogsRepository.GetAsync(emailId); emailLog = UpdateMappedEmailLog(emailLog, emailObject); emailLog.ApplicationId = applicationId; @@ -102,19 +103,19 @@ public async Task GetEmailsChesWithNoResponseCountAsync() return loggedEmail; } - public async Task InitializeEmailLog(string emailTo, string body, string subject, Guid applicationId, string? emailFrom, string? emailTemplateName) + public async Task InitializeEmailLog(string emailTo, string body, string subject, Guid applicationId, string? emailFrom, string? emailTemplateName, string? emailCC = null, string? emailBCC = null) { - return await InitializeEmailLog(emailTo, body, subject, applicationId, emailFrom, EmailStatus.Initialized, emailTemplateName); + return await InitializeEmailLog(emailTo, body, subject, applicationId, emailFrom, EmailStatus.Initialized, emailTemplateName, emailCC, emailBCC); } [RemoteService(false)] - public async Task InitializeEmailLog(string emailTo, string body, string subject, Guid applicationId, string? emailFrom, string? status, string? emailTemplateName) + public async Task InitializeEmailLog(string emailTo, string body, string subject, Guid applicationId, string? emailFrom, string? status, string? emailTemplateName, string? emailCC = null, string? emailBCC = null) { if (string.IsNullOrEmpty(emailTo)) { return null; } - var emailObject = await GetEmailObjectAsync(emailTo, body, subject, emailFrom, "html", emailTemplateName); + var emailObject = await GetEmailObjectAsync(emailTo, body, subject, emailFrom, "html", emailTemplateName, emailCC, emailBCC); EmailLog emailLog = new EmailLog(); emailLog = UpdateMappedEmailLog(emailLog, emailObject); emailLog.ApplicationId = applicationId; @@ -218,9 +219,12 @@ public async Task SendCommentNotification(EmailCommentDto i /// The body of the email /// Subject Message /// From Email Address - /// Type of body email: html or text + /// Type of body email: html or text + /// Template name for the email + /// CC email addresses + /// BCC email addresses /// HttpResponseMessage indicating the result of the operation - public async Task SendEmailNotification(string emailTo, string body, string subject, string? emailFrom, string? emailBodyType, string? emailTemplateName) + public async Task SendEmailNotification(string emailTo, string body, string subject, string? emailFrom, string? emailBodyType, string? emailTemplateName, string? emailCC = null, string? emailBCC = null) { try { @@ -234,7 +238,7 @@ public async Task SendEmailNotification(string emailTo, str } // Send the email using the CHES client service - var emailObject = await GetEmailObjectAsync(emailTo, body, subject, emailFrom, emailBodyType, emailTemplateName); + var emailObject = await GetEmailObjectAsync(emailTo, body, subject, emailFrom, emailBodyType, emailTemplateName, emailCC, emailBCC); var response = await _chesClientService.SendAsync(emailObject); // Assuming SendAsync returns a HttpResponseMessage or equivalent: @@ -312,15 +316,11 @@ public async Task SendEmailToQueue(EmailLog emailLog) await _emailQueueService.SendToEmailEventQueueAsync(emailNotificationEvent); } - protected virtual async Task GetEmailObjectAsync(string emailTo, string body, string subject, string? emailFrom, string? emailBodyType, string? emailTemplateName) + protected virtual async Task GetEmailObjectAsync(string emailTo, string body, string subject, string? emailFrom, string? emailBodyType, string? emailTemplateName, string? emailCC = null, string? emailBCC = null) { - List toList = new(); - string[] emails = emailTo.Split([',', ';'], StringSplitOptions.RemoveEmptyEntries); - - foreach (string email in emails) - { - toList.Add(email.Trim()); - } + var toList = emailTo.ParseEmailList() ?? []; + var ccList = emailCC.ParseEmailList(); + var bccList = emailBCC.ParseEmailList(); var defaultFromAddress = await SettingProvider.GetOrNullAsync(NotificationsSettings.Mailing.DefaultFromAddress); @@ -328,6 +328,8 @@ protected virtual async Task GetEmailObjectAsync(string emailTo, string { body, bodyType = emailBodyType ?? "text", + cc = ccList, + bcc = bccList, encoding = "utf-8", from = emailFrom ?? defaultFromAddress ?? "NoReply@gov.bc.ca", priority = "normal", @@ -345,7 +347,9 @@ protected virtual EmailLog UpdateMappedEmailLog(EmailLog emailLog, dynamic email emailLog.Subject = emailDynamicObject.subject; emailLog.BodyType = emailDynamicObject.bodyType; emailLog.FromAddress = emailDynamicObject.from; - emailLog.ToAddress = String.Join(",", emailDynamicObject.to); + emailLog.ToAddress = string.Join(",", emailDynamicObject.to); + emailLog.CC = emailDynamicObject.cc != null ? string.Join(",", (IEnumerable)emailDynamicObject.cc) : string.Empty; + emailLog.BCC = emailDynamicObject.bcc != null ? string.Join(",", (IEnumerable)emailDynamicObject.bcc) : string.Empty; emailLog.TemplateName = emailDynamicObject.templateName; return emailLog; } @@ -357,7 +361,8 @@ public async Task UpdateSettings(NotificationsSettingsDto settingsDto) await UpdateTenantSettings(NotificationsSettings.Mailing.EmailMaxRetryAttempts, settingsDto.MaximumRetryAttempts); } - private async Task UpdateTenantSettings(string settingKey, string valueString) { + private async Task UpdateTenantSettings(string settingKey, string valueString) + { if (!valueString.IsNullOrWhiteSpace()) { await _settingManager.SetForCurrentTenantAsync(settingKey, valueString); diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/IEmailNotificationService.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/IEmailNotificationService.cs index 5cb556889..d1be9e42a 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/IEmailNotificationService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/IEmailNotificationService.cs @@ -10,12 +10,12 @@ namespace Unity.Notifications.EmailNotifications { public interface IEmailNotificationService : IApplicationService { - Task UpdateEmailLog(Guid emailId, string emailTo, string body, string subject, Guid applicationId, string? emailFrom, string? status, string? emailTemplateName); - Task InitializeEmailLog(string emailTo, string body, string subject, Guid applicationId, string? emailFrom, string? status, string? emailTemplateName); - Task InitializeEmailLog(string emailTo, string body, string subject, Guid applicationId, string? emailFrom, string? emailTemplateName); + Task UpdateEmailLog(Guid emailId, string emailTo, string body, string subject, Guid applicationId, string? emailFrom, string? status, string? emailTemplateName, string? emailCC = null, string? emailBCC = null); + Task InitializeEmailLog(string emailTo, string body, string subject, Guid applicationId, string? emailFrom, string? status, string? emailTemplateName, string? emailCC = null, string? emailBCC = null); + Task InitializeEmailLog(string emailTo, string body, string subject, Guid applicationId, string? emailFrom, string? emailTemplateName, string? emailCC = null, string? emailBCC = null); Task GetEmailLogById(Guid id); Task SendCommentNotification(EmailCommentDto input); - Task SendEmailNotification(string emailTo, string body, string subject, string? emailFrom, string? emailBodyType, string? emailTemplateName); + Task SendEmailNotification(string emailTo, string body, string subject, string? emailFrom, string? emailBodyType, string? emailTemplateName, string? emailCC = null, string? emailBCC = null); Task SendEmailToQueue(EmailLog emailLog); Task> GetHistoryByApplicationId(Guid applicationId); Task UpdateSettings(NotificationsSettingsDto settingsDto); diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Events/EmailNotificationEvent.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Events/EmailNotificationEvent.cs index a502e50a8..4d14db077 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Events/EmailNotificationEvent.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Events/EmailNotificationEvent.cs @@ -16,7 +16,9 @@ public class EmailNotificationEvent public string Subject { get; set; } = string.Empty; public string? EmailFrom { get; set; } = string.Empty; public string EmailAddress { get; set; } = string.Empty; - public List EmailAddressList { get; set; } = new List(); + public List EmailAddressList { get; set; } = []; + public IEnumerable Cc { get; set; } = []; + public IEnumerable Bcc { get; set; } = []; [JsonConverter(typeof(JsonStringEnumConverter))] public EmailAction Action { get; set; } diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Events/EmailNotificationHandler.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Events/EmailNotificationHandler.cs index f924ad56a..b53c3c860 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Events/EmailNotificationHandler.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Events/EmailNotificationHandler.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Threading.Tasks; using Unity.Notifications.EmailNotifications; using Unity.Notifications.Emails; @@ -25,7 +26,7 @@ public async Task HandleEventAsync(EmailNotificationEvent eventData) } } - private async Task InitializeAndSendEmailToQueue(string emailTo, string body, string subject, Guid applicationId, string? emailFrom, string? emailTemplateName) + private async Task InitializeAndSendEmailToQueue(string emailTo, string body, string subject, Guid applicationId, string? emailFrom, string? emailTemplateName, string? emailCC = null, string? emailBCC = null) { EmailLog emailLog = await InitializeEmail( emailTo, @@ -34,12 +35,14 @@ private async Task InitializeAndSendEmailToQueue(string emailTo, string body, st applicationId, emailFrom, EmailStatus.Initialized, - emailTemplateName); + emailTemplateName, + emailCC, + emailBCC); await emailNotificationService.SendEmailToQueue(emailLog); } - private async Task InitializeEmail(string emailTo, string body, string subject, Guid applicationId, string? emailFrom, string status, string? emailTemplateName) + private async Task InitializeEmail(string emailTo, string body, string subject, Guid applicationId, string? emailFrom, string status, string? emailTemplateName, string? emailCC = null, string? emailBCC = null) { EmailLog emailLog = await emailNotificationService.InitializeEmailLog( emailTo, @@ -48,7 +51,9 @@ private async Task InitializeEmail(string emailTo, string body, string applicationId, emailFrom, status, - emailTemplateName) ?? throw new UserFriendlyException("Unable to Initialize Email Log"); + emailTemplateName, + emailCC, + emailBCC) ?? throw new UserFriendlyException("Unable to Initialize Email Log"); return emailLog; } @@ -85,9 +90,12 @@ private async Task HandleSendCustomEmail(EmailNotificationEvent eventData) 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; + if (eventData.Id == Guid.Empty) { - await InitializeAndSendEmailToQueue(emailToAddress, eventData.Body, eventData.Subject, eventData.ApplicationId, eventData.EmailFrom,eventData.EmailTemplateName); + await InitializeAndSendEmailToQueue(emailToAddress, eventData.Body, eventData.Subject, eventData.ApplicationId, eventData.EmailFrom, eventData.EmailTemplateName, emailCC, emailBCC); } else { @@ -99,7 +107,9 @@ private async Task HandleSendCustomEmail(EmailNotificationEvent eventData) eventData.ApplicationId, eventData.EmailFrom, EmailStatus.Initialized, - eventData.EmailTemplateName); + eventData.EmailTemplateName, + emailCC, + emailBCC); if (emailLog != null) { @@ -115,9 +125,9 @@ private async Task HandleSendCustomEmail(EmailNotificationEvent eventData) private async Task HandleSaveDraftEmail(EmailNotificationEvent eventData) { - - 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; if (eventData.Id != Guid.Empty) { @@ -129,7 +139,9 @@ await emailNotificationService.UpdateEmailLog( eventData.ApplicationId, eventData.EmailFrom, EmailStatus.Draft, - eventData.EmailTemplateName); + eventData.EmailTemplateName, + emailCC, + emailBCC); } else { @@ -140,7 +152,9 @@ await InitializeEmail( eventData.ApplicationId, eventData.EmailFrom, EmailStatus.Draft, - eventData.EmailTemplateName); + eventData.EmailTemplateName, + emailCC, + emailBCC); } } diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Integrations/RabbitMQ/EmailConsumer.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Integrations/RabbitMQ/EmailConsumer.cs index 2c901596c..51e5ce76d 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Integrations/RabbitMQ/EmailConsumer.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Integrations/RabbitMQ/EmailConsumer.cs @@ -64,7 +64,9 @@ private async Task ProcessEmailLogAsync(EmailLog emailLog, EmailNotificationEven emailLog.Body, emailLog.Subject, emailLog.FromAddress, "html", - emailLog.TemplateName); + emailLog.TemplateName, + emailLog.CC, + emailLog.BCC); // Update the response emailLog.ChesResponse = JsonConvert.SerializeObject(response); emailLog.ChesStatus = response.StatusCode.ToString(); diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Emails/EmailLog.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Emails/EmailLog.cs index ae9b3c16b..9d304685a 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Emails/EmailLog.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Emails/EmailLog.cs @@ -14,21 +14,21 @@ public class EmailLog : AuditedAggregateRoot, IMultiTenant public Guid AssessmentId { get; set; } public Guid ApplicationId { get; set; } public Guid ApplicantId { get; set; } - public string FromAddress { get; set; } = ""; - public string ToAddress { get; set; } = ""; - public string CC { get; set; } = ""; - public string BCC { get; set; } = ""; - public string Subject { get; set; } = ""; - public string Body { get; set; } = ""; - public string BodyType { get; set; } = ""; - public string Priority { get; set; } = ""; - public string Tag { get; set; } = ""; + public string FromAddress { get; set; } = string.Empty; + public string ToAddress { get; set; } = string.Empty; + public string CC { get; set; } = string.Empty; + public string BCC { get; set; } = string.Empty; + public string Subject { get; set; } = string.Empty; + public string Body { get; set; } = string.Empty; + public string BodyType { get; set; } = string.Empty; + public string Priority { get; set; } = string.Empty; + public string Tag { get; set; } = string.Empty; public int RetryAttempts { get; set; } public Guid? ChesMsgId { get; set; } - public string ChesResponse { get; set; } = ""; - public string ChesStatus { get; set; } = ""; - public string Status { get; set; } = ""; + public string ChesResponse { get; set; } = string.Empty; + public string ChesStatus { get; set; } = string.Empty; + public string Status { get; set; } = string.Empty; public DateTime? SendOnDateTime { get; set; } public DateTime? SentDateTime { get; set; } - public string TemplateName { get; set; } = ""; + public string TemplateName { get; set; } = string.Empty; } diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/PaymentsManager.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/PaymentsManager.cs index d53a2defe..73bd07d3c 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/PaymentsManager.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/PaymentsManager.cs @@ -135,12 +135,12 @@ public async Task TriggerAction(Guid paymentRequestsId, PaymentA else if (triggerAction == PaymentApprovalAction.Submit) { - if (HasPermission(PaymentsPermissions.Payments.L2ApproveOrDecline)) + if (HasPermission(PaymentsPermissions.Payments.L2ApproveOrDecline) && paymentRequest.Status == PaymentRequestStatus.L2Pending) { var index = paymentRequest.ExpenseApprovals.FindIndex(i => i.Type == ExpenseApprovalType.Level2); paymentRequest.ExpenseApprovals[index].Approve(currentUserId); } - else if (HasPermission(PaymentsPermissions.Payments.L3ApproveOrDecline)) + else if (HasPermission(PaymentsPermissions.Payments.L3ApproveOrDecline) && paymentRequest.Status == PaymentRequestStatus.L3Pending) { var index = paymentRequest.ExpenseApprovals.FindIndex(i => i.Type == ExpenseApprovalType.Level3); paymentRequest.ExpenseApprovals[index].Approve(currentUserId); diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/PaymentRequestAppService.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/PaymentRequestAppService.cs index 421acc35e..c34ff1413 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/PaymentRequestAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/PaymentRequestAppService.cs @@ -20,6 +20,7 @@ using Volo.Abp.Users; using Unity.Payments.Domain.PaymentThresholds; using Volo.Abp.Domain.Repositories; +using Unity.GrantManager.Applications; namespace Unity.Payments.PaymentRequests { @@ -30,6 +31,8 @@ public class PaymentRequestAppService( ICurrentUser currentUser, IDataFilter dataFilter, IExternalUserLookupServiceProvider externalUserLookupServiceProvider, + IApplicationRepository applicationRepository, + IApplicationFormRepository applicationFormRepository, IPaymentConfigurationRepository paymentConfigurationRepository, IPaymentsManager paymentsManager, IPaymentRequestRepository paymentRequestsRepository, @@ -219,28 +222,78 @@ private async Task DetermineTriggerActionAsync( UpdatePaymentStatusRequestDto dto, PaymentRequest payment) { - if (await CanPerformLevel1ActionAsync(payment.Status)) + if (payment == null) { - return dto.IsApprove ? PaymentApprovalAction.L1Approve : PaymentApprovalAction.L1Decline; + Logger.LogWarning("Payment is null in DetermineTriggerActionAsync."); + return PaymentApprovalAction.None; } - if (await CanPerformLevel2ActionAsync(payment, dto.IsApprove)) + try { - if (dto.IsApprove) - { - return PaymentApprovalAction.Submit; - } - return PaymentApprovalAction.L2Decline; - } + if (await CanPerformLevel1ActionAsync(payment.Status)) + return dto.IsApprove ? PaymentApprovalAction.L1Approve : PaymentApprovalAction.L1Decline; - if (await CanPerformLevel3ActionAsync(payment.Status)) + if (await CanPerformLevel2ActionAsync(payment, dto.IsApprove)) + return await GetLevel2ApprovalActionAsync(dto, payment); + + if (await CanPerformLevel3ActionAsync(payment.Status)) + return dto.IsApprove ? PaymentApprovalAction.Submit : PaymentApprovalAction.L3Decline; + } + catch (Exception ex) { - return dto.IsApprove ? PaymentApprovalAction.Submit : PaymentApprovalAction.L3Decline; + Logger.LogException(ex); } return PaymentApprovalAction.None; } + private async Task GetLevel2ApprovalActionAsync(UpdatePaymentStatusRequestDto dto, PaymentRequest payment) + { + if (!dto.IsApprove) + return PaymentApprovalAction.L2Decline; + + decimal? threshold = null; + try + { + decimal? userPaymentThreshold = await GetUserPaymentThresholdAsync(); + threshold = await GetPaymentRequestThresholdByApplicationIdAsync(payment.CorrelationId, userPaymentThreshold); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to get payment threshold for applicationId: {CorrelationId}", payment.CorrelationId); + } + + if (threshold.HasValue && payment.Amount > threshold.Value) + return PaymentApprovalAction.L2Approve; + + return PaymentApprovalAction.Submit; + } + public async Task GetPaymentRequestThresholdByApplicationIdAsync(Guid applicationId, decimal? userPaymentThreshold = null) + { + var application = await (await applicationRepository.GetQueryableAsync()) + .Include(a => a.ApplicationForm) + .FirstOrDefaultAsync(a => a.Id == applicationId); + + if (application == null) + { + throw new BusinessException($"Application with Id {applicationId} not found."); + } + + var appForm = application.ApplicationForm ?? + (application.ApplicationFormId != Guid.Empty + ? await applicationFormRepository.GetAsync(application.ApplicationFormId) + : null); + + var formThreshold = appForm?.PaymentApprovalThreshold; + + if (formThreshold.HasValue && userPaymentThreshold.HasValue) + { + return Math.Min(formThreshold.Value, userPaymentThreshold.Value); + } + + return formThreshold ?? userPaymentThreshold ?? 0m; + } + private async Task CanPerformLevel1ActionAsync(PaymentRequestStatus status) { List level1Approvals = new() { PaymentRequestStatus.L1Pending, PaymentRequestStatus.L1Declined }; diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.TestBase/PaymentsTestBase.cs b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.TestBase/PaymentsTestBase.cs index a03d5e689..0260896c3 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.TestBase/PaymentsTestBase.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.TestBase/PaymentsTestBase.cs @@ -11,6 +11,7 @@ using Unity.Payments.Security; using Volo.Abp.SettingManagement; using Volo.Abp.TenantManagement; +using Unity.GrantManager.Applications; namespace Unity.Payments; @@ -69,6 +70,10 @@ protected override void AfterAddApplication(IServiceCollection services) featureMock.IsEnabledAsync(Arg.Any()).Returns(true); services.AddSingleton(featureMock); + // Mock the repositories to avoid database access + services.AddSingleton(Substitute.For()); + services.AddSingleton(Substitute.For()); + var externalUserLookupMock = Substitute.For(); services.AddSingleton(externalUserLookupMock); diff --git a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Utils/StringExtensions.cs b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Utils/StringExtensions.cs index 018d89f13..d28d8f508 100644 --- a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Utils/StringExtensions.cs +++ b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Utils/StringExtensions.cs @@ -34,5 +34,32 @@ public static double CompareStrings(this string str1, string str2) double percentage = union == 0 ? 0.0 : 2.0 * intersection * 100 / union; return Math.Round(Math.Min(percentage, 100.0), 2); // Ensure it does not exceed 100% and round to 2 decimals } + + public static List? ParseEmailList(this string? emailAddresses) + { + if (string.IsNullOrWhiteSpace(emailAddresses)) + return null; + + var emailAddressList = emailAddresses.Split([',', ';'], StringSplitOptions.RemoveEmptyEntries) + .Select(e => e.Trim()) + .Where(e => !string.IsNullOrWhiteSpace(e)) + .Distinct() + .ToList(); + + return emailAddressList.Count > 0 ? emailAddressList : null; + } + + public static HashSet? ParseEmailSet(this string? emailAddresses) + { + if (string.IsNullOrWhiteSpace(emailAddresses)) + return null; + + var emailAddressList = emailAddresses.Split([',', ';'], StringSplitOptions.RemoveEmptyEntries) + .Select(e => e.Trim()) + .Where(e => !string.IsNullOrWhiteSpace(e)) + .ToHashSet(); + + return emailAddressList.Count > 0 ? emailAddressList : null; + } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Emails/CreateEmailDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Emails/CreateEmailDto.cs index f6ddffba4..8433ffe51 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Emails/CreateEmailDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Emails/CreateEmailDto.cs @@ -18,6 +18,11 @@ public class CreateEmailDto [Required] public string EmailBody { get; set; } = string.Empty; + + public string? EmailCC { get; set; } + + public string? EmailBCC { get; set; } + public Guid ApplicationId { get; set; } public Guid OwnerId { get; set; } public Guid EmailId { get; set; } = Guid.Empty; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Emails/EmailAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Emails/EmailAppService.cs index db050d151..352622ac4 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Emails/EmailAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Emails/EmailAppService.cs @@ -1,7 +1,6 @@ using Microsoft.AspNetCore.Authorization; -using System; -using System.Collections.Generic; using System.Threading.Tasks; +using Unity.Modules.Shared.Utils; using Unity.Notifications.Emails; using Unity.Notifications.Events; using Volo.Abp.Application.Services; @@ -33,15 +32,11 @@ public async Task SaveDraftAsync(CreateEmailDto dto) private static EmailNotificationEvent GetEmailNotificationEvent(CreateEmailDto dto) { - List toList = []; - string[] emails = dto.EmailTo.Split([',', ';'], StringSplitOptions.RemoveEmptyEntries); + var toList = dto.EmailTo.ParseEmailList() ?? []; + var ccList = dto.EmailCC.ParseEmailList() ?? []; + var bccList = dto.EmailBCC.ParseEmailList() ?? []; - foreach (string email in emails) - { - toList.Add(email.Trim()); - } - return - new EmailNotificationEvent + return new EmailNotificationEvent { Id = dto.EmailId, ApplicationId = dto.ApplicationId, @@ -49,6 +44,8 @@ private static EmailNotificationEvent GetEmailNotificationEvent(CreateEmailDto d EmailAddress = dto.EmailTo, EmailAddressList = toList, EmailFrom = dto.EmailFrom, + Cc = ccList, + Bcc = bccList, Subject = dto.EmailSubject, Body = dto.EmailBody, EmailTemplateName = dto.EmailTemplateName diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationApplicantAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationApplicantAppService.cs index dba7b46c6..b6c9a02de 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationApplicantAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationApplicantAppService.cs @@ -164,7 +164,7 @@ public async Task UpdatePartialApplicantInfoAsync(Guid appl if (input.Data.ContactInfo != null && await AuthorizationService.IsGrantedAsync(UnitySelector.Applicant.Contact.Update)) { - await CreateOrUpdateContactInfoAsync(application.ApplicantId, input.Data.ContactInfo); + await CreateOrUpdateContactInfoAsync(applicationId, application.ApplicantId, input.Data.ContactInfo); } //-- APPLICANT INFO - SIGNING AUTHORITY (APPLICATION) @@ -250,13 +250,13 @@ public async Task UpdatePartialApplicantInfoAsync(Guid appl /// /// [Authorize(UnitySelector.Applicant.Contact.Update)] - protected internal async Task CreateOrUpdateContactInfoAsync(Guid applicantId, ContactInfoDto contactInfo) + protected internal async Task CreateOrUpdateContactInfoAsync(Guid applicationId, Guid applicantId, ContactInfoDto contactInfo) { - var applicantAgent = await applicantAgentRepository.FirstOrDefaultAsync(a => a.ApplicantId == applicantId) + var applicantAgent = await applicantAgentRepository.FirstOrDefaultAsync(a => a.ApplicantId == applicantId && a.ApplicationId == applicationId) ?? new ApplicantAgent { ApplicantId = applicantId, - ApplicationId = contactInfo.ApplicationId, + ApplicationId = applicationId, }; ObjectMapper.Map(contactInfo, applicantAgent); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.cshtml index 832884db3..3627fab78 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.cshtml @@ -65,18 +65,20 @@ -