From 9098487b7eece365ebc74ea6e24c525b1640e5a5 Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Tue, 26 May 2026 15:39:15 -0700 Subject: [PATCH 1/2] feature/AB#32841-AddQRandPaymentDescription-Fixes --- .../Integrations/Cas/InvoiceService.cs | 374 ++++++++++++------ .../FinancialNotificationSummaryWorker.cs | 68 ++-- .../Unity.SharedKernel.csproj | 1 + .../Utilities/AbpUserTenantAccessor.cs | 29 +- .../TenantRepository_Tests.cs | 1 - .../Middleware/AbpUserTenantAccessor.cs | 50 +-- 6 files changed, 316 insertions(+), 207 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/InvoiceService.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/InvoiceService.cs index 01a02afc5a..03ed621231 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/InvoiceService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/InvoiceService.cs @@ -20,6 +20,7 @@ using System.Linq; using Volo.Abp.Domain.Repositories; using Unity.SharedKernel.Utilities; +using Volo.Abp.Identity; namespace Unity.Payments.Integrations.Cas { @@ -31,10 +32,14 @@ public class InvoiceService( IEndpointManagementAppService endpointManagementAppService, ICasTokenService iTokenService, IResilientHttpRequest resilientHttpRequest, - IInvoiceManager invoiceManager) : ApplicationService, IInvoiceService + IInvoiceManager invoiceManager, + IRepository expenseApprovalRepository, + IRepository identityUserRepository) : ApplicationService, IInvoiceService { private const string CFS_APINVOICE = "cfs/apinvoice"; - protected new ICurrentTenant CurrentTenant => LazyServiceProvider.LazyGetRequiredService(); + + protected new ICurrentTenant CurrentTenant => + LazyServiceProvider.LazyGetRequiredService(); private readonly Dictionary CASPaymentGroup = new() { @@ -42,127 +47,192 @@ public class InvoiceService( [(int)PaymentGroup.Cheque] = "GEN CHQ" }; - protected virtual async Task InitializeCASInvoice(PaymentRequest paymentRequest, - string? accountDistributionCode) + protected virtual async Task InitializeCASInvoice( + PaymentRequest paymentRequest, + string? accountDistributionCode) { - Invoice? casInvoice = new(); Site? site = await invoiceManager.GetSiteByPaymentRequestAsync(paymentRequest); - if (site != null && site.Supplier != null && site.Supplier.Number != null && accountDistributionCode != null) + if (site == null || + site.Supplier == null || + string.IsNullOrWhiteSpace(site.Supplier.Number) || + string.IsNullOrWhiteSpace(accountDistributionCode)) + { + return null; + } + + + // This can not be UTC Now it is sent to cas and can not be in the future - this is not being stored in Unity as a date + var vancouverTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"); + var localDateTime = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, vancouverTimeZone); + var currentMonth = localDateTime.ToString("MMM").Trim('.'); + var currentDay = localDateTime.ToString("dd"); + var currentYear = localDateTime.ToString("yyyy"); + var dateStringDayMonYear = $"{currentDay}-{currentMonth}-{currentYear}"; + + if (!CASPaymentGroup.TryGetValue((int)site.PaymentGroup, out var payGroup)) { - // This can not be UTC Now it is sent to cas and can not be in the future - this is not being stored in Unity as a date - var vancouverTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"); - var localDateTime = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, vancouverTimeZone); - var currentMonth = localDateTime.ToString("MMM").Trim('.'); - var currentDay = localDateTime.ToString("dd"); - var currentYear = localDateTime.ToString("yyyy"); - var dateStringDayMonYear = $"{currentDay}-{currentMonth}-{currentYear}"; - - casInvoice.SupplierNumber = site.Supplier.Number; // This is from each Applicant - casInvoice.SupplierName = site.Supplier.Name; - casInvoice.SupplierSiteNumber = site.Number; - casInvoice.PayGroup = CASPaymentGroup[(int)site.PaymentGroup]; // GEN CHQ - other options - casInvoice.InvoiceNumber = paymentRequest.InvoiceNumber; - casInvoice.InvoiceDate = dateStringDayMonYear; //DD-MMM-YYYY - casInvoice.DateInvoiceReceived = dateStringDayMonYear; - casInvoice.GlDate = dateStringDayMonYear; - casInvoice.InvoiceAmount = paymentRequest.Amount; - casInvoice.InvoiceBatchName = paymentRequest.BatchName; + throw new UserFriendlyException( + $"Unsupported payment group: {site.PaymentGroup}"); + } + + var casInvoice = new Invoice + { + SupplierNumber = site.Supplier.Number, + SupplierName = site.Supplier.Name, + SupplierSiteNumber = site.Number, + PayGroup = payGroup, + InvoiceNumber = paymentRequest.InvoiceNumber, + InvoiceDate = dateStringDayMonYear, + DateInvoiceReceived = dateStringDayMonYear, + GlDate = dateStringDayMonYear, + InvoiceAmount = paymentRequest.Amount, + InvoiceBatchName = paymentRequest.BatchName, + // Payment description: build or use existing - casInvoice.PaymentAdviceComments = await BuildPaymentDescriptionAsync(paymentRequest.Description); + PaymentAdviceComments = + await BuildPaymentDescriptionAsync(paymentRequest.Description), - // Set QualifiedReceiver to the Level1 approver's user name (lookup by DecisionUserId when possible) - casInvoice.QualifiedReceiver = await GetLevel1DecisionUserNameAsync(paymentRequest); + // Level1 approver username + QualifiedReceiver = + await GetLevel1DecisionUserNameAsync(paymentRequest), - InvoiceLineDetail invoiceLineDetail = new() + InvoiceLineDetails = new List { - InvoiceLineNumber = 1, - InvoiceLineAmount = paymentRequest.Amount, - DefaultDistributionAccount = accountDistributionCode // This will be at the tenant level - }; - casInvoice.InvoiceLineDetails = new List { invoiceLineDetail }; - } + new() + { + InvoiceLineNumber = 1, + InvoiceLineAmount = paymentRequest.Amount, + DefaultDistributionAccount = accountDistributionCode + } + } + }; return casInvoice; } - private async Task GetLevel1DecisionUserNameAsync(PaymentRequest paymentRequest) + private async Task GetLevel1DecisionUserNameAsync( + PaymentRequest? paymentRequest) { - if (paymentRequest?.ExpenseApprovals == null) + if (paymentRequest == null) + { return string.Empty; + } - var decisionUserId = paymentRequest.ExpenseApprovals.FirstOrDefault(x => x.Type == ExpenseApprovalType.Level1)?.DecisionUserId; - if (decisionUserId == null || decisionUserId == Guid.Empty) - return string.Empty; + Guid? decisionUserId = null; try { - // Try to resolve a repository for IdentityUser: IRepository - var repoType = typeof(IRepository<,>).MakeGenericType(typeof(Volo.Abp.Identity.IdentityUser), typeof(Guid)); - var repoObj = LazyServiceProvider.LazyGetService(repoType); - if (repoObj != null) + if (paymentRequest.ExpenseApprovals == null || + paymentRequest.ExpenseApprovals.Count == 0) { - // Use dynamic to call FindAsync - dynamic repo = repoObj; - var user = await repo.FindAsync((Guid)decisionUserId); - if (user != null) - { - // Prefer UserName, then full name (Name + Surname), then fallback to id - string? userName = user.UserName as string; - if (!string.IsNullOrWhiteSpace(userName)) - return userName; - - var givenName = user.Name as string; - var surname = user.Surname as string; - if (!string.IsNullOrWhiteSpace(givenName) || !string.IsNullOrWhiteSpace(surname)) - return $"{givenName} {surname}".Trim(); - } + var approvals = await expenseApprovalRepository.GetListAsync( + a => a.PaymentRequestId == paymentRequest.Id && + a.Type == ExpenseApprovalType.Level1); + + decisionUserId = approvals + .FirstOrDefault()? + .DecisionUserId; + } + else + { + decisionUserId = paymentRequest.ExpenseApprovals + .FirstOrDefault(x => x.Type == ExpenseApprovalType.Level1)? + .DecisionUserId; + } + + if (decisionUserId == null || decisionUserId == Guid.Empty) + { + return string.Empty; + } + + var user = await identityUserRepository.FindAsync( + (Guid)decisionUserId); + + if (user == null) + { + return string.Empty; + } + + if (!string.IsNullOrWhiteSpace(user.UserName)) + { + return user.UserName; } + + var fullName = $"{user.Name} {user.Surname}".Trim(); + + return string.IsNullOrWhiteSpace(fullName) + ? string.Empty + : fullName; } - catch + catch (Exception ex) { - // ignore and fallback - } + Logger.LogWarning( + ex, + "Failed resolving Level1 approver for payment request {PaymentRequestId}", + paymentRequest.Id); - return string.Empty; + return string.Empty; + } } - // Tenant/CurrentUser lookups: resolve from IServiceProvider (no dependency on Web project utilities) - private async Task BuildPaymentDescriptionAsync(string? existingDescription) + private async Task BuildPaymentDescriptionAsync( + string? existingDescription) { if (!string.IsNullOrWhiteSpace(existingDescription)) { var trimmed = existingDescription.Trim(); - return trimmed.Length > 50 ? trimmed.Substring(0, 50) : trimmed; + + return trimmed.Length > 50 + ? trimmed[..50] + : trimmed; } - // Resolve tenant name via shared helper - var serviceProvider = LazyServiceProvider.LazyGetRequiredService(); - var tenantDesc = await AbpUserTenantAccessor.GetCurrentTenantNameAsync(serviceProvider) ?? string.Empty; + + var serviceProvider = + LazyServiceProvider.LazyGetRequiredService(); + + var tenantDesc = + await AbpUserTenantAccessor.GetCurrentTenantNameAsync(serviceProvider) + ?? string.Empty; + var generated = string.IsNullOrWhiteSpace(tenantDesc) ? "Grant Payment" : $"{tenantDesc} – Grant Payment"; + if (generated.Length > 50) - generated = generated.Substring(0, 50); + { + generated = generated[..50]; + } + return generated; } - public async Task CreateInvoiceByPaymentRequestAsync(string invoiceNumber) + public async Task CreateInvoiceByPaymentRequestAsync( + string invoiceNumber) { InvoiceResponse invoiceResponse = new(); + try { - var paymentRequestData = await invoiceManager.GetPaymentRequestDataAsync(invoiceNumber); + var paymentRequestData = + await invoiceManager.GetPaymentRequestDataAsync(invoiceNumber); - if (!string.IsNullOrEmpty(paymentRequestData.AccountDistributionCode)) + if (!string.IsNullOrWhiteSpace( + paymentRequestData.AccountDistributionCode)) { - Invoice? invoice = await InitializeCASInvoice(paymentRequestData.PaymentRequest, paymentRequestData.AccountDistributionCode); + var invoice = await InitializeCASInvoice( + paymentRequestData.PaymentRequest, + paymentRequestData.AccountDistributionCode); - if (invoice is not null) + if (invoice != null) { invoiceResponse = await CreateInvoiceAsync(invoice); - if (invoiceResponse is not null) + + if (invoiceResponse != null) { - await invoiceManager.UpdatePaymentRequestWithInvoiceAsync(paymentRequestData.PaymentRequest.Id, invoiceResponse); + await invoiceManager.UpdatePaymentRequestWithInvoiceAsync( + paymentRequestData.PaymentRequest.Id, + invoiceResponse); } } } @@ -178,59 +248,90 @@ private async Task BuildPaymentDescriptionAsync(string? existingDescript public async Task CreateInvoiceAsync(Invoice casAPInvoice) { - string jsonString = JsonSerializer.Serialize(casAPInvoice); + string jsonString = JsonSerializer.Serialize(casAPInvoice); var authToken = await iTokenService.GetAuthTokenAsync(CurrentTenant.Id ?? Guid.Empty); string casBaseUrl = await endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.PAYMENT_API_BASE); var resource = $"{casBaseUrl}/{CFS_APINVOICE}/"; var response = await resilientHttpRequest.HttpAsync(HttpMethod.Post, resource, jsonString, authToken); - if (response != null) + if (response == null) { - if (response.Content != null && response.StatusCode != HttpStatusCode.NotFound) - { - var contentString = await ResilientHttpRequest.ContentToStringAsync(response.Content); - var result = JsonSerializer.Deserialize(contentString) - ?? throw new UserFriendlyException("CAS InvoiceService CreateInvoiceAsync Exception: " + response); - result.CASHttpStatusCode = response.StatusCode; - return result; - } - else if (response.RequestMessage != null) - { - throw new UserFriendlyException("CAS InvoiceService CreateInvoiceAsync Exception: " + response.RequestMessage); - } - else - { - throw new UserFriendlyException("CAS InvoiceService CreateInvoiceAsync Exception: " + response); - } + throw new UserFriendlyException( + "CAS InvoiceService CreateInvoiceAsync: Null response"); } - else + + if (response.Content != null && + response.StatusCode != HttpStatusCode.NotFound) { - throw new UserFriendlyException("CAS InvoiceService CreateInvoiceAsync: Null response"); + var contentString = + await ResilientHttpRequest.ContentToStringAsync( + response.Content); + + var result = + JsonSerializer.Deserialize(contentString) + ?? throw new UserFriendlyException( + $"CAS InvoiceService CreateInvoiceAsync Exception: {response}"); + + result.CASHttpStatusCode = response.StatusCode; + + return result; } + + if (response.RequestMessage != null) + { + throw new UserFriendlyException( + $"CAS InvoiceService CreateInvoiceAsync Exception: {response.RequestMessage}"); + } + + throw new UserFriendlyException( + $"CAS InvoiceService CreateInvoiceAsync Exception: {response}"); } - public async Task GetCasInvoiceAsync(string invoiceNumber, string supplierNumber, string supplierSiteCode) + public async Task GetCasInvoiceAsync( + string invoiceNumber, + string supplierNumber, + string supplierSiteCode) { - var authToken = await iTokenService.GetAuthTokenAsync(CurrentTenant.Id ?? Guid.Empty); - var casBaseUrl = await endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.PAYMENT_API_BASE); - var resource = $"{casBaseUrl}/{CFS_APINVOICE}/{invoiceNumber}/{supplierNumber}/{supplierSiteCode}"; - var response = await resilientHttpRequest.HttpAsync(HttpMethod.Get, resource, body: null, authToken); + var authToken = + await iTokenService.GetAuthTokenAsync( + CurrentTenant.Id ?? Guid.Empty); + + var casBaseUrl = + await endpointManagementAppService.GetUgmUrlByKeyNameAsync( + DynamicUrlKeyNames.PAYMENT_API_BASE); - if (response != null - && response.Content != null - && response.IsSuccessStatusCode) + var resource = + $"{casBaseUrl}/{CFS_APINVOICE}/{invoiceNumber}/{supplierNumber}/{supplierSiteCode}"; + + var response = await resilientHttpRequest.HttpAsync( + HttpMethod.Get, + resource, + body: null, + authToken); + + if (response != null && + response.Content != null && + response.IsSuccessStatusCode) { - string contentString = await ResilientHttpRequest.ContentToStringAsync(response.Content); - var result = JsonSerializer.Deserialize(contentString); + string contentString = + await ResilientHttpRequest.ContentToStringAsync( + response.Content); + + var result = + JsonSerializer.Deserialize( + contentString); + return result ?? new CasPaymentSearchResult(); } - else - { - return new CasPaymentSearchResult() { }; - } + + return new CasPaymentSearchResult(); } - public async Task GetCasPaymentAsync(Guid tenantId, string invoiceNumber, string supplierNumber, string siteNumber) + public async Task GetCasPaymentAsync( + Guid tenantId, + string invoiceNumber, + string supplierNumber, + string siteNumber) { Logger.LogInformation("GetCasPaymentAsync for Invoice: {InvoiceNumber}, SupplierNumber: {SupplierNumber}, SiteNumber: {SiteNumber}, TenantId: {TenantId}", invoiceNumber, supplierNumber, siteNumber, tenantId); var authToken = await iTokenService.GetAuthTokenAsync(tenantId); @@ -239,15 +340,20 @@ public async Task GetCasPaymentAsync(Guid tenantId, stri var response = await resilientHttpRequest.HttpAsync(HttpMethod.Get, resource, body: null, authToken); CasPaymentSearchResult casPaymentSearchResult = new(); - if (response != null - && response.Content != null - && response.IsSuccessStatusCode) + if (response != null && + response.Content != null && + response.IsSuccessStatusCode) { - var content = response.Content.ReadAsStringAsync(); - var result = JsonSerializer.Deserialize(content.Result); + var content = + await response.Content.ReadAsStringAsync(); + + var result = + JsonSerializer.Deserialize(content); + return result ?? casPaymentSearchResult; } - else if (response != null) + + if (response != null) { casPaymentSearchResult.InvoiceStatus = response.StatusCode.ToString(); } @@ -256,22 +362,27 @@ public async Task GetCasPaymentAsync(Guid tenantId, stri } } -#pragma warning disable S125 // Sections of code should not be commented out +#pragma warning disable S125 /* // - Example Response for GET: - { - "invoice_number": "TESTINVOICE2", - "invoice_status": "Validated", - "payment_status": " Paid", - "payment_number": "009877676", - "payment_date": "25-Aug-2017" - } + + Example Response for GET: + + { + "invoice_number": "TESTINVOICE2", + "invoice_status": "Validated", + "payment_status": " Paid", + "payment_number": "009877676", + "payment_date": "25-Aug-2017" + } Void Payment Webservices Request Format, Type POST + https://:/ords/cas/cfs/apinvoice/ - Sample JSON File – Regular Standard Invoice - Web Service + + Sample JSON File – Regular Standard Invoice - Web Service + { "invoiceType": "Standard", "supplierNumber": "3125635", @@ -296,8 +407,8 @@ Sample JSON File – Regular Standard Invoice - Web Service "glDate": "06-MAR-2023", "invoiceBatchName": "CASAPWEB1", "currencyCode": "CAD", - "invoiceLineDetails": - [{ + "invoiceLineDetails": + [{ "invoiceLineNumber": 1, "invoiceLineType": "Item", "lineCode": "DR", @@ -309,8 +420,9 @@ Sample JSON File – Regular Standard Invoice - Web Service "info1": "", "info2": "", "info3": "" - }] + }] } */ -#pragma warning restore S125 // Sections of code should not be commented out + +#pragma warning restore S125 } \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/BackgroundJobWorkers/FinancialNotificationSummaryWorker.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/BackgroundJobWorkers/FinancialNotificationSummaryWorker.cs index f8ada042ec..84e5e516d0 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/BackgroundJobWorkers/FinancialNotificationSummaryWorker.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/BackgroundJobWorkers/FinancialNotificationSummaryWorker.cs @@ -18,6 +18,8 @@ public class FinancialNotificationSummaryWorker : QuartzBackgroundWorkerBase private readonly FinancialSummaryNotifier _financialSummaryNotifier; private readonly IEnumerable _strategies; + private const string FallbackCron = "0 0 9 1/1 * ? *"; + public FinancialNotificationSummaryWorker( ISettingManager settingManager, FinancialSummaryNotifier financialSummaryNotifier, @@ -27,40 +29,52 @@ public FinancialNotificationSummaryWorker( _financialSummaryNotifier = financialSummaryNotifier; _strategies = strategies; - logger.LogInformation("FinancialNotificationSummary Constructor: Email strategies registered."); + var cronExpression = ResolveCronExpression(settingManager, logger); - string casFinancialNotificationExpression = ""; + JobDetail = JobBuilder + .Create() + .WithIdentity(nameof(FinancialNotificationSummaryWorker)) + .Build(); - try - { - casFinancialNotificationExpression = SettingDefinitions - .GetSettingsValue(settingManager, - PaymentSettingsConstants.BackgroundJobs.CasFinancialNotificationSummary_ProducerExpression); - } - catch - { - casFinancialNotificationExpression = "0 0 9 1/1 * ? *"; - } - - if (!casFinancialNotificationExpression.IsNullOrEmpty()) - { - - JobDetail = JobBuilder - .Create() - .WithIdentity(nameof(FinancialNotificationSummaryWorker)) - .Build(); - - Trigger = TriggerBuilder - .Create() - .WithIdentity(nameof(FinancialNotificationSummaryWorker)) - .WithSchedule(CronScheduleBuilder.CronSchedule(casFinancialNotificationExpression) + Trigger = TriggerBuilder + .Create() + .WithIdentity(nameof(FinancialNotificationSummaryWorker)) + .WithSchedule(CronScheduleBuilder + .CronSchedule(cronExpression) .WithMisfireHandlingInstructionIgnoreMisfires()) - .Build(); - } + .Build(); } public override async Task Execute(IJobExecutionContext context) { + Logger.LogInformation("FinancialNotificationSummary Execute"); await _financialSummaryNotifier.NotifyFailedPayments(_strategies); } + + private static string ResolveCronExpression(ISettingManager settingManager, ILogger logger) + { + try + { + var expression = SettingDefinitions.GetSettingsValue( + settingManager, + PaymentSettingsConstants.BackgroundJobs.CasFinancialNotificationSummary_ProducerExpression); + + if (!expression.IsNullOrEmpty()) + { + return expression; + } + + logger.LogWarning( + "FinancialNotificationSummary: Cron expression setting was empty. Using fallback: {Fallback}", + FallbackCron); + } + catch (Exception ex) + { + logger.LogWarning(ex, + "FinancialNotificationSummary: Failed to read cron expression setting. Using fallback: {Fallback}", + FallbackCron); + } + + return FallbackCron; + } } \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Unity.SharedKernel.csproj b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Unity.SharedKernel.csproj index ef4d1c6ff8..9a51a4f477 100644 --- a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Unity.SharedKernel.csproj +++ b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Unity.SharedKernel.csproj @@ -20,6 +20,7 @@ + diff --git a/applications/Unity.GrantManager/modules/Unity.SharedKernel/src/Unity.SharedKernel/Utilities/AbpUserTenantAccessor.cs b/applications/Unity.GrantManager/modules/Unity.SharedKernel/src/Unity.SharedKernel/Utilities/AbpUserTenantAccessor.cs index b607d27b37..ac60ae4cee 100644 --- a/applications/Unity.GrantManager/modules/Unity.SharedKernel/src/Unity.SharedKernel/Utilities/AbpUserTenantAccessor.cs +++ b/applications/Unity.GrantManager/modules/Unity.SharedKernel/src/Unity.SharedKernel/Utilities/AbpUserTenantAccessor.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Users; using Volo.Abp.MultiTenancy; +using Volo.Abp.TenantManagement; namespace Unity.SharedKernel.Utilities { @@ -25,7 +26,9 @@ public static class AbpUserTenantAccessor public static async Task GetCurrentTenantNameAsync(IServiceProvider serviceProvider) { + // Try resolving ICurrentTenant first var currentTenant = serviceProvider.GetService(); + if (currentTenant != null) { var nameProp = currentTenant.GetType().GetProperty("Name"); @@ -39,11 +42,31 @@ public static class AbpUserTenantAccessor } } + // Fall back to current user tenant id if available var currentUser = serviceProvider.GetService(); - if (currentUser?.TenantId != null && currentUser.TenantId != Guid.Empty) + if (currentUser?.TenantId != null) { - // If a tenant repository is not available in this project, return the tenant id string as fallback. - return currentUser.TenantId.ToString(); + try + { + // Get the current tenant id (returns Guid.Empty when not set) + if (currentUser.TenantId != Guid.Empty) + { + // Try tenant repository (may not be registered in some host contexts) + var tenantRepo = serviceProvider.GetService(); + if (tenantRepo != null) + { + var tenant = await tenantRepo.FindAsync(currentUser.TenantId.Value); + if (tenant != null) + { + return tenant.Name; + } + } + } + } + catch + { + // Swallow any errors and fall back to other methods + } } return null; diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/test/Unity.TenantManagement.TestBase/TenantRepository_Tests.cs b/applications/Unity.GrantManager/modules/Unity.TenantManagement/test/Unity.TenantManagement.TestBase/TenantRepository_Tests.cs index 077dc81c5e..f4bddd0351 100644 --- a/applications/Unity.GrantManager/modules/Unity.TenantManagement/test/Unity.TenantManagement.TestBase/TenantRepository_Tests.cs +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/test/Unity.TenantManagement.TestBase/TenantRepository_Tests.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Threading.Tasks; using Shouldly; using Volo.Abp.Modularity; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Middleware/AbpUserTenantAccessor.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Middleware/AbpUserTenantAccessor.cs index 91ac3e43f7..09131c0257 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Middleware/AbpUserTenantAccessor.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Middleware/AbpUserTenantAccessor.cs @@ -1,65 +1,25 @@ using System; using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.Users; -using Volo.Abp.MultiTenancy; + namespace Unity.GrantManager.Web.Middleware { - // Lightweight, local shim that mirrors the shared helper implementation - // without introducing a project reference to Unity.SharedKernel. + // Accessor used by middleware to resolve current user/tenant information from a service provider. internal static class AbpUserTenantAccessor { public static string? GetCurrentUserName(IServiceProvider serviceProvider) { - var currentUser = serviceProvider.GetService(); - if (currentUser == null) return null; - - var given = currentUser.Name; - var surname = currentUser.SurName; - if (!string.IsNullOrWhiteSpace(given) || !string.IsNullOrWhiteSpace(surname)) - { - return $"{given} {surname}".Trim(); - } - - return currentUser.UserName; + return SharedKernel.Utilities.AbpUserTenantAccessor.GetCurrentUserName(serviceProvider); } public static async Task GetCurrentTenantNameAsync(IServiceProvider serviceProvider) { - var currentTenant = serviceProvider.GetService(); - if (currentTenant != null) - { - var nameProp = currentTenant.GetType().GetProperty("Name"); - if (nameProp != null) - { - var value = nameProp.GetValue(currentTenant) as string; - if (!string.IsNullOrWhiteSpace(value)) - { - return value; - } - } - } - - var currentUser = serviceProvider.GetService(); - if (currentUser?.TenantId != null && currentUser.TenantId != Guid.Empty) - { - // If a tenant repository is not available in this project, return the tenant id string as fallback. - return currentUser.TenantId.ToString(); - } - - return null; + return await SharedKernel.Utilities.AbpUserTenantAccessor.GetCurrentTenantNameAsync(serviceProvider); } public static string? GetCurrentTenantId(IServiceProvider serviceProvider) { - var currentUser = serviceProvider.GetService(); - if (currentUser?.TenantId != null) - { - return currentUser.TenantId.ToString(); - } - - return null; + return SharedKernel.Utilities.AbpUserTenantAccessor.GetCurrentTenantId(serviceProvider); } } } \ No newline at end of file From 99e21026a2bb3c3fb0e7be991e57321d7e4360e8 Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Tue, 26 May 2026 15:45:36 -0700 Subject: [PATCH 2/2] feature/AB#32841-AddQRandPaymentDescription-Fixes --- .../Domain/Services/InvoiceManager.cs | 76 +++++++++---------- .../Repositories/PaymentRequestRepository.cs | 4 +- .../Integrations/Cas/InvoiceService.cs | 16 ++-- 3 files changed, 44 insertions(+), 52 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/InvoiceManager.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/InvoiceManager.cs index 599a96d37a..197b75a943 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/InvoiceManager.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/InvoiceManager.cs @@ -63,50 +63,48 @@ public async Task UpdatePaymentRequestWithInvoiceAsync(Guid paymentRequestId, In try { // Each attempt must have a fresh UoW - using (var uow = unitOfWorkManager.Begin()) - { - // Load with tracking - var paymentRequest = await paymentRequestRepository.GetAsync(paymentRequestId); - - if (paymentRequest == null) - { - Logger.LogWarning("PaymentRequest {Id} not found. Skipping update.", paymentRequestId); - return; - } - - // Idempotency: do not re-process - if (paymentRequest.InvoiceStatus == CasPaymentRequestStatus.SentToCas) - { - Logger.LogInformation( - "PaymentRequest {Id} already invoiced. Skipping update.", - paymentRequestId - ); - return; - } - - // Apply CAS response info - paymentRequest.SetCasHttpStatusCode((int)invoiceResponse.CASHttpStatusCode); - paymentRequest.SetCasResponse(invoiceResponse.CASReturnedMessages); - - // Set status - paymentRequest.SetInvoiceStatus( - invoiceResponse.IsSuccess() - ? CasPaymentRequestStatus.SentToCas - : CasPaymentRequestStatus.ErrorFromCas - ); + using var uow = unitOfWorkManager.Begin(); + // Load with tracking + var paymentRequest = await paymentRequestRepository.GetAsync(paymentRequestId); - await paymentRequestRepository.UpdateAsync(paymentRequest, autoSave: false); - - // Commit this attempt - await uow.CompleteAsync(); + if (paymentRequest == null) + { + Logger.LogWarning("PaymentRequest {Id} not found. Skipping update.", paymentRequestId); + return; + } + // Idempotency: do not re-process + if (paymentRequest.InvoiceStatus == CasPaymentRequestStatus.SentToCas) + { Logger.LogInformation( - "PaymentRequest {Id} updated successfully on attempt {Attempt}.", - paymentRequestId, - attempt + "PaymentRequest {Id} already invoiced. Skipping update.", + paymentRequestId ); - return; // success + return; } + + // Apply CAS response info + paymentRequest.SetCasHttpStatusCode((int)invoiceResponse.CASHttpStatusCode); + paymentRequest.SetCasResponse(invoiceResponse.CASReturnedMessages); + + // Set status + paymentRequest.SetInvoiceStatus( + invoiceResponse.IsSuccess() + ? CasPaymentRequestStatus.SentToCas + : CasPaymentRequestStatus.ErrorFromCas + ); + + await paymentRequestRepository.UpdateAsync(paymentRequest, autoSave: false); + + // Commit this attempt + await uow.CompleteAsync(); + + Logger.LogInformation( + "PaymentRequest {Id} updated successfully on attempt {Attempt}.", + paymentRequestId, + attempt + ); + return; // success } catch (Exception ex) when ( ex is AbpDbConcurrencyException || diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/EntityFrameworkCore/Repositories/PaymentRequestRepository.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/EntityFrameworkCore/Repositories/PaymentRequestRepository.cs index 2b2e2c831e..c46f71e5a3 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/EntityFrameworkCore/Repositories/PaymentRequestRepository.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/EntityFrameworkCore/Repositories/PaymentRequestRepository.cs @@ -15,8 +15,8 @@ namespace Unity.Payments.Repositories { public class PaymentRequestRepository : EfCoreRepository, IPaymentRequestRepository { - private List ReCheckStatusList { get; set; } = new List(); - private List FailedStatusList { get; set; } = new List(); + private List ReCheckStatusList { get; set; } = []; + private List FailedStatusList { get; set; } = []; public PaymentRequestRepository(IDbContextProvider dbContextProvider) : base(dbContextProvider) { diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/InvoiceService.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/InvoiceService.cs index 03ed621231..3cc3b643e4 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/InvoiceService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/InvoiceService.cs @@ -97,15 +97,15 @@ await BuildPaymentDescriptionAsync(paymentRequest.Description), QualifiedReceiver = await GetLevel1DecisionUserNameAsync(paymentRequest), - InvoiceLineDetails = new List - { + InvoiceLineDetails = + [ new() { InvoiceLineNumber = 1, InvoiceLineAmount = paymentRequest.Amount, DefaultDistributionAccount = accountDistributionCode } - } + ] }; return casInvoice; @@ -252,14 +252,8 @@ public async Task CreateInvoiceAsync(Invoice casAPInvoice) var authToken = await iTokenService.GetAuthTokenAsync(CurrentTenant.Id ?? Guid.Empty); string casBaseUrl = await endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.PAYMENT_API_BASE); var resource = $"{casBaseUrl}/{CFS_APINVOICE}/"; - var response = await resilientHttpRequest.HttpAsync(HttpMethod.Post, resource, jsonString, authToken); - - if (response == null) - { - throw new UserFriendlyException( - "CAS InvoiceService CreateInvoiceAsync: Null response"); - } - + var response = await resilientHttpRequest.HttpAsync(HttpMethod.Post, resource, jsonString, authToken) + ?? throw new UserFriendlyException("CAS InvoiceService CreateInvoiceAsync: Null response"); if (response.Content != null && response.StatusCode != HttpStatusCode.NotFound) {