From 75e2163bb6de7ffe65768915343af2a1ac639089 Mon Sep 17 00:00:00 2001 From: jpasta Date: Wed, 21 May 2025 10:23:10 -0700 Subject: [PATCH 01/55] feature/AB#26441-DynamicUrls-FixDomainName --- .../Collectors/BCAddressCollector.cs | 2 +- .../IEmailNotificationService.cs | 2 - .../ITeamsNotificationService.cs | 13 +++ .../NotificationsDataSeedContributor.cs | 12 +- .../Settings/DynamicUrl.cs | 14 +++ .../Settings/IDynamicUrlRepository.cs | 9 ++ .../GrantManagerDbContext.cs | 19 ++++ .../Repositories/DynamicUrlRepository.cs | 18 +++ .../ReportViewSync/ReportViewSyncModal.cshtml | 2 +- .../Endpoints/CreateModal.cshtml | 24 ++++ .../Endpoints/CreateModal.cshtml.cs | 24 ++++ .../Endpoints/EndpointManagementPageModel.cs | 11 ++ .../EndpointManagement/Endpoints/Index.cshtml | 54 +++++++++ .../Endpoints/Index.cshtml.cs | 16 +++ .../EndpointManagement/Endpoints/Index.css | 16 +++ .../EndpointManagement/Endpoints/Index.js | 107 ++++++++++++++++++ .../Endpoints/UpdateModal.cshtml | 25 ++++ .../Endpoints/UpdateModal.cshtml.cs | 28 +++++ .../EndpointManagement/_ViewImports.cshtml | 4 + .../Tenants/TenantManagementPageModel.cs | 3 +- ...ityTenantManagementWebAutoMapperProfile.cs | 1 + .../Integration/Chefs/IFormsApiService.cs | 2 +- .../Chefs/ISubmissionsApiService.cs | 2 +- .../Integration/Css/CssUser.cs | 2 +- .../Integration/Css/CssUserAttributes.cs | 2 +- .../Integration/Css/ICssUsersApiService.cs | 2 +- .../Integration/Css/InvalidResponse.cs | 2 +- .../Css/TokenValidationResponse.cs | 2 +- .../Integration/Css/UserSearchResult.cs | 2 +- .../Endpoints/CreateUpdateDynamicUrlDto.cs | 12 ++ .../Integration/Endpoints/DynamicUrlDto.cs | 14 +++ .../IEndpointManagementAppService.cs | 19 ++++ .../Integration/Geocoder/AddressDetailsDto.cs | 2 +- .../Geocoder/IGeocoderApiService.cs | 2 +- .../Integration/OrgBook/IOrgBookService.cs | 2 +- .../Applicants/ApplicantAppService.cs | 2 +- .../ApplicationFormAppService.cs | 16 +-- .../ApplicationFormVersionAppService.cs | 2 +- .../Identity/UserImportAppService.cs | 2 +- .../Integrations/Chefs/FormsApiService.cs | 2 +- .../Chefs/SubmissionsApiService.cs | 1 - .../Integrations/Css/CssApiService.cs | 2 +- .../Endpoints/EndpointManagementAppService.cs | 25 ++++ .../Geocoder/GeocoderApiService.cs | 2 +- .../Integrations/Geocoder/ResultMapper.cs | 2 +- .../Integrations/OrgBook/OrgBookService.cs | 2 +- .../Applications/IDynamicUrlRepository.cs | 10 ++ .../Integrations/DynamicUrl.cs | 14 +++ .../Permissions/DynamicUrlDataSeeder.cs | 70 ++++++++++++ .../GrantManagerDbContext.cs | 12 ++ .../Repositories/DynamicUrlRepository.cs | 19 ++++ .../Controllers/FormController.cs | 2 +- .../Menus/GrantManagerMenuContributor.cs | 13 +++ .../Menus/GrantManagerMenus.cs | 1 + .../ChefsEventSubscriptionServiceTests.cs | 2 +- 55 files changed, 628 insertions(+), 44 deletions(-) create mode 100644 applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/TeamsNotifications/ITeamsNotificationService.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Settings/DynamicUrl.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Settings/IDynamicUrlRepository.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/EntityFrameworkCore/GrantManagerDbContext.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/Repositories/DynamicUrlRepository.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/CreateModal.cshtml create mode 100644 applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/CreateModal.cshtml.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/EndpointManagementPageModel.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.cshtml create mode 100644 applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.cshtml.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.css create mode 100644 applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.js create mode 100644 applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/UpdateModal.cshtml create mode 100644 applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/UpdateModal.cshtml.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/_ViewImports.cshtml create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/CreateUpdateDynamicUrlDto.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/DynamicUrlDto.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/IEndpointManagementAppService.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IDynamicUrlRepository.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrl.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Permissions/DynamicUrlDataSeeder.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/DynamicUrlRepository.cs diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/Collectors/BCAddressCollector.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/Collectors/BCAddressCollector.cs index 86bb2dcbf..5d78c484a 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/Collectors/BCAddressCollector.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/Collectors/BCAddressCollector.cs @@ -4,7 +4,7 @@ using System.Text.Json; using System.Threading.Tasks; using Unity.Flex.Worksheets.Values; -using Unity.GrantManager.Integration.Geocoder; +using Unity.GrantManager.Integrations.Geocoder; namespace Unity.Flex.Worksheets.Collectors { 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 a4304c06c..0e0cebb9f 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 @@ -17,8 +17,6 @@ public interface IEmailNotificationService : IApplicationService Task SendCommentNotification(EmailCommentDto input); Task SendEmailNotification(string emailTo, string body, string subject, string? emailFrom, string? emailBodyType, string? emailTemplateName); Task SendEmailToQueue(EmailLog emailLog); - string GetApprovalBody(); - string GetDeclineBody(); Task> GetHistoryByApplicationId(Guid applicationId); Task UpdateSettings(NotificationsSettingsDto settingsDto); Task DeleteEmail(Guid id); diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/TeamsNotifications/ITeamsNotificationService.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/TeamsNotifications/ITeamsNotificationService.cs new file mode 100644 index 000000000..85d6fbfa7 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/TeamsNotifications/ITeamsNotificationService.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace Unity.Notifications.TeamsNotifications +{ + public interface ITeamsNotificationService : IApplicationService + { + Task PostFactsToTeamsAsync(string activityTitle, string activitySubtitle); + Task NotifyChefsEventToTeamsAsync(string subscriptionEvent, dynamic form, dynamic chefsFormVersion); + Task PostToTeamsAsync(string activityTitle, string activitySubtitle, List facts); + } +} \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/NotificationsDataSeedContributor.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/NotificationsDataSeedContributor.cs index 7d885b285..454c51fab 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/NotificationsDataSeedContributor.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/NotificationsDataSeedContributor.cs @@ -3,17 +3,13 @@ using Unity.Notifications.Templates; using Volo.Abp.Data; using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; namespace Unity.Notifications; -public class NotificationsDataSeedContributor : IDataSeedContributor, ITransientDependency +public class NotificationsDataSeedContributor(ITemplateVariablesRepository templateVariablesRepository) : IDataSeedContributor, ITransientDependency { - private readonly ITemplateVariablesRepository _templateVariablesRepository; - public NotificationsDataSeedContributor(ITemplateVariablesRepository templateVariablesRepository) - { - _templateVariablesRepository = templateVariablesRepository; - } public async Task SeedAsync(DataSeedContext context) { @@ -46,10 +42,10 @@ public async Task SeedAsync(DataSeedContext context) foreach (var template in emailTemplateVariableDtos) { - var existingVariable = await _templateVariablesRepository.FindAsync(tv => tv.Token == template.Token); + var existingVariable = await templateVariablesRepository.FirstOrDefaultAsync(tv => tv.Token == template.Token); if (existingVariable == null) { - await _templateVariablesRepository.InsertAsync( + await templateVariablesRepository.InsertAsync( new TemplateVariable { Name = template.Name, Token = template.Token, MapTo = template.MapTo }, autoSave: true ); diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Settings/DynamicUrl.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Settings/DynamicUrl.cs new file mode 100644 index 000000000..436936cc8 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Settings/DynamicUrl.cs @@ -0,0 +1,14 @@ +using System; +using Volo.Abp.Domain.Entities.Auditing; + +namespace Unity.GrantManager.Notifications.Settings; + +public class DynamicUrl : AuditedAggregateRoot +{ + public string KeyName { get; set; } = string.Empty; + + public string Url { get; set; } = string.Empty; + + public string Description { get; set; } = string.Empty; + +} diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Settings/IDynamicUrlRepository.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Settings/IDynamicUrlRepository.cs new file mode 100644 index 000000000..b4ac701f3 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Settings/IDynamicUrlRepository.cs @@ -0,0 +1,9 @@ +using System; +using Volo.Abp.Domain.Repositories; + +namespace Unity.GrantManager.Notifications.Settings; + +public interface IDynamicUrlRepository : IRepository +{ + +} diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/EntityFrameworkCore/GrantManagerDbContext.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/EntityFrameworkCore/GrantManagerDbContext.cs new file mode 100644 index 000000000..4d457fb2d --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/EntityFrameworkCore/GrantManagerDbContext.cs @@ -0,0 +1,19 @@ +using Microsoft.EntityFrameworkCore; +using Volo.Abp.Data; +using Volo.Abp.EntityFrameworkCore; +using Unity.GrantManager.Notifications.Settings; + +namespace Unity.Notifications.EntityFrameworkCore; + +[ConnectionStringName("Default")] +public class GrantManagerDbContext : AbpDbContext +{ + public DbSet DynamicUrls { get; set; } + + // Add DbSet for each Aggregate Root here. + public GrantManagerDbContext(DbContextOptions options) + : base(options) + { + + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/Repositories/DynamicUrlRepository.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/Repositories/DynamicUrlRepository.cs new file mode 100644 index 000000000..9875551fe --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/Repositories/DynamicUrlRepository.cs @@ -0,0 +1,18 @@ +using System; +using Unity.GrantManager.Notifications.Settings; +using Unity.Notifications.EntityFrameworkCore; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +namespace Unity.GrantManager.Repositories +{ + [Dependency(ReplaceServices = true)] + [ExposeServices(typeof(IDynamicUrlRepository))] + public class DynamicUrlRepository : EfCoreRepository, IDynamicUrlRepository + { + public DynamicUrlRepository(IDbContextProvider dbContextProvider) : base(dbContextProvider) + { + } + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Pages/ReportViewSync/ReportViewSyncModal.cshtml b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Pages/ReportViewSync/ReportViewSyncModal.cshtml index b17dd2b64..1be6619bc 100644 --- a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Pages/ReportViewSync/ReportViewSyncModal.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Pages/ReportViewSync/ReportViewSyncModal.cshtml @@ -9,7 +9,7 @@
- +
diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/CreateModal.cshtml b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/CreateModal.cshtml new file mode 100644 index 000000000..3483fed16 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/CreateModal.cshtml @@ -0,0 +1,24 @@ +@page +@using Unity.GrantManager.Localization +@using Microsoft.Extensions.Localization +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal + +@model Unity.GrantManager.Web.Pages.EndpointManagement.CreateModalModel + +@inject IStringLocalizer L +@{ + Layout = null; +} + + + + + + + + + + + + + diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/CreateModal.cshtml.cs b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/CreateModal.cshtml.cs new file mode 100644 index 000000000..984318707 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/CreateModal.cshtml.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using Unity.GrantManager.Integrations; +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; + +namespace Unity.GrantManager.Web.Pages.EndpointManagement; + +public class CreateModalModel(IEndpointManagementAppService endpointManagementAppService) : AbpPageModel +{ + [BindProperty] + public CreateUpdateDynamicUrlDto Endpoint { get; set; } + + + public void OnGet() + { + Endpoint = new(); + } + + public async Task OnPostAsync() + { + await endpointManagementAppService.CreateAsync(Endpoint!); + return NoContent(); + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/EndpointManagementPageModel.cs b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/EndpointManagementPageModel.cs new file mode 100644 index 000000000..1b6e030d2 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/EndpointManagementPageModel.cs @@ -0,0 +1,11 @@ +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; + +namespace Unity.TenantManagement.Web.Pages.EndpointManagement; + +public abstract class EndpointManagementPageModel : AbpPageModel +{ + protected EndpointManagementPageModel() + { + + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.cshtml b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.cshtml new file mode 100644 index 000000000..a6b077237 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.cshtml @@ -0,0 +1,54 @@ +@page +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Mvc.Localization +@using Unity.TenantManagement.Web.Navigation; +@using Volo.Abp.AspNetCore.Mvc.UI.Layout +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Pages.Shared.Components.AbpPageToolbar +@using Volo.Abp.FeatureManagement +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; +@using Volo.Abp.TenantManagement.Localization +@using Unity.TenantManagement.Web.Pages.TenantManagement.Tenants +@model IndexModel +@inject IHtmlLocalizer L +@inject IAuthorizationService Authorization +@inject IPageLayout PageLayout +@{ + PageLayout.Content.BreadCrumb.Add(L["Menu:Endpoints"].Value); + PageLayout.Content.MenuItemName = TenantManagementMenuNames.Endpoints; +} +@section styles { + + + +} +@section scripts { + + + + +} + + +
+
+
+

Endpoint Management

+
+
+ +
+ +
+ +
+
+
+ + + + + +
+
+ + diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.cshtml.cs b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.cshtml.cs new file mode 100644 index 000000000..1ad3aabea --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.cshtml.cs @@ -0,0 +1,16 @@ + +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Unity.TenantManagement.Web.Pages.EndpointManagement; + +public class IndexModel : PageModel +{ + public IndexModel() + { + } + + public void OnGet() + { + // Method intentionally left empty. + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.css b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.css new file mode 100644 index 000000000..3dd9b19c0 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.css @@ -0,0 +1,16 @@ +::-ms-reveal { + display: none; +} + +#AbpContentToolbar { + display: flex !important; + background-color: transparent; +} + +#EndpointManagementWrapper { + background-color: transparent; +} + +#UserSearchTable tbody tr { + cursor: pointer; +} \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.js b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.js new file mode 100644 index 000000000..d77b220a6 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.js @@ -0,0 +1,107 @@ +(function () { + console.log('in here') + const l = abp.localization.getResource('GrantManager'); + let createModal = new abp.ModalManager(abp.appPath + 'EndpointManagement/Endpoints/CreateModal'); + let updateModal = new abp.ModalManager(abp.appPath + 'EndpointManagement/Endpoints/UpdateModal'); + + /** + * Intakes: List All + */ + $.fn.dataTable.Buttons.defaults.dom.button.className = 'btn flex-none'; + let actionButtons = [ + { + text: ' ' + l('Common:Command:Create') + '', + titleAttr: l('Common:Command:Create'), + id: 'CreateButton', + className: 'btn-light rounded-1', + action: (e, dt, node, config) => createBtn(e) + } + ]; + + const listColumns = [ + { + title: "Key Name", + name: "keyName", + data: "keyName", + index: 0 + }, + { + title: "Url", + name: "url", + data: "url", + index: 1 + }, + { + title: "Description", + name: "description", + data: "description", + index: 2 + }, + { + title: l('Actions'), + data: 'id', + orderable: false, + className: 'notexport text-center', + name: 'rowActions', + index: 3, + rowAction: { + items: + [ + { + text: l('Common:Command:Edit'), + action: (data) => updateModal.open({ id: data.record.id }) + } + ] + } + } + ]; + + const defaultVisibleColumns = [ + 'keyName', + 'url', + 'description', + 'rowActions' + ]; + + let responseCallback = function (result) { + return { + recordsTotal: result.totalCount, + recordsFiltered: result.items.length, + data: result.items + }; + }; + + let dt = $('#EndpointsTable'); + + let dataTable = initializeDataTable({ + dt, + defaultVisibleColumns, + listColumns, + maxRowsPerPage: 25, + defaultSortColumn: 0, + dataEndpoint: unity.grantManager.integrations.endpoints.endpointManagement.getList, + data: {}, + responseCallback, + actionButtons, + pagingEnabled: true, + reorderEnabled: false, + languageSetValues: {}, + dataTableName: 'EndpointsTable', + dynamicButtonContainerId: 'dynamicButtonContainerId', + useNullPlaceholder: true, + externalSearchId: 'search-endpoints' + }); + + createModal.onResult(function () { + dataTable.ajax.reload(); + }); + + updateModal.onResult(function () { + dataTable.ajax.reload(); + }); + + function createBtn(e) { + e.preventDefault(); + createModal.open(); + }; +})(); diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/UpdateModal.cshtml b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/UpdateModal.cshtml new file mode 100644 index 000000000..5c97de00f --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/UpdateModal.cshtml @@ -0,0 +1,25 @@ +@page +@using Unity.GrantManager.Localization +@using Microsoft.Extensions.Localization +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal + +@model Unity.GrantManager.Web.Pages.EndpointManagement.UpdateModalModel + +@inject IStringLocalizer L +@{ + Layout = null; +} + + + + + + + + + + + + + + diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/UpdateModal.cshtml.cs b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/UpdateModal.cshtml.cs new file mode 100644 index 000000000..1425980b1 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/UpdateModal.cshtml.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Threading.Tasks; +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; +using Unity.GrantManager.Integrations; +namespace Unity.GrantManager.Web.Pages.EndpointManagement; + +public class UpdateModalModel(IEndpointManagementAppService endpointManagementAppService) : AbpPageModel +{ + [HiddenInput] + [BindProperty(SupportsGet = true)] + public Guid Id { get; set; } + + [BindProperty] + public CreateUpdateDynamicUrlDto Endpoint { get; set; } + + public async Task OnGetAsync() + { + var endpointDto = await endpointManagementAppService.GetAsync(Id); + Endpoint = ObjectMapper.Map(endpointDto); + } + + public async Task OnPostAsync() + { + await endpointManagementAppService.UpdateAsync(Id, Endpoint!); + return NoContent(); + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/_ViewImports.cshtml b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/_ViewImports.cshtml new file mode 100644 index 000000000..c1da1f5f1 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/_ViewImports.cshtml @@ -0,0 +1,4 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Tenants/TenantManagementPageModel.cs b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Tenants/TenantManagementPageModel.cs index cf76f69de..bd26beedd 100644 --- a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Tenants/TenantManagementPageModel.cs +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Tenants/TenantManagementPageModel.cs @@ -1,5 +1,4 @@ -using Unity.TenantManagement.Web; -using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; namespace Unity.TenantManagement.Web.Pages.TenantManagement.Tenants; diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/UnityTenantManagementWebAutoMapperProfile.cs b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/UnityTenantManagementWebAutoMapperProfile.cs index 7f6854262..6b6e812c8 100644 --- a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/UnityTenantManagementWebAutoMapperProfile.cs +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/UnityTenantManagementWebAutoMapperProfile.cs @@ -8,6 +8,7 @@ public class AbpTenantManagementWebAutoMapperProfile : Profile { public AbpTenantManagementWebAutoMapperProfile() { + //List CreateMap() .MapExtraProperties(); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Chefs/IFormsApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Chefs/IFormsApiService.cs index f5671ffa1..b4181449e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Chefs/IFormsApiService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Chefs/IFormsApiService.cs @@ -2,7 +2,7 @@ using System.Threading.Tasks; using Volo.Abp.Application.Services; -namespace Unity.GrantManager.Integration.Chefs +namespace Unity.GrantManager.Integrations.Chefs { public interface IFormsApiService : IApplicationService { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Chefs/ISubmissionsApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Chefs/ISubmissionsApiService.cs index a90018508..a58cfac68 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Chefs/ISubmissionsApiService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Chefs/ISubmissionsApiService.cs @@ -2,7 +2,7 @@ using System.Threading.Tasks; using Volo.Abp.Application.Services; -namespace Unity.GrantManager.Integration.Chefs +namespace Unity.GrantManager.Integrations.Chefs { public interface ISubmissionsApiService : IApplicationService { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/CssUser.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/CssUser.cs index 18a09ca03..b0d5260dd 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/CssUser.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/CssUser.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Unity.GrantManager.Integration.Css +namespace Unity.GrantManager.Integrations.Css { public class CssUser { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/CssUserAttributes.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/CssUserAttributes.cs index 7b5db60b6..2659cb1e4 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/CssUserAttributes.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/CssUserAttributes.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Unity.GrantManager.Integration.Css +namespace Unity.GrantManager.Integrations.Css { public class CssUserAttributes { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/ICssUsersApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/ICssUsersApiService.cs index 5f6cd46a6..d08adfe68 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/ICssUsersApiService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/ICssUsersApiService.cs @@ -1,7 +1,7 @@ using System.Threading.Tasks; using Volo.Abp.Application.Services; -namespace Unity.GrantManager.Integration.Css +namespace Unity.GrantManager.Integrations.Css { public interface ICssUsersApiService : IApplicationService { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/InvalidResponse.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/InvalidResponse.cs index f93fe7147..35c4a011d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/InvalidResponse.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/InvalidResponse.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Unity.GrantManager.Integration.Css +namespace Unity.GrantManager.Integrations.Css { public class InvalidResponse { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/TokenValidationResponse.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/TokenValidationResponse.cs index dd19c4c80..c9ce345ed 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/TokenValidationResponse.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/TokenValidationResponse.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Unity.GrantManager.Integration.Css +namespace Unity.GrantManager.Integrations.Css { public class TokenValidationResponse { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/UserSearchResult.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/UserSearchResult.cs index 6332f2021..2f548dfe1 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/UserSearchResult.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/UserSearchResult.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Unity.GrantManager.Integration.Css +namespace Unity.GrantManager.Integrations.Css { public class UserSearchResult { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/CreateUpdateDynamicUrlDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/CreateUpdateDynamicUrlDto.cs new file mode 100644 index 000000000..acc48f029 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/CreateUpdateDynamicUrlDto.cs @@ -0,0 +1,12 @@ +using System.ComponentModel; + +namespace Unity.GrantManager.Integrations +{ + public class CreateUpdateDynamicUrlDto + { + [DisplayName("Key Name")] + public string KeyName { get; set; } = string.Empty; + public string Url { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/DynamicUrlDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/DynamicUrlDto.cs new file mode 100644 index 000000000..06a99169d --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/DynamicUrlDto.cs @@ -0,0 +1,14 @@ +using System; +using Volo.Abp.Application.Dtos; + +namespace Unity.GrantManager.Integrations +{ + public class DynamicUrlDto : AuditedEntityDto + { + public string KeyName { get; set; } = string.Empty; + + public string Url { get; set; } = string.Empty; + + public string Description { get; set; } = string.Empty; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/IEndpointManagementAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/IEndpointManagementAppService.cs new file mode 100644 index 000000000..35e769b04 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/IEndpointManagementAppService.cs @@ -0,0 +1,19 @@ +using System; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; + +namespace Unity.GrantManager.Integrations; + +/// +/// Represents a collection of functions to interact with the API endpoints +/// + +public interface IEndpointManagementAppService : ICrudAppService< + DynamicUrlDto, + Guid, + PagedAndSortedResultRequestDto, + CreateUpdateDynamicUrlDto> + +{ + +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Geocoder/AddressDetailsDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Geocoder/AddressDetailsDto.cs index 290da6b26..6bc393c59 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Geocoder/AddressDetailsDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Geocoder/AddressDetailsDto.cs @@ -1,4 +1,4 @@ -namespace Unity.GrantManager.Integration.Geocoder +namespace Unity.GrantManager.Integrations.Geocoder { public class AddressDetailsDto { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Geocoder/IGeocoderApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Geocoder/IGeocoderApiService.cs index 119fc637a..06cb36082 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Geocoder/IGeocoderApiService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Geocoder/IGeocoderApiService.cs @@ -1,7 +1,7 @@ using System.Threading.Tasks; using Volo.Abp.Application.Services; -namespace Unity.GrantManager.Integration.Geocoder +namespace Unity.GrantManager.Integrations.Geocoder { public interface IGeocoderApiService : IApplicationService { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/OrgBook/IOrgBookService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/OrgBook/IOrgBookService.cs index 67dac5200..602f09cba 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/OrgBook/IOrgBookService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/OrgBook/IOrgBookService.cs @@ -2,7 +2,7 @@ using System.Threading.Tasks; using Volo.Abp.Application.Services; -namespace Unity.GrantManager.Integration.Orgbook +namespace Unity.GrantManager.Integrations.Orgbook { public interface IOrgBookService : IApplicationService { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantAppService.cs index 56c537b6a..645d2ebde 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantAppService.cs @@ -8,7 +8,7 @@ using Unity.Payments.Events; using Volo.Abp; using System.Collections.Generic; -using Unity.GrantManager.Integration.Orgbook; +using Unity.GrantManager.Integrations.Orgbook; using Newtonsoft.Json.Linq; using System.Linq; using Unity.Modules.Shared.Utils; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormAppService.cs index bffa9a3d1..d6fe01224 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormAppService.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using Unity.GrantManager.Applications; using Unity.GrantManager.Forms; -using Unity.GrantManager.Integration.Chefs; +using Unity.GrantManager.Integrations.Chefs; using Unity.GrantManager.Permissions; using Volo.Abp; using Volo.Abp.Application.Dtos; @@ -64,10 +64,10 @@ public override async Task UpdateAsync(Guid id, CreateUpdate if (hasFormGuidChanged || hasFormApiKeyChanged) { return await InitializeFormVersion(id, input); - } - else - { - return await base.UpdateAsync(id, input); + } + else + { + return await base.UpdateAsync(id, input); } } @@ -131,9 +131,9 @@ public async Task SaveApplicationFormScoresheet(FormScoresheetDto dto) } public async Task UpdateOtherConfig(Guid id, OtherConfigDto config) - { - var appForm = await _applicationFormRepository.GetAsync(id); - appForm.IsDirectApproval = config.IsDirectApproval; + { + var appForm = await _applicationFormRepository.GetAsync(id); + appForm.IsDirectApproval = config.IsDirectApproval; await _applicationFormRepository.UpdateAsync(appForm); return ObjectMapper.Map(appForm); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormVersionAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormVersionAppService.cs index 71db731c3..316b55741 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormVersionAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormVersionAppService.cs @@ -8,7 +8,7 @@ using Unity.GrantManager.Applications; using Unity.GrantManager.Forms; using Unity.GrantManager.Intakes; -using Unity.GrantManager.Integration.Chefs; +using Unity.GrantManager.Integrations.Chefs; using Unity.GrantManager.Reporting.FieldGenerators; using Unity.Modules.Shared.Features; using Volo.Abp.Application.Dtos; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Identity/UserImportAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Identity/UserImportAppService.cs index 5c9336fe5..dbf527204 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Identity/UserImportAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Identity/UserImportAppService.cs @@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; -using Unity.GrantManager.Integration.Css; +using Unity.GrantManager.Integrations.Css; using Volo.Abp; using Volo.Abp.Data; using Volo.Abp.Identity; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs index 8e87a2d9d..ea5dbac1d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Threading.Tasks; using Unity.GrantManager.Applications; -using Unity.GrantManager.Integration.Chefs; +using Unity.GrantManager.Integrations.Chefs; using Unity.GrantManager.Integrations.Exceptions; using Unity.GrantManager.Integrations.Http; using Volo.Abp; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/SubmissionsApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/SubmissionsApiService.cs index fba7ad473..f9ce8ad1a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/SubmissionsApiService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/SubmissionsApiService.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Threading.Tasks; using Unity.GrantManager.Applications; -using Unity.GrantManager.Integration.Chefs; using Unity.GrantManager.Integrations.Exceptions; using Unity.GrantManager.Integrations.Http; using Volo.Abp; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Css/CssApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Css/CssApiService.cs index 3a8aa178d..c999231e5 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Css/CssApiService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Css/CssApiService.cs @@ -13,7 +13,7 @@ using System.Linq; using Volo.Abp; using Volo.Abp.DependencyInjection; -using Unity.GrantManager.Integration.Css; +using Unity.GrantManager.Integrations.Css; namespace Unity.GrantManager.Integrations.Sso { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs new file mode 100644 index 000000000..ce8469d24 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs @@ -0,0 +1,25 @@ +using System; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; + +namespace Unity.GrantManager.Integrations.Endpoints +{ + + [ExposeServices(typeof(EndpointManagementAppService), typeof(IEndpointManagementAppService))] + public class EndpointManagementAppService : + CrudAppService< + DynamicUrl, + DynamicUrlDto, + Guid, + PagedAndSortedResultRequestDto, + CreateUpdateDynamicUrlDto>, + IEndpointManagementAppService + { + public EndpointManagementAppService(IRepository repository) + : base(repository) + { + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Geocoder/GeocoderApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Geocoder/GeocoderApiService.cs index 5352da0eb..cc3f4c86b 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Geocoder/GeocoderApiService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Geocoder/GeocoderApiService.cs @@ -2,7 +2,7 @@ using Newtonsoft.Json; using RestSharp; using System.Threading.Tasks; -using Unity.GrantManager.Integration.Geocoder; +using Unity.GrantManager.Integrations.Geocoder; using Unity.GrantManager.Integrations.Exceptions; using Unity.GrantManager.Integrations.Http; using Volo.Abp; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Geocoder/ResultMapper.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Geocoder/ResultMapper.cs index 29b5b3614..ca2378c13 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Geocoder/ResultMapper.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Geocoder/ResultMapper.cs @@ -1,4 +1,4 @@ -using Unity.GrantManager.Integration.Geocoder; +using Unity.GrantManager.Integrations.Geocoder; namespace Unity.GrantManager.Integrations.Geocoder { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/OrgBook/OrgBookService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/OrgBook/OrgBookService.cs index 75d05d31c..0f37c7a69 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/OrgBook/OrgBookService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/OrgBook/OrgBookService.cs @@ -2,7 +2,7 @@ using RestSharp; using System.Text.Json; using System.Threading.Tasks; -using Unity.GrantManager.Integration.Orgbook; +using Unity.GrantManager.Integrations.Orgbook; using Unity.GrantManager.Integrations.Exceptions; using Unity.GrantManager.Integrations.Http; using Volo.Abp.Application.Services; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IDynamicUrlRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IDynamicUrlRepository.cs new file mode 100644 index 000000000..6d8aa1d64 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IDynamicUrlRepository.cs @@ -0,0 +1,10 @@ +using System; +using Unity.GrantManager.Integrations; +using Volo.Abp.Domain.Repositories; + +namespace Unity.GrantManager.Applications; + +public interface IDynamicUrlRepository : IRepository +{ + +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrl.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrl.cs new file mode 100644 index 000000000..a905a7e2c --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrl.cs @@ -0,0 +1,14 @@ +using System; +using Volo.Abp.Domain.Entities.Auditing; + +namespace Unity.GrantManager.Integrations; + +public class DynamicUrl : AuditedAggregateRoot +{ + public string KeyName { get; set; } = string.Empty; + + public string Url { get; set; } = string.Empty; + + public string Description { get; set; } = string.Empty; + +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Permissions/DynamicUrlDataSeeder.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Permissions/DynamicUrlDataSeeder.cs new file mode 100644 index 000000000..7f68027a0 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Permissions/DynamicUrlDataSeeder.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Unity.GrantManager.Applications; +using Unity.GrantManager.Integrations; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; + +namespace Unity.GrantManager.Permissions +{ + [Dependency(ReplaceServices = true)] + [ExposeServices(typeof(DynamicUrlDataSeeder), typeof(IDataSeedContributor))] + public class DynamicUrlDataSeeder(IDynamicUrlRepository DynamicUrlRepository) : IDataSeedContributor, ITransientDependency + { + + public async Task SeedAsync(DataSeedContext context) + { + await SeedDynamicUrlAsync(); + } + + public static class DynamicUrlKeyNames + { + public const string INTAKE_API_BASE = "INTAKE_API_BASE"; + public const string PAYMENT_API_BASE = "PAYMENT_API_BASE"; + public const string NOTFICATION_API_BASE = "NOTFICATION_API_BASE"; + public const string NOTFICATION_AUTH = "NOTFICATION_AUTH"; + public const string DIRECT_MESSAGE_KEY_PREFIX = "DIRECT_MESSAGE_"; // Teams Direct Message URL Weebhook- Dynamically incremented + public const string WEBHOOK_KEY_PREFIX = "WEBHOOK_"; // General Webhook URL - Dynamically incremented + } + + public static class DynamicUrls + { + public const string PROTOCOL = "https://"; + public const string CHEFS_PROD_URL = $"{PROTOCOL}submit.digital.gov.bc.ca/app/api/v1"; + public const string CAS_PROD_URL = ""; // Not entered for security reasons + public const string CHES_PROD_URL = $"{PROTOCOL}ches.api.gov.bc.ca/api/v1"; + public const string CHES_PROD_AUTH = $"{PROTOCOL}loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token"; + } + + private async Task SeedDynamicUrlAsync() + { + int messageIndex = 0; + int webhookIndex = 0; + var dynamicUrls = new List + { + new() { KeyName = DynamicUrlKeyNames.PAYMENT_API_BASE, Url = DynamicUrls.CAS_PROD_URL, Description = "BC Corporate Accounting Services API" }, + new() { KeyName = DynamicUrlKeyNames.INTAKE_API_BASE, Url = DynamicUrls.CHEFS_PROD_URL, Description = "Common Hosted Forms Service API" }, + new() { KeyName = DynamicUrlKeyNames.NOTFICATION_API_BASE, Url = DynamicUrls.CHES_PROD_URL, Description = "Common Hosted Email Service API" }, + new() { KeyName = DynamicUrlKeyNames.NOTFICATION_AUTH, Url = DynamicUrls.CHES_PROD_AUTH, Description = "Common Hosted Email Service OAUTH" }, + new() { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{messageIndex++}", Url = "", Description = $"Direct message webhook {messageIndex}" }, + new() { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{messageIndex++}", Url = "", Description = $"Direct message webhook {messageIndex}" }, + new() { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{messageIndex++}", Url = "", Description = $"Direct message webhook {messageIndex}" }, + new() { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{webhookIndex++}", Url = "", Description = $"Webhook {webhookIndex}" }, + new() { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{webhookIndex++}", Url = "", Description = $"Webhook {webhookIndex}" }, + new() { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{webhookIndex++}", Url = "", Description = $"Webhook {webhookIndex}" }, + }; + + foreach (var dynamicUrl in dynamicUrls) + { + var existing = await DynamicUrlRepository.FirstOrDefaultAsync(s => s.KeyName == dynamicUrl.KeyName); + if (existing == null) + { + await DynamicUrlRepository.InsertAsync(dynamicUrl); + } + } + } + + + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantManagerDbContext.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantManagerDbContext.cs index b1fbcef6d..62a2e0a7f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantManagerDbContext.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantManagerDbContext.cs @@ -18,6 +18,7 @@ using Volo.Abp.TenantManagement.EntityFrameworkCore; using AppAny.Quartz.EntityFrameworkCore.Migrations; using AppAny.Quartz.EntityFrameworkCore.Migrations.PostgreSQL; +using Unity.GrantManager.Integrations; namespace Unity.GrantManager.EntityFrameworkCore; @@ -31,6 +32,7 @@ public class GrantManagerDbContext : { /* Add DbSet properties for your Aggregate Roots / Entities here. */ + public DbSet DynamicUrls { get; set; } public DbSet Sectors { get; set; } public DbSet SubSectors { get; set; } public DbSet EconomicRegion { get; set; } @@ -92,6 +94,16 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.AddQuartz(builder => builder.UsePostgreSql("qrtz_", null)); /* Configure your own tables/entities inside here */ + modelBuilder.Entity(b => + { + b.ToTable(GrantManagerConsts.DbTablePrefix + "DynamicUrls", + GrantManagerConsts.DbSchema); + b.HasKey(x => x.Id); + b.Property(x => x.KeyName).IsRequired().HasMaxLength(128); + b.Property(x => x.Url).IsRequired(); + b.Property(x => x.Description).HasMaxLength(256); + b.ConfigureByConvention(); + }); modelBuilder.Entity(b => { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/DynamicUrlRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/DynamicUrlRepository.cs new file mode 100644 index 000000000..e5f4c24d8 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/DynamicUrlRepository.cs @@ -0,0 +1,19 @@ +using System; +using Unity.GrantManager.Applications; +using Unity.GrantManager.EntityFrameworkCore; +using Unity.GrantManager.Integrations; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +namespace Unity.GrantManager.Repositories +{ + [Dependency(ReplaceServices = true)] + [ExposeServices(typeof(IDynamicUrlRepository))] + public class DynamicUrlRepository : EfCoreRepository, IDynamicUrlRepository + { + public DynamicUrlRepository(IDbContextProvider dbContextProvider) : base(dbContextProvider) + { + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/FormController.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/FormController.cs index e1888f74d..8b239054b 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/FormController.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/FormController.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using Unity.GrantManager.ApplicationForms; using Unity.GrantManager.Applications; -using Unity.GrantManager.Integration.Chefs; +using Unity.GrantManager.Integrations.Chefs; using Volo.Abp; using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.Domain.Entities; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenuContributor.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenuContributor.cs index 00ab97c38..d8d65c39b 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenuContributor.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenuContributor.cs @@ -2,6 +2,7 @@ using Unity.GrantManager.Localization; using Unity.GrantManager.Permissions; using Unity.Identity.Web.Navigation; +using Unity.Modules.Shared.Permissions; using Unity.TenantManagement; using Unity.TenantManagement.Web.Navigation; using Volo.Abp.Identity; @@ -92,6 +93,9 @@ private static Task ConfigureMainMenuAsync(MenuConfigurationContext context) ) ); + + // ******************** + // Admin - Tenant Management context.Menu.AddItem( new ApplicationMenuItem( TenantManagementMenuNames.Tenants, @@ -103,6 +107,15 @@ private static Task ConfigureMainMenuAsync(MenuConfigurationContext context) ) ); + context.Menu.AddItem( + new ApplicationMenuItem( + GrantManagerMenus.EndpointManagement, + displayName: "Endpoint Management", + "~/EndpointManagement/Endpoints", + requiredPermissionName: IdentityConsts.ITAdminPermissionName + ) + ); + // End Admin ******************** #pragma warning disable S125 // Sections of code should not be commented out /* - will complete later after fixing ui sub menu issue */ //var administration = context.Menu.GetAdministration(); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenus.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenus.cs index 07b33690b..ff26a1487 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenus.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenus.cs @@ -16,4 +16,5 @@ public static class GrantManagerMenus public const string Welcome = Prefix + ".Welcome"; public const string Intakes = Prefix + ".Intakes"; public const string ApplicationForms = Prefix + ".ApplicationForms"; + public const string EndpointManagement = Prefix + ".EndpointManagement"; } diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Events/ChefsEventSubscriptionServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Events/ChefsEventSubscriptionServiceTests.cs index e76667ef7..e1d1794c8 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Events/ChefsEventSubscriptionServiceTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Events/ChefsEventSubscriptionServiceTests.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using Unity.GrantManager.Applications; using Unity.GrantManager.Intakes; -using Unity.GrantManager.Integration.Chefs; +using Unity.GrantManager.Integrations.Chefs; using Volo.Abp.Domain.Repositories; using Volo.Abp.Uow; using Volo.Abp.Users; From f4ee868b9d3d052e47d602ed8ed8d8a33d2110ce Mon Sep 17 00:00:00 2001 From: jpasta Date: Wed, 21 May 2025 10:24:03 -0700 Subject: [PATCH 02/55] feature/AB#26441-DynamicUrls-Fix popover not available for datatables - timing on loading --- .../src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js index 3f7aaa9f2..18750b4cd 100644 --- a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js +++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js @@ -178,7 +178,10 @@ function initializeDataTable(options) { updateFilter(iDt, dt[0].id, filterData); }); - initializeFilterButtonPopover(iDt); + // On the ITAdministrator pages, bootstrap popover is not loaded and causes the js to fail + if($('#btn-toggle-filter').popover+"" != "undefined") { + initializeFilterButtonPopover(iDt); + } searchFilter(iDt); From a77a5336b39adc01d9dee5f6219a7dbe2adb2f64 Mon Sep 17 00:00:00 2001 From: jpasta Date: Wed, 21 May 2025 10:25:12 -0700 Subject: [PATCH 03/55] feature/AB#26441-DynamicUrls-Initiailizedb --- .../Navigation/TenantManagementMenuNames.cs | 2 + ...rantManagerApplicationAutoMapperProfile.cs | 8 +- .../Localization/GrantManager/en.json | 1 + .../GrantManagerDataSeederContributor.cs | 170 +- .../20250514165300_DynamicUrls.Designer.cs | 2537 +++++++++++++++++ .../20250514165300_DynamicUrls.cs | 43 + .../GrantManagerDbContextModelSnapshot.cs | 49 + 7 files changed, 2687 insertions(+), 123 deletions(-) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20250514165300_DynamicUrls.Designer.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20250514165300_DynamicUrls.cs diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Navigation/TenantManagementMenuNames.cs b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Navigation/TenantManagementMenuNames.cs index 9850c50f7..fa2e8b47b 100644 --- a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Navigation/TenantManagementMenuNames.cs +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Navigation/TenantManagementMenuNames.cs @@ -5,4 +5,6 @@ public static class TenantManagementMenuNames public const string GroupName = "TenantManagement"; public const string Tenants = GroupName + ".Tenants"; + + public const string Endpoints = GroupName + ".Endpoints"; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationAutoMapperProfile.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationAutoMapperProfile.cs index 9d67e624b..9d06c0461 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationAutoMapperProfile.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationAutoMapperProfile.cs @@ -9,6 +9,7 @@ using Unity.GrantManager.GrantApplications; using Unity.GrantManager.Identity; using Unity.GrantManager.Intakes; +using Unity.GrantManager.Integrations; using Unity.GrantManager.Locality; using Unity.GrantManager.Zones; @@ -21,7 +22,12 @@ public GrantManagerApplicationAutoMapperProfile() /* You can configure your AutoMapper mapping configuration here. * Alternatively, you can split your mapping configurations * into multiple profile classes for a better organization. */ - + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); CreateMap(); CreateMap() diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json index 1a3a9ed2d..729728677 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json @@ -15,6 +15,7 @@ "Menu:Intakes": "Intakes", "Menu:ApplicationForms": "Forms", "Menu:TenantManagement": "Tenants", + "Menu:EndpointManagement": "Endpoints", "Welcome": "Welcome", "LongWelcomeMessage": "Welcome to Unity Grant Manager", diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/GrantManagerDataSeederContributor.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/GrantManagerDataSeederContributor.cs index 75c425f5d..9fe38d74e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/GrantManagerDataSeederContributor.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/GrantManagerDataSeederContributor.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Threading.Tasks; using Unity.GrantManager.Applications; using Unity.GrantManager.GrantApplications; @@ -7,138 +8,63 @@ namespace Unity.GrantManager; -public class GrantManagerDataSeederContributor : IDataSeedContributor, ITransientDependency +public class GrantManagerDataSeederContributor( + IApplicationStatusRepository applicationStatusRepository) : IDataSeeder, ITransientDependency { - private readonly IApplicationStatusRepository _applicationStatusRepository; - - public GrantManagerDataSeederContributor(IApplicationStatusRepository applicationStatusRepository) + public static class GrantApplicationStates { - _applicationStatusRepository = applicationStatusRepository; + public const string SUBMITTED = "Submitted"; + public const string ASSIGNED = "Assigned"; + public const string WITHDRAWN = "Withdrawn"; + public const string CLOSED = "Closed"; + public const string UNDER_REVIEW = "Under Review"; + public const string UNDER_INITIAL_REVIEW = "Under Initial Review"; + public const string INITITAL_REVIEW_COMPLETED = "Initial Review Completed"; + public const string UNDER_ASSESSMENT = "Under Assessment"; + public const string ASSESSMENT_COMPLETED = "Assessment Completed"; + public const string GRANT_APPROVED = "Grant Approved"; + public const string DECLINED = "Declined"; + public const string DEFER = "Deferred"; + public const string ON_HOLD = "On Hold"; } public async Task SeedAsync(DataSeedContext context) { - if (context.TenantId != null) // only try seed into a tenant database - { - ApplicationStatus? status1 = await _applicationStatusRepository.FirstOrDefaultAsync(s => s.StatusCode == GrantApplicationState.SUBMITTED); - status1 ??= await _applicationStatusRepository.InsertAsync( - new ApplicationStatus - { - StatusCode = GrantApplicationState.SUBMITTED, - ExternalStatus = "Submitted", - InternalStatus = "Submitted" - } - ); - - ApplicationStatus? status2 = await _applicationStatusRepository.FirstOrDefaultAsync(s => s.StatusCode == GrantApplicationState.ASSIGNED); - status2 ??= await _applicationStatusRepository.InsertAsync( - new ApplicationStatus - { - StatusCode = GrantApplicationState.ASSIGNED, - ExternalStatus = "Under Review", - InternalStatus = "Assigned" - } - ); - - ApplicationStatus? status3 = await _applicationStatusRepository.FirstOrDefaultAsync(s => s.StatusCode == GrantApplicationState.WITHDRAWN); - status3 ??= await _applicationStatusRepository.InsertAsync( - new ApplicationStatus - { - StatusCode = GrantApplicationState.WITHDRAWN, - ExternalStatus = "Withdrawn", - InternalStatus = "Withdrawn" - } - ); - - ApplicationStatus? status4 = await _applicationStatusRepository.FirstOrDefaultAsync(s => s.StatusCode == GrantApplicationState.CLOSED); - status4 ??= await _applicationStatusRepository.InsertAsync( - new ApplicationStatus - { - StatusCode = GrantApplicationState.CLOSED, - ExternalStatus = "Closed", - InternalStatus = "Closed" - } - ); - - ApplicationStatus? status5 = await _applicationStatusRepository.FirstOrDefaultAsync(s => s.StatusCode == GrantApplicationState.UNDER_INITIAL_REVIEW); - status5 ??= await _applicationStatusRepository.InsertAsync( - new ApplicationStatus - { - StatusCode = GrantApplicationState.UNDER_INITIAL_REVIEW, - ExternalStatus = "Under Review", - InternalStatus = "Under Initial Review" - } - ); - - ApplicationStatus? status6 = await _applicationStatusRepository.FirstOrDefaultAsync(s => s.StatusCode == GrantApplicationState.INITITAL_REVIEW_COMPLETED); - status6 ??= await _applicationStatusRepository.InsertAsync( - new ApplicationStatus - { - StatusCode = GrantApplicationState.INITITAL_REVIEW_COMPLETED, - ExternalStatus = "Under Review", - InternalStatus = "Initial Review Completed" - } - ); - ApplicationStatus? status7 = await _applicationStatusRepository.FirstOrDefaultAsync(s => s.StatusCode == GrantApplicationState.UNDER_ASSESSMENT); - status7 ??= await _applicationStatusRepository.InsertAsync( - new ApplicationStatus - { - StatusCode = GrantApplicationState.UNDER_ASSESSMENT, - ExternalStatus = "Under Review", - InternalStatus = "Under Assessment" - } - ); - - ApplicationStatus? status8 = await _applicationStatusRepository.FirstOrDefaultAsync(s => s.StatusCode == GrantApplicationState.ASSESSMENT_COMPLETED); - status8 ??= await _applicationStatusRepository.InsertAsync( - new ApplicationStatus - { - StatusCode = GrantApplicationState.ASSESSMENT_COMPLETED, - ExternalStatus = "Under Review", - InternalStatus = "Assessment Completed" - } - ); - - ApplicationStatus? status9 = await _applicationStatusRepository.FirstOrDefaultAsync(s => s.StatusCode == GrantApplicationState.GRANT_APPROVED); - status9 ??= await _applicationStatusRepository.InsertAsync( - new ApplicationStatus - { - StatusCode = GrantApplicationState.GRANT_APPROVED, - ExternalStatus = "Approved", - InternalStatus = "Approved" - } - ); + if (context.TenantId == null) // only seed into a tenant database + { + return; + } - ApplicationStatus? status10 = await _applicationStatusRepository.FirstOrDefaultAsync(s => s.StatusCode == GrantApplicationState.GRANT_NOT_APPROVED); - status10 ??= await _applicationStatusRepository.InsertAsync( - new ApplicationStatus - { - StatusCode = GrantApplicationState.GRANT_NOT_APPROVED, - ExternalStatus = "Declined", - InternalStatus = "Declined" - } - ); + await SeedApplicationStatusAsync(); + } - ApplicationStatus? status11 = await _applicationStatusRepository.FirstOrDefaultAsync(s => s.StatusCode == GrantApplicationState.DEFER); - status11 ??= await _applicationStatusRepository.InsertAsync( - new ApplicationStatus - { - StatusCode = GrantApplicationState.DEFER, - ExternalStatus = "Deferred", - InternalStatus = "Deferred" - } - ); + + private async Task SeedApplicationStatusAsync() + { + var statuses = new List + { + new() { StatusCode = GrantApplicationState.SUBMITTED, ExternalStatus = GrantApplicationStates.SUBMITTED, InternalStatus = GrantApplicationStates.SUBMITTED }, + new() { StatusCode = GrantApplicationState.ASSIGNED, ExternalStatus = GrantApplicationStates.UNDER_REVIEW, InternalStatus = GrantApplicationStates.ASSIGNED }, + new() { StatusCode = GrantApplicationState.WITHDRAWN, ExternalStatus = GrantApplicationStates.WITHDRAWN, InternalStatus = GrantApplicationStates.WITHDRAWN }, + new() { StatusCode = GrantApplicationState.CLOSED, ExternalStatus = GrantApplicationStates.CLOSED, InternalStatus = GrantApplicationStates.CLOSED }, + new() { StatusCode = GrantApplicationState.UNDER_INITIAL_REVIEW, ExternalStatus = GrantApplicationStates.UNDER_REVIEW, InternalStatus = GrantApplicationStates.UNDER_INITIAL_REVIEW }, + new() { StatusCode = GrantApplicationState.INITITAL_REVIEW_COMPLETED, ExternalStatus = GrantApplicationStates.UNDER_REVIEW, InternalStatus = GrantApplicationStates.INITITAL_REVIEW_COMPLETED }, + new() { StatusCode = GrantApplicationState.UNDER_ASSESSMENT, ExternalStatus = GrantApplicationStates.UNDER_REVIEW, InternalStatus = GrantApplicationStates.UNDER_ASSESSMENT }, + new() { StatusCode = GrantApplicationState.ASSESSMENT_COMPLETED, ExternalStatus = GrantApplicationStates.UNDER_REVIEW, InternalStatus = GrantApplicationStates.ASSESSMENT_COMPLETED }, + new() { StatusCode = GrantApplicationState.GRANT_APPROVED, ExternalStatus = GrantApplicationStates.GRANT_APPROVED, InternalStatus = GrantApplicationStates.GRANT_APPROVED }, + new() { StatusCode = GrantApplicationState.GRANT_NOT_APPROVED, ExternalStatus = GrantApplicationStates.DECLINED, InternalStatus = GrantApplicationStates.DECLINED }, + new() { StatusCode = GrantApplicationState.DEFER, ExternalStatus = GrantApplicationStates.DEFER, InternalStatus = GrantApplicationStates.DEFER }, + new() { StatusCode = GrantApplicationState.ON_HOLD, ExternalStatus = GrantApplicationStates.ON_HOLD, InternalStatus = GrantApplicationStates.ON_HOLD }, + }; - ApplicationStatus? status12 = await _applicationStatusRepository.FirstOrDefaultAsync(s => s.StatusCode == GrantApplicationState.ON_HOLD); - status12 ??= await _applicationStatusRepository.InsertAsync( - new ApplicationStatus - { - StatusCode = GrantApplicationState.ON_HOLD, - ExternalStatus = "On Hold", - InternalStatus = "On Hold" - } - ); + foreach (var status in statuses) + { + var existing = await applicationStatusRepository.FirstOrDefaultAsync(s => s.StatusCode == status.StatusCode); + if (existing == null) + { + await applicationStatusRepository.InsertAsync(status); + } } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20250514165300_DynamicUrls.Designer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20250514165300_DynamicUrls.Designer.cs new file mode 100644 index 000000000..afdb96491 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20250514165300_DynamicUrls.Designer.cs @@ -0,0 +1,2537 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Unity.GrantManager.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +#nullable disable + +namespace Unity.GrantManager.Migrations.HostMigrations +{ + [DbContext(typeof(GrantManagerDbContext))] + [Migration("20250514165300_DynamicUrls")] + partial class DynamicUrls + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.PostgreSql) + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_blob_triggers", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("qrtz_calendars", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_cron_triggers", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_j_req_recovery"); + + b.ToTable("qrtz_job_details", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("qrtz_locks", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("qrtz_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("qrtz_scheduler_state", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_simple_triggers", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("smallint") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_qrtz_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_qrtz_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_qrtz_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("qrtz_triggers", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Intakes.ChefsMissedSubmission", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ChefsApplicationFormGuid") + .HasColumnType("text"); + + b.Property("ChefsSubmissionGuids") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("ChefsMissedSubmissions", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Locality.Community", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegionalDistrictCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Communities", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Locality.EconomicRegion", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("EconomicRegionCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("EconomicRegionName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.HasKey("Id"); + + b.ToTable("EconomicRegions", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Locality.ElectoralDistrict", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ElectoralDistrictCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ElectoralDistrictName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.HasKey("Id"); + + b.ToTable("ElectoralDistricts", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Locality.RegionalDistrict", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("EconomicRegionCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("RegionalDistrictCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegionalDistrictName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("RegionalDistricts", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Locality.Sector", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SectorCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SectorName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Sectors", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Locality.SubSector", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SectorId") + .HasColumnType("uuid"); + + b.Property("SubSectorCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SubSectorName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("SectorId"); + + b.ToTable("SubSectors", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Tokens.TenantToken", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("TenantTokens", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApplicationName") + .HasMaxLength(96) + .HasColumnType("character varying(96)") + .HasColumnName("ApplicationName"); + + b.Property("BrowserInfo") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("BrowserInfo"); + + b.Property("ClientId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("ClientId"); + + b.Property("ClientIpAddress") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("ClientIpAddress"); + + b.Property("ClientName") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("ClientName"); + + b.Property("Comments") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("Comments"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("CorrelationId"); + + b.Property("Exceptions") + .HasColumnType("text"); + + b.Property("ExecutionDuration") + .HasColumnType("integer") + .HasColumnName("ExecutionDuration"); + + b.Property("ExecutionTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("HttpMethod") + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasColumnName("HttpMethod"); + + b.Property("HttpStatusCode") + .HasColumnType("integer") + .HasColumnName("HttpStatusCode"); + + b.Property("ImpersonatorTenantId") + .HasColumnType("uuid") + .HasColumnName("ImpersonatorTenantId"); + + b.Property("ImpersonatorTenantName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("ImpersonatorTenantName"); + + b.Property("ImpersonatorUserId") + .HasColumnType("uuid") + .HasColumnName("ImpersonatorUserId"); + + b.Property("ImpersonatorUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("ImpersonatorUserName"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TenantName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("TenantName"); + + b.Property("Url") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("Url"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("UserName"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ExecutionTime"); + + b.HasIndex("TenantId", "UserId", "ExecutionTime"); + + b.ToTable("AuditLogs", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.AuditLogAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuditLogId") + .HasColumnType("uuid") + .HasColumnName("AuditLogId"); + + b.Property("ExecutionDuration") + .HasColumnType("integer") + .HasColumnName("ExecutionDuration"); + + b.Property("ExecutionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("ExecutionTime"); + + b.Property("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("MethodName") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("MethodName"); + + b.Property("Parameters") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("Parameters"); + + b.Property("ServiceName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("ServiceName"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AuditLogId"); + + b.HasIndex("TenantId", "ServiceName", "MethodName", "ExecutionTime"); + + b.ToTable("AuditLogActions", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.EntityChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuditLogId") + .HasColumnType("uuid") + .HasColumnName("AuditLogId"); + + b.Property("ChangeTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("ChangeTime"); + + b.Property("ChangeType") + .HasColumnType("smallint") + .HasColumnName("ChangeType"); + + b.Property("EntityId") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("EntityId"); + + b.Property("EntityTenantId") + .HasColumnType("uuid"); + + b.Property("EntityTypeFullName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("EntityTypeFullName"); + + b.Property("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AuditLogId"); + + b.HasIndex("TenantId", "EntityTypeFullName", "EntityId"); + + b.ToTable("EntityChanges", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.EntityPropertyChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EntityChangeId") + .HasColumnType("uuid"); + + b.Property("NewValue") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("NewValue"); + + b.Property("OriginalValue") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("OriginalValue"); + + b.Property("PropertyName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("PropertyName"); + + b.Property("PropertyTypeFullName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("PropertyTypeFullName"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("EntityChangeId"); + + b.ToTable("EntityPropertyChanges", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.BackgroundJobs.BackgroundJobRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsAbandoned") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JobArgs") + .IsRequired() + .HasMaxLength(1048576) + .HasColumnType("character varying(1048576)"); + + b.Property("JobName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("LastTryTime") + .HasColumnType("timestamp without time zone"); + + b.Property("NextTryTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Priority") + .ValueGeneratedOnAdd() + .HasColumnType("smallint") + .HasDefaultValue((byte)15); + + b.Property("TryCount") + .ValueGeneratedOnAdd() + .HasColumnType("smallint") + .HasDefaultValue((short)0); + + b.HasKey("Id"); + + b.HasIndex("IsAbandoned", "NextTryTime"); + + b.ToTable("BackgroundJobs", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.FeatureManagement.FeatureDefinitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AllowedProviders") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DefaultValue") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("IsAvailableToHost") + .HasColumnType("boolean"); + + b.Property("IsVisibleToClients") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ParentName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ValueType") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.HasKey("Id"); + + b.HasIndex("GroupName"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Features", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.FeatureManagement.FeatureGroupDefinitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("FeatureGroups", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.FeatureManagement.FeatureValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ProviderKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ProviderName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.HasKey("Id"); + + b.HasIndex("Name", "ProviderName", "ProviderKey") + .IsUnique(); + + b.ToTable("FeatureValues", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityClaimType", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsStatic") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Regex") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("RegexDescription") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Required") + .HasColumnType("boolean"); + + b.Property("ValueType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ClaimTypes", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityLinkUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("SourceTenantId") + .HasColumnType("uuid"); + + b.Property("SourceUserId") + .HasColumnType("uuid"); + + b.Property("TargetTenantId") + .HasColumnType("uuid"); + + b.Property("TargetUserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("SourceUserId", "SourceTenantId", "TargetUserId", "TargetTenantId") + .IsUnique(); + + b.ToTable("LinkUsers", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("EntityVersion") + .HasColumnType("integer"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDefault") + .HasColumnType("boolean") + .HasColumnName("IsDefault"); + + b.Property("IsPublic") + .HasColumnType("boolean") + .HasColumnName("IsPublic"); + + b.Property("IsStatic") + .HasColumnType("boolean") + .HasColumnName("IsStatic"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName"); + + b.ToTable("Roles", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClaimType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ClaimValue") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("RoleClaims", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentitySecurityLog", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Action") + .HasMaxLength(96) + .HasColumnType("character varying(96)"); + + b.Property("ApplicationName") + .HasMaxLength(96) + .HasColumnType("character varying(96)"); + + b.Property("BrowserInfo") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("ClientId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ClientIpAddress") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("Identity") + .HasMaxLength(96) + .HasColumnType("character varying(96)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TenantName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Action"); + + b.HasIndex("TenantId", "ApplicationName"); + + b.HasIndex("TenantId", "Identity"); + + b.HasIndex("TenantId", "UserId"); + + b.ToTable("SecurityLogs", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentitySession", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClientId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Device") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("DeviceInfo") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("IpAddresses") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LastAccessed") + .HasColumnType("timestamp without time zone"); + + b.Property("SessionId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("SignedIn") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Device"); + + b.HasIndex("SessionId"); + + b.HasIndex("TenantId", "UserId"); + + b.ToTable("Sessions", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessFailedCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("Email"); + + b.Property("EmailConfirmed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("EmailConfirmed"); + + b.Property("EntityVersion") + .HasColumnType("integer"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("IsActive"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsExternal") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsExternal"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastPasswordChangeTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LockoutEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("LockoutEnabled"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("Name"); + + b.Property("NormalizedEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("NormalizedEmail"); + + b.Property("NormalizedUserName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("NormalizedUserName"); + + b.Property("OidcSub") + .HasColumnType("text"); + + b.Property("PasswordHash") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("PasswordHash"); + + b.Property("PhoneNumber") + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasColumnName("PhoneNumber"); + + b.Property("PhoneNumberConfirmed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("PhoneNumberConfirmed"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("SecurityStamp"); + + b.Property("ShouldChangePasswordOnNextLogin") + .HasColumnType("boolean"); + + b.Property("Surname") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("Surname"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TwoFactorEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("TwoFactorEnabled"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("UserName"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("NormalizedEmail"); + + b.HasIndex("NormalizedUserName"); + + b.HasIndex("UserName"); + + b.ToTable("Users", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClaimType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ClaimValue") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserDelegation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("EndTime") + .HasColumnType("timestamp without time zone"); + + b.Property("SourceUserId") + .HasColumnType("uuid"); + + b.Property("StartTime") + .HasColumnType("timestamp without time zone"); + + b.Property("TargetUserId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("UserDelegations", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserLogin", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("LoginProvider") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ProviderDisplayName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ProviderKey") + .IsRequired() + .HasMaxLength(196) + .HasColumnType("character varying(196)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("UserId", "LoginProvider"); + + b.HasIndex("LoginProvider", "ProviderKey"); + + b.ToTable("UserLogins", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserOrganizationUnit", b => + { + b.Property("OrganizationUnitId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("OrganizationUnitId", "UserId"); + + b.HasIndex("UserId", "OrganizationUnitId"); + + b.ToTable("UserOrganizationUnits", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId", "UserId"); + + b.ToTable("UserRoles", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("LoginProvider") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Name") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("UserTokens", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnit", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(95) + .HasColumnType("character varying(95)") + .HasColumnName("Code"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("DisplayName"); + + b.Property("EntityVersion") + .HasColumnType("integer"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ParentId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("Code"); + + b.HasIndex("ParentId"); + + b.ToTable("OrganizationUnits", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnitRole", b => + { + b.Property("OrganizationUnitId") + .HasColumnType("uuid"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("OrganizationUnitId", "RoleId"); + + b.HasIndex("RoleId", "OrganizationUnitId"); + + b.ToTable("OrganizationUnitRoles", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.PermissionManagement.PermissionDefinitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("MultiTenancySide") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ParentName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Providers") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("StateCheckers") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("GroupName"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Permissions", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.PermissionManagement.PermissionGrant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ProviderKey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ProviderName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Name", "ProviderName", "ProviderKey") + .IsUnique(); + + b.ToTable("PermissionGrants", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.PermissionManagement.PermissionGroupDefinitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("PermissionGroups", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.SettingManagement.Setting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ProviderKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ProviderName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.HasKey("Id"); + + b.HasIndex("Name", "ProviderName", "ProviderKey") + .IsUnique(); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.SettingManagement.SettingDefinitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DefaultValue") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsEncrypted") + .HasColumnType("boolean"); + + b.Property("IsInherited") + .HasColumnType("boolean"); + + b.Property("IsVisibleToClients") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Providers") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("SettingDefinitions", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.TenantManagement.Tenant", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EntityVersion") + .HasColumnType("integer"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.HasIndex("NormalizedName"); + + b.ToTable("Tenants", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.TenantManagement.TenantConnectionString", b => + { + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Name") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("TenantId", "Name"); + + b.ToTable("TenantConnectionStrings", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("Unity.GrantManager.Locality.SubSector", b => + { + b.HasOne("Unity.GrantManager.Locality.Sector", "Sector") + .WithMany("SubSectors") + .HasForeignKey("SectorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Sector"); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.AuditLogAction", b => + { + b.HasOne("Volo.Abp.AuditLogging.AuditLog", null) + .WithMany("Actions") + .HasForeignKey("AuditLogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.EntityChange", b => + { + b.HasOne("Volo.Abp.AuditLogging.AuditLog", null) + .WithMany("EntityChanges") + .HasForeignKey("AuditLogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.EntityPropertyChange", b => + { + b.HasOne("Volo.Abp.AuditLogging.EntityChange", null) + .WithMany("PropertyChanges") + .HasForeignKey("EntityChangeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityRoleClaim", b => + { + b.HasOne("Volo.Abp.Identity.IdentityRole", null) + .WithMany("Claims") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserClaim", b => + { + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("Claims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserLogin", b => + { + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("Logins") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserOrganizationUnit", b => + { + b.HasOne("Volo.Abp.Identity.OrganizationUnit", null) + .WithMany() + .HasForeignKey("OrganizationUnitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("OrganizationUnits") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserRole", b => + { + b.HasOne("Volo.Abp.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("Roles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserToken", b => + { + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("Tokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnit", b => + { + b.HasOne("Volo.Abp.Identity.OrganizationUnit", null) + .WithMany() + .HasForeignKey("ParentId"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnitRole", b => + { + b.HasOne("Volo.Abp.Identity.OrganizationUnit", null) + .WithMany("Roles") + .HasForeignKey("OrganizationUnitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Volo.Abp.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.TenantManagement.TenantConnectionString", b => + { + b.HasOne("Volo.Abp.TenantManagement.Tenant", null) + .WithMany("ConnectionStrings") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); + + modelBuilder.Entity("Unity.GrantManager.Locality.Sector", b => + { + b.Navigation("SubSectors"); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.AuditLog", b => + { + b.Navigation("Actions"); + + b.Navigation("EntityChanges"); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.EntityChange", b => + { + b.Navigation("PropertyChanges"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityRole", b => + { + b.Navigation("Claims"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUser", b => + { + b.Navigation("Claims"); + + b.Navigation("Logins"); + + b.Navigation("OrganizationUnits"); + + b.Navigation("Roles"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnit", b => + { + b.Navigation("Roles"); + }); + + modelBuilder.Entity("Volo.Abp.TenantManagement.Tenant", b => + { + b.Navigation("ConnectionStrings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20250514165300_DynamicUrls.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20250514165300_DynamicUrls.cs new file mode 100644 index 000000000..407fef6b7 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20250514165300_DynamicUrls.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Unity.GrantManager.Migrations.HostMigrations +{ + /// + public partial class DynamicUrls : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "DynamicUrls", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + KeyName = table.Column(type: "text", nullable: false), + Url = table.Column(type: "text", nullable: false), + Description = table.Column(type: "text", nullable: false), + TenantId = table.Column(type: "uuid", nullable: true), + ExtraProperties = table.Column(type: "text", nullable: false), + ConcurrencyStamp = table.Column(type: "character varying(40)", maxLength: 40, nullable: false), + CreationTime = table.Column(type: "timestamp without time zone", nullable: false), + CreatorId = table.Column(type: "uuid", nullable: true), + LastModificationTime = table.Column(type: "timestamp without time zone", nullable: true), + LastModifierId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_DynamicUrls", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "DynamicUrls"); + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/GrantManagerDbContextModelSnapshot.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/GrantManagerDbContextModelSnapshot.cs index de37c050a..2a3adf250 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/GrantManagerDbContextModelSnapshot.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/GrantManagerDbContextModelSnapshot.cs @@ -752,6 +752,55 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Sectors", (string)null); }); + modelBuilder.Entity("Unity.GrantManager.Locality.DynamicUrl", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.Property("KeyName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("DynamicUrls", (string)null); + }); + modelBuilder.Entity("Unity.GrantManager.Locality.SubSector", b => { b.Property("Id") From a7caea149ef1978244f2f2c8b44eebadbbbee2a0 Mon Sep 17 00:00:00 2001 From: jpasta Date: Wed, 28 May 2025 10:51:50 -0700 Subject: [PATCH 04/55] feature/AB#26441-DynamicUrls --- .../EmailNotificationService.cs | 93 ++--------- .../ITeamsNotificationService.cs | 13 -- .../TeamsNotificationService.cs | 11 +- .../Repositories/PaymentTagRepository.cs | 51 +++++- .../Integrations/Cas/InvoiceService.cs | 7 +- .../Http/IResilientHttpRequest.cs | 8 + .../Http/ResilientHttpRequest.cs | 99 ++++++----- .../Integration/Chefs/IFormsApiService.cs | 8 +- .../Chefs/ISubmissionsApiService.cs | 11 -- .../IEndpointManagementAppService.cs | 9 +- .../CreateEmailDto.cs | 0 .../{Emails => Notifications}/EmailDto.cs | 0 .../Notifications}/Facts.cs | 24 +-- .../IEmailAppService.cs | 0 .../IEmailsService.cs | 0 .../Notifications/INotificationsAppService.cs | 14 ++ .../UpdateEmailDto.cs | 0 .../ApplicationFormSycnronizationService.cs | 29 ++-- .../ApplicationFormVersionAppService.cs | 6 + .../ConfigureIntakeClientOptions.cs | 31 ++++ .../Events/ChefsEventSubscriptionService.cs | 81 ++++----- .../GrantManagerApplicationModule.cs | 92 ++++------- .../INotificationApiUrlProvider.cs | 8 + .../Intakes/IntakeSubmissionAppService.cs | 73 +++------ .../Integrations/Chefs/FormsApiService.cs | 125 ++++++++------ .../Chefs/SubmissionsApiService.cs | 61 ------- .../Integrations/Css/CssApiService.cs | 155 ++++++++---------- .../Endpoints/EndpointManagementAppService.cs | 15 +- .../Geocoder/GeocoderApiService.cs | 88 +++++----- .../Http/IResilientHttpRequest.cs | 17 -- .../Integrations/Http/ResilientHttpRequest.cs | 108 ------------ .../Integrations/OrgBook/OrgBookService.cs | 24 +-- .../EmailAppService.cs | 0 .../Norifications/NotificationsAppService.cs | 87 ++++++++++ .../NotificationApiUrlHolder.cs | 6 + .../NotificationApiUrlProvider.cs | 40 +++++ .../Repositories/ApplicationRepository.cs | 125 ++++++++++++-- .../Controllers/FormController.cs | 14 +- .../Pages/ApplicationForms/Mapping.cshtml.cs | 2 +- 39 files changed, 813 insertions(+), 722 deletions(-) delete mode 100644 applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/TeamsNotifications/ITeamsNotificationService.cs delete mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Chefs/ISubmissionsApiService.cs rename applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/{Emails => Notifications}/CreateEmailDto.cs (100%) rename applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/{Emails => Notifications}/EmailDto.cs (100%) rename applications/Unity.GrantManager/{modules/Unity.Notifications/src/Unity.Notifications.Application/TeamsNotifications => src/Unity.GrantManager.Application.Contracts/Notifications}/Facts.cs (96%) rename applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/{Emails => Notifications}/IEmailAppService.cs (100%) rename applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/{Emails => Notifications}/IEmailsService.cs (100%) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/INotificationsAppService.cs rename applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/{Emails => Notifications}/UpdateEmailDto.cs (100%) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/ConfigureIntakeClientOptions.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/INotificationApiUrlProvider.cs delete mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/SubmissionsApiService.cs delete mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Http/IResilientHttpRequest.cs delete mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Http/ResilientHttpRequest.cs rename applications/Unity.GrantManager/src/Unity.GrantManager.Application/{Emails => Norifications}/EmailAppService.cs (100%) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/Norifications/NotificationsAppService.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/NotificationApiUrlHolder.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/NotificationApiUrlProvider.cs 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 1cfd91f39..3538c16d3 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,5 +1,4 @@ using Microsoft.AspNetCore.Authorization; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; @@ -22,80 +21,27 @@ using Volo.Abp; using Volo.Abp.Features; using Microsoft.AspNetCore.Http; +using Unity.GrantManager.Notifications; namespace Unity.Notifications.EmailNotifications; [Dependency(ReplaceServices = false)] [ExposeServices(typeof(EmailNotificationService), typeof(IEmailNotificationService))] -public class EmailNotificationService : ApplicationService, IEmailNotificationService -{ - private readonly IChesClientService _chesClientService; - private readonly IConfiguration _configuration; - private readonly EmailQueueService _emailQueueService; - private readonly IEmailLogsRepository _emailLogsRepository; - private readonly IExternalUserLookupServiceProvider _externalUserLookupServiceProvider; - private readonly ISettingManager _settingManager; - private readonly IFeatureChecker _featureChecker; - private readonly IHttpContextAccessor _httpContextAccessor; - - public EmailNotificationService( +public class EmailNotificationService( + INotificationsAppService notificationAppService, IEmailLogsRepository emailLogsRepository, - IConfiguration configuration, IChesClientService chesClientService, EmailQueueService emailQueueService, IExternalUserLookupServiceProvider externalUserLookupServiceProvider, ISettingManager settingManager, IFeatureChecker featureChecker, - IHttpContextAccessor httpContextAccessor - ) - { - _emailLogsRepository = emailLogsRepository; - _configuration = configuration; - _chesClientService = chesClientService; - _emailQueueService = emailQueueService; - _externalUserLookupServiceProvider = externalUserLookupServiceProvider; - _settingManager = settingManager; - _featureChecker = featureChecker; - _httpContextAccessor = httpContextAccessor; - } - - private const string approvalBody = - @"Hello,
-
- Thank you for your grant application. We are pleased to inform you that your project has been approved for funding.
- A representative from our Program Area will be reaching out to you shortly with more information on next steps.
-
- Kind regards.
-
- *ATTENTION - Please do not reply to this email as it is an automated notification which is unable to receive replies.
"; - - private const string declineBody = - @"Hello,
-
- 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.
-
- We know that a lot of effort goes into developing a proposed project and we appreciate the time you took to prepare your application.
-
- If you have any questions or concerns, please reach out to program team members who will provide further details regarding the funding decision.
-
- Thank you again for your application.
-
- *ATTENTION - Please do not reply to this email as it is an automated notification which is unable to receive replies.
"; - - public string GetApprovalBody() - { - return approvalBody; - } - - public string GetDeclineBody() - { - return declineBody; - } + IHttpContextAccessor httpContextAccessor) : ApplicationService, IEmailNotificationService +{ public async Task DeleteEmail(Guid id) { - await _emailLogsRepository.DeleteAsync(id); + await emailLogsRepository.DeleteAsync(id); } public async Task UpdateEmailLog(Guid emailId, string emailTo, string body, string subject, Guid applicationId, string? emailFrom, string? status,string? emailTemplateName) @@ -106,14 +52,14 @@ public async Task DeleteEmail(Guid id) } var emailObject = await GetEmailObjectAsync(emailTo, body, subject, emailFrom, "html", emailTemplateName); - EmailLog emailLog = await _emailLogsRepository.GetAsync(emailId); + EmailLog emailLog = await emailLogsRepository.GetAsync(emailId); emailLog = UpdateMappedEmailLog(emailLog, emailObject); emailLog.ApplicationId = applicationId; emailLog.Id = emailId; emailLog.Status = status ?? EmailStatus.Initialized; // When being called here the current tenant is in context - verified by looking at the tenant id - EmailLog loggedEmail = await _emailLogsRepository.UpdateAsync(emailLog, autoSave: true); + EmailLog loggedEmail = await emailLogsRepository.UpdateAsync(emailLog, autoSave: true); return loggedEmail; } @@ -136,7 +82,7 @@ public async Task DeleteEmail(Guid id) emailLog.Status = status ?? EmailStatus.Initialized; // When being called here the current tenant is in context - verified by looking at the tenant id - EmailLog loggedEmail = await _emailLogsRepository.InsertAsync(emailLog, autoSave: true); + EmailLog loggedEmail = await emailLogsRepository.InsertAsync(emailLog, autoSave: true); return loggedEmail; } @@ -145,9 +91,7 @@ protected virtual async Task NotifyTeamsChannel(string chesEmailError) string? envInfo = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); string activityTitle = "CHES Email error: " + chesEmailError; string activitySubtitle = "Environment: " + envInfo; - string teamsChannel = _configuration["Notifications:TeamsNotificationsWebhook"] ?? ""; - List facts = new() { }; - await TeamsNotificationService.PostToTeamsAsync(teamsChannel, activityTitle, activitySubtitle, facts); + await notificationAppService.PostToTeamsAsync(activityTitle, activitySubtitle); } public async Task SendCommentNotification(EmailCommentDto input) @@ -155,11 +99,11 @@ public async Task SendCommentNotification(EmailCommentDto i HttpResponseMessage res = new(); try { - if (await _featureChecker.IsEnabledAsync("Unity.Notifications")) + if (await featureChecker.IsEnabledAsync("Unity.Notifications")) { var defaultFromAddress = await SettingProvider.GetOrNullAsync(NotificationsSettings.Mailing.DefaultFromAddress); var scheme = "https"; - var request = _httpContextAccessor.HttpContext?.Request; + var request = httpContextAccessor.HttpContext?.Request; if (request == null) { @@ -226,7 +170,6 @@ public async Task SendCommentNotification(EmailCommentDto i return res; } - /// /// Send Email Notfication /// @@ -251,7 +194,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 response = await _chesClientService.SendAsync(emailObject); + var response = await chesClientService.SendAsync(emailObject); // Assuming SendAsync returns a HttpResponseMessage or equivalent: return response; @@ -271,7 +214,7 @@ public async Task SendEmailNotification(string emailTo, str EmailLog emailLog = new EmailLog(); try { - emailLog = await _emailLogsRepository.GetAsync(id); + emailLog = await emailLogsRepository.GetAsync(id); } catch (EntityNotFoundException ex) { @@ -284,7 +227,7 @@ public async Task SendEmailNotification(string emailTo, str [Authorize] public virtual async Task> GetHistoryByApplicationId(Guid applicationId) { - var entityList = await _emailLogsRepository.GetByApplicationIdAsync(applicationId); + var entityList = await emailLogsRepository.GetByApplicationIdAsync(applicationId); var dtoList = ObjectMapper.Map, List>(entityList); var sentByUserIds = dtoList @@ -296,7 +239,7 @@ public virtual async Task> GetHistoryByApplicationId(Guid foreach (var userId in sentByUserIds) { - var userInfo = await _externalUserLookupServiceProvider.FindByIdAsync(userId); + var userInfo = await externalUserLookupServiceProvider.FindByIdAsync(userId); if (userInfo != null) { userDictionary[userId] = ObjectMapper.Map(userInfo); @@ -325,7 +268,7 @@ public async Task SendEmailToQueue(EmailLog emailLog) emailNotificationEvent.Id = emailLog.Id; emailNotificationEvent.TenantId = emailLog.TenantId; emailNotificationEvent.RetryAttempts = emailLog.RetryAttempts; - await _emailQueueService.SendToEmailEventQueueAsync(emailNotificationEvent); + await emailQueueService.SendToEmailEventQueueAsync(emailNotificationEvent); } protected virtual async Task GetEmailObjectAsync(string emailTo, string body, string subject, string? emailFrom, string? emailBodyType, string? emailTemplateName) @@ -376,7 +319,7 @@ public async Task UpdateSettings(NotificationsSettingsDto settingsDto) private async Task UpdateTenantSettings(string settingKey, string valueString) { if (!valueString.IsNullOrWhiteSpace()) { - await _settingManager.SetForCurrentTenantAsync(settingKey, valueString); + await settingManager.SetForCurrentTenantAsync(settingKey, valueString); } } } \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/TeamsNotifications/ITeamsNotificationService.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/TeamsNotifications/ITeamsNotificationService.cs deleted file mode 100644 index 85d6fbfa7..000000000 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/TeamsNotifications/ITeamsNotificationService.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Volo.Abp.Application.Services; - -namespace Unity.Notifications.TeamsNotifications -{ - public interface ITeamsNotificationService : IApplicationService - { - Task PostFactsToTeamsAsync(string activityTitle, string activitySubtitle); - Task NotifyChefsEventToTeamsAsync(string subscriptionEvent, dynamic form, dynamic chefsFormVersion); - Task PostToTeamsAsync(string activityTitle, string activitySubtitle, List facts); - } -} \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/TeamsNotifications/TeamsNotificationService.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/TeamsNotifications/TeamsNotificationService.cs index 5bc0e6caa..bbdc54886 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/TeamsNotifications/TeamsNotificationService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/TeamsNotifications/TeamsNotificationService.cs @@ -13,6 +13,13 @@ namespace Unity.Notifications.TeamsNotifications public class TeamsNotificationService { public TeamsNotificationService() : base() { } + + public const string DIRECT_MESSAGE_KEY_PREFIX = "DIRECT_MESSAGE_"; + public const string TEAMS_NOTIFICATION = $"{DIRECT_MESSAGE_KEY_PREFIX}0"; + public const string TEAMS_NOTIFICATION_1 = $"{DIRECT_MESSAGE_KEY_PREFIX}1"; + + public static string TeamsChannel { get; set; } = string.Empty; + private readonly List _facts = new List(); public async Task PostFactsToTeamsAsync(string teamsChannel, string activityTitle, string activitySubtitle) @@ -51,7 +58,7 @@ private static class ChefsEventTypesConsts public const string FORM_DRAFT_PUBLISHED = "eventFormDraftPublished"; } - private static string InitializeMessageCard(string activityTitle, string activitySubtitle, List facts) + public static string InitializeMessageCard(string activityTitle, string activitySubtitle, List facts) { dynamic messageCard = MessageCard.GetMessageCard(); JObject jsonObj = JsonConvert.DeserializeObject(messageCard)!; @@ -148,7 +155,7 @@ public static async Task PostChefsEventToTeamsAsync(string teamsChannel, string await PostToTeamsAsync(teamsChannel, activityTitle, activitySubtitle, facts); } - private static async Task PostToTeamsChannelAsync(string teamsChannel, string messageCard) { + public static async Task PostToTeamsChannelAsync(string teamsChannel, string messageCard) { using var httpClient = new HttpClient(); using var request = new HttpRequestMessage(new HttpMethod("POST"), teamsChannel); request.Content = new StringContent(messageCard); diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/EntityFrameworkCore/Repositories/PaymentTagRepository.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/EntityFrameworkCore/Repositories/PaymentTagRepository.cs index 4ac8ddd15..686b874b1 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/EntityFrameworkCore/Repositories/PaymentTagRepository.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/EntityFrameworkCore/Repositories/PaymentTagRepository.cs @@ -1,4 +1,5 @@ -using System; +using Microsoft.EntityFrameworkCore; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -17,7 +18,53 @@ public PaymentTagRepository(IDbContextProvider dbContextProvi public async Task> GetTagsByPaymentRequestIdAsync(Guid paymentRequestId) { var dbSet = await GetDbSetAsync(); - return dbSet.Where(p => p.PaymentRequestId.Equals(paymentRequestId)).ToList(); + return await dbSet.Where(p => p.PaymentRequestId.Equals(paymentRequestId)).ToListAsync(); + } + + //public virtual async Task> GetTagSummary() + //{ + // var dbSet = await GetDbSetAsync(); + // var results = dbSet + // .AsNoTracking() + // .AsEnumerable() // Forces client-side evaluation + // .SelectMany(tag => tag.Text.Split(',', StringSplitOptions.RemoveEmptyEntries) + // .Select(t => t.Trim())) + // .GroupBy(tag => tag) + // .Select(group => new TagSummaryCount( + // group.Key, + // group.Count() + // )).ToList(); + + // return results; + //} + + /// + /// For a given Tag, finds the maximum length available for renaming. + /// + /// The tag to be replaced. + /// The maximum length available for renaming + public virtual async Task GetMaxRenameLengthAsync(string originalTag) + { + var dbContext = await GetDbContextAsync(); + var entityType = dbContext.Model.FindEntityType(typeof(PaymentTag)); + var property = entityType?.FindProperty(nameof(PaymentTag.Text)); + + int maxColumnLength = property?.GetMaxLength() ?? 0; + + var dbSet = await GetDbSetAsync(); + int? maxTagSetLength = await dbSet + .AsNoTracking() + .Where(t => t.Text.Contains(originalTag)) + .Select(t => t.Text.Length) + .OrderByDescending(len => len) + .FirstOrDefaultAsync(); + + if (maxTagSetLength == null || maxTagSetLength == 0) + { + return maxColumnLength; + } + + return maxColumnLength + originalTag.Length - maxTagSetLength.Value; } } } 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 bd878752e..e31a10ac6 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 @@ -70,9 +70,10 @@ public InvoiceService( if (site != null && site.Supplier != null && site.Supplier.Number != null && accountDistributionCode != 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 currentMonth = DateTime.Now.ToString("MMM").Trim('.'); - var currentDay = DateTime.Now.ToString("dd"); - var currentYear = DateTime.Now.ToString("yyyy"); + var localDateTime = DateTime.UtcNow.ToLocalTime(); + 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 diff --git a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/IResilientHttpRequest.cs b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/IResilientHttpRequest.cs index 537c0127c..3cfa35156 100644 --- a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/IResilientHttpRequest.cs +++ b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/IResilientHttpRequest.cs @@ -8,5 +8,13 @@ public interface IResilientHttpRequest : IRemoteService { Task HttpAsyncWithBody(HttpMethod httpVerb, string resource, string? body = null, string? authToken = null); Task HttpAsync(HttpMethod httpVerb, string resource, string? authToken = null); + Task ExecuteRequestAsync( + HttpMethod httpVerb, + string resource, + string? body, + string? authToken, + (string username, string password)? basicAuth = null); + + void SetBaseUrl(string baseUrl); } } diff --git a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/ResilientHttpRequest.cs b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/ResilientHttpRequest.cs index c8d30015d..40cabf52e 100644 --- a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/ResilientHttpRequest.cs +++ b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/ResilientHttpRequest.cs @@ -14,10 +14,12 @@ namespace Unity.Modules.Shared.Http public class ResilientHttpRequest : IResilientHttpRequest { private readonly IHttpClientFactory _httpClientFactory; + private static int _maxRetryAttempts = 3; - private const int OneMinuteInSeconds = 60; + private const int OneMinuteInSeconds = 60; private static TimeSpan _pauseBetweenFailures = TimeSpan.FromSeconds(2); private static TimeSpan _httpRequestTimeout = TimeSpan.FromSeconds(OneMinuteInSeconds); + private string? _baseUrl; public ResilientHttpRequest(IHttpClientFactory httpClientFactory) { @@ -25,47 +27,54 @@ public ResilientHttpRequest(IHttpClientFactory httpClientFactory) } public static void SetPipelineOptions( - int maxRetryAttempts, + int maxRetryAttempts, TimeSpan pauseBetweenFailures, TimeSpan httpRequestTimeout - ) + ) { _maxRetryAttempts = maxRetryAttempts; _pauseBetweenFailures = pauseBetweenFailures; _httpRequestTimeout = httpRequestTimeout; } - private static bool ReprocessBasedOnStatusCode(HttpStatusCode statusCode) + public void SetBaseUrl(string baseUrl) + { + if (!Uri.TryCreate(baseUrl, UriKind.Absolute, out _)) + { + throw new ArgumentException("Base URL is not a valid absolute URI.", nameof(baseUrl)); + } + + _baseUrl = baseUrl.TrimEnd('/'); + } + + private static bool ShouldRetry(HttpStatusCode statusCode) { - HttpStatusCode[] reprocessStatusCodes = { - HttpStatusCode.TooManyRequests, - HttpStatusCode.InternalServerError, - HttpStatusCode.BadGateway, - HttpStatusCode.ServiceUnavailable, - HttpStatusCode.GatewayTimeout, + HttpStatusCode[] retryableStatusCodes = { + HttpStatusCode.TooManyRequests, + HttpStatusCode.InternalServerError, + HttpStatusCode.BadGateway, + HttpStatusCode.ServiceUnavailable, + HttpStatusCode.GatewayTimeout, }; - return reprocessStatusCodes.Contains(statusCode); + return retryableStatusCodes.Contains(statusCode); } private static ResiliencePipeline _pipeline = - new ResiliencePipelineBuilder() - .AddRetry(new RetryStrategyOptions - { - ShouldHandle = new PredicateBuilder() - .Handle() - .HandleResult(result => ReprocessBasedOnStatusCode(result.StatusCode)), - Delay = _pauseBetweenFailures, - MaxRetryAttempts = _maxRetryAttempts, - UseJitter = true, - BackoffType = DelayBackoffType.Exponential, - OnRetry = args => - { - return default; - } - }) - .AddTimeout(_httpRequestTimeout) - .Build(); + new ResiliencePipelineBuilder() + .AddRetry(new RetryStrategyOptions + { + ShouldHandle = new PredicateBuilder() + .Handle() + .HandleResult(result => ShouldRetry(result.StatusCode)), + Delay = _pauseBetweenFailures, + MaxRetryAttempts = _maxRetryAttempts, + UseJitter = true, + BackoffType = DelayBackoffType.Exponential, + OnRetry = args => default + }) + .AddTimeout(_httpRequestTimeout) + .Build(); public async Task HttpAsync(HttpMethod httpVerb, string resource, string? authToken = null) { @@ -83,21 +92,35 @@ public static string ContentToString(HttpContent httpContent) return readAsStringAsync.Result; } - private async Task ExecuteRequestAsync( - HttpMethod httpVerb, + public async Task ExecuteRequestAsync( + HttpMethod httpVerb, string resource, string? body, - string? authToken) + string? authToken, + (string username, string password)? basicAuth = null) { - //specify to use TLS 1.2 as default connection if 1.3 is not available + if (string.IsNullOrWhiteSpace(_baseUrl)) + { + throw new InvalidOperationException("Base URL has not been set. Call SetBaseUrl() before making requests."); + } + ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; - HttpRequestMessage requestMessage = new HttpRequestMessage(httpVerb, resource) { Version = new Version(3, 0) }; - using HttpClient httpClient = _httpClientFactory.CreateClient(); + + var baseUri = new Uri(_baseUrl, UriKind.Absolute); + var relativeUri = new Uri(resource, UriKind.Relative); + var fullUrl = new Uri(baseUri, relativeUri); + + var requestMessage = new HttpRequestMessage(httpVerb, fullUrl) + { + Version = new Version(3, 0) + }; + + using var httpClient = _httpClientFactory.CreateClient(); httpClient.DefaultRequestHeaders.Accept.Clear(); httpClient.DefaultRequestHeaders.Clear(); httpClient.DefaultRequestHeaders.ConnectionClose = true; - if (!authToken.IsNullOrEmpty()) + if (!authToken.IsNullOrWhiteSpace()) { httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {authToken}"); } @@ -107,8 +130,10 @@ private async Task ExecuteRequestAsync( requestMessage.Content = new StringContent(body, Encoding.UTF8, "application/json"); } - HttpResponseMessage restResponse = await _pipeline.ExecuteAsync(async ct => await httpClient.SendAsync(requestMessage, ct)); - return await Task.FromResult(restResponse); + HttpResponseMessage response = await _pipeline.ExecuteAsync(async ct => + await httpClient.SendAsync(requestMessage, ct)); + + return response; } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Chefs/IFormsApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Chefs/IFormsApiService.cs index b4181449e..d3b6276b7 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Chefs/IFormsApiService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Chefs/IFormsApiService.cs @@ -1,4 +1,5 @@ -using System; +using Newtonsoft.Json.Linq; +using System; using System.Threading.Tasks; using Volo.Abp.Application.Services; @@ -6,7 +7,8 @@ namespace Unity.GrantManager.Integrations.Chefs { public interface IFormsApiService : IApplicationService { - Task GetFormDataAsync(string chefsFormId, string chefsFormVersionId); - Task GetForm(Guid? formId, string chefsApplicationFormGuid, string encryptedApiKey); + Task GetFormDataAsync(string chefsFormId, string chefsFormVersionId); + Task GetForm(Guid? formId, string chefsApplicationFormGuid, string encryptedApiKey); + Task GetSubmissionDataAsync(Guid chefsFormId, Guid submissionId); } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Chefs/ISubmissionsApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Chefs/ISubmissionsApiService.cs deleted file mode 100644 index a58cfac68..000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Chefs/ISubmissionsApiService.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Threading.Tasks; -using Volo.Abp.Application.Services; - -namespace Unity.GrantManager.Integrations.Chefs -{ - public interface ISubmissionsApiService : IApplicationService - { - Task GetSubmissionDataAsync(Guid chefsFormId, Guid submissionId); - } -} \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/IEndpointManagementAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/IEndpointManagementAppService.cs index 35e769b04..ef26c8994 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/IEndpointManagementAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/IEndpointManagementAppService.cs @@ -1,12 +1,10 @@ using System; +using System.Threading.Tasks; using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; namespace Unity.GrantManager.Integrations; - -/// -/// Represents a collection of functions to interact with the API endpoints -/// + public interface IEndpointManagementAppService : ICrudAppService< DynamicUrlDto, @@ -15,5 +13,6 @@ public interface IEndpointManagementAppService : ICrudAppService< CreateUpdateDynamicUrlDto> { - + Task GetChefsApiBaseUrlAsync(); + Task GetUrlByKeyNameAsync(string keyName); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Emails/CreateEmailDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/CreateEmailDto.cs similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Emails/CreateEmailDto.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/CreateEmailDto.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Emails/EmailDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/EmailDto.cs similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Emails/EmailDto.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/EmailDto.cs diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/TeamsNotifications/Facts.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/Facts.cs similarity index 96% rename from applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/TeamsNotifications/Facts.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/Facts.cs index b2a6ab0ab..6569cfe5e 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/TeamsNotifications/Facts.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/Facts.cs @@ -1,13 +1,13 @@ -using System.Text.Json.Serialization; - -namespace Unity.Notifications.TeamsNotifications -{ - public class Fact - { - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; - - [JsonPropertyName("value")] - public string Value { get; set; } = string.Empty; - } +using System.Text.Json.Serialization; + +namespace Unity.Notifications.TeamsNotifications +{ + public class Fact + { + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("value")] + public string Value { get; set; } = string.Empty; + } } \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Emails/IEmailAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/IEmailAppService.cs similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Emails/IEmailAppService.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/IEmailAppService.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Emails/IEmailsService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/IEmailsService.cs similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Emails/IEmailsService.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/IEmailsService.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/INotificationsAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/INotificationsAppService.cs new file mode 100644 index 000000000..74c164345 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/INotificationsAppService.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Unity.Notifications.TeamsNotifications; + +namespace Unity.GrantManager.Notifications +{ + public interface INotificationsAppService + { + Task NotifyChefsEventToTeamsAsync(string factName, string factValue); + Task PostChefsEventToTeamsAsync(string subscriptionEvent, dynamic form, dynamic chefsFormVersion); + Task PostToTeamsAsync(string activityTitle, string activitySubtitle); + Task PostToTeamsAsync(string activityTitle, string activitySubtitle, List facts); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Emails/UpdateEmailDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/UpdateEmailDto.cs similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Emails/UpdateEmailDto.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/UpdateEmailDto.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormSycnronizationService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormSycnronizationService.cs index f0265e1f6..eed94c034 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormSycnronizationService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormSycnronizationService.cs @@ -1,5 +1,4 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; using RestSharp; using RestSharp.Authenticators; @@ -14,7 +13,7 @@ using Unity.GrantManager.Applications; using Unity.GrantManager.Forms; using Unity.GrantManager.Intakes; -using Unity.GrantManager.Integration.Chefs; +using Unity.GrantManager.Integrations.Chefs; using Volo.Abp; using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; @@ -22,6 +21,7 @@ using Volo.Abp.MultiTenancy; using Volo.Abp.Security.Encryption; using Volo.Abp.TenantManagement; +using Unity.GrantManager.Notifications; namespace Unity.GrantManager.ApplicationForms { @@ -41,40 +41,39 @@ public class ApplicationFormSycnronizationService : private readonly ICurrentTenant _currentTenant; private readonly IApplicationFormSubmissionRepository _applicationFormSubmissionRepository; private readonly IApplicationFormVersionAppService _applicationFormVersionAppService; - private readonly IConfiguration _configuration; - private readonly ISubmissionsApiService _submissionsApiService; + private readonly IFormsApiService _formsApiService; private readonly IIntakeFormSubmissionManager _intakeFormSubmissionManager; + private readonly INotificationsAppService _notificationsAppService; private List _facts = new(); private readonly RestClient _intakeClient; private readonly ITenantRepository _tenantRepository; public List? applicationFormDtoList { get; set; } public HashSet FormVersionsInitializedVersionHash { get; set; } = new HashSet(); - - + public ApplicationFormSycnronizationService( + INotificationsAppService notificationsAppService, ICurrentTenant currentTenant, IRepository repository, ITenantRepository tenantRepository, RestClient restClient, - IConfiguration configuration, IStringEncryptionService stringEncryptionService, IApplicationFormRepository applicationFormRepository, IApplicationFormSubmissionRepository applicationFormSubmissionRepository, IApplicationFormVersionAppService applicationFormVersionAppService, - ISubmissionsApiService submissionsApiService, + IFormsApiService formsApiService, IIntakeFormSubmissionManager intakeFormSubmissionManager) : base(repository) { _currentTenant = currentTenant; _tenantRepository = tenantRepository; _intakeClient = restClient; - _configuration = configuration; _stringEncryptionService = stringEncryptionService; _applicationFormRepository = applicationFormRepository; - _submissionsApiService = submissionsApiService; + _formsApiService = formsApiService; _applicationFormSubmissionRepository = applicationFormSubmissionRepository; _applicationFormVersionAppService = applicationFormVersionAppService; _intakeFormSubmissionManager = intakeFormSubmissionManager; + _notificationsAppService = notificationsAppService; } private async Task SynchronizeFormSubmissions(HashSet missingSubmissions, ApplicationFormDto applicationFormDto) @@ -101,7 +100,7 @@ private async Task ProcessSingleSubmission(string submissionGuid, ApplicationFor return; } - JObject? submissionData = await _submissionsApiService.GetSubmissionDataAsync(chefsFormId, chefsSubmissionId); + JObject? submissionData = await _formsApiService.GetSubmissionDataAsync(chefsFormId, chefsSubmissionId); if (submissionData == null) { Logger.LogInformation("ApplicationFormSycnronizationService->SynchronizeFormSubmissions submissionData is null"); @@ -177,7 +176,7 @@ public async Task> GetMissingSubmissions(int numberOfDaysToCheck HashSet missingSubmissions = new HashSet(); // Get all forms with api keys - applicationFormDtoList = (List?)await GetConnectedApplicationFormsAsync(); + applicationFormDtoList = (List?) await GetConnectedApplicationFormsAsync(); if (applicationFormDtoList != null) { @@ -226,8 +225,8 @@ public async Task> GetMissingSubmissions(int numberOfDaysToCheck string? envInfo = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); string activityTitle = "Review Missed Chefs Submissions " + tenantName; string activitySubtitle = "Environment: " + envInfo; - string teamsChannel = _configuration["Notifications:TeamsNotificationsWebhook"] ?? ""; - await TeamsNotificationService.PostToTeamsAsync(teamsChannel, activityTitle, activitySubtitle, _facts); + + await _notificationsAppService.PostToTeamsAsync(activityTitle, activitySubtitle, _facts); return missingSubmissions ?? new HashSet(); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormVersionAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormVersionAppService.cs index 316b55741..e1b8dddd4 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormVersionAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormVersionAppService.cs @@ -144,6 +144,12 @@ private async Task FormVersionDoesNotExist(string formVersionId) => }; var formVersion = await _formsApiService.GetFormDataAsync(formId, formVersionId); + if (formVersion == null) // Ensure formVersion is not null + { + Logger.LogWarning("Form version data is null for formId: {FormId}, formVersionId: {FormVersionId}", formId, formVersionId); + return null; + } + applicationFormVersion.AvailableChefsFields = _formSubmissionMapper.InitializeAvailableFormFields(formVersion); if (formVersion is JObject formVersionObject) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ConfigureIntakeClientOptions.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ConfigureIntakeClientOptions.cs new file mode 100644 index 000000000..042d2f6ee --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ConfigureIntakeClientOptions.cs @@ -0,0 +1,31 @@ + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; +using Unity.GrantManager.Intake; + +namespace Unity.GrantManager; + +public class ConfigureIntakeClientOptions : IConfigureOptions +{ + private readonly IConfiguration _configuration; + const string PROTOCOL = "https://"; + const string DefaultBaseUri = $"{PROTOCOL}submit.digital.gov.bc.ca/app/api/v1"; + + + private readonly INotificationApiUrlProvider _urlProvider; + + public ConfigureIntakeClientOptions(INotificationApiUrlProvider urlProvider, IConfiguration configuration) + { + _urlProvider = urlProvider; + _configuration = configuration; + } + + public void Configure(IntakeClientOptions options) + { + options.BaseUri = DefaultBaseUri; + options.BearerTokenPlaceholder = _configuration["Intake:BearerTokenPlaceholder"] ?? ""; + options.UseBearerToken = _configuration.GetValue("Intake:UseBearerToken"); + options.AllowUnregisteredVersions = _configuration.GetValue("Intake:AllowUnregisteredVersions"); + } +} + diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Events/ChefsEventSubscriptionService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Events/ChefsEventSubscriptionService.cs index 033aa56a9..67acb7853 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Events/ChefsEventSubscriptionService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Events/ChefsEventSubscriptionService.cs @@ -1,4 +1,3 @@ -using Microsoft.Extensions.Configuration; using System; using System.Linq; using System.Threading.Tasks; @@ -6,53 +5,46 @@ using Unity.GrantManager.Applications; using Unity.GrantManager.Exceptions; using Unity.GrantManager.Intakes; -using Unity.GrantManager.Integration.Chefs; -using Unity.Notifications.TeamsNotifications; +using Unity.GrantManager.Integrations.Chefs; +using Unity.GrantManager.Notifications; using Volo.Abp; using Volo.Abp.Domain.Entities; namespace Unity.GrantManager.Events { [RemoteService(false)] - public class ChefsEventSubscriptionService : GrantManagerAppService, IChefsEventSubscriptionService - { - private readonly IApplicationFormRepository _applicationFormRepository; - private readonly IApplicationFormManager _applicationFormManager; - private readonly IIntakeFormSubmissionMapper _intakeFormSubmissionMapper; - private readonly ISubmissionsApiService _submissionsIntService; - private readonly IFormsApiService _formsApiService; - private readonly IApplicationFormVersionAppService _applicationFormVersionAppService; - private readonly IConfiguration _configuration; - - public ChefsEventSubscriptionService( - IConfiguration configuration, + public class ChefsEventSubscriptionService( + INotificationsAppService notificationsAppService, IIntakeFormSubmissionMapper intakeFormSubmissionMapper, IApplicationFormManager applicationFormManager, - ISubmissionsApiService submissionsIntService, + IFormsApiService submissionsIntService, IApplicationFormRepository applicationFormRepository, IFormsApiService formsApiService, - IApplicationFormVersionAppService applicationFormVersionAppService) - { - _configuration = configuration; - _intakeFormSubmissionMapper = intakeFormSubmissionMapper; - _submissionsIntService = submissionsIntService; - _applicationFormRepository = applicationFormRepository; - _formsApiService = formsApiService; - _applicationFormManager = applicationFormManager; - _applicationFormVersionAppService = applicationFormVersionAppService; - } + IApplicationFormVersionAppService applicationFormVersionAppService) : GrantManagerAppService, IChefsEventSubscriptionService + { public async Task CreateIntakeMappingAsync(EventSubscriptionDto eventSubscriptionDto) { - var applicationForm = (await _applicationFormRepository + _ = (await applicationFormRepository .GetQueryableAsync()) .Where(s => s.ChefsApplicationFormGuid == eventSubscriptionDto.FormId.ToString()) .OrderBy(s => s.CreationTime) .FirstOrDefault() ?? throw new EntityNotFoundException("Application Form Not Registered"); + + var submissionData = await submissionsIntService.GetSubmissionDataAsync(eventSubscriptionDto.FormId, eventSubscriptionDto.SubmissionId) + ?? throw new InvalidFormDataSubmissionException(); - var submissionData = await _submissionsIntService.GetSubmissionDataAsync(eventSubscriptionDto.FormId, eventSubscriptionDto.SubmissionId) ?? throw new InvalidFormDataSubmissionException(); - var formVersion = await _formsApiService.GetFormDataAsync(eventSubscriptionDto.FormId.ToString(), submissionData.submission.formVersionId.ToString()) ?? throw new InvalidFormDataSubmissionException(); - var result = _intakeFormSubmissionMapper.InitializeAvailableFormFields(formVersion); + // Access the 'submission' property from the JObject using the appropriate key. + var submission = submissionData["submission"] + ?? throw new InvalidFormDataSubmissionException(); + + // Ensure the 'formVersionId' is accessed correctly from the 'submission' object. + var formVersionId = submission["formVersionId"]?.ToString() + ?? throw new InvalidFormDataSubmissionException(); + + var formVersion = await formsApiService.GetFormDataAsync(eventSubscriptionDto.FormId.ToString(), formVersionId) + ?? throw new InvalidFormDataSubmissionException(); + var result = intakeFormSubmissionMapper.InitializeAvailableFormFields(formVersion); return !result.IsNullOrEmpty(); } @@ -61,7 +53,7 @@ public async Task PublishedFormAsync(EventSubscriptionDto eventSubscriptio string formId = eventSubscriptionDto.FormId.ToString(); string formVersionId = eventSubscriptionDto.FormVersion.ToString(); - var applicationForm = (await _applicationFormRepository + var applicationForm = (await applicationFormRepository .GetQueryableAsync()) .Where(s => s.ChefsApplicationFormGuid == formId) .OrderBy(s => s.CreationTime) @@ -72,18 +64,27 @@ public async Task PublishedFormAsync(EventSubscriptionDto eventSubscriptio && applicationForm.ChefsApplicationFormGuid != null) { // Go grab the new name/description/version and map the new available fields - var formVersion = await _formsApiService.GetFormDataAsync(formId, formVersionId); - dynamic form = await _formsApiService.GetForm(Guid.Parse(applicationForm.ChefsApplicationFormGuid), applicationForm.ChefsApplicationFormGuid.ToString(), applicationForm.ApiKey); - applicationForm = _applicationFormManager.SynchronizePublishedForm(applicationForm, formVersion, form); - await _applicationFormVersionAppService.UpdateOrCreateApplicationFormVersion(formId, formVersionId, applicationForm.Id, formVersion); - applicationForm = await _applicationFormRepository.UpdateAsync(applicationForm); - string teamsChannel = _configuration["Notifications:TeamsNotificationsWebhook"] ?? ""; - TeamsNotificationService.PostChefsEventToTeamsAsync(teamsChannel, eventSubscriptionDto.SubscriptionEvent, form, formVersion); + var formVersion = await formsApiService.GetFormDataAsync(formId, formVersionId); + if (formVersion == null) + { + throw new InvalidFormDataSubmissionException("Form version data is null."); + } + + dynamic form = await formsApiService.GetForm(Guid.Parse(applicationForm.ChefsApplicationFormGuid), applicationForm.ChefsApplicationFormGuid.ToString(), applicationForm.ApiKey); + if (form == null) + { + throw new InvalidFormDataSubmissionException("Form data is null."); + } + + applicationForm = applicationFormManager.SynchronizePublishedForm(applicationForm, formVersion, form); + await applicationFormVersionAppService.UpdateOrCreateApplicationFormVersion(formId, formVersionId, applicationForm.Id, formVersion); + applicationForm = await applicationFormRepository.UpdateAsync(applicationForm); + notificationsAppService.PostChefsEventToTeamsAsync(eventSubscriptionDto.SubscriptionEvent, form, formVersion); } - else if(applicationForm == null) + else if (applicationForm == null) { EventSubscription eventSubscription = ObjectMapper.Map(eventSubscriptionDto); - applicationForm = await _applicationFormManager.InitializeApplicationForm(eventSubscription); + applicationForm = await applicationFormManager.InitializeApplicationForm(eventSubscription); } return applicationForm != null; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs index 7692b4bfd..623f0fb61 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs @@ -41,6 +41,10 @@ using StackExchange.Redis; using Unity.GrantManager.Locks; using Unity.GrantManager.Zones; +using Volo.Abp; +using JsonSerializerOptions = System.Text.Json.JsonSerializerOptions; +using Unity.GrantManager.Integrations.Chefs; +using Unity.Modules.Shared.Http; namespace Unity.GrantManager; @@ -123,22 +127,14 @@ public override void ConfigureServices(ServiceConfigurationContext context) options.AddMaps(); }); - context.Services.AddSingleton(); - context.Services.AddSingleton(); - - Configure(options => - { - // This fails unit tests unless set to a non empty string - // RestClient will throw an error - baseUrl can not be empty - options.BaseUri = configuration["Intake:BaseUri"] ?? "https://submit.digital.gov.bc.ca/app/api/v1"; - options.BearerTokenPlaceholder = configuration["Intake:BearerTokenPlaceholder"] ?? ""; - options.UseBearerToken = configuration.GetValue("Intake:UseBearerToken"); - options.AllowUnregisteredVersions = configuration.GetValue("Intake:AllowUnregisteredVersions"); - }); - - context.Services.Configure(configuration.GetSection(key: "Payments")); - context.Services.Configure(configuration.GetSection(key: "CssApi")); - context.Services.Configure(configuration.GetSection(key: "Notifications")); + context.Services.AddSingleton(); + context.Services.AddScoped(); + context.Services.AddTransient, ConfigureIntakeClientOptions>(); + context.Services.AddTransient(); + context.Services.AddTransient(); + context.Services.Configure(configuration.GetSection("Payments")); + context.Services.Configure(configuration.GetSection("CssApi")); + context.Services.Configure(configuration.GetSection("Notifications")); ConfigureBackgroundServices(configuration); ConfigureDistributedCache(context, configuration); @@ -147,53 +143,32 @@ public override void ConfigureServices(ServiceConfigurationContext context) context.Services.ConfigureRabbitMQ(); context.Services.AddScoped(); - _ = context.Services.AddSingleton(provider => + context.Services.AddSingleton(provider => { var options = provider.GetService>()?.Value; - if (null != options) - { - var restOptions = new RestClientOptions(options.BaseUri) + var restOptions = options != null + ? new RestClientOptions(options.BaseUri) { - // NOTE: Basic authentication only works for fetching forms and lists of form submissions - // Authenticator = options.UseBearerToken ? - // new JwtAuthenticator(options.BearerTokenPlaceholder) : - // new HttpBasicAuthenticator(options.FormId, options.ApiKey), - FailOnDeserializationError = true, ThrowOnDeserializationError = true - }; + } + : new RestClientOptions(); - return new RestClient( - restOptions, - configureSerialization: s => - s.UseSystemTextJson(new System.Text.Json.JsonSerializerOptions - { - WriteIndented = true, - PropertyNameCaseInsensitive = true, - ReadCommentHandling = JsonCommentHandling.Skip, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - }) - ); - } - else - { - return new RestClient( - configureSerialization: s => - s.UseSystemTextJson(new System.Text.Json.JsonSerializerOptions - { - WriteIndented = true, - PropertyNameCaseInsensitive = true, - ReadCommentHandling = JsonCommentHandling.Skip, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - }) - ); - } + return new RestClient( + restOptions, + configureSerialization: s => s.UseSystemTextJson(new JsonSerializerOptions + { + WriteIndented = true, + PropertyNameCaseInsensitive = true, + ReadCommentHandling = JsonCommentHandling.Skip, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }) + ); }); - // Set the max defaults as max - we are using non serverside paging and this effect this + // Max paging limits ExtensibleLimitedResultRequestDto.DefaultMaxResultCount = int.MaxValue; ExtensibleLimitedResultRequestDto.MaxMaxResultCount = int.MaxValue; - LimitedResultRequestDto.DefaultMaxResultCount = int.MaxValue; LimitedResultRequestDto.MaxMaxResultCount = int.MaxValue; } @@ -214,7 +189,8 @@ private static void ConfigureDistributedLocking(ServiceConfigurationContext cont private void ConfigureBackgroundServices(IConfiguration configuration) { - if (!Convert.ToBoolean(configuration["BackgroundJobs:IsJobExecutionEnabled"])) return; + if (!Convert.ToBoolean(configuration["BackgroundJobs:IsJobExecutionEnabled"])) + return; Configure(options => { @@ -226,9 +202,6 @@ private void ConfigureBackgroundServices(IConfiguration configuration) options.IsAutoRegisterEnabled = configuration.GetValue("BackgroundJobs:Quartz:IsAutoRegisterEnabled"); }); - /* - * There are Global Retry Options that can be configured, configure if required, or if handled per job - */ Configure(options => { options.IsJobExecutionEnabled = configuration.GetValue("BackgroundJobs:IsJobExecutionEnabled"); @@ -238,7 +211,8 @@ private void ConfigureBackgroundServices(IConfiguration configuration) private void ConfigureDistributedCache(ServiceConfigurationContext context, IConfiguration configuration) { - if (!Convert.ToBoolean(configuration["Redis:IsEnabled"])) return; + if (!Convert.ToBoolean(configuration["Redis:IsEnabled"])) + return; context.Services.AddStackExchangeRedisCache(options => { @@ -257,4 +231,4 @@ private void ConfigureDistributedCache(ServiceConfigurationContext context, ICon options.KeyPrefix = configuration["Redis:KeyPrefix"] ?? "unity"; }); } -} \ No newline at end of file +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/INotificationApiUrlProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/INotificationApiUrlProvider.cs new file mode 100644 index 000000000..bd7aad2e3 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/INotificationApiUrlProvider.cs @@ -0,0 +1,8 @@ + +using System.Threading.Tasks; + +namespace Unity.GrantManager; +public interface INotificationApiUrlProvider +{ + Task GetBaseUrlAsync(); +} \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/IntakeSubmissionAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/IntakeSubmissionAppService.cs index fe242daf1..c9a74ab28 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/IntakeSubmissionAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/IntakeSubmissionAppService.cs @@ -1,5 +1,4 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options; using Newtonsoft.Json.Linq; using System; using System.Linq; @@ -9,49 +8,30 @@ using Unity.GrantManager.Events; using Unity.GrantManager.Exceptions; using Unity.GrantManager.Intake; -using Unity.GrantManager.Integration.Chefs; -using Unity.Notifications.TeamsNotifications; +using Unity.GrantManager.Integrations.Chefs; +using Unity.GrantManager.Notifications; using Volo.Abp; namespace Unity.GrantManager.Intakes { [RemoteService(false)] - public class IntakeSubmissionAppService : GrantManagerAppService, IIntakeSubmissionAppService + public class IntakeSubmissionAppService(INotificationsAppService notificationsAppService, + IIntakeFormSubmissionManager intakeFormSubmissionManager, + IFormsApiService formsApiService, + IApplicationFormRepository applicationFormRepository, + IApplicationFormVersionAppService applicationFormVersionAppService, + IOptions intakeClientOptions) : GrantManagerAppService, IIntakeSubmissionAppService { - private readonly IApplicationFormRepository _applicationFormRepository; - private readonly IIntakeFormSubmissionManager _intakeFormSubmissionManager; - private readonly IFormsApiService _formsApiService; - private readonly ISubmissionsApiService _submissionsIntService; - private readonly IApplicationFormVersionAppService _applicationFormVersionAppService; - private readonly IOptions _intakeClientOptions; - private readonly IConfiguration _configuration; - - public IntakeSubmissionAppService(IIntakeFormSubmissionManager intakeFormSubmissionManager, - IFormsApiService formsApiService, - ISubmissionsApiService submissionsIntService, - IApplicationFormRepository applicationFormRepository, - IApplicationFormVersionAppService applicationFormVersionAppService, - IOptions intakeClientOptions, - IConfiguration configuration) - { - _intakeFormSubmissionManager = intakeFormSubmissionManager; - _submissionsIntService = submissionsIntService; - _applicationFormRepository = applicationFormRepository; - _formsApiService = formsApiService; - _applicationFormVersionAppService = applicationFormVersionAppService; - _intakeClientOptions = intakeClientOptions; - _configuration = configuration; - } public async Task CreateIntakeSubmissionAsync(EventSubscriptionDto eventSubscriptionDto) { - var applicationForm = (await _applicationFormRepository + var applicationForm = (await applicationFormRepository .GetQueryableAsync()) .Where(s => s.ChefsApplicationFormGuid == eventSubscriptionDto.FormId.ToString()) .OrderBy(s => s.CreationTime) .FirstOrDefault() ?? throw new ApplicationFormSetupException("Application Form Not Registered"); - JObject submissionData = await _submissionsIntService.GetSubmissionDataAsync(eventSubscriptionDto.FormId, eventSubscriptionDto.SubmissionId) ?? throw new InvalidFormDataSubmissionException(); + JObject submissionData = await formsApiService.GetSubmissionDataAsync(eventSubscriptionDto.FormId, eventSubscriptionDto.SubmissionId) ?? throw new InvalidFormDataSubmissionException(); bool validSubmission = await ValidateSubmission(eventSubscriptionDto, submissionData); if (!validSubmission) { @@ -64,7 +44,7 @@ public async Task CreateIntakeSubmissionAsync( await StoreChefsFieldMappingAsync(eventSubscriptionDto, applicationForm, token); } - var result = await _intakeFormSubmissionManager.ProcessFormSubmissionAsync(applicationForm, submissionData); + var result = await intakeFormSubmissionManager.ProcessFormSubmissionAsync(applicationForm, submissionData); return new EventSubscriptionConfirmationDto() { ConfirmationId = result }; } @@ -75,7 +55,7 @@ private async Task ValidateSubmission(EventSubscriptionDto eventSubscripti if (tokenDraft != null && tokenDraft.ToString() == "True") { string factName = "A draft submission was submitted and should not have been"; string factValue = $"FormId: {eventSubscriptionDto.FormId} SubmissionID: {eventSubscriptionDto.SubmissionId}"; - await SendTeamsNotification(factName, factValue); + await notificationsAppService.NotifyChefsEventToTeamsAsync(factName, factValue); return false; } @@ -83,54 +63,43 @@ private async Task ValidateSubmission(EventSubscriptionDto eventSubscripti if (tokenDeleted != null && tokenDeleted.ToString() == "True") { string factName = "A deleted submission was submitted - user navigated back and got a success message from chefs"; string factValue = $"FormId: {eventSubscriptionDto.FormId} SubmissionID: {eventSubscriptionDto.SubmissionId}"; - await SendTeamsNotification(factName, factValue); + await notificationsAppService.NotifyChefsEventToTeamsAsync(factName, factValue); } // If there are no mappings initialize the available - bool formVersionExists = await _applicationFormVersionAppService.FormVersionExists(eventSubscriptionDto.FormVersion.ToString()); + bool formVersionExists = await applicationFormVersionAppService.FormVersionExists(eventSubscriptionDto.FormVersion.ToString()); if (!formVersionExists) { - dynamic? formVersion = await _formsApiService.GetFormDataAsync(eventSubscriptionDto.FormId.ToString(), + dynamic? formVersion = await formsApiService.GetFormDataAsync(eventSubscriptionDto.FormId.ToString(), eventSubscriptionDto.FormVersion.ToString()); if(formVersion == null) { string factName = "Application Form Version Not Registered - Unknown Version"; string factValue = $"FormId: {eventSubscriptionDto.FormId} FormVersion: {eventSubscriptionDto.FormVersion}"; - await SendTeamsNotification(factName, factValue); + await notificationsAppService.NotifyChefsEventToTeamsAsync(factName, factValue); return false; - } else if(!_intakeClientOptions.Value.AllowUnregisteredVersions) + } else if(!intakeClientOptions.Value.AllowUnregisteredVersions) { var version = ((JObject)formVersion!).SelectToken("version"); var published = ((JObject)formVersion!).SelectToken("published"); string factName = "Application Form Version Not Registered - Unknown Version"; string factValue = $"Application Form Version Not Registerd - Version: {version} Published: {published}"; - await SendTeamsNotification(factName, factValue); + await notificationsAppService.NotifyChefsEventToTeamsAsync(factName, factValue); return false; } } return true; } - private async Task SendTeamsNotification(string factName, string factValue) - { - string? envInfo = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); - string activityTitle = "Chefs Submission Event Validation Error"; - string activitySubtitle = "Environment: " + envInfo; - string teamsChannel = _configuration["Notifications:TeamsNotificationsWebhook"] ?? ""; - TeamsNotificationService teamsNotificationService = new TeamsNotificationService(); - teamsNotificationService.AddFact(factName, factValue); - await teamsNotificationService.PostFactsToTeamsAsync(teamsChannel, activityTitle, activitySubtitle); - } - private async Task StoreChefsFieldMappingAsync(EventSubscriptionDto eventSubscriptionDto, ApplicationForm applicationForm, JToken token) { Guid formVersionId = Guid.Parse(token.ToString()); - var formData = await _formsApiService.GetFormDataAsync(eventSubscriptionDto.FormId.ToString(), formVersionId.ToString()) ?? throw new InvalidFormDataSubmissionException(); + var formData = await formsApiService.GetFormDataAsync(eventSubscriptionDto.FormId.ToString(), formVersionId.ToString()) ?? throw new InvalidFormDataSubmissionException(); string chefsFormId = eventSubscriptionDto.FormId.ToString(); string chefsFormVersionId = eventSubscriptionDto.FormVersion.ToString(); - await _applicationFormVersionAppService.UpdateOrCreateApplicationFormVersion(chefsFormId, chefsFormVersionId, applicationForm.Id, formData); + await applicationFormVersionAppService.UpdateOrCreateApplicationFormVersion(chefsFormId, chefsFormVersionId, applicationForm.Id, formData); } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs index ea5dbac1d..37ff51056 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs @@ -1,82 +1,105 @@ using Newtonsoft.Json; -using RestSharp; -using RestSharp.Authenticators; +using Newtonsoft.Json.Linq; using System; using System.Linq; +using System.Net.Http; using System.Threading.Tasks; using Unity.GrantManager.Applications; -using Unity.GrantManager.Integrations.Chefs; using Unity.GrantManager.Integrations.Exceptions; -using Unity.GrantManager.Integrations.Http; using Volo.Abp; using Volo.Abp.Security.Encryption; +using Microsoft.Extensions.Logging; +using Volo.Abp.DependencyInjection; +using Unity.Modules.Shared.Http; namespace Unity.GrantManager.Integrations.Chefs { - [IntegrationService] - public class FormsApiService : GrantManagerAppService, IFormsApiService - { - private readonly IApplicationFormRepository _applicationFormRepository; - private readonly IStringEncryptionService _stringEncryptionService; - private readonly IResilientHttpRequest _resilientRestClient; - - public FormsApiService(IApplicationFormRepository applicationFormRepository, + [IntegrationService] // <- registers the class as the interface it implements + [ExposeServices(typeof(IFormsApiService))] // <- ensure it's registered under its interface + public class FormsApiService( + IEndpointManagementAppService endpointManagementAppService, + IApplicationFormRepository applicationFormRepository, IStringEncryptionService stringEncryptionService, - IResilientHttpRequest resilientRestClient) - { - _applicationFormRepository = applicationFormRepository; - _stringEncryptionService = stringEncryptionService; - _resilientRestClient = resilientRestClient; - } - - public async Task GetFormDataAsync(string chefsFormId, string chefsFormVersionId) + IResilientHttpRequest resilientRestClient, + ILogger logger) : GrantManagerAppService, IFormsApiService + { + public async Task GetFormDataAsync(string chefsFormId, string chefsFormVersionId) { - var applicationForm = (await _applicationFormRepository - .GetQueryableAsync()) + var applicationForm = (await applicationFormRepository.GetQueryableAsync()) .Where(s => s.ChefsApplicationFormGuid == chefsFormId) .OrderBy(s => s.CreationTime) .FirstOrDefault(); - if (applicationForm == null) return null; - - var response = await _resilientRestClient - .HttpAsync(Method.Get, $"/forms/{chefsFormId}/versions/{chefsFormVersionId}", - null, - null, - new HttpBasicAuthenticator(applicationForm.ChefsApplicationFormGuid!, _stringEncryptionService.Decrypt(applicationForm.ApiKey!) ?? string.Empty)); - - if (response != null - && response.Content != null - && response.IsSuccessStatusCode) + if (applicationForm == null) { - string content = response.Content; - return JsonConvert.DeserializeObject(content)!; + logger.LogWarning("No application form found for FormId: {FormId}", chefsFormId); + return null; } - else + + string url = $"/forms/{chefsFormId}/versions/{chefsFormVersionId}"; + var response = await GetRequestAsync(url, applicationForm.ChefsApplicationFormGuid!, applicationForm.ApiKey!); + + var content = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(content); + } + + public async Task GetForm(Guid? formId, string chefsApplicationFormGuid, string encryptedApiKey) + { + if (string.IsNullOrEmpty(chefsApplicationFormGuid) || string.IsNullOrEmpty(encryptedApiKey)) { - throw new IntegrationServiceException($"Error with integrating with request resource"); + throw new ArgumentException("Form GUID and API Key must be provided"); } + + string url = $"/forms/{formId}"; + var response = await GetRequestAsync(url, chefsApplicationFormGuid, encryptedApiKey); + + var content = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(content)!; } - public async Task GetForm(Guid? formId, string chefsApplicationFormGuid, string encryptedApiKey) + public async Task GetSubmissionDataAsync(Guid chefsFormId, Guid submissionId) { - var response = await _resilientRestClient - .HttpAsync(Method.Get, $"/forms/{formId}", - null, - null, - new HttpBasicAuthenticator(chefsApplicationFormGuid!, _stringEncryptionService.Decrypt(encryptedApiKey!) ?? string.Empty)); - - if (response != null - && response.Content != null - && response.IsSuccessStatusCode) + var applicationForm = (await applicationFormRepository.GetQueryableAsync()) + .Where(s => s.ChefsApplicationFormGuid == chefsFormId.ToString()) + .OrderBy(s => s.CreationTime) + .FirstOrDefault(); + + if (applicationForm == null) { - return response.Content; + logger.LogWarning("No application form found for SubmissionId: {SubmissionId}", submissionId); + return null; } - else + + string url = $"/submissions/{submissionId}"; + var response = await GetRequestAsync(url, applicationForm.ChefsApplicationFormGuid!, applicationForm.ApiKey!); + + var content = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(content); + } + + private async Task GetRequestAsync(string url, string chefsApplicationFormGuid, string encryptedApiKey) + { + var decryptedApiKey = stringEncryptionService.Decrypt(encryptedApiKey ?? string.Empty) ?? string.Empty; + string chefsApi = await endpointManagementAppService.GetChefsApiBaseUrlAsync(); + + resilientRestClient.SetBaseUrl(chefsApi); // Set the base URL for the API + logger.LogInformation("Sending GET request to {Url} using basic auth", url); + + var response = await resilientRestClient.ExecuteRequestAsync( + HttpMethod.Get, + url, + null, + null, + basicAuth: (chefsApplicationFormGuid, decryptedApiKey)); + + if (!response.IsSuccessStatusCode) { - throw new IntegrationServiceException($"Error with integrating with request resource"); + var content = await response.Content.ReadAsStringAsync(); + logger.LogError("Request to {Url} failed with status {StatusCode}. Response: {Content}", url, response.StatusCode, content); + throw new IntegrationServiceException($"Failed to get data. Status: {response.StatusCode}, URL: {url}"); } + + return response; } } } - diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/SubmissionsApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/SubmissionsApiService.cs deleted file mode 100644 index f9ce8ad1a..000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/SubmissionsApiService.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Newtonsoft.Json; -using RestSharp; -using RestSharp.Authenticators; -using System; -using System.Linq; -using System.Threading.Tasks; -using Unity.GrantManager.Applications; -using Unity.GrantManager.Integrations.Exceptions; -using Unity.GrantManager.Integrations.Http; -using Volo.Abp; -using Volo.Abp.Security.Encryption; - -namespace Unity.GrantManager.Integrations.Chefs -{ - [IntegrationService] - public class SubmissionsApiService : GrantManagerAppService, ISubmissionsApiService - { - private readonly IResilientHttpRequest _resilientRestClient; - private readonly IApplicationFormRepository _applicationFormRepository; - private readonly IStringEncryptionService _stringEncryptionService; - - public SubmissionsApiService(IResilientHttpRequest resilientRestClient, - IApplicationFormRepository applicationFormRepository, - IStringEncryptionService stringEncryptionService) - { - _resilientRestClient = resilientRestClient; - _applicationFormRepository = applicationFormRepository; - _stringEncryptionService = stringEncryptionService; - } - - public async Task GetSubmissionDataAsync(Guid chefsFormId, Guid submissionId) - { - var applicationForm = (await _applicationFormRepository - .GetQueryableAsync()) - .Where(s => s.ChefsApplicationFormGuid == chefsFormId.ToString()) - .OrderBy(s => s.CreationTime) - .FirstOrDefault(); - - if (applicationForm == null) return null; - - var response = await _resilientRestClient - .HttpAsync(Method.Get, $"/submissions/{submissionId}", - null, - null, - new HttpBasicAuthenticator(applicationForm.ChefsApplicationFormGuid!, _stringEncryptionService.Decrypt(applicationForm.ApiKey!) ?? string.Empty)); - - if (response != null - && response.Content != null - && response.IsSuccessStatusCode) - { - string content = response.Content; - return JsonConvert.DeserializeObject(content)!; - } - else - { - throw new IntegrationServiceException($"Error with integrating with request resource"); - } - } - } -} - diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Css/CssApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Css/CssApiService.cs index c999231e5..5567937ca 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Css/CssApiService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Css/CssApiService.cs @@ -1,135 +1,118 @@ -using RestSharp; -using RestSharp.Authenticators; -using System.Collections.Generic; -using System.Net; -using System.Threading.Tasks; -using Unity.GrantManager.Integrations.Http; -using Volo.Abp.Application.Services; -using System.Text.Json; -using System; -using Volo.Abp.Caching; -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using System.Collections.Generic; using System.Linq; +using System; +using Unity.GrantManager.Integrations.Css; using Volo.Abp; +using Volo.Abp.Application.Services; using Volo.Abp.DependencyInjection; -using Unity.GrantManager.Integrations.Css; +using Volo.Abp.Caching; +using Unity.Modules.Shared.Http; namespace Unity.GrantManager.Integrations.Sso { [IntegrationService] [ExposeServices(typeof(CssApiService), typeof(ICssUsersApiService))] - public class CssApiService(IResilientHttpRequest resilientHttpRequest, - IDistributedCache accessTokenCache, - IOptions cssApiOptions, - RestClient restClient) : ApplicationService, ICssUsersApiService + public class CssApiService : ApplicationService, ICssUsersApiService { + private readonly IResilientHttpRequest _resilientHttpRequest; + private readonly IDistributedCache _accessTokenCache; + private readonly CssApiOptions _cssApiOptions; private const string CSS_API_KEY = "CssApiKey"; - public async Task FindUserAsync(string directory, string guid) + public CssApiService( + IResilientHttpRequest resilientHttpRequest, + IDistributedCache accessTokenCache, + IOptions cssApiOptions) { - var paramDictionary = new Dictionary(); - - if (guid != null) - { - paramDictionary.Add(nameof(guid), guid); - } + _resilientHttpRequest = resilientHttpRequest; + _accessTokenCache = accessTokenCache; + _cssApiOptions = cssApiOptions.Value; + } - return await SearchSsoAsync(directory, paramDictionary); + public async Task FindUserAsync(string directory, string guid) + { + var parameters = new Dictionary { { nameof(guid), guid } }; + return await SearchSsoAsync(directory, parameters); } public async Task SearchUsersAsync(string directory, string? firstName = null, string? lastName = null) - { - var paramDictionary = new Dictionary(); + { + var parameters = new Dictionary(); - if (firstName != null && firstName.Length >= 2) - { - paramDictionary.Add(nameof(firstName), firstName); - } + if (!string.IsNullOrWhiteSpace(firstName) && firstName.Length >= 2) + parameters.Add(nameof(firstName), firstName); - if (lastName != null && lastName.Length >= 2) - { - paramDictionary.Add(nameof(lastName), lastName); - } + if (!string.IsNullOrWhiteSpace(lastName) && lastName.Length >= 2) + parameters.Add(nameof(lastName), lastName); - return await SearchSsoAsync(directory, paramDictionary); + return await SearchSsoAsync(directory, parameters); } - private async Task SearchSsoAsync(string directory, Dictionary paramDictionary) + private async Task SearchSsoAsync(string directory, Dictionary parameters) { var tokenResponse = await GetAccessTokenAsync(); + var baseUrl = $"{_cssApiOptions.Url}/{_cssApiOptions.Env}/{directory}/users"; + var url = BuildUrlWithQuery(baseUrl, parameters); - var resource = BuildUrlWithQueryStringUsingUriBuilder($"{cssApiOptions.Value.Url}/{cssApiOptions.Value.Env}/{directory}/users?", paramDictionary); - - var authHeaders = new Dictionary - { - { "Authorization", $"Bearer {tokenResponse.AccessToken}" } - }; - - var response = await resilientHttpRequest.HttpAsync(Method.Get, resource, authHeaders); + _resilientHttpRequest.SetBaseUrl(""); + var response = await _resilientHttpRequest.ExecuteRequestAsync(HttpMethod.Get, url, null, tokenResponse.AccessToken); - if (response != null - && response.Content != null - && response.IsSuccessStatusCode) + if (response != null && response.IsSuccessStatusCode && response.Content != null) { - string content = response.Content; - var result = JsonSerializer.Deserialize(content) ?? throw new UserFriendlyException("SearchSsoAsync -> Could not Deserialize"); - + var json = await response.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize(json) ?? throw new UserFriendlyException("Could not deserialize user search result."); result.Success = true; return result; } - else + + return new UserSearchResult { - return new UserSearchResult() { Error = "", Success = false, Data = Array.Empty() }; - } + Success = false, + Error = "Failed to search users", + Data = Array.Empty() + }; } - private static string BuildUrlWithQueryStringUsingUriBuilder(string basePath, Dictionary queryParams) + private static string BuildUrlWithQuery(string basePath, Dictionary queryParams) { - var uriBuilder = new UriBuilder(basePath) - { - Query = string.Join("&", queryParams.Select(kvp => $"{kvp.Key}={kvp.Value}")) - }; - return uriBuilder.Uri.AbsoluteUri; + var query = string.Join("&", queryParams.Select(kv => $"{kv.Key}={Uri.EscapeDataString(kv.Value)}")); + return string.IsNullOrWhiteSpace(query) ? basePath : $"{basePath}?{query}"; } private async Task GetAccessTokenAsync() { - var cachedTokenResponse = await accessTokenCache.GetAsync(CSS_API_KEY); + var cachedToken = await _accessTokenCache.GetAsync(CSS_API_KEY); + if (cachedToken != null) + return cachedToken; - if (cachedTokenResponse != null) - { - return cachedTokenResponse; - } + var client = new HttpClient(); + var request = new HttpRequestMessage(HttpMethod.Post, _cssApiOptions.TokenUrl); - var grantType = "client_credentials"; + var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{_cssApiOptions.ClientId}:{_cssApiOptions.ClientSecret}")); + request.Headers.Authorization = new AuthenticationHeaderValue("Basic", credentials); + request.Content = new StringContent("grant_type=client_credentials", Encoding.UTF8, "application/x-www-form-urlencoded"); - var request = new RestRequest($"{cssApiOptions.Value.TokenUrl}") - { - Authenticator = new HttpBasicAuthenticator(cssApiOptions.Value.ClientId, cssApiOptions.Value.ClientSecret) - }; - request.AddHeader("content-type", "application/x-www-form-urlencoded"); - request.AddParameter("application/x-www-form-urlencoded", $"grant_type={grantType}", ParameterType.RequestBody); - - var response = await restClient.ExecuteAsync(request, Method.Post); + var response = await client.SendAsync(request); - if (response.Content == null) + if (!response.IsSuccessStatusCode || response.Content == null) { - throw new UserFriendlyException($"Error fetching Css API token - content empty {response.StatusCode} {response.ErrorMessage}"); + var errorContent = response.Content != null ? await response.Content.ReadAsStringAsync() : "No content"; + Logger.LogError("Failed to fetch CSS API token. Status: {StatusCode}, Content: {ErrorContent}", response.StatusCode, errorContent); + throw new UserFriendlyException($"Error fetching token: {response.StatusCode}"); } - if (response.StatusCode != HttpStatusCode.OK) - { - Logger.LogError(response.ErrorException, "Error fetching Css API token {StatusCode} {ErrorMessage} {ErrorException}", response.StatusCode, response.ErrorMessage, response.ErrorException); - - if (response.StatusCode == HttpStatusCode.Unauthorized) - { - throw new UnauthorizedAccessException(response.ErrorMessage); - } - } + var content = await response.Content.ReadAsStringAsync(); + var tokenResponse = JsonSerializer.Deserialize(content) ?? throw new UserFriendlyException("Could not parse token response."); - var tokenResponse = JsonSerializer.Deserialize(response.Content) ?? throw new UserFriendlyException($"Error deserializing token response {response.StatusCode} {response.ErrorMessage}"); - await accessTokenCache.SetAsync(CSS_API_KEY, tokenResponse, new Microsoft.Extensions.Caching.Distributed.DistributedCacheEntryOptions() + await _accessTokenCache.SetAsync(CSS_API_KEY, tokenResponse, new DistributedCacheEntryOptions { AbsoluteExpiration = DateTimeOffset.UtcNow.AddSeconds(tokenResponse.ExpiresIn) }); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs index ce8469d24..537bd7bc4 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; using Volo.Abp.DependencyInjection; @@ -6,7 +7,6 @@ namespace Unity.GrantManager.Integrations.Endpoints { - [ExposeServices(typeof(EndpointManagementAppService), typeof(IEndpointManagementAppService))] public class EndpointManagementAppService : CrudAppService< @@ -21,5 +21,18 @@ public EndpointManagementAppService(IRepository repository) : base(repository) { } + + public async Task GetChefsApiBaseUrlAsync() + { + var url = await GetUrlByKeyNameAsync("INTAKE_API_BASE"); + if (string.IsNullOrWhiteSpace(url)) throw new Exception("CHEFS API base URL not configured."); + return url; + } + + public async Task GetUrlByKeyNameAsync(string keyName) + { + var dynamicUrl = await Repository.FirstOrDefaultAsync(x => x.KeyName == keyName); + return dynamicUrl?.Url ?? string.Empty; + } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Geocoder/GeocoderApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Geocoder/GeocoderApiService.cs index cc3f4c86b..039560efe 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Geocoder/GeocoderApiService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Geocoder/GeocoderApiService.cs @@ -1,24 +1,25 @@ using Microsoft.Extensions.Configuration; using Newtonsoft.Json; -using RestSharp; +using System.Net.Http; using System.Threading.Tasks; -using Unity.GrantManager.Integrations.Geocoder; using Unity.GrantManager.Integrations.Exceptions; -using Unity.GrantManager.Integrations.Http; +using Unity.Modules.Shared.Http; using Volo.Abp; using Volo.Abp.Application.Services; using Volo.Abp.DependencyInjection; namespace Unity.GrantManager.Integrations.Geocoder { - [IntegrationService] + [IntegrationService] [ExposeServices(typeof(GeocoderApiService), typeof(IGeocoderApiService))] public class GeocoderApiService : ApplicationService, IGeocoderApiService { private readonly IResilientHttpRequest _resilientRestClient; private readonly IConfiguration _configuration; - public GeocoderApiService(IResilientHttpRequest resilientRestClient, IConfiguration configuration) + public GeocoderApiService( + IResilientHttpRequest resilientRestClient, + IConfiguration configuration) { _resilientRestClient = resilientRestClient; _configuration = configuration; @@ -27,65 +28,70 @@ public GeocoderApiService(IResilientHttpRequest resilientRestClient, IConfigurat public async Task GetAddressDetailsAsync(string address) { var resource = $"{_configuration["Geocoder:LocationDetails:BaseUri"]}/addresses.json?outputSRS=3005&addressString={address}"; - - return ResultMapper.MapToLocation(await GetGeoCodeDataSegmentAsync(resource)); + var result = await GetGeoCodeDataSegmentAsync(resource); + return ResultMapper.MapToLocation(result); } public async Task GetElectoralDistrictAsync(LocationCoordinates locationCoordinates) { - var resource = $"{_configuration["Geocoder:BaseUri"]}" + - $"{_configuration["Geocoder:ElectoralDistrict:feature"]}" + - $"&srsname=EPSG:4326" + - $"&propertyName={_configuration["Geocoder:ElectoralDistrict:property"]}" + - $"&outputFormat=application/json" + - $"&cql_filter=INTERSECTS({_configuration["Geocoder:ElectoralDistrict:querytype"]}" + - $",POINT(" + locationCoordinates.Latitude.ToString() + " " + locationCoordinates.Longitude.ToString() + "))"; - - return ResultMapper.MapToElectoralDistrict(await GetGeoCodeDataSegmentAsync(resource)); + var resource = BuildGeocoderQuery( + featureKey: "ElectoralDistrict:feature", + propertyKey: "ElectoralDistrict:property", + queryTypeKey: "ElectoralDistrict:querytype", + coordinates: locationCoordinates); + var result = await GetGeoCodeDataSegmentAsync(resource); + return ResultMapper.MapToElectoralDistrict(result); } public async Task GetEconomicRegionAsync(LocationCoordinates locationCoordinates) { - var resource = $"{_configuration["Geocoder:BaseUri"]}" + - $"{_configuration["Geocoder:EconomicRegion:feature"]}" + - $"&srsname=EPSG:4326" + - $"&propertyName={_configuration["Geocoder:EconomicRegion:property"]}" + - $"&outputFormat=application%2Fjson" + - $"&cql_filter=INTERSECTS({_configuration["Geocoder:EconomicRegion:querytype"]}" + - $",POINT(" + locationCoordinates.Latitude.ToString() + " " + locationCoordinates.Longitude.ToString() + "))"; - - return ResultMapper.MapToEconomicRegion(await GetGeoCodeDataSegmentAsync(resource)); + var resource = BuildGeocoderQuery( + featureKey: "EconomicRegion:feature", + propertyKey: "EconomicRegion:property", + queryTypeKey: "EconomicRegion:querytype", + coordinates: locationCoordinates); + var result = await GetGeoCodeDataSegmentAsync(resource); + return ResultMapper.MapToEconomicRegion(result); } public async Task GetRegionalDistrictAsync(LocationCoordinates locationCoordinates) { - var resource = $"{_configuration["Geocoder:BaseUri"]}" + - $"{_configuration["Geocoder:RegionalDistrict:feature"]}" + - $"&srsname=EPSG:4326" + - $"&propertyName={_configuration["Geocoder:RegionalDistrict:property"]}" + - $"&outputFormat=application/json" + - $"&cql_filter=INTERSECTS({_configuration["Geocoder:RegionalDistrict:querytype"]}" + - $",POINT(" + locationCoordinates.Latitude.ToString() + " " + locationCoordinates.Longitude.ToString() + "))"; - - return ResultMapper.MapToRegionalDistrict(await GetGeoCodeDataSegmentAsync(resource)); + var resource = BuildGeocoderQuery( + featureKey: "RegionalDistrict:feature", + propertyKey: "RegionalDistrict:property", + queryTypeKey: "RegionalDistrict:querytype", + coordinates: locationCoordinates); + var result = await GetGeoCodeDataSegmentAsync(resource); + return ResultMapper.MapToRegionalDistrict(result); } private async Task GetGeoCodeDataSegmentAsync(string resource) { - var response = await _resilientRestClient.HttpAsync(Method.Get, resource); + var response = await _resilientRestClient.ExecuteRequestAsync(HttpMethod.Get, resource, null, null); - if (response != null - && response.Content != null - && response.IsSuccessStatusCode) + if (response != null && response.IsSuccessStatusCode && response.Content != null) { - string content = response.Content; + var content = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject(content)!; } else { - throw new IntegrationServiceException($"Error with integrating with request resource"); + throw new IntegrationServiceException($"Error integrating with resource: {resource}. Status: {response?.StatusCode}"); } } + + private string BuildGeocoderQuery(string featureKey, string propertyKey, string queryTypeKey, LocationCoordinates coordinates) + { + var baseUri = _configuration["Geocoder:BaseUri"]; + var feature = _configuration[$"Geocoder:{featureKey}"]; + var property = _configuration[$"Geocoder:{propertyKey}"]; + var queryType = _configuration[$"Geocoder:{queryTypeKey}"]; + + return $"{baseUri}{feature}" + + $"&srsname=EPSG:4326" + + $"&propertyName={property}" + + $"&outputFormat=application/json" + + $"&cql_filter=INTERSECTS({queryType},POINT({coordinates.Latitude} {coordinates.Longitude}))"; + } } } - diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Http/IResilientHttpRequest.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Http/IResilientHttpRequest.cs deleted file mode 100644 index 899311a98..000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Http/IResilientHttpRequest.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Polly.CircuitBreaker; -using Polly.Retry; -using RestSharp; -using RestSharp.Authenticators; -using System.Collections.Generic; -using System.Threading.Tasks; -using Volo.Abp.Application.Services; - -namespace Unity.GrantManager.Integrations.Http -{ - public interface IResilientHttpRequest : IApplicationService - { - Task HttpAsync(Method httpVerb, string resource, Dictionary? headers = null, object? requestObject = null, IAuthenticator? authenticator = null); - Task HttpAsync(Method httpVerb, string resource, AsyncRetryPolicy retryPolicy, Dictionary? headers = null, object? requestObject = null, IAuthenticator? authenticator = null); - Task HttpAsync(Method httpVerb, string resource, AsyncRetryPolicy retryPolicy, AsyncCircuitBreakerPolicy circuitBreakerPolicy, Dictionary? headers = null, object? requestObject = null, IAuthenticator? authenticator = null); - } -} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Http/ResilientHttpRequest.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Http/ResilientHttpRequest.cs deleted file mode 100644 index 8e281ee60..000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Http/ResilientHttpRequest.cs +++ /dev/null @@ -1,108 +0,0 @@ -using Microsoft.Extensions.Logging; -using Polly; -using Polly.CircuitBreaker; -using Polly.Retry; -using RestSharp; -using RestSharp.Authenticators; -using System; -using System.Collections.Generic; -using System.Net; -using System.Threading.Tasks; -using Volo.Abp; - -namespace Unity.GrantManager.Integrations.Http -{ - [IntegrationService] - public class ResilientHttpRequest : GrantManagerAppService, IResilientHttpRequest - { - private readonly RestClient _restClient; - private static int _maxRetryAttempts = 3; - private static TimeSpan _pauseBetweenFailures = TimeSpan.FromSeconds(10); - - public ResilientHttpRequest(RestClient restClient) - { - _restClient = restClient; - } - - public static void SetMaxRetryAttemptsAndPauseBetweenFailures(int maxRetryAttempts, TimeSpan pauseBetweenFailures) - { - _maxRetryAttempts = maxRetryAttempts; - _pauseBetweenFailures = pauseBetweenFailures; - } - - public async Task HttpAsync(Method httpVerb, string resource, Dictionary? headers = null, object? requestObject = null, IAuthenticator? authenticator = null) - { - return await ExecuteRequestAsync(httpVerb, resource, headers, requestObject, null, null, authenticator); - } - - public async Task HttpAsync(Method httpVerb, string resource, AsyncRetryPolicy retryPolicy, Dictionary? headers = null, object? requestObject = null, IAuthenticator? authenticator = null) - { - return await ExecuteRequestAsync(httpVerb, resource, headers, requestObject, retryPolicy, null, authenticator); - } - - public async Task HttpAsync(Method httpVerb, string resource, AsyncRetryPolicy retryPolicy, AsyncCircuitBreakerPolicy circuitBreakerPolicy, Dictionary? headers = null, object? requestObject = null, IAuthenticator? authenticator = null) - { - return await ExecuteRequestAsync(httpVerb, resource, headers, requestObject, retryPolicy, circuitBreakerPolicy, authenticator); - } - - private async Task ExecuteRequestAsync(Method httpVerb, string resource, Dictionary? headers, object? requestObject, AsyncRetryPolicy? retryPolicy = null, AsyncCircuitBreakerPolicy? circuitBreakerPolicy = null, IAuthenticator? authenticator = null) - { - RestResponse? restResponse; - - try - { - var restRequest = new RestRequest(resource, httpVerb); - if (authenticator != null) - { - restRequest.Authenticator = authenticator; - } - restRequest.AddHeader("cache-control", "no-cache"); - if (headers != null && headers.Count > 0) - foreach (var header in headers) - restRequest.AddHeader(header.Key, header.Value); - - if (httpVerb != Method.Get && requestObject != null) - { - restRequest.RequestFormat = DataFormat.Json; - restRequest.AddJsonBody(requestObject); - } - restResponse = await RestResponseWithPolicyAsync(restRequest, retryPolicy, circuitBreakerPolicy); - } - catch (Exception ex) - { - restResponse = new RestResponse - { - Content = ex.Message, - ErrorMessage = ex.Message, - ResponseStatus = ResponseStatus.TimedOut, - StatusCode = HttpStatusCode.ServiceUnavailable - }; - } - - return await Task.FromResult(restResponse); - } - - private async Task RestResponseWithPolicyAsync(RestRequest restRequest, AsyncRetryPolicy? retryPolicy = null, AsyncCircuitBreakerPolicy? circuitBreakerPolicy = null) - { - retryPolicy ??= Policy - .HandleResult(x => !x.IsSuccessful) - .WaitAndRetryAsync(_maxRetryAttempts, x => _pauseBetweenFailures, async (iRestResponse, timeSpan, retryCount, context) => - { - await Task.Run(() => Logger.LogError("The request failed. HttpStatusCode={statusCode}. Waiting {timeSpan} seconds before retry. Number attempt {retryCount}. Uri={responseUri}; RequestResponse={responseContent}", iRestResponse.Result.StatusCode, timeSpan, retryCount, iRestResponse.Result.ResponseUri, iRestResponse.Result.Content)); - }); - - circuitBreakerPolicy ??= Policy - .HandleResult(x => x.StatusCode == HttpStatusCode.ServiceUnavailable || x.StatusCode == HttpStatusCode.TooManyRequests) - .CircuitBreakerAsync(1, TimeSpan.FromSeconds(30), onBreak: async (iRestResponse, timespan, context) => - { - await Task.Run(() => Logger.LogError("Circuit went into a fault state. Reason: {resultContent}", iRestResponse.Result.Content)); - }, - onReset: async (context) => - { - await Task.Run(() => Logger.LogError($"Circuit left the fault state.")); - }); - - return await retryPolicy.WrapAsync(circuitBreakerPolicy).ExecuteAsync(async () => await _restClient.ExecuteAsync(restRequest)); - } - } -} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/OrgBook/OrgBookService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/OrgBook/OrgBookService.cs index 0f37c7a69..c96508cc4 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/OrgBook/OrgBookService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/OrgBook/OrgBookService.cs @@ -1,12 +1,11 @@ using Newtonsoft.Json; -using RestSharp; using System.Text.Json; using System.Threading.Tasks; -using Unity.GrantManager.Integrations.Orgbook; using Unity.GrantManager.Integrations.Exceptions; -using Unity.GrantManager.Integrations.Http; using Volo.Abp.Application.Services; using Volo.Abp.DependencyInjection; +using System.Net.Http; +using Unity.Modules.Shared.Http; namespace Unity.GrantManager.Integrations.Orgbook { @@ -26,11 +25,12 @@ public OrgBookService(IResilientHttpRequest resilientRestClient) { public async Task GetOrgBookQueryAsync(string orgBookQuery) { var response = await _resilientRestClient - .HttpAsync(Method.Get, $"{orgbook_base_api}/v4/search/topic?q={orgBookQuery}&{orgbook_query_match}"); + .ExecuteRequestAsync(HttpMethod.Get, $"{orgbook_base_api}/v4/search/topic?q={orgBookQuery}&{orgbook_query_match}", null, null, null); if (response != null && response.Content != null) { - string content = response.Content; + // Fix: Read the content as a string before deserializing it + var content = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject(content)!; } else @@ -47,12 +47,13 @@ public async Task GetOrgBookAutocompleteQueryAsync(string? orgBook } var response = await _resilientRestClient - .HttpAsync(Method.Get, $"{orgbook_base_api}/v3/search/autocomplete?q={orgBookQuery}&revoked=false&inactive="); + .ExecuteRequestAsync(HttpMethod.Get, $"{orgbook_base_api}/v3/search/autocomplete?q={orgBookQuery}&revoked=false&inactive=", null, null, null); if (response != null && response.Content != null) { - - return JsonDocument.Parse(response.Content); + // Fix: Read the content as a string before parsing it into a JsonDocument + var content = await response.Content.ReadAsStringAsync(); + return JsonDocument.Parse(content); } else { @@ -68,12 +69,13 @@ public async Task GetOrgBookDetailsQueryAsync(string? orgBookId) } var response = await _resilientRestClient - .HttpAsync(Method.Get, $"{orgbook_base_api}/v2/topic/ident/registration.registries.ca/{orgBookId}/formatted"); + .ExecuteRequestAsync(HttpMethod.Get, $"{orgbook_base_api}/v2/topic/ident/registration.registries.ca/{orgBookId}/formatted", null, null, null); if (response != null && response.Content != null) { - - return JsonDocument.Parse(response.Content); + // Fix: Read the content as a string before parsing it into a JsonDocument + var content = await response.Content.ReadAsStringAsync(); + return JsonDocument.Parse(content); } else { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Emails/EmailAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Norifications/EmailAppService.cs similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/Emails/EmailAppService.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application/Norifications/EmailAppService.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Norifications/NotificationsAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Norifications/NotificationsAppService.cs new file mode 100644 index 000000000..6fecf772b --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Norifications/NotificationsAppService.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Unity.GrantManager.Applications; +using Unity.GrantManager.Integrations; +using Unity.Notifications.TeamsNotifications; +using Volo.Abp; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; + +namespace Unity.GrantManager.Notifications +{ + // This class is responsible for first lookup up the Teams channel URL from the database and then posting notifications to the Teams Service. + [Dependency(ReplaceServices = true)] + [ExposeServices(typeof(NotificationsAppService), typeof(INotificationsAppService))] + public class NotificationsAppService : INotificationsAppService, ITransientDependency + { + private readonly IDynamicUrlRepository _dynamicUrlRepository; + private readonly TeamsNotificationService _teamsNotificationService; + + public NotificationsAppService(IDynamicUrlRepository dynamicUrlRepository) + { + _dynamicUrlRepository = dynamicUrlRepository; + _teamsNotificationService = new TeamsNotificationService(); + } + + public async Task InitializeTeamsChannelAsync(string keyName) + { + DynamicUrl? teamsChannel = await _dynamicUrlRepository.FirstOrDefaultAsync(q => q.KeyName == keyName); + if (teamsChannel?.Url == null) + { + return ""; + } + return teamsChannel.Url; + } + + [RemoteService(false)] + public async Task NotifyChefsEventToTeamsAsync(string factName, string factValue) + { + string teamsChannel = await InitializeTeamsChannelAsync(TeamsNotificationService.TEAMS_NOTIFICATION); + if (teamsChannel.IsNullOrEmpty()) + { + return; + } + + string? envInfo = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); + string activityTitle = "Chefs Submission Event Validation Error"; + string activitySubtitle = "Environment: " + envInfo; + _teamsNotificationService.AddFact(factName, factValue); + await _teamsNotificationService.PostFactsToTeamsAsync(teamsChannel, activityTitle, activitySubtitle); + } + + public async Task PostToTeamsAsync(string activityTitle, string activitySubtitle, List facts) + { + string teamsChannel = await InitializeTeamsChannelAsync(TeamsNotificationService.TEAMS_NOTIFICATION); + if (teamsChannel.IsNullOrEmpty()) + { + return; + } + + string messageCard = TeamsNotificationService.InitializeMessageCard(activityTitle, activitySubtitle, facts); + await TeamsNotificationService.PostToTeamsChannelAsync(teamsChannel, messageCard); + } + + public async Task PostToTeamsAsync(string activityTitle, string activitySubtitle) + { + string teamsChannel = await InitializeTeamsChannelAsync(TeamsNotificationService.TEAMS_NOTIFICATION); + if (teamsChannel.IsNullOrEmpty()) + { + return; + } + List facts = new() { }; + string messageCard = TeamsNotificationService.InitializeMessageCard(activityTitle, activitySubtitle, facts); + await TeamsNotificationService.PostToTeamsChannelAsync(teamsChannel, messageCard); + } + + public async Task PostChefsEventToTeamsAsync(string subscriptionEvent, dynamic form, dynamic chefsFormVersion) + { + string teamsChannel = await InitializeTeamsChannelAsync(TeamsNotificationService.TEAMS_NOTIFICATION_1); + if (teamsChannel.IsNullOrEmpty()) + { + return; + } + await TeamsNotificationService.PostChefsEventToTeamsAsync(teamsChannel, subscriptionEvent, form, chefsFormVersion); + } + } +} \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/NotificationApiUrlHolder.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/NotificationApiUrlHolder.cs new file mode 100644 index 000000000..6dd0e861c --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/NotificationApiUrlHolder.cs @@ -0,0 +1,6 @@ + +namespace Unity.GrantManager; +public class NotificationApiUrlHolder +{ + public string? BaseUrl { get; set; } +} \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/NotificationApiUrlProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/NotificationApiUrlProvider.cs new file mode 100644 index 000000000..d56b92156 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/NotificationApiUrlProvider.cs @@ -0,0 +1,40 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Threading.Tasks; +using Unity.GrantManager.Applications; +using Volo.Abp.Domain.Repositories; + +namespace Unity.GrantManager; + + +public class NotificationApiUrlProvider : INotificationApiUrlProvider +{ + private readonly IDynamicUrlRepository _repository; + private readonly ILogger _logger; + private string? _cachedUrl; + + public NotificationApiUrlProvider(IDynamicUrlRepository repository, ILogger logger) + { + _repository = repository; + _logger = logger; + } + + public async Task GetBaseUrlAsync() + { + if (_cachedUrl != null) + return _cachedUrl; + + try + { + var record = await _repository.FirstOrDefaultAsync(x => x.KeyName == "NOTIFICATION_API_BASE"); + _cachedUrl = record?.Url ?? "https://submit.digital.gov.bc.ca/app/api/v1"; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to fetch NOTIFICATION_API_BASE from database."); + _cachedUrl = "https://submit.digital.gov.bc.ca/app/api/v1"; + } + + return _cachedUrl; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicationRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicationRepository.cs index a9cfc0eb4..c86aa3e61 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicationRepository.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicationRepository.cs @@ -22,7 +22,37 @@ public ApplicationRepository(IDbContextProvider dbContextP public async Task>> WithFullDetailsGroupedAsync(int skipCount, int maxResultCount, string? sorting = null) { - var query = (await GetQueryableAsync()) + return await WithFullDetailsGroupedAsync(skipCount, maxResultCount, sorting, null); + } + + public async Task>> WithFullDetailsGroupedAsync(int skipCount, int maxResultCount, string? sorting = null, string? filter = null) + { + var query = await BuildBaseQueryAsync(); + + // Apply filter + if (!string.IsNullOrWhiteSpace(filter)) + { + query = query.Where(a => + a.ProjectName.Contains(filter) || + a.ReferenceNo.Contains(filter) + ); + } + // Apply sorting + query = ApplySorting(query, sorting); + + var groupedResult = query + .AsEnumerable() + .GroupBy(s => s.Id) + .Skip(skipCount) + .Take(maxResultCount) + .ToList(); + + return groupedResult; + } + + private async Task> BuildBaseQueryAsync() + { + return (await GetQueryableAsync()) .AsNoTracking() .Include(s => s.ApplicationStatus) .Include(s => s.ApplicationForm) @@ -31,22 +61,91 @@ public async Task>> WithFullDetailsGroupedAsyn .Include(s => s.ApplicationAssignments!) .ThenInclude(t => t.Assignee) .Include(s => s.Applicant) - .Include(s => s.ApplicantAgent); + .Include(s => s.ApplicantAgent) + .AsQueryable(); + } - if (!string.IsNullOrEmpty(sorting)) + private static IQueryable ApplySorting(IQueryable query, string? sorting) + { + if (string.IsNullOrEmpty(sorting)) { - query.OrderBy(sorting); + return query; } - var groupBy = query - .OrderBy(s => s.Id) - .GroupBy(s => s.Id) - .AsEnumerable() - .Skip(skipCount) - .Take(maxResultCount) - .ToList(); + var sortingFields = sorting + .Split(',') + .Select(f => f.Trim()) + .Where(f => !f.StartsWith("rowCount", StringComparison.OrdinalIgnoreCase)) + .Select(MapSortingField) + .Where(f => f != null) + .ToArray(); + + if (sortingFields.Length > 0) + { + var sortingExpression = string.Join(",", sortingFields); + query = query.OrderBy(sortingExpression); + } - return groupBy; + return query; + } + + private static string? MapSortingField(string field) + { + if (field.StartsWith("status ", StringComparison.OrdinalIgnoreCase) || field.Equals("status", StringComparison.OrdinalIgnoreCase)) + { + return field.Replace("status", "ApplicationStatus.InternalStatus", StringComparison.OrdinalIgnoreCase); + } + if (field.StartsWith("category ", StringComparison.OrdinalIgnoreCase) || field.Equals("category", StringComparison.OrdinalIgnoreCase)) + { + return field.Replace("category", "ApplicationForm.Category", StringComparison.OrdinalIgnoreCase); + } + if (field.StartsWith("assignees ", StringComparison.OrdinalIgnoreCase) || field.Equals("assignees", StringComparison.OrdinalIgnoreCase)) + { + var parts = field.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); + return parts.Length == 2 ? $"ApplicationAssignments.Count() {parts[1]}" : "ApplicationAssignments.Count()"; + } + if (field.StartsWith("totalPaidAmount ", StringComparison.OrdinalIgnoreCase) || field.Equals("totalPaidAmount", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + if (field.StartsWith("subStatusDisplayValue ", StringComparison.OrdinalIgnoreCase) || field.Equals("subStatusDisplayValue", StringComparison.OrdinalIgnoreCase)) + { + return field.Replace("subStatusDisplayValue", "SubStatus", StringComparison.OrdinalIgnoreCase); + } + if (field.StartsWith("applicationTag ", StringComparison.OrdinalIgnoreCase) || field.Equals("applicationTag", StringComparison.OrdinalIgnoreCase)) + { + var parts = field.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); + return parts.Length == 2 ? $"ApplicationTags.FirstOrDefault().Text {parts[1]}" : "ApplicationTags.FirstOrDefault().Text"; + } + if (field.StartsWith("organizationType ", StringComparison.OrdinalIgnoreCase) || field.Equals("organizationType", StringComparison.OrdinalIgnoreCase)) + { + return field.Replace("organizationType", "Applicant.OrganizationType", StringComparison.OrdinalIgnoreCase); + } + if (field.StartsWith("organizationName ", StringComparison.OrdinalIgnoreCase) || field.Equals("organizationName", StringComparison.OrdinalIgnoreCase)) + { + return field.Replace("organizationName", "Applicant.OrgName", StringComparison.OrdinalIgnoreCase); + } + if (field.StartsWith("contactFullName ", StringComparison.OrdinalIgnoreCase) || field.Equals("contactFullName", StringComparison.OrdinalIgnoreCase)) + { + return field.Replace("contactFullName", "ApplicantAgent.Name", StringComparison.OrdinalIgnoreCase); + } + if (field.StartsWith("contactTitle ", StringComparison.OrdinalIgnoreCase) || field.Equals("contactTitle", StringComparison.OrdinalIgnoreCase)) + { + return field.Replace("contactTitle", "ApplicantAgent.Title", StringComparison.OrdinalIgnoreCase); + } + if (field.StartsWith("contactEmail ", StringComparison.OrdinalIgnoreCase) || field.Equals("contactEmail", StringComparison.OrdinalIgnoreCase)) + { + return field.Replace("contactEmail", "ApplicantAgent.Email", StringComparison.OrdinalIgnoreCase); + } + if (field.StartsWith("contactBusinessPhone ", StringComparison.OrdinalIgnoreCase) || field.Equals("contactBusinessPhone", StringComparison.OrdinalIgnoreCase)) + { + return field.Replace("contactBusinessPhone", "ApplicantAgent.Phone", StringComparison.OrdinalIgnoreCase); + } + if (field.StartsWith("contactCellPhone ", StringComparison.OrdinalIgnoreCase) || field.Equals("contactCellPhone", StringComparison.OrdinalIgnoreCase)) + { + return field.Replace("contactCellPhone", "ApplicantAgent.Phone2", StringComparison.OrdinalIgnoreCase); + } + return field; } public async Task WithBasicDetailsAsync(Guid id) @@ -96,4 +195,6 @@ public override async Task> WithDetailsAsync() .AsNoTracking() // read?only; drop this line if you need tracking .FirstOrDefaultAsync(a => a.Id == id); } + + } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/FormController.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/FormController.cs index 8b239054b..7a40c0f79 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/FormController.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/FormController.cs @@ -5,12 +5,12 @@ using System.Threading.Tasks; using Unity.GrantManager.ApplicationForms; using Unity.GrantManager.Applications; -using Unity.GrantManager.Integrations.Chefs; using Volo.Abp; using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.Domain.Entities; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Unity.GrantManager.Integrations.Chefs; namespace Unity.GrantManager.Controllers { @@ -63,10 +63,16 @@ public async Task SynchronizeChefsAvailableFields(string formId, } var chefsFormVersion = await _formsApiService.GetFormDataAsync(formId, formVersionId); - + + // Ensure chefsFormVersion is not null before proceeding + if (chefsFormVersion == null) + { + throw new BusinessException("Chefs Form Version data could not be retrieved."); + } + var result = await _applicationFormVersionAppService - .UpdateOrCreateApplicationFormVersion(formId, formVersionId, applicationForm.Id, chefsFormVersion); - + .UpdateOrCreateApplicationFormVersion(formId, formVersionId, applicationForm.Id, chefsFormVersion); + return Ok(result); } catch (EntityNotFoundException ex) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/Mapping.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/Mapping.cshtml.cs index 09f81f9d7..5be554dd1 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/Mapping.cshtml.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/Mapping.cshtml.cs @@ -77,7 +77,7 @@ public async Task OnGetAsync() { ApplicationFormDto = await _applicationFormAppService.GetAsync(ApplicationId); ScoresheetId = ApplicationFormDto.ScoresheetId; - ApplicationFormVersionDtoList = (List?)await _applicationFormAppService.GetVersionsAsync(ApplicationFormDto.Id); + ApplicationFormVersionDtoList = (List?) await _applicationFormAppService.GetVersionsAsync(ApplicationFormDto.Id); FlexEnabled = await _featureChecker.IsEnabledAsync("Unity.Flex"); if (ApplicationFormVersionDtoList != null) From ff6a48e43d721aa40556a415e4025d1cd9dfbc44 Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Mon, 18 Aug 2025 16:06:25 -0700 Subject: [PATCH 05/55] feature/AB#26441-DynamicUrls --- .../Collectors/BCAddressCollector.cs | 2 +- .../EmailNotificationService.cs | 85 +- .../TeamsNotificationService.cs | 29 +- .../NotificationsDataSeedContributor.cs | 223 +- .../Settings/DynamicUrl.cs | 14 + .../Settings/IDynamicUrlRepository.cs | 9 + .../GrantManagerDbContext.cs | 19 + .../Repositories/DynamicUrlRepository.cs | 18 + .../Integrations/Cas/InvoiceService.cs | 15 +- .../Http/IResilientHttpRequest.cs | 8 + .../Http/ResilientHttpRequest.cs | 155 +- .../Navigation/TenantManagementMenuNames.cs | 1 + .../Endpoints/CreateModal.cshtml | 24 + .../Endpoints/CreateModal.cshtml.cs | 24 + .../Endpoints/EndpointManagementPageModel.cs | 11 + .../EndpointManagement/Endpoints/Index.cshtml | 54 + .../Endpoints/Index.cshtml.cs | 16 + .../EndpointManagement/Endpoints/Index.css | 16 + .../EndpointManagement/Endpoints/Index.js | 106 + .../Endpoints/UpdateModal.cshtml | 25 + .../Endpoints/UpdateModal.cshtml.cs | 28 + .../EndpointManagement/_ViewImports.cshtml | 4 + .../Tenants/TenantManagementPageModel.cs | 3 +- .../Integration/Chefs/IFormsApiService.cs | 10 +- .../Chefs/ISubmissionsApiService.cs | 4 +- .../Integration/Css/CssUser.cs | 2 +- .../Integration/Css/CssUserAttributes.cs | 2 +- .../Integration/Css/ICssUsersApiService.cs | 2 +- .../Integration/Css/InvalidResponse.cs | 2 +- .../Css/TokenValidationResponse.cs | 2 +- .../Integration/Css/UserSearchResult.cs | 2 +- .../Endpoints/CreateUpdateDynamicUrlDto.cs | 12 + .../Integration/Endpoints/DynamicUrlDto.cs | 14 + .../IEndpointManagementAppService.cs | 18 + .../Integration/Geocoder/AddressDetailsDto.cs | 2 +- .../Geocoder/IGeocoderApiService.cs | 2 +- .../Integration/OrgBook/IOrgBookService.cs | 2 +- .../CreateEmailDto.cs | 1 - .../{Emails => Notifications}/EmailDto.cs | 0 .../Notifications}/Facts.cs | 24 +- .../IEmailAppService.cs | 0 .../IEmailsService.cs | 0 .../Notifications/INotificationsAppService.cs | 14 + .../UpdateEmailDto.cs | 0 .../Applicants/ApplicantAppService.cs | 8 +- .../ApplicationFormAppService.cs | 2 +- .../ApplicationFormSycnronizationService.cs | 29 +- .../ApplicationFormVersionAppService.cs | 8 +- .../ConfigureIntakeClientOptions.cs | 28 + .../Events/ChefsEventSubscriptionService.cs | 81 +- ...rantManagerApplicationAutoMapperProfile.cs | 8 +- .../GrantManagerApplicationModule.cs | 112 +- .../Identity/UserImportAppService.cs | 2 +- .../Intakes/ApplicationIntakeAdminService.cs | 2 +- .../DetermineElectoralDistrictHandler.cs | 2 +- .../Intakes/IntakeSubmissionAppService.cs | 76 +- .../Integrations/Chefs/FormsApiService.cs | 146 +- .../Chefs/SubmissionsApiService.cs | 62 - .../Integrations/Css/CssApiService.cs | 155 +- .../Endpoints/EndpointManagementAppService.cs | 34 + .../Integrations/Geocoder/ResultMapper.cs | 6 +- .../Http/IResilientHttpRequest.cs | 17 - .../Integrations/Http/ResilientHttpRequest.cs | 108 - .../Integrations/OrgBook/OrgBookService.cs | 21 +- .../EmailAppService.cs | 0 .../Norifications/NotificationsAppService.cs | 87 + .../NotificationApiUrlHolder.cs | 6 + .../Localization/GrantManager/en.json | 1 + .../Applications/IDynamicUrlRepository.cs | 10 + .../GrantManagerDataSeederContributor.cs | 170 +- .../Integrations/DynamicUrl.cs | 11 + .../Integrations/DynamicUrlDataSeeder.cs | 59 + .../Integrations/DynamicUrlKeyNames.cs | 13 + .../GrantManagerDbContext.cs | 12 + .../20250514165300_DynamicUrls.Designer.cs | 2537 +++++++++++++++++ .../20250514165300_DynamicUrls.cs | 43 + .../GrantManagerDbContextModelSnapshot.cs | 49 + .../GrantTenantDbContextModelSnapshot.cs | 4 + .../Repositories/ApplicationRepository.cs | 2 +- .../Repositories/DynamicUrlRepository.cs | 19 + .../Controllers/FormController.cs | 48 +- .../GrantManagerWebModule.cs | 4 +- .../Menus/GrantManagerMenuContributor.cs | 13 + .../Menus/GrantManagerMenus.cs | 1 + .../Pages/ApplicationForms/Mapping.cshtml.cs | 2 +- .../wwwroot/teams/MessageCard.json | 2 +- .../ChefsEventSubscriptionServiceTests.cs | 18 +- 87 files changed, 4090 insertions(+), 932 deletions(-) create mode 100644 applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Settings/DynamicUrl.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Settings/IDynamicUrlRepository.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/EntityFrameworkCore/GrantManagerDbContext.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/Repositories/DynamicUrlRepository.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/CreateModal.cshtml create mode 100644 applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/CreateModal.cshtml.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/EndpointManagementPageModel.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.cshtml create mode 100644 applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.cshtml.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.css create mode 100644 applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.js create mode 100644 applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/UpdateModal.cshtml create mode 100644 applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/UpdateModal.cshtml.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/_ViewImports.cshtml create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/CreateUpdateDynamicUrlDto.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/DynamicUrlDto.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/IEndpointManagementAppService.cs rename applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/{Emails => Notifications}/CreateEmailDto.cs (99%) rename applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/{Emails => Notifications}/EmailDto.cs (100%) rename applications/Unity.GrantManager/{modules/Unity.Notifications/src/Unity.Notifications.Application/TeamsNotifications => src/Unity.GrantManager.Application.Contracts/Notifications}/Facts.cs (96%) rename applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/{Emails => Notifications}/IEmailAppService.cs (100%) rename applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/{Emails => Notifications}/IEmailsService.cs (100%) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/INotificationsAppService.cs rename applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/{Emails => Notifications}/UpdateEmailDto.cs (100%) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/ConfigureIntakeClientOptions.cs delete mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/SubmissionsApiService.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs delete mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Http/IResilientHttpRequest.cs delete mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Http/ResilientHttpRequest.cs rename applications/Unity.GrantManager/src/Unity.GrantManager.Application/{Emails => Norifications}/EmailAppService.cs (100%) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/Norifications/NotificationsAppService.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/NotificationApiUrlHolder.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IDynamicUrlRepository.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrl.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlDataSeeder.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlKeyNames.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20250514165300_DynamicUrls.Designer.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20250514165300_DynamicUrls.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/DynamicUrlRepository.cs diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/Collectors/BCAddressCollector.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/Collectors/BCAddressCollector.cs index 86bb2dcbf..5d78c484a 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/Collectors/BCAddressCollector.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/Collectors/BCAddressCollector.cs @@ -4,7 +4,7 @@ using System.Text.Json; using System.Threading.Tasks; using Unity.Flex.Worksheets.Values; -using Unity.GrantManager.Integration.Geocoder; +using Unity.GrantManager.Integrations.Geocoder; namespace Unity.Flex.Worksheets.Collectors { 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 5c084550a..42afa17cc 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,6 +1,4 @@ using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; @@ -23,49 +21,31 @@ using Volo.Abp.Domain.Entities; using Volo.Abp.Features; using Volo.Abp.SettingManagement; +using Microsoft.AspNetCore.Http; using Volo.Abp.Users; +using Unity.GrantManager.Notifications; namespace Unity.Notifications.EmailNotifications; - [Dependency(ReplaceServices = false)] [ExposeServices(typeof(EmailNotificationService), typeof(IEmailNotificationService))] -public class EmailNotificationService : ApplicationService, IEmailNotificationService -{ - private readonly IChesClientService _chesClientService; - private readonly IConfiguration _configuration; - private readonly EmailQueueService _emailQueueService; - private readonly IEmailLogsRepository _emailLogsRepository; - private readonly IExternalUserLookupServiceProvider _externalUserLookupServiceProvider; - private readonly ISettingManager _settingManager; - private readonly IFeatureChecker _featureChecker; - private readonly IHttpContextAccessor _httpContextAccessor; - - public EmailNotificationService( +#pragma warning disable S107 // Methods should not have too many parameters +public class EmailNotificationService( + INotificationsAppService notificationAppService, IEmailLogsRepository emailLogsRepository, - IConfiguration configuration, IChesClientService chesClientService, EmailQueueService emailQueueService, IExternalUserLookupServiceProvider externalUserLookupServiceProvider, ISettingManager settingManager, IFeatureChecker featureChecker, - IHttpContextAccessor httpContextAccessor - ) - { - _emailLogsRepository = emailLogsRepository; - _configuration = configuration; - _chesClientService = chesClientService; - _emailQueueService = emailQueueService; - _externalUserLookupServiceProvider = externalUserLookupServiceProvider; - _settingManager = settingManager; - _featureChecker = featureChecker; - _httpContextAccessor = httpContextAccessor; - } + IHttpContextAccessor httpContextAccessor) : ApplicationService, IEmailNotificationService +#pragma warning restore S107 // Methods should not have too many parameters +{ public async Task DeleteEmail(Guid id) { - await _emailLogsRepository.DeleteAsync(id); - } + await emailLogsRepository.DeleteAsync(id); + } public async Task GetEmailsChesWithNoResponseCountAsync() { @@ -77,7 +57,7 @@ public async Task GetEmailsChesWithNoResponseCountAsync() (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 allEmailLogs = await emailLogsRepository.GetListAsync(); var emailLogs = allEmailLogs.Where(filter.Compile()).ToList(); // Ensure we're returning 0 if no logs are found @@ -90,16 +70,16 @@ public async Task GetEmailsChesWithNoResponseCountAsync() { return null; } - - var emailObject = await GetEmailObjectAsync(emailTo, body, subject, emailFrom, "html", emailTemplateName, emailCC, emailBCC); - EmailLog emailLog = await _emailLogsRepository.GetAsync(emailId); + + var emailObject = await GetEmailObjectAsync(emailTo, body, subject, emailFrom, "html", emailTemplateName); + EmailLog emailLog = await emailLogsRepository.GetAsync(emailId); emailLog = UpdateMappedEmailLog(emailLog, emailObject); emailLog.ApplicationId = applicationId; emailLog.Id = emailId; emailLog.Status = status ?? EmailStatus.Initialized; // When being called here the current tenant is in context - verified by looking at the tenant id - EmailLog loggedEmail = await _emailLogsRepository.UpdateAsync(emailLog, autoSave: true); + EmailLog loggedEmail = await emailLogsRepository.UpdateAsync(emailLog, autoSave: true); return loggedEmail; } @@ -122,7 +102,7 @@ public async Task GetEmailsChesWithNoResponseCountAsync() emailLog.Status = status ?? EmailStatus.Initialized; // When being called here the current tenant is in context - verified by looking at the tenant id - EmailLog loggedEmail = await _emailLogsRepository.InsertAsync(emailLog, autoSave: true); + EmailLog loggedEmail = await emailLogsRepository.InsertAsync(emailLog, autoSave: true); return loggedEmail; } @@ -131,9 +111,7 @@ protected virtual async Task NotifyTeamsChannel(string chesEmailError) string? envInfo = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); string activityTitle = "CHES Email error: " + chesEmailError; string activitySubtitle = "Environment: " + envInfo; - string teamsChannel = _configuration["Notifications:TeamsNotificationsWebhook"] ?? ""; - List facts = new() { }; - await TeamsNotificationService.PostToTeamsAsync(teamsChannel, activityTitle, activitySubtitle, facts); + await notificationAppService.PostToTeamsAsync(activityTitle, activitySubtitle); } public async Task SendCommentNotification(EmailCommentDto input) @@ -141,11 +119,11 @@ public async Task SendCommentNotification(EmailCommentDto i HttpResponseMessage res = new(); try { - if (await _featureChecker.IsEnabledAsync("Unity.Notifications")) + if (await featureChecker.IsEnabledAsync("Unity.Notifications")) { var defaultFromAddress = await SettingProvider.GetOrNullAsync(NotificationsSettings.Mailing.DefaultFromAddress); var scheme = "https"; - var request = _httpContextAccessor.HttpContext?.Request; + var request = httpContextAccessor.HttpContext?.Request; if (request == null) { @@ -211,7 +189,6 @@ public async Task SendCommentNotification(EmailCommentDto i return res; } - /// /// Send Email Notfication /// @@ -238,8 +215,8 @@ 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, emailCC, emailBCC); - var response = await _chesClientService.SendAsync(emailObject); + var emailObject = await GetEmailObjectAsync(emailTo, body, subject, emailFrom, emailBodyType, emailTemplateName); + var response = await chesClientService.SendAsync(emailObject); // Assuming SendAsync returns a HttpResponseMessage or equivalent: return response; @@ -259,7 +236,7 @@ public async Task SendEmailNotification(string emailTo, str EmailLog emailLog = new EmailLog(); try { - emailLog = await _emailLogsRepository.GetAsync(id); + emailLog = await emailLogsRepository.GetAsync(id); } catch (EntityNotFoundException ex) { @@ -272,7 +249,7 @@ public async Task SendEmailNotification(string emailTo, str [Authorize] public virtual async Task> GetHistoryByApplicationId(Guid applicationId) { - var entityList = await _emailLogsRepository.GetByApplicationIdAsync(applicationId); + var entityList = await emailLogsRepository.GetByApplicationIdAsync(applicationId); var dtoList = ObjectMapper.Map, List>(entityList); var sentByUserIds = dtoList @@ -284,7 +261,7 @@ public virtual async Task> GetHistoryByApplicationId(Guid foreach (var userId in sentByUserIds) { - var userInfo = await _externalUserLookupServiceProvider.FindByIdAsync(userId); + var userInfo = await externalUserLookupServiceProvider.FindByIdAsync(userId); if (userInfo != null) { userDictionary[userId] = ObjectMapper.Map(userInfo); @@ -313,10 +290,18 @@ public async Task SendEmailToQueue(EmailLog emailLog) emailNotificationEvent.Id = emailLog.Id; emailNotificationEvent.TenantId = emailLog.TenantId; emailNotificationEvent.RetryAttempts = emailLog.RetryAttempts; - await _emailQueueService.SendToEmailEventQueueAsync(emailNotificationEvent); + await emailQueueService.SendToEmailEventQueueAsync(emailNotificationEvent); } - protected virtual async Task GetEmailObjectAsync(string emailTo, string body, string subject, string? emailFrom, string? emailBodyType, string? emailTemplateName, string? emailCC = null, string? emailBCC = null) + protected virtual async Task GetEmailObjectAsync( + string emailTo, + string body, + string subject, + string? emailFrom, + string? emailBodyType, + string? emailTemplateName, + string? emailCC = null, + string? emailBCC = null) { var toList = emailTo.ParseEmailList() ?? []; var ccList = emailCC.ParseEmailList(); @@ -365,7 +350,7 @@ private async Task UpdateTenantSettings(string settingKey, string valueString) { if (!valueString.IsNullOrWhiteSpace()) { - await _settingManager.SetForCurrentTenantAsync(settingKey, valueString); + await settingManager.SetForCurrentTenantAsync(settingKey, valueString); } } } \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/TeamsNotifications/TeamsNotificationService.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/TeamsNotifications/TeamsNotificationService.cs index 5bc0e6caa..84159fd7a 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/TeamsNotifications/TeamsNotificationService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/TeamsNotifications/TeamsNotificationService.cs @@ -13,6 +13,13 @@ namespace Unity.Notifications.TeamsNotifications public class TeamsNotificationService { public TeamsNotificationService() : base() { } + + public const string DIRECT_MESSAGE_KEY_PREFIX = "DIRECT_MESSAGE_"; + public const string TEAMS_NOTIFICATION = $"{DIRECT_MESSAGE_KEY_PREFIX}0"; + public const string TEAMS_NOTIFICATION_1 = $"{DIRECT_MESSAGE_KEY_PREFIX}1"; + + public static string TeamsChannel { get; set; } = string.Empty; + private readonly List _facts = new List(); public async Task PostFactsToTeamsAsync(string teamsChannel, string activityTitle, string activitySubtitle) @@ -51,7 +58,7 @@ private static class ChefsEventTypesConsts public const string FORM_DRAFT_PUBLISHED = "eventFormDraftPublished"; } - private static string InitializeMessageCard(string activityTitle, string activitySubtitle, List facts) + public static string InitializeMessageCard(string activityTitle, string activitySubtitle, List facts) { dynamic messageCard = MessageCard.GetMessageCard(); JObject jsonObj = JsonConvert.DeserializeObject(messageCard)!; @@ -148,12 +155,24 @@ public static async Task PostChefsEventToTeamsAsync(string teamsChannel, string await PostToTeamsAsync(teamsChannel, activityTitle, activitySubtitle, facts); } - private static async Task PostToTeamsChannelAsync(string teamsChannel, string messageCard) { - using var httpClient = new HttpClient(); - using var request = new HttpRequestMessage(new HttpMethod("POST"), teamsChannel); + private static readonly HttpClient httpClient = new(); + + /// + /// Posts a message card to the specified Microsoft Teams channel using an HTTP POST request. + /// + /// The webhook URL of the Teams channel. + /// The message card payload in JSON format. + public static async Task PostToTeamsChannelAsync(string teamsChannel, string messageCard) + { + using var request = new HttpRequestMessage(HttpMethod.Post, teamsChannel); request.Content = new StringContent(messageCard); request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - await httpClient.SendAsync(request); + var response = await httpClient.SendAsync(request); + if (!response.IsSuccessStatusCode) + { + // Optionally log or throw an exception here + throw new HttpRequestException($"Failed to post to Teams channel. Status code: {response.StatusCode}"); + } } } } diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/NotificationsDataSeedContributor.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/NotificationsDataSeedContributor.cs index 902524266..6cf6f92d5 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/NotificationsDataSeedContributor.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/NotificationsDataSeedContributor.cs @@ -1,117 +1,110 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Unity.Notifications.EmailGroups; -using Unity.Notifications.Templates; -using Volo.Abp.Data; -using Volo.Abp.DependencyInjection; - - -namespace Unity.Notifications; - -public class NotificationsDataSeedContributor : IDataSeedContributor, ITransientDependency -{ - private readonly ITemplateVariablesRepository _templateVariablesRepository; - private readonly IEmailGroupsRepository _emailGroupsRepository; - - public NotificationsDataSeedContributor(ITemplateVariablesRepository templateVariablesRepository, IEmailGroupsRepository emailGroupsRepository) - { - _templateVariablesRepository = templateVariablesRepository; - _emailGroupsRepository = emailGroupsRepository; - } - - public async Task SeedAsync(DataSeedContext context) - { - if (context.TenantId == null) // only seed into a tenant database - { - return; - } - - var emailTemplateVariableDtos = new List - { - new EmailTempateVariableDto { Name = "Applicant name", Token = "applicant_name", MapTo = "applicant.applicantName" }, - new EmailTempateVariableDto { Name = "Submission #", Token = "submission_number", MapTo = "referenceNo" }, - new EmailTempateVariableDto { Name = "Submission Date", Token = "submission_date", MapTo = "submissionDate" }, - new EmailTempateVariableDto { Name = "Category", Token = "category", MapTo = "applicationForm.category" }, - new EmailTempateVariableDto { Name = "Status", Token = "status", MapTo = "status" }, - new EmailTempateVariableDto { Name = "Approved Amount", Token = "approved_amount", MapTo = "approvedAmount" }, - new EmailTempateVariableDto { Name = "Approval date", Token = "approval_date", MapTo = "finalDecisionDate" }, - new EmailTempateVariableDto { Name = "Community", Token = "community", MapTo = "community" }, - new EmailTempateVariableDto { Name = "Contact Full Name", Token = "contact_full_name", MapTo = "contactFullName" }, - new EmailTempateVariableDto { Name = "Contact Title", Token = "contact_title", MapTo = "contactTitle" }, - new EmailTempateVariableDto { Name = "Decline Rationale", Token = "decline_rationale", MapTo = "declineRational" }, - new EmailTempateVariableDto { Name = "Registered Organization Name", Token = "organization_name", MapTo = "organizationName" }, - new EmailTempateVariableDto { Name = "Project Start Date", Token = "project_start_date", MapTo = "projectStartDate" }, - new EmailTempateVariableDto { Name = "Project End Date", Token = "project_end_date", MapTo = "projectEndDate" }, - new EmailTempateVariableDto { Name = "Project Name", Token = "project_name", MapTo = "projectName" }, - new EmailTempateVariableDto { Name = "Project Summary", Token = "project_summary", MapTo = "projectSummary" }, - new EmailTempateVariableDto { Name = "Signing Authority Full Name", Token = "signing_authority_full_name", MapTo = "signingAuthorityFullName" }, - new EmailTempateVariableDto { Name = "Signing Authority Title", Token = "signing_authority_title", MapTo = "signingAuthorityTitle" }, - new EmailTempateVariableDto { Name = "Applicant ID", Token = "applicant_id", MapTo = "applicant.unityApplicantId" } - }; - - try - { - foreach (var template in emailTemplateVariableDtos) - { - var existingVariable = await _templateVariablesRepository.FindAsync(tv => tv.Token == template.Token); - if (existingVariable == null) - { - await _templateVariablesRepository.InsertAsync( - new TemplateVariable { Name = template.Name, Token = template.Token, MapTo = template.MapTo }, - autoSave: true - ); - } - else if (existingVariable.Token == "category" && existingVariable.MapTo == "category") - { - existingVariable.MapTo = "applicationForm.category"; - await _templateVariablesRepository.UpdateAsync(existingVariable, autoSave: true); - } - } - } - catch (Exception ex) +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Unity.Notifications.EmailGroups; +using Unity.Notifications.Templates; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; + + +namespace Unity.Notifications; + +public class NotificationsDataSeedContributor(ITemplateVariablesRepository templateVariablesRepository, + IEmailGroupsRepository emailGroupsRepository) : IDataSeedContributor, ITransientDependency +{ + + public async Task SeedAsync(DataSeedContext context) + { + if (context.TenantId == null) // only seed into a tenant database { - throw new InvalidOperationException($"Error seeding Notifications Data for Templates: {ex.Message}"); - } - - var emailGroups = new List - { - new EmailGroupDto {Name = "FSB-AP", Description = "This group manages the recipients for PO-related payments, which will be sent to FSB-AP to update contracts and initiate payment creation.",Type = "static"}, - new EmailGroupDto {Name = "Payments", Description = "This group manages the recipients for payment notifications, such as failures or errors",Type = "dynamic"} - }; - try - { - var allGroups = await _emailGroupsRepository.GetListAsync(); - foreach (var emailGroup in emailGroups) - { - var existingGroup = allGroups.FirstOrDefault(g => g.Name == emailGroup.Name); - if (existingGroup == null) - { - await _emailGroupsRepository.InsertAsync( - new EmailGroup { Name = emailGroup.Name, Description = emailGroup.Description, Type = emailGroup.Type }, - autoSave: true - ); - } - } - - } - catch (Exception ex) - { - throw new InvalidOperationException($"Error seeding Notifications Data for Email Groups: {ex.Message}"); - } - } - - internal class EmailGroupDto - { - public string Name { get; set; } = string.Empty; - public string Description { get; set; } = string.Empty; - public string Type { get; set; } = string.Empty; - } - internal class EmailTempateVariableDto - { - public string Name { get; set; } = string.Empty; - public string Token { get; set; } = string.Empty; - public string MapTo { get; set; } = string.Empty; - } + return; + } + + var emailTemplateVariableDtos = new List + { + new EmailTempateVariableDto { Name = "Applicant name", Token = "applicant_name", MapTo = "applicant.applicantName" }, + new EmailTempateVariableDto { Name = "Submission #", Token = "submission_number", MapTo = "referenceNo" }, + new EmailTempateVariableDto { Name = "Submission Date", Token = "submission_date", MapTo = "submissionDate" }, + new EmailTempateVariableDto { Name = "Category", Token = "category", MapTo = "applicationForm.category" }, + new EmailTempateVariableDto { Name = "Status", Token = "status", MapTo = "status" }, + new EmailTempateVariableDto { Name = "Approved Amount", Token = "approved_amount", MapTo = "approvedAmount" }, + new EmailTempateVariableDto { Name = "Approval date", Token = "approval_date", MapTo = "finalDecisionDate" }, + new EmailTempateVariableDto { Name = "Community", Token = "community", MapTo = "community" }, + new EmailTempateVariableDto { Name = "Contact Full Name", Token = "contact_full_name", MapTo = "contactFullName" }, + new EmailTempateVariableDto { Name = "Contact Title", Token = "contact_title", MapTo = "contactTitle" }, + new EmailTempateVariableDto { Name = "Decline Rationale", Token = "decline_rationale", MapTo = "declineRational" }, + new EmailTempateVariableDto { Name = "Registered Organization Name", Token = "organization_name", MapTo = "organizationName" }, + new EmailTempateVariableDto { Name = "Project Start Date", Token = "project_start_date", MapTo = "projectStartDate" }, + new EmailTempateVariableDto { Name = "Project End Date", Token = "project_end_date", MapTo = "projectEndDate" }, + new EmailTempateVariableDto { Name = "Project Name", Token = "project_name", MapTo = "projectName" }, + new EmailTempateVariableDto { Name = "Project Summary", Token = "project_summary", MapTo = "projectSummary" }, + new EmailTempateVariableDto { Name = "Signing Authority Full Name", Token = "signing_authority_full_name", MapTo = "signingAuthorityFullName" }, + new EmailTempateVariableDto { Name = "Signing Authority Title", Token = "signing_authority_title", MapTo = "signingAuthorityTitle" }, + new EmailTempateVariableDto { Name = "Applicant ID", Token = "applicant_id", MapTo = "applicant.unityApplicantId" } + }; + + try + { + foreach (var template in emailTemplateVariableDtos) + { + var existingVariable = await templateVariablesRepository.FindAsync(tv => tv.Token == template.Token); + if (existingVariable == null) + { + await templateVariablesRepository.InsertAsync( + new TemplateVariable { Name = template.Name, Token = template.Token, MapTo = template.MapTo }, + autoSave: true + ); + } + else if (existingVariable.Token == "category" && existingVariable.MapTo == "category") + { + existingVariable.MapTo = "applicationForm.category"; + await templateVariablesRepository.UpdateAsync(existingVariable, autoSave: true); + } + } + } + catch (Exception ex) + { + throw new InvalidOperationException($"Error seeding Notifications Data for Templates: {ex.Message}"); + } + + var emailGroups = new List + { + new EmailGroupDto {Name = "FSB-AP", Description = "This group manages the recipients for PO-related payments, which will be sent to FSB-AP to update contracts and initiate payment creation.",Type = "static"}, + new EmailGroupDto {Name = "Payments", Description = "This group manages the recipients for payment notifications, such as failures or errors",Type = "dynamic"} + }; + try + { + var allGroups = await emailGroupsRepository.GetListAsync(); + foreach (var emailGroup in emailGroups) + { + var existingGroup = allGroups.FirstOrDefault(g => g.Name == emailGroup.Name); + if (existingGroup == null) + { + await emailGroupsRepository.InsertAsync( + new EmailGroup { Name = emailGroup.Name, Description = emailGroup.Description, Type = emailGroup.Type }, + autoSave: true + ); + } + } + + } + catch (Exception ex) + { + throw new InvalidOperationException($"Error seeding Notifications Data for Email Groups: {ex.Message}"); + } + } + + internal class EmailGroupDto + { + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public string Type { get; set; } = string.Empty; + } + internal class EmailTempateVariableDto + { + public string Name { get; set; } = string.Empty; + public string Token { get; set; } = string.Empty; + public string MapTo { get; set; } = string.Empty; + } } \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Settings/DynamicUrl.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Settings/DynamicUrl.cs new file mode 100644 index 000000000..436936cc8 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Settings/DynamicUrl.cs @@ -0,0 +1,14 @@ +using System; +using Volo.Abp.Domain.Entities.Auditing; + +namespace Unity.GrantManager.Notifications.Settings; + +public class DynamicUrl : AuditedAggregateRoot +{ + public string KeyName { get; set; } = string.Empty; + + public string Url { get; set; } = string.Empty; + + public string Description { get; set; } = string.Empty; + +} diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Settings/IDynamicUrlRepository.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Settings/IDynamicUrlRepository.cs new file mode 100644 index 000000000..b4ac701f3 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Settings/IDynamicUrlRepository.cs @@ -0,0 +1,9 @@ +using System; +using Volo.Abp.Domain.Repositories; + +namespace Unity.GrantManager.Notifications.Settings; + +public interface IDynamicUrlRepository : IRepository +{ + +} diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/EntityFrameworkCore/GrantManagerDbContext.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/EntityFrameworkCore/GrantManagerDbContext.cs new file mode 100644 index 000000000..4d457fb2d --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/EntityFrameworkCore/GrantManagerDbContext.cs @@ -0,0 +1,19 @@ +using Microsoft.EntityFrameworkCore; +using Volo.Abp.Data; +using Volo.Abp.EntityFrameworkCore; +using Unity.GrantManager.Notifications.Settings; + +namespace Unity.Notifications.EntityFrameworkCore; + +[ConnectionStringName("Default")] +public class GrantManagerDbContext : AbpDbContext +{ + public DbSet DynamicUrls { get; set; } + + // Add DbSet for each Aggregate Root here. + public GrantManagerDbContext(DbContextOptions options) + : base(options) + { + + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/Repositories/DynamicUrlRepository.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/Repositories/DynamicUrlRepository.cs new file mode 100644 index 000000000..9875551fe --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/Repositories/DynamicUrlRepository.cs @@ -0,0 +1,18 @@ +using System; +using Unity.GrantManager.Notifications.Settings; +using Unity.Notifications.EntityFrameworkCore; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +namespace Unity.GrantManager.Repositories +{ + [Dependency(ReplaceServices = true)] + [ExposeServices(typeof(IDynamicUrlRepository))] + public class DynamicUrlRepository : EfCoreRepository, IDynamicUrlRepository + { + public DynamicUrlRepository(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 1eb47ec87..5fc82be56 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 @@ -79,7 +79,7 @@ public class InvoiceService( InvoiceLineAmount = paymentRequest.Amount, DefaultDistributionAccount = accountDistributionCode // This will be at the tenant level }; - casInvoice.InvoiceLineDetails = new List { invoiceLineDetail }; + casInvoice.InvoiceLineDetails = [invoiceLineDetail]; } return casInvoice; @@ -101,16 +101,11 @@ public class InvoiceService( InvoiceResponse invoiceResponse = new(); try { - PaymentRequest? paymentRequest = await paymentRequestRepository.GetPaymentRequestByInvoiceNumber(invoiceNumber); - if (paymentRequest is null) - { - throw new UserFriendlyException("CreateInvoiceByPaymentRequestAsync: Payment Request not found"); - } + var paymentRequest = await paymentRequestRepository.GetPaymentRequestByInvoiceNumber(invoiceNumber) + ?? throw new UserFriendlyException("CreateInvoiceByPaymentRequestAsync: Payment Request not found"); if (!paymentRequest.AccountCodingId.HasValue) - { throw new UserFriendlyException("CreateInvoiceByPaymentRequestAsync: Account Coding - Payment Request - not found"); - } AccountCoding accountCoding = await accountCodingRepository.GetAsync(paymentRequest.AccountCodingId.Value); string accountDistributionCode = await paymentConfigurationAppService.GetAccountDistributionCode(accountCoding);// this will be on the payment request @@ -177,7 +172,7 @@ public async Task CreateInvoiceAsync(Invoice casAPInvoice) { if (response.Content != null && response.StatusCode != HttpStatusCode.NotFound) { - var contentString = ResilientHttpRequest.ContentToString(response.Content); + var contentString = await ResilientHttpRequest.ContentToStringAsync(response.Content); var result = JsonSerializer.Deserialize(contentString) ?? throw new UserFriendlyException("CAS InvoiceService CreateInvoiceAsync Exception: " + response); result.CASHttpStatusCode = response.StatusCode; @@ -208,7 +203,7 @@ public async Task GetCasInvoiceAsync(string invoiceNumbe && response.Content != null && response.IsSuccessStatusCode) { - string contentString = ResilientHttpRequest.ContentToString(response.Content); + string contentString = await ResilientHttpRequest.ContentToStringAsync(response.Content); var result = JsonSerializer.Deserialize(contentString); return result ?? new CasPaymentSearchResult(); } diff --git a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/IResilientHttpRequest.cs b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/IResilientHttpRequest.cs index 537c0127c..3cfa35156 100644 --- a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/IResilientHttpRequest.cs +++ b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/IResilientHttpRequest.cs @@ -8,5 +8,13 @@ public interface IResilientHttpRequest : IRemoteService { Task HttpAsyncWithBody(HttpMethod httpVerb, string resource, string? body = null, string? authToken = null); Task HttpAsync(HttpMethod httpVerb, string resource, string? authToken = null); + Task ExecuteRequestAsync( + HttpMethod httpVerb, + string resource, + string? body, + string? authToken, + (string username, string password)? basicAuth = null); + + void SetBaseUrl(string baseUrl); } } diff --git a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/ResilientHttpRequest.cs b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/ResilientHttpRequest.cs index 7af446d75..76499540d 100644 --- a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/ResilientHttpRequest.cs +++ b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/ResilientHttpRequest.cs @@ -4,68 +4,77 @@ using System.Linq; using System.Net; using System.Net.Http; -using System.Text; using System.Threading.Tasks; using Volo.Abp; namespace Unity.Modules.Shared.Http { [IntegrationService] - public class ResilientHttpRequest : IResilientHttpRequest + public class ResilientHttpRequest(HttpClient httpClient) : IResilientHttpRequest { - private readonly IHttpClientFactory _httpClientFactory; private static int _maxRetryAttempts = 3; - private const int OneMinuteInSeconds = 60; private static TimeSpan _pauseBetweenFailures = TimeSpan.FromSeconds(2); - private static TimeSpan _httpRequestTimeout = TimeSpan.FromSeconds(OneMinuteInSeconds); + private static TimeSpan _httpRequestTimeout = TimeSpan.FromSeconds(60); - public ResilientHttpRequest(IHttpClientFactory httpClientFactory) - { - _httpClientFactory = httpClientFactory; - } + private const string AuthorizationHeader = "Authorization"; + + private static ResiliencePipeline _pipeline = BuildPipeline(); + + private string? _baseUrl; + private readonly HttpClient _httpClient = httpClient; public static void SetPipelineOptions( - int maxRetryAttempts, + int maxRetryAttempts, TimeSpan pauseBetweenFailures, - TimeSpan httpRequestTimeout - ) + TimeSpan httpRequestTimeout) { _maxRetryAttempts = maxRetryAttempts; _pauseBetweenFailures = pauseBetweenFailures; _httpRequestTimeout = httpRequestTimeout; + + _pipeline = BuildPipeline(); // rebuild with new settings } - private static bool ReprocessBasedOnStatusCode(HttpStatusCode statusCode) + private static ResiliencePipeline BuildPipeline() { - HttpStatusCode[] reprocessStatusCodes = { - HttpStatusCode.TooManyRequests, - HttpStatusCode.InternalServerError, - HttpStatusCode.BadGateway, - HttpStatusCode.ServiceUnavailable, - HttpStatusCode.GatewayTimeout, - }; + return new ResiliencePipelineBuilder() + .AddRetry(new RetryStrategyOptions + { + ShouldHandle = new PredicateBuilder() + .Handle() + .HandleResult(result => ShouldRetry(result.StatusCode)), + Delay = _pauseBetweenFailures, + MaxRetryAttempts = _maxRetryAttempts, + UseJitter = true, + BackoffType = DelayBackoffType.Exponential + }) + .AddTimeout(_httpRequestTimeout) + .Build(); + } + + public void SetBaseUrl(string baseUrl) + { + if (!Uri.TryCreate(baseUrl, UriKind.Absolute, out _)) + { + throw new ArgumentException("Base URL is not a valid absolute URI.", nameof(baseUrl)); + } - return reprocessStatusCodes.Contains(statusCode); + _baseUrl = baseUrl.TrimEnd('/'); } - private static ResiliencePipeline _pipeline = - new ResiliencePipelineBuilder() - .AddRetry(new RetryStrategyOptions - { - ShouldHandle = new PredicateBuilder() - .Handle() - .HandleResult(result => ReprocessBasedOnStatusCode(result.StatusCode)), - Delay = _pauseBetweenFailures, - MaxRetryAttempts = _maxRetryAttempts, - UseJitter = true, - BackoffType = DelayBackoffType.Exponential, - OnRetry = args => - { - return default; - } - }) - .AddTimeout(_httpRequestTimeout) - .Build(); + private static bool ShouldRetry(HttpStatusCode statusCode) + { + var retryableStatusCodes = new[] + { + HttpStatusCode.TooManyRequests, + HttpStatusCode.InternalServerError, + HttpStatusCode.BadGateway, + HttpStatusCode.ServiceUnavailable, + HttpStatusCode.GatewayTimeout + }; + + return retryableStatusCodes.Contains(statusCode); + } public async Task HttpAsync(HttpMethod httpVerb, string resource, string? authToken = null) { @@ -77,37 +86,61 @@ public async Task HttpAsyncWithBody(HttpMethod httpVerb, st return await ExecuteRequestAsync(httpVerb, resource, body, authToken); } - public static string ContentToString(HttpContent httpContent) + public static async Task ContentToStringAsync(HttpContent httpContent) { - var readAsStringAsync = httpContent.ReadAsStringAsync(); - return readAsStringAsync.Result; + return await httpContent.ReadAsStringAsync(); } - private async Task ExecuteRequestAsync( - HttpMethod httpVerb, - string resource, - string? body, - string? authToken) + public async Task ExecuteRequestAsync( + HttpMethod httpVerb, + string resource, + string? body, + string? authToken, + (string username, string password)? basicAuth = null) { - // HttpClient uses the system default TLS settings; no need to set ServicePointManager.SecurityProtocol. - HttpRequestMessage requestMessage = new HttpRequestMessage(httpVerb, resource) { Version = new Version(3, 0) }; - using HttpClient httpClient = _httpClientFactory.CreateClient(); - httpClient.DefaultRequestHeaders.Accept.Clear(); - httpClient.DefaultRequestHeaders.Clear(); - httpClient.DefaultRequestHeaders.ConnectionClose = true; - - if (!authToken.IsNullOrEmpty()) + // Determine final URL + if (!Uri.TryCreate(resource, UriKind.Absolute, out Uri? fullUrl)) { - httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {authToken}"); + if (string.IsNullOrWhiteSpace(_baseUrl)) + { + throw new InvalidOperationException("Base URL must be set for relative paths."); + } + fullUrl = new Uri(new Uri(_baseUrl, UriKind.Absolute), resource); } - if (httpVerb != HttpMethod.Get && body != null) + // Execute through resilience pipeline + return await _pipeline.ExecuteAsync(async ct => { - requestMessage.Content = new StringContent(body, Encoding.UTF8, "application/json"); - } + using var requestMessage = new HttpRequestMessage(httpVerb, fullUrl) + { + Version = new Version(3, 0) + }; + + // Headers are per-request, not global + requestMessage.Headers.Accept.Clear(); + requestMessage.Headers.ConnectionClose = true; + + if (!string.IsNullOrWhiteSpace(authToken)) + { + requestMessage.Headers.Remove(AuthorizationHeader); + requestMessage.Headers.Add(AuthorizationHeader, $"Bearer {authToken}"); + } + else if (basicAuth.HasValue) + { + var credentials = Convert.ToBase64String( + System.Text.Encoding.ASCII.GetBytes($"{basicAuth.Value.username}:{basicAuth.Value.password}") + ); + requestMessage.Headers.Remove(AuthorizationHeader); + requestMessage.Headers.Add(AuthorizationHeader, $"Basic {credentials}"); + } + + if (!string.IsNullOrWhiteSpace(body)) + { + requestMessage.Content = new StringContent(body); + } - HttpResponseMessage restResponse = await _pipeline.ExecuteAsync(async ct => await httpClient.SendAsync(requestMessage, ct)); - return await Task.FromResult(restResponse); + return await _httpClient.SendAsync(requestMessage, ct); + }); } } } diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Navigation/TenantManagementMenuNames.cs b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Navigation/TenantManagementMenuNames.cs index 8f6795200..007c2d6ab 100644 --- a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Navigation/TenantManagementMenuNames.cs +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Navigation/TenantManagementMenuNames.cs @@ -5,5 +5,6 @@ public static class TenantManagementMenuNames public const string GroupName = "TenantManagement"; public const string Tenants = GroupName + ".Tenants"; + public const string Endpoints = GroupName + ".Endpoints"; public const string Reconciliation = GroupName + ".Reconciliation"; } diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/CreateModal.cshtml b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/CreateModal.cshtml new file mode 100644 index 000000000..3483fed16 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/CreateModal.cshtml @@ -0,0 +1,24 @@ +@page +@using Unity.GrantManager.Localization +@using Microsoft.Extensions.Localization +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal + +@model Unity.GrantManager.Web.Pages.EndpointManagement.CreateModalModel + +@inject IStringLocalizer L +@{ + Layout = null; +} + + + + + + + + + + + + + diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/CreateModal.cshtml.cs b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/CreateModal.cshtml.cs new file mode 100644 index 000000000..984318707 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/CreateModal.cshtml.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using Unity.GrantManager.Integrations; +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; + +namespace Unity.GrantManager.Web.Pages.EndpointManagement; + +public class CreateModalModel(IEndpointManagementAppService endpointManagementAppService) : AbpPageModel +{ + [BindProperty] + public CreateUpdateDynamicUrlDto Endpoint { get; set; } + + + public void OnGet() + { + Endpoint = new(); + } + + public async Task OnPostAsync() + { + await endpointManagementAppService.CreateAsync(Endpoint!); + return NoContent(); + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/EndpointManagementPageModel.cs b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/EndpointManagementPageModel.cs new file mode 100644 index 000000000..1b6e030d2 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/EndpointManagementPageModel.cs @@ -0,0 +1,11 @@ +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; + +namespace Unity.TenantManagement.Web.Pages.EndpointManagement; + +public abstract class EndpointManagementPageModel : AbpPageModel +{ + protected EndpointManagementPageModel() + { + + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.cshtml b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.cshtml new file mode 100644 index 000000000..ff13f26ce --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.cshtml @@ -0,0 +1,54 @@ +@page +@using Microsoft.AspNetCore.Authorization; +@using Microsoft.AspNetCore.Mvc.Localization; +@using Unity.TenantManagement.Web.Navigation; +@using Volo.Abp.AspNetCore.Mvc.UI.Layout; +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Pages.Shared.Components.AbpPageToolbar; +@using Volo.Abp.FeatureManagement; +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; +@using Volo.Abp.TenantManagement.Localization; +@using Unity.TenantManagement.Web.Pages.TenantManagement.Tenants; +@model IndexModel +@inject IHtmlLocalizer L +@inject IAuthorizationService Authorization +@inject IPageLayout PageLayout +@{ + PageLayout.Content.BreadCrumb.Add(L["Menu:Endpoints"].Value); + PageLayout.Content.MenuItemName = TenantManagementMenuNames.Endpoints; +} +@section styles { + + + +} +@section scripts { + + + + +} + + +
+
+
+

Endpoint Management

+
+
+ +
+ +
+ +
+
+
+ + + + + +
+
+ + diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.cshtml.cs b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.cshtml.cs new file mode 100644 index 000000000..1ad3aabea --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.cshtml.cs @@ -0,0 +1,16 @@ + +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Unity.TenantManagement.Web.Pages.EndpointManagement; + +public class IndexModel : PageModel +{ + public IndexModel() + { + } + + public void OnGet() + { + // Method intentionally left empty. + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.css b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.css new file mode 100644 index 000000000..3dd9b19c0 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.css @@ -0,0 +1,16 @@ +::-ms-reveal { + display: none; +} + +#AbpContentToolbar { + display: flex !important; + background-color: transparent; +} + +#EndpointManagementWrapper { + background-color: transparent; +} + +#UserSearchTable tbody tr { + cursor: pointer; +} \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.js b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.js new file mode 100644 index 000000000..d872831bc --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.js @@ -0,0 +1,106 @@ +(function () { + const l = abp.localization.getResource('GrantManager'); + let createModal = new abp.ModalManager(abp.appPath + 'EndpointManagement/Endpoints/CreateModal'); + let updateModal = new abp.ModalManager(abp.appPath + 'EndpointManagement/Endpoints/UpdateModal'); + + /** + * Intakes: List All + */ + $.fn.dataTable.Buttons.defaults.dom.button.className = 'btn flex-none'; + let actionButtons = [ + { + text: ' ' + l('Common:Command:Create') + '', + titleAttr: l('Common:Command:Create'), + id: 'CreateButton', + className: 'btn-light rounded-1', + action: (e, dt, node, config) => createBtn(e) + } + ]; + + const listColumns = [ + { + title: "Key Name", + name: "keyName", + data: "keyName", + index: 0 + }, + { + title: "Url", + name: "url", + data: "url", + index: 1 + }, + { + title: "Description", + name: "description", + data: "description", + index: 2 + }, + { + title: l('Actions'), + data: 'id', + orderable: false, + className: 'notexport text-center', + name: 'rowActions', + index: 3, + rowAction: { + items: + [ + { + text: l('Common:Command:Edit'), + action: (data) => updateModal.open({ id: data.record.id }) + } + ] + } + } + ]; + + const defaultVisibleColumns = [ + 'keyName', + 'url', + 'description', + 'rowActions' + ]; + + let responseCallback = function (result) { + return { + recordsTotal: result.totalCount, + recordsFiltered: result.items.length, + data: result.items + }; + }; + + let dt = $('#EndpointsTable'); + + let dataTable = initializeDataTable({ + dt, + defaultVisibleColumns, + listColumns, + maxRowsPerPage: 25, + defaultSortColumn: 0, + dataEndpoint: unity.grantManager.integrations.endpoints.endpointManagement.getList, + data: {}, + responseCallback, + actionButtons, + pagingEnabled: true, + reorderEnabled: false, + languageSetValues: {}, + dataTableName: 'EndpointsTable', + dynamicButtonContainerId: 'dynamicButtonContainerId', + useNullPlaceholder: true, + externalSearchId: 'search-endpoints' + }); + + createModal.onResult(function () { + dataTable.ajax.reload(); + }); + + updateModal.onResult(function () { + dataTable.ajax.reload(); + }); + + function createBtn(e) { + e.preventDefault(); + createModal.open(); + }; +})(); diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/UpdateModal.cshtml b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/UpdateModal.cshtml new file mode 100644 index 000000000..5c97de00f --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/UpdateModal.cshtml @@ -0,0 +1,25 @@ +@page +@using Unity.GrantManager.Localization +@using Microsoft.Extensions.Localization +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal + +@model Unity.GrantManager.Web.Pages.EndpointManagement.UpdateModalModel + +@inject IStringLocalizer L +@{ + Layout = null; +} + + + + + + + + + + + + + + diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/UpdateModal.cshtml.cs b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/UpdateModal.cshtml.cs new file mode 100644 index 000000000..1425980b1 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/UpdateModal.cshtml.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Threading.Tasks; +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; +using Unity.GrantManager.Integrations; +namespace Unity.GrantManager.Web.Pages.EndpointManagement; + +public class UpdateModalModel(IEndpointManagementAppService endpointManagementAppService) : AbpPageModel +{ + [HiddenInput] + [BindProperty(SupportsGet = true)] + public Guid Id { get; set; } + + [BindProperty] + public CreateUpdateDynamicUrlDto Endpoint { get; set; } + + public async Task OnGetAsync() + { + var endpointDto = await endpointManagementAppService.GetAsync(Id); + Endpoint = ObjectMapper.Map(endpointDto); + } + + public async Task OnPostAsync() + { + await endpointManagementAppService.UpdateAsync(Id, Endpoint!); + return NoContent(); + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/_ViewImports.cshtml b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/_ViewImports.cshtml new file mode 100644 index 000000000..c1da1f5f1 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/_ViewImports.cshtml @@ -0,0 +1,4 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Tenants/TenantManagementPageModel.cs b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Tenants/TenantManagementPageModel.cs index cf76f69de..bd26beedd 100644 --- a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Tenants/TenantManagementPageModel.cs +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Tenants/TenantManagementPageModel.cs @@ -1,5 +1,4 @@ -using Unity.TenantManagement.Web; -using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; namespace Unity.TenantManagement.Web.Pages.TenantManagement.Tenants; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Chefs/IFormsApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Chefs/IFormsApiService.cs index f5671ffa1..d3b6276b7 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Chefs/IFormsApiService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Chefs/IFormsApiService.cs @@ -1,12 +1,14 @@ -using System; +using Newtonsoft.Json.Linq; +using System; using System.Threading.Tasks; using Volo.Abp.Application.Services; -namespace Unity.GrantManager.Integration.Chefs +namespace Unity.GrantManager.Integrations.Chefs { public interface IFormsApiService : IApplicationService { - Task GetFormDataAsync(string chefsFormId, string chefsFormVersionId); - Task GetForm(Guid? formId, string chefsApplicationFormGuid, string encryptedApiKey); + Task GetFormDataAsync(string chefsFormId, string chefsFormVersionId); + Task GetForm(Guid? formId, string chefsApplicationFormGuid, string encryptedApiKey); + Task GetSubmissionDataAsync(Guid chefsFormId, Guid submissionId); } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Chefs/ISubmissionsApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Chefs/ISubmissionsApiService.cs index a90018508..0ab321867 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Chefs/ISubmissionsApiService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Chefs/ISubmissionsApiService.cs @@ -1,8 +1,8 @@ -using System; +using System; using System.Threading.Tasks; using Volo.Abp.Application.Services; -namespace Unity.GrantManager.Integration.Chefs +namespace Unity.GrantManager.Integrations.Chefs { public interface ISubmissionsApiService : IApplicationService { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/CssUser.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/CssUser.cs index 18a09ca03..b0d5260dd 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/CssUser.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/CssUser.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Unity.GrantManager.Integration.Css +namespace Unity.GrantManager.Integrations.Css { public class CssUser { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/CssUserAttributes.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/CssUserAttributes.cs index 7b5db60b6..2659cb1e4 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/CssUserAttributes.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/CssUserAttributes.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Unity.GrantManager.Integration.Css +namespace Unity.GrantManager.Integrations.Css { public class CssUserAttributes { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/ICssUsersApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/ICssUsersApiService.cs index 5f6cd46a6..d08adfe68 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/ICssUsersApiService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/ICssUsersApiService.cs @@ -1,7 +1,7 @@ using System.Threading.Tasks; using Volo.Abp.Application.Services; -namespace Unity.GrantManager.Integration.Css +namespace Unity.GrantManager.Integrations.Css { public interface ICssUsersApiService : IApplicationService { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/InvalidResponse.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/InvalidResponse.cs index f93fe7147..35c4a011d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/InvalidResponse.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/InvalidResponse.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Unity.GrantManager.Integration.Css +namespace Unity.GrantManager.Integrations.Css { public class InvalidResponse { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/TokenValidationResponse.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/TokenValidationResponse.cs index dd19c4c80..c9ce345ed 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/TokenValidationResponse.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/TokenValidationResponse.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Unity.GrantManager.Integration.Css +namespace Unity.GrantManager.Integrations.Css { public class TokenValidationResponse { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/UserSearchResult.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/UserSearchResult.cs index 6332f2021..2f548dfe1 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/UserSearchResult.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Css/UserSearchResult.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Unity.GrantManager.Integration.Css +namespace Unity.GrantManager.Integrations.Css { public class UserSearchResult { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/CreateUpdateDynamicUrlDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/CreateUpdateDynamicUrlDto.cs new file mode 100644 index 000000000..acc48f029 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/CreateUpdateDynamicUrlDto.cs @@ -0,0 +1,12 @@ +using System.ComponentModel; + +namespace Unity.GrantManager.Integrations +{ + public class CreateUpdateDynamicUrlDto + { + [DisplayName("Key Name")] + public string KeyName { get; set; } = string.Empty; + public string Url { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/DynamicUrlDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/DynamicUrlDto.cs new file mode 100644 index 000000000..06a99169d --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/DynamicUrlDto.cs @@ -0,0 +1,14 @@ +using System; +using Volo.Abp.Application.Dtos; + +namespace Unity.GrantManager.Integrations +{ + public class DynamicUrlDto : AuditedEntityDto + { + public string KeyName { get; set; } = string.Empty; + + public string Url { get; set; } = string.Empty; + + public string Description { get; set; } = string.Empty; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/IEndpointManagementAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/IEndpointManagementAppService.cs new file mode 100644 index 000000000..ef26c8994 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/IEndpointManagementAppService.cs @@ -0,0 +1,18 @@ +using System; +using System.Threading.Tasks; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; + +namespace Unity.GrantManager.Integrations; + + +public interface IEndpointManagementAppService : ICrudAppService< + DynamicUrlDto, + Guid, + PagedAndSortedResultRequestDto, + CreateUpdateDynamicUrlDto> + +{ + Task GetChefsApiBaseUrlAsync(); + Task GetUrlByKeyNameAsync(string keyName); +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Geocoder/AddressDetailsDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Geocoder/AddressDetailsDto.cs index 290da6b26..6bc393c59 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Geocoder/AddressDetailsDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Geocoder/AddressDetailsDto.cs @@ -1,4 +1,4 @@ -namespace Unity.GrantManager.Integration.Geocoder +namespace Unity.GrantManager.Integrations.Geocoder { public class AddressDetailsDto { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Geocoder/IGeocoderApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Geocoder/IGeocoderApiService.cs index 119fc637a..06cb36082 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Geocoder/IGeocoderApiService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Geocoder/IGeocoderApiService.cs @@ -1,7 +1,7 @@ using System.Threading.Tasks; using Volo.Abp.Application.Services; -namespace Unity.GrantManager.Integration.Geocoder +namespace Unity.GrantManager.Integrations.Geocoder { public interface IGeocoderApiService : IApplicationService { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/OrgBook/IOrgBookService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/OrgBook/IOrgBookService.cs index 67dac5200..602f09cba 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/OrgBook/IOrgBookService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/OrgBook/IOrgBookService.cs @@ -2,7 +2,7 @@ using System.Threading.Tasks; using Volo.Abp.Application.Services; -namespace Unity.GrantManager.Integration.Orgbook +namespace Unity.GrantManager.Integrations.Orgbook { public interface IOrgBookService : IApplicationService { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Emails/CreateEmailDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/CreateEmailDto.cs similarity index 99% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Emails/CreateEmailDto.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/CreateEmailDto.cs index 8433ffe51..b2fc69250 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Emails/CreateEmailDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/CreateEmailDto.cs @@ -14,7 +14,6 @@ public class CreateEmailDto [Required] [MaxLength(1023)] // Max for CHES public string EmailSubject { get; set; } = string.Empty; - [Required] public string EmailBody { get; set; } = string.Empty; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Emails/EmailDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/EmailDto.cs similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Emails/EmailDto.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/EmailDto.cs diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/TeamsNotifications/Facts.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/Facts.cs similarity index 96% rename from applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/TeamsNotifications/Facts.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/Facts.cs index b2a6ab0ab..6569cfe5e 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/TeamsNotifications/Facts.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/Facts.cs @@ -1,13 +1,13 @@ -using System.Text.Json.Serialization; - -namespace Unity.Notifications.TeamsNotifications -{ - public class Fact - { - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; - - [JsonPropertyName("value")] - public string Value { get; set; } = string.Empty; - } +using System.Text.Json.Serialization; + +namespace Unity.Notifications.TeamsNotifications +{ + public class Fact + { + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("value")] + public string Value { get; set; } = string.Empty; + } } \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Emails/IEmailAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/IEmailAppService.cs similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Emails/IEmailAppService.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/IEmailAppService.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Emails/IEmailsService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/IEmailsService.cs similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Emails/IEmailsService.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/IEmailsService.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/INotificationsAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/INotificationsAppService.cs new file mode 100644 index 000000000..74c164345 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/INotificationsAppService.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Unity.Notifications.TeamsNotifications; + +namespace Unity.GrantManager.Notifications +{ + public interface INotificationsAppService + { + Task NotifyChefsEventToTeamsAsync(string factName, string factValue); + Task PostChefsEventToTeamsAsync(string subscriptionEvent, dynamic form, dynamic chefsFormVersion); + Task PostToTeamsAsync(string activityTitle, string activitySubtitle); + Task PostToTeamsAsync(string activityTitle, string activitySubtitle, List facts); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Emails/UpdateEmailDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/UpdateEmailDto.cs similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Emails/UpdateEmailDto.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/UpdateEmailDto.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantAppService.cs index 920670030..0f8e35109 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantAppService.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using System; @@ -10,13 +10,13 @@ using Unity.GrantManager.GrantApplications; using Unity.GrantManager.Intakes; using Unity.GrantManager.Intakes.Mapping; -using Unity.GrantManager.Integration.Orgbook; +using Unity.Payments.Events; +using Volo.Abp; +using Unity.GrantManager.Integrations.Orgbook; using Unity.Modules.Shared.Utils; using Unity.Payments.Domain.Suppliers; -using Unity.Payments.Events; using Unity.Payments.Integrations.Cas; using Unity.Payments.Suppliers; -using Volo.Abp; using Volo.Abp.DependencyInjection; namespace Unity.GrantManager.Applicants; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormAppService.cs index afb6b1cf3..a72cf471e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormAppService.cs @@ -7,7 +7,7 @@ using Unity.GrantManager.Applications; using Unity.GrantManager.Forms; using Unity.GrantManager.GrantApplications; -using Unity.GrantManager.Integration.Chefs; +using Unity.GrantManager.Integrations.Chefs; using Unity.GrantManager.Permissions; using Unity.Payments.Permissions; using Volo.Abp; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormSycnronizationService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormSycnronizationService.cs index ca61e4fa1..d7b60781b 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormSycnronizationService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormSycnronizationService.cs @@ -1,5 +1,4 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; using RestSharp; using RestSharp.Authenticators; @@ -14,7 +13,7 @@ using Unity.GrantManager.Applications; using Unity.GrantManager.Forms; using Unity.GrantManager.Intakes; -using Unity.GrantManager.Integration.Chefs; +using Unity.GrantManager.Integrations.Chefs; using Volo.Abp; using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; @@ -22,6 +21,7 @@ using Volo.Abp.MultiTenancy; using Volo.Abp.Security.Encryption; using Volo.Abp.TenantManagement; +using Unity.GrantManager.Notifications; namespace Unity.GrantManager.ApplicationForms { @@ -41,40 +41,39 @@ public class ApplicationFormSycnronizationService : private readonly ICurrentTenant _currentTenant; private readonly IApplicationFormSubmissionRepository _applicationFormSubmissionRepository; private readonly IApplicationFormVersionAppService _applicationFormVersionAppService; - private readonly IConfiguration _configuration; - private readonly ISubmissionsApiService _submissionsApiService; + private readonly IFormsApiService _formsApiService; private readonly IIntakeFormSubmissionManager _intakeFormSubmissionManager; + private readonly INotificationsAppService _notificationsAppService; private List _facts = new(); private readonly RestClient _intakeClient; private readonly ITenantRepository _tenantRepository; public List? applicationFormDtoList { get; set; } public HashSet FormVersionsInitializedVersionHash { get; set; } = new HashSet(); - - + public ApplicationFormSycnronizationService( + INotificationsAppService notificationsAppService, ICurrentTenant currentTenant, IRepository repository, ITenantRepository tenantRepository, RestClient restClient, - IConfiguration configuration, IStringEncryptionService stringEncryptionService, IApplicationFormRepository applicationFormRepository, IApplicationFormSubmissionRepository applicationFormSubmissionRepository, IApplicationFormVersionAppService applicationFormVersionAppService, - ISubmissionsApiService submissionsApiService, + IFormsApiService formsApiService, IIntakeFormSubmissionManager intakeFormSubmissionManager) : base(repository) { _currentTenant = currentTenant; _tenantRepository = tenantRepository; _intakeClient = restClient; - _configuration = configuration; _stringEncryptionService = stringEncryptionService; _applicationFormRepository = applicationFormRepository; - _submissionsApiService = submissionsApiService; + _formsApiService = formsApiService; _applicationFormSubmissionRepository = applicationFormSubmissionRepository; _applicationFormVersionAppService = applicationFormVersionAppService; _intakeFormSubmissionManager = intakeFormSubmissionManager; + _notificationsAppService = notificationsAppService; } private async Task SynchronizeFormSubmissions(HashSet missingSubmissions, ApplicationFormDto applicationFormDto) @@ -101,7 +100,7 @@ private async Task ProcessSingleSubmission(string submissionGuid, ApplicationFor return; } - JObject? submissionData = await _submissionsApiService.GetSubmissionDataAsync(chefsFormId, chefsSubmissionId); + JObject? submissionData = await _formsApiService.GetSubmissionDataAsync(chefsFormId, chefsSubmissionId); if (submissionData == null) { Logger.LogInformation("ApplicationFormSycnronizationService->SynchronizeFormSubmissions submissionData is null"); @@ -179,7 +178,7 @@ private async Task ProcessSubmission(ApplicationFormDto applicationFormDto, JObj HashSet missingSubmissions = new HashSet(); // Get all forms with api keys - applicationFormDtoList = (List?)await GetConnectedApplicationFormsAsync(); + applicationFormDtoList = (List?) await GetConnectedApplicationFormsAsync(); if (applicationFormDtoList != null) { @@ -237,8 +236,8 @@ private async Task ProcessSubmission(ApplicationFormDto applicationFormDto, JObj string? envInfo = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); string activityTitle = "Review Missed Chefs Submissions " + tenantName; string activitySubtitle = "Environment: " + envInfo; - string teamsChannel = _configuration["Notifications:TeamsNotificationsWebhook"] ?? ""; - await TeamsNotificationService.PostToTeamsAsync(teamsChannel, activityTitle, activitySubtitle, _facts); + + await _notificationsAppService.PostToTeamsAsync(activityTitle, activitySubtitle, _facts); } return (missingSubmissions ?? new HashSet(), missingSubmissionsReportBuilder.ToString()); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormVersionAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormVersionAppService.cs index de58359ed..1fd73c1cd 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormVersionAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormVersionAppService.cs @@ -8,7 +8,7 @@ using Unity.GrantManager.Applications; using Unity.GrantManager.Forms; using Unity.GrantManager.Intakes; -using Unity.GrantManager.Integration.Chefs; +using Unity.GrantManager.Integrations.Chefs; using Unity.GrantManager.Reporting.FieldGenerators; using Unity.Modules.Shared.Features; using Volo.Abp.Application.Dtos; @@ -144,6 +144,12 @@ private async Task FormVersionDoesNotExist(string formVersionId) => }; var formVersion = await _formsApiService.GetFormDataAsync(formId, formVersionId); + if (formVersion == null) // Ensure formVersion is not null + { + Logger.LogWarning("Form version data is null for formId: {FormId}, formVersionId: {FormVersionId}", formId, formVersionId); + return null; + } + applicationFormVersion.AvailableChefsFields = _formSubmissionMapper.InitializeAvailableFormFields(formVersion); if (formVersion is JObject formVersionObject) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ConfigureIntakeClientOptions.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ConfigureIntakeClientOptions.cs new file mode 100644 index 000000000..da6124905 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ConfigureIntakeClientOptions.cs @@ -0,0 +1,28 @@ + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; +using Unity.GrantManager.Intake; + +namespace Unity.GrantManager; + +public class ConfigureIntakeClientOptions : IConfigureOptions +{ + private readonly IConfiguration _configuration; + const string PROTOCOL = "https://"; + const string DefaultBaseUri = $"{PROTOCOL}submit.digital.gov.bc.ca/app/api/v1"; + + + public ConfigureIntakeClientOptions(IConfiguration configuration) + { + _configuration = configuration; + } + + public void Configure(IntakeClientOptions options) + { + options.BaseUri = DefaultBaseUri; + options.BearerTokenPlaceholder = _configuration["Intake:BearerTokenPlaceholder"] ?? ""; + options.UseBearerToken = _configuration.GetValue("Intake:UseBearerToken"); + options.AllowUnregisteredVersions = _configuration.GetValue("Intake:AllowUnregisteredVersions"); + } +} + diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Events/ChefsEventSubscriptionService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Events/ChefsEventSubscriptionService.cs index 033aa56a9..67acb7853 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Events/ChefsEventSubscriptionService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Events/ChefsEventSubscriptionService.cs @@ -1,4 +1,3 @@ -using Microsoft.Extensions.Configuration; using System; using System.Linq; using System.Threading.Tasks; @@ -6,53 +5,46 @@ using Unity.GrantManager.Applications; using Unity.GrantManager.Exceptions; using Unity.GrantManager.Intakes; -using Unity.GrantManager.Integration.Chefs; -using Unity.Notifications.TeamsNotifications; +using Unity.GrantManager.Integrations.Chefs; +using Unity.GrantManager.Notifications; using Volo.Abp; using Volo.Abp.Domain.Entities; namespace Unity.GrantManager.Events { [RemoteService(false)] - public class ChefsEventSubscriptionService : GrantManagerAppService, IChefsEventSubscriptionService - { - private readonly IApplicationFormRepository _applicationFormRepository; - private readonly IApplicationFormManager _applicationFormManager; - private readonly IIntakeFormSubmissionMapper _intakeFormSubmissionMapper; - private readonly ISubmissionsApiService _submissionsIntService; - private readonly IFormsApiService _formsApiService; - private readonly IApplicationFormVersionAppService _applicationFormVersionAppService; - private readonly IConfiguration _configuration; - - public ChefsEventSubscriptionService( - IConfiguration configuration, + public class ChefsEventSubscriptionService( + INotificationsAppService notificationsAppService, IIntakeFormSubmissionMapper intakeFormSubmissionMapper, IApplicationFormManager applicationFormManager, - ISubmissionsApiService submissionsIntService, + IFormsApiService submissionsIntService, IApplicationFormRepository applicationFormRepository, IFormsApiService formsApiService, - IApplicationFormVersionAppService applicationFormVersionAppService) - { - _configuration = configuration; - _intakeFormSubmissionMapper = intakeFormSubmissionMapper; - _submissionsIntService = submissionsIntService; - _applicationFormRepository = applicationFormRepository; - _formsApiService = formsApiService; - _applicationFormManager = applicationFormManager; - _applicationFormVersionAppService = applicationFormVersionAppService; - } + IApplicationFormVersionAppService applicationFormVersionAppService) : GrantManagerAppService, IChefsEventSubscriptionService + { public async Task CreateIntakeMappingAsync(EventSubscriptionDto eventSubscriptionDto) { - var applicationForm = (await _applicationFormRepository + _ = (await applicationFormRepository .GetQueryableAsync()) .Where(s => s.ChefsApplicationFormGuid == eventSubscriptionDto.FormId.ToString()) .OrderBy(s => s.CreationTime) .FirstOrDefault() ?? throw new EntityNotFoundException("Application Form Not Registered"); + + var submissionData = await submissionsIntService.GetSubmissionDataAsync(eventSubscriptionDto.FormId, eventSubscriptionDto.SubmissionId) + ?? throw new InvalidFormDataSubmissionException(); - var submissionData = await _submissionsIntService.GetSubmissionDataAsync(eventSubscriptionDto.FormId, eventSubscriptionDto.SubmissionId) ?? throw new InvalidFormDataSubmissionException(); - var formVersion = await _formsApiService.GetFormDataAsync(eventSubscriptionDto.FormId.ToString(), submissionData.submission.formVersionId.ToString()) ?? throw new InvalidFormDataSubmissionException(); - var result = _intakeFormSubmissionMapper.InitializeAvailableFormFields(formVersion); + // Access the 'submission' property from the JObject using the appropriate key. + var submission = submissionData["submission"] + ?? throw new InvalidFormDataSubmissionException(); + + // Ensure the 'formVersionId' is accessed correctly from the 'submission' object. + var formVersionId = submission["formVersionId"]?.ToString() + ?? throw new InvalidFormDataSubmissionException(); + + var formVersion = await formsApiService.GetFormDataAsync(eventSubscriptionDto.FormId.ToString(), formVersionId) + ?? throw new InvalidFormDataSubmissionException(); + var result = intakeFormSubmissionMapper.InitializeAvailableFormFields(formVersion); return !result.IsNullOrEmpty(); } @@ -61,7 +53,7 @@ public async Task PublishedFormAsync(EventSubscriptionDto eventSubscriptio string formId = eventSubscriptionDto.FormId.ToString(); string formVersionId = eventSubscriptionDto.FormVersion.ToString(); - var applicationForm = (await _applicationFormRepository + var applicationForm = (await applicationFormRepository .GetQueryableAsync()) .Where(s => s.ChefsApplicationFormGuid == formId) .OrderBy(s => s.CreationTime) @@ -72,18 +64,27 @@ public async Task PublishedFormAsync(EventSubscriptionDto eventSubscriptio && applicationForm.ChefsApplicationFormGuid != null) { // Go grab the new name/description/version and map the new available fields - var formVersion = await _formsApiService.GetFormDataAsync(formId, formVersionId); - dynamic form = await _formsApiService.GetForm(Guid.Parse(applicationForm.ChefsApplicationFormGuid), applicationForm.ChefsApplicationFormGuid.ToString(), applicationForm.ApiKey); - applicationForm = _applicationFormManager.SynchronizePublishedForm(applicationForm, formVersion, form); - await _applicationFormVersionAppService.UpdateOrCreateApplicationFormVersion(formId, formVersionId, applicationForm.Id, formVersion); - applicationForm = await _applicationFormRepository.UpdateAsync(applicationForm); - string teamsChannel = _configuration["Notifications:TeamsNotificationsWebhook"] ?? ""; - TeamsNotificationService.PostChefsEventToTeamsAsync(teamsChannel, eventSubscriptionDto.SubscriptionEvent, form, formVersion); + var formVersion = await formsApiService.GetFormDataAsync(formId, formVersionId); + if (formVersion == null) + { + throw new InvalidFormDataSubmissionException("Form version data is null."); + } + + dynamic form = await formsApiService.GetForm(Guid.Parse(applicationForm.ChefsApplicationFormGuid), applicationForm.ChefsApplicationFormGuid.ToString(), applicationForm.ApiKey); + if (form == null) + { + throw new InvalidFormDataSubmissionException("Form data is null."); + } + + applicationForm = applicationFormManager.SynchronizePublishedForm(applicationForm, formVersion, form); + await applicationFormVersionAppService.UpdateOrCreateApplicationFormVersion(formId, formVersionId, applicationForm.Id, formVersion); + applicationForm = await applicationFormRepository.UpdateAsync(applicationForm); + notificationsAppService.PostChefsEventToTeamsAsync(eventSubscriptionDto.SubscriptionEvent, form, formVersion); } - else if(applicationForm == null) + else if (applicationForm == null) { EventSubscription eventSubscription = ObjectMapper.Map(eventSubscriptionDto); - applicationForm = await _applicationFormManager.InitializeApplicationForm(eventSubscription); + applicationForm = await applicationFormManager.InitializeApplicationForm(eventSubscription); } return applicationForm != null; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationAutoMapperProfile.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationAutoMapperProfile.cs index 1ad3ec85a..15866c572 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationAutoMapperProfile.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationAutoMapperProfile.cs @@ -11,6 +11,7 @@ using Unity.GrantManager.GrantApplications; using Unity.GrantManager.Identity; using Unity.GrantManager.Intakes; +using Unity.GrantManager.Integrations; using Unity.GrantManager.Locality; using Unity.GrantManager.Zones; using Unity.Payments.Domain.AccountCodings; @@ -25,7 +26,12 @@ public GrantManagerApplicationAutoMapperProfile() /* You can configure your AutoMapper mapping configuration here. * Alternatively, you can split your mapping configurations * into multiple profile classes for a better organization. */ - + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); CreateMap(); CreateMap() diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs index 06ddfc9be..8b77662ca 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs @@ -19,7 +19,6 @@ using Volo.Abp.PermissionManagement; using Volo.Abp.SettingManagement; using Volo.Abp.BackgroundWorkers.Quartz; -using Unity.GrantManager.GrantApplications; using Volo.Abp.Application.Dtos; using Unity.Notifications; using Unity.Notifications.Integrations.Ches; @@ -38,6 +37,12 @@ using Unity.GrantManager.Infrastructure; using Medallion.Threading; using Unity.GrantManager.Locks; +using Volo.Abp; +using JsonSerializerOptions = System.Text.Json.JsonSerializerOptions; +using Unity.GrantManager.Integrations.Chefs; +using Unity.Modules.Shared.Http; +using Volo.Abp.Caching; +using Microsoft.Extensions.Caching.StackExchangeRedis; namespace Unity.GrantManager; @@ -121,21 +126,12 @@ public override void ConfigureServices(ServiceConfigurationContext context) }); context.Services.AddSingleton(); - context.Services.AddSingleton(); - - Configure(options => - { - // This fails unit tests unless set to a non empty string - // RestClient will throw an error - baseUrl can not be empty - options.BaseUri = configuration["Intake:BaseUri"] ?? "https://submit.digital.gov.bc.ca/app/api/v1"; - options.BearerTokenPlaceholder = configuration["Intake:BearerTokenPlaceholder"] ?? ""; - options.UseBearerToken = configuration.GetValue("Intake:UseBearerToken"); - options.AllowUnregisteredVersions = configuration.GetValue("Intake:AllowUnregisteredVersions"); - }); - - context.Services.Configure(configuration.GetSection(key: "Payments")); - context.Services.Configure(configuration.GetSection(key: "CssApi")); - context.Services.Configure(configuration.GetSection(key: "Notifications")); + context.Services.AddTransient, ConfigureIntakeClientOptions>(); + context.Services.AddTransient(); + context.Services.AddTransient(); + context.Services.Configure(configuration.GetSection("Payments")); + context.Services.Configure(configuration.GetSection("CssApi")); + context.Services.Configure(configuration.GetSection("Notifications")); ConfigureBackgroundServices(configuration); @@ -152,60 +148,40 @@ public override void ConfigureServices(ServiceConfigurationContext context) context.Services.ConfigureRabbitMQ(); context.Services.AddScoped(); - _ = context.Services.AddSingleton(provider => + context.Services.AddSingleton(provider => { var options = provider.GetService>()?.Value; - if (null != options) - { - var restOptions = new RestClientOptions(options.BaseUri) + var restOptions = options != null + ? new RestClientOptions(options.BaseUri) { - // NOTE: Basic authentication only works for fetching forms and lists of form submissions - // Authenticator = options.UseBearerToken ? - // new JwtAuthenticator(options.BearerTokenPlaceholder) : - // new HttpBasicAuthenticator(options.FormId, options.ApiKey), - FailOnDeserializationError = true, ThrowOnDeserializationError = true - }; + } + : new RestClientOptions(); - return new RestClient( - restOptions, - configureSerialization: s => - s.UseSystemTextJson(new System.Text.Json.JsonSerializerOptions - { - WriteIndented = true, - PropertyNameCaseInsensitive = true, - ReadCommentHandling = JsonCommentHandling.Skip, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - }) - ); - } - else - { - return new RestClient( - configureSerialization: s => - s.UseSystemTextJson(new System.Text.Json.JsonSerializerOptions - { - WriteIndented = true, - PropertyNameCaseInsensitive = true, - ReadCommentHandling = JsonCommentHandling.Skip, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - }) - ); - } + return new RestClient( + restOptions, + configureSerialization: s => s.UseSystemTextJson(new JsonSerializerOptions + { + WriteIndented = true, + PropertyNameCaseInsensitive = true, + ReadCommentHandling = JsonCommentHandling.Skip, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }) + ); }); - // Set the max defaults as max - we are using non serverside paging and this effect this + // Max paging limits ExtensibleLimitedResultRequestDto.DefaultMaxResultCount = int.MaxValue; ExtensibleLimitedResultRequestDto.MaxMaxResultCount = int.MaxValue; - LimitedResultRequestDto.DefaultMaxResultCount = int.MaxValue; LimitedResultRequestDto.MaxMaxResultCount = int.MaxValue; } private void ConfigureBackgroundServices(IConfiguration configuration) { - if (!Convert.ToBoolean(configuration["BackgroundJobs:IsJobExecutionEnabled"])) return; + if (!Convert.ToBoolean(configuration["BackgroundJobs:IsJobExecutionEnabled"])) + return; Configure(options => { @@ -217,13 +193,33 @@ private void ConfigureBackgroundServices(IConfiguration configuration) options.IsAutoRegisterEnabled = configuration.GetValue("BackgroundJobs:Quartz:IsAutoRegisterEnabled"); }); - /* - * There are Global Retry Options that can be configured, configure if required, or if handled per job - */ Configure(options => { options.IsJobExecutionEnabled = configuration.GetValue("BackgroundJobs:IsJobExecutionEnabled"); options.Quartz.IsAutoRegisterEnabled = configuration.GetValue("BackgroundJobs:Quartz:IsAutoRegisterEnabled"); }); } -} \ No newline at end of file + + private void ConfigureDistributedCache(ServiceConfigurationContext context, IConfiguration configuration) + { + if (!Convert.ToBoolean(configuration["Redis:IsEnabled"])) + return; + + context.Services.AddStackExchangeRedisCache(options => + { + options.InstanceName = configuration["Redis:InstanceName"]; + options.Configuration = $"{configuration["Redis:Host"]}:{configuration["Redis:Port"]},password={configuration["Redis:Password"]}"; + }); + + Configure(options => + { + options.InstanceName = configuration["Redis:InstanceName"]; + options.Configuration = $"{configuration["Redis:Host"]}:{configuration["Redis:Port"]},password={configuration["Redis:Password"]}"; + }); + + Configure(options => + { + options.KeyPrefix = configuration["Redis:KeyPrefix"] ?? "unity"; + }); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Identity/UserImportAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Identity/UserImportAppService.cs index 5c9336fe5..dbf527204 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Identity/UserImportAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Identity/UserImportAppService.cs @@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; -using Unity.GrantManager.Integration.Css; +using Unity.GrantManager.Integrations.Css; using Volo.Abp; using Volo.Abp.Data; using Volo.Abp.Identity; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/ApplicationIntakeAdminService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/ApplicationIntakeAdminService.cs index 4d820df70..81a212f1b 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/ApplicationIntakeAdminService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/ApplicationIntakeAdminService.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Threading.Tasks; using Unity.GrantManager.Applications; -using Unity.GrantManager.Integration.Chefs; +using Unity.GrantManager.Integrations.Chefs; using Volo.Abp.Domain.Entities; namespace Unity.GrantManager.Intakes diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/Handlers/DetermineElectoralDistrictHandler.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/Handlers/DetermineElectoralDistrictHandler.cs index 487a6bac1..edb597df8 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/Handlers/DetermineElectoralDistrictHandler.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/Handlers/DetermineElectoralDistrictHandler.cs @@ -3,7 +3,7 @@ using System.Linq; using System.Threading.Tasks; using Unity.GrantManager.Intakes.Events; -using Unity.GrantManager.Integration.Geocoder; +using Unity.GrantManager.Integrations.Geocoder; using Volo.Abp.DependencyInjection; using Volo.Abp.EventBus; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/IntakeSubmissionAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/IntakeSubmissionAppService.cs index a19e4db14..bc044b31e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/IntakeSubmissionAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/IntakeSubmissionAppService.cs @@ -1,5 +1,4 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options; using Newtonsoft.Json.Linq; using System; using System.Linq; @@ -9,49 +8,33 @@ using Unity.GrantManager.Events; using Unity.GrantManager.Exceptions; using Unity.GrantManager.Intake; -using Unity.GrantManager.Integration.Chefs; -using Unity.Notifications.TeamsNotifications; +using Unity.GrantManager.Integrations; +using Unity.GrantManager.Integrations.Chefs; +using Unity.GrantManager.Notifications; + using Volo.Abp; namespace Unity.GrantManager.Intakes { [RemoteService(false)] - public class IntakeSubmissionAppService : GrantManagerAppService, IIntakeSubmissionAppService + public class IntakeSubmissionAppService(INotificationsAppService notificationsAppService, + IIntakeFormSubmissionManager intakeFormSubmissionManager, + IEndpointManagementAppService endpointManagementAppService, + IFormsApiService formsApiService, + IApplicationFormRepository applicationFormRepository, + IApplicationFormVersionAppService applicationFormVersionAppService, + IOptions intakeClientOptions) : GrantManagerAppService, IIntakeSubmissionAppService { - private readonly IApplicationFormRepository _applicationFormRepository; - private readonly IIntakeFormSubmissionManager _intakeFormSubmissionManager; - private readonly IFormsApiService _formsApiService; - private readonly ISubmissionsApiService _submissionsIntService; - private readonly IApplicationFormVersionAppService _applicationFormVersionAppService; - private readonly IOptions _intakeClientOptions; - private readonly IConfiguration _configuration; - - public IntakeSubmissionAppService(IIntakeFormSubmissionManager intakeFormSubmissionManager, - IFormsApiService formsApiService, - ISubmissionsApiService submissionsIntService, - IApplicationFormRepository applicationFormRepository, - IApplicationFormVersionAppService applicationFormVersionAppService, - IOptions intakeClientOptions, - IConfiguration configuration) - { - _intakeFormSubmissionManager = intakeFormSubmissionManager; - _submissionsIntService = submissionsIntService; - _applicationFormRepository = applicationFormRepository; - _formsApiService = formsApiService; - _applicationFormVersionAppService = applicationFormVersionAppService; - _intakeClientOptions = intakeClientOptions; - _configuration = configuration; - } public async Task CreateIntakeSubmissionAsync(EventSubscriptionDto eventSubscriptionDto) { - var applicationForm = (await _applicationFormRepository + var applicationForm = (await applicationFormRepository .GetQueryableAsync()) .Where(s => s.ChefsApplicationFormGuid == eventSubscriptionDto.FormId.ToString()) .OrderBy(s => s.CreationTime) .FirstOrDefault() ?? throw new ApplicationFormSetupException("Application Form Not Registered"); - JObject submissionData = await _submissionsIntService.GetSubmissionDataAsync(eventSubscriptionDto.FormId, eventSubscriptionDto.SubmissionId) ?? throw new InvalidFormDataSubmissionException(); + JObject submissionData = await formsApiService.GetSubmissionDataAsync(eventSubscriptionDto.FormId, eventSubscriptionDto.SubmissionId) ?? throw new InvalidFormDataSubmissionException(); bool validSubmission = await ValidateSubmission(eventSubscriptionDto, submissionData); if (!validSubmission) { @@ -64,7 +47,7 @@ public async Task CreateIntakeSubmissionAsync( await StoreChefsFieldMappingAsync(eventSubscriptionDto, applicationForm, token); } - var result = await _intakeFormSubmissionManager.ProcessFormSubmissionAsync(applicationForm, submissionData); + var result = await intakeFormSubmissionManager.ProcessFormSubmissionAsync(applicationForm, submissionData); return new EventSubscriptionConfirmationDto() { ConfirmationId = result }; } @@ -75,7 +58,7 @@ private async Task ValidateSubmission(EventSubscriptionDto eventSubscripti if (tokenDraft != null && tokenDraft.ToString() == "True") { string factName = "A draft submission was submitted and should not have been"; string factValue = $"FormId: {eventSubscriptionDto.FormId} SubmissionID: {eventSubscriptionDto.SubmissionId}"; - await SendTeamsNotification(factName, factValue); + await notificationsAppService.NotifyChefsEventToTeamsAsync(factName, factValue); return false; } @@ -83,54 +66,43 @@ private async Task ValidateSubmission(EventSubscriptionDto eventSubscripti if (tokenDeleted != null && tokenDeleted.ToString() == "True") { string factName = "A deleted submission was submitted - user navigated back and got a success message from chefs"; string factValue = $"FormId: {eventSubscriptionDto.FormId} SubmissionID: {eventSubscriptionDto.SubmissionId}"; - await SendTeamsNotification(factName, factValue); + await notificationsAppService.NotifyChefsEventToTeamsAsync(factName, factValue); } // If there are no mappings initialize the available - bool formVersionExists = await _applicationFormVersionAppService.FormVersionExists(eventSubscriptionDto.FormVersion.ToString()); + bool formVersionExists = await applicationFormVersionAppService.FormVersionExists(eventSubscriptionDto.FormVersion.ToString()); if (!formVersionExists) { - dynamic? formVersion = await _formsApiService.GetFormDataAsync(eventSubscriptionDto.FormId.ToString(), + dynamic? formVersion = await formsApiService.GetFormDataAsync(eventSubscriptionDto.FormId.ToString(), eventSubscriptionDto.FormVersion.ToString()); if(formVersion == null) { string factName = "Application Form Version Not Registered - Unknown Version"; string factValue = $"FormId: {eventSubscriptionDto.FormId} FormVersion: {eventSubscriptionDto.FormVersion}"; - await SendTeamsNotification(factName, factValue); + await notificationsAppService.NotifyChefsEventToTeamsAsync(factName, factValue); return false; - } else if(!_intakeClientOptions.Value.AllowUnregisteredVersions) + } else //if(!intakeClientOptions.Value.AllowUnregisteredVersions) { var version = ((JObject)formVersion!).SelectToken("version"); var published = ((JObject)formVersion!).SelectToken("published"); string factName = "Application Form Version Not Registered - Unknown Version"; string factValue = $"Application Form Version Not Registerd - Version: {version} Published: {published}"; - await SendTeamsNotification(factName, factValue); + await notificationsAppService.NotifyChefsEventToTeamsAsync(factName, factValue); return false; } } return true; } - private async Task SendTeamsNotification(string factName, string factValue) - { - string? envInfo = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); - string activityTitle = "Chefs Submission Event Validation Error"; - string activitySubtitle = "Environment: " + envInfo; - string teamsChannel = _configuration["Notifications:TeamsNotificationsWebhook"] ?? ""; - TeamsNotificationService teamsNotificationService = new(); - teamsNotificationService.AddFact(factName, factValue); - await teamsNotificationService.PostFactsToTeamsAsync(teamsChannel, activityTitle, activitySubtitle); - } - private async Task StoreChefsFieldMappingAsync(EventSubscriptionDto eventSubscriptionDto, ApplicationForm applicationForm, JToken token) { Guid formVersionId = Guid.Parse(token.ToString()); - var formData = await _formsApiService.GetFormDataAsync(eventSubscriptionDto.FormId.ToString(), formVersionId.ToString()) ?? throw new InvalidFormDataSubmissionException(); + var formData = await formsApiService.GetFormDataAsync(eventSubscriptionDto.FormId.ToString(), formVersionId.ToString()) ?? throw new InvalidFormDataSubmissionException(); string chefsFormId = eventSubscriptionDto.FormId.ToString(); string chefsFormVersionId = eventSubscriptionDto.FormVersion.ToString(); - await _applicationFormVersionAppService.UpdateOrCreateApplicationFormVersion(chefsFormId, chefsFormVersionId, applicationForm.Id, formData); + await applicationFormVersionAppService.UpdateOrCreateApplicationFormVersion(chefsFormId, chefsFormVersionId, applicationForm.Id, formData); } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs index 8e87a2d9d..b3f80eab7 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs @@ -1,82 +1,148 @@ using Newtonsoft.Json; -using RestSharp; -using RestSharp.Authenticators; +using Newtonsoft.Json.Linq; using System; using System.Linq; +using System.Net.Http; using System.Threading.Tasks; using Unity.GrantManager.Applications; -using Unity.GrantManager.Integration.Chefs; using Unity.GrantManager.Integrations.Exceptions; -using Unity.GrantManager.Integrations.Http; using Volo.Abp; using Volo.Abp.Security.Encryption; +using Microsoft.Extensions.Logging; +using Volo.Abp.DependencyInjection; +using Unity.Modules.Shared.Http; +using Microsoft.EntityFrameworkCore; namespace Unity.GrantManager.Integrations.Chefs { - [IntegrationService] + [IntegrationService] + [ExposeServices(typeof(IFormsApiService))] public class FormsApiService : GrantManagerAppService, IFormsApiService { + private readonly IEndpointManagementAppService _endpointManagementAppService; private readonly IApplicationFormRepository _applicationFormRepository; private readonly IStringEncryptionService _stringEncryptionService; private readonly IResilientHttpRequest _resilientRestClient; + private readonly ILogger _logger; - public FormsApiService(IApplicationFormRepository applicationFormRepository, + private string? _chefsApiBaseUrl; + + public FormsApiService( + IEndpointManagementAppService endpointManagementAppService, + IApplicationFormRepository applicationFormRepository, IStringEncryptionService stringEncryptionService, - IResilientHttpRequest resilientRestClient) + IResilientHttpRequest resilientRestClient, + ILogger logger) { + _endpointManagementAppService = endpointManagementAppService; _applicationFormRepository = applicationFormRepository; _stringEncryptionService = stringEncryptionService; _resilientRestClient = resilientRestClient; + _logger = logger; + } + + private async Task GetChefsApiBaseUrlAsync() + { + if (_chefsApiBaseUrl == null) + { + _chefsApiBaseUrl = await _endpointManagementAppService.GetChefsApiBaseUrlAsync(); + } + return _chefsApiBaseUrl; } - public async Task GetFormDataAsync(string chefsFormId, string chefsFormVersionId) + public async Task GetFormDataAsync(string chefsFormId, string chefsFormVersionId) { - var applicationForm = (await _applicationFormRepository - .GetQueryableAsync()) + var applicationForm = await (await _applicationFormRepository.GetQueryableAsync()) .Where(s => s.ChefsApplicationFormGuid == chefsFormId) .OrderBy(s => s.CreationTime) - .FirstOrDefault(); - - if (applicationForm == null) return null; - - var response = await _resilientRestClient - .HttpAsync(Method.Get, $"/forms/{chefsFormId}/versions/{chefsFormVersionId}", - null, - null, - new HttpBasicAuthenticator(applicationForm.ChefsApplicationFormGuid!, _stringEncryptionService.Decrypt(applicationForm.ApiKey!) ?? string.Empty)); + .FirstOrDefaultAsync(); - if (response != null - && response.Content != null - && response.IsSuccessStatusCode) + if (applicationForm == null) { - string content = response.Content; - return JsonConvert.DeserializeObject(content)!; + _logger.LogWarning("No application form found for FormId: {FormId}", chefsFormId); + return null; } - else + + string chefsApi = await GetChefsApiBaseUrlAsync(); + string url = $"{chefsApi}/forms/{chefsFormId}/versions/{chefsFormVersionId}"; + + var response = await GetRequestAsync(url, applicationForm.ChefsApplicationFormGuid!, applicationForm.ApiKey!); + return await ParseJsonResponseAsync(response); + } + + public async Task GetForm(Guid? formId, string chefsApplicationFormGuid, string encryptedApiKey) + { + if (string.IsNullOrWhiteSpace(chefsApplicationFormGuid) || string.IsNullOrWhiteSpace(encryptedApiKey)) { - throw new IntegrationServiceException($"Error with integrating with request resource"); + throw new ArgumentException("Form GUID and API Key must be provided"); } + + string chefsApi = await GetChefsApiBaseUrlAsync(); + string url = $"{chefsApi}/forms/{formId}"; + + var response = await GetRequestAsync(url, chefsApplicationFormGuid, encryptedApiKey); + return await ParseJsonResponseAsync(response) ?? new JObject(); } - public async Task GetForm(Guid? formId, string chefsApplicationFormGuid, string encryptedApiKey) + public async Task GetSubmissionDataAsync(Guid chefsFormId, Guid submissionId) { - var response = await _resilientRestClient - .HttpAsync(Method.Get, $"/forms/{formId}", - null, - null, - new HttpBasicAuthenticator(chefsApplicationFormGuid!, _stringEncryptionService.Decrypt(encryptedApiKey!) ?? string.Empty)); - - if (response != null - && response.Content != null - && response.IsSuccessStatusCode) + var applicationForm = await (await _applicationFormRepository.GetQueryableAsync()) + .Where(s => s.ChefsApplicationFormGuid == chefsFormId.ToString()) + .OrderBy(s => s.CreationTime) + .FirstOrDefaultAsync(); + + if (applicationForm == null) { - return response.Content; + _logger.LogWarning("No application form found for SubmissionId: {SubmissionId}", submissionId); + return null; } - else + + string chefsApi = await GetChefsApiBaseUrlAsync(); + string url = $"{chefsApi}/submissions/{submissionId}"; + + var response = await GetRequestAsync(url, applicationForm.ChefsApplicationFormGuid!, applicationForm.ApiKey!); + return await ParseJsonResponseAsync(response); + } + + private async Task GetRequestAsync(string url, string chefsApplicationFormGuid, string encryptedApiKey) + { + if (string.IsNullOrWhiteSpace(encryptedApiKey)) + throw new ArgumentException("API key is missing or empty"); + + var decryptedApiKey = _stringEncryptionService.Decrypt(encryptedApiKey) ?? string.Empty; + _logger.LogInformation( + "Sending GET request to {Url} using basic auth with FormGuid: {FormGuid}", + url, + chefsApplicationFormGuid + ); + + var response = await _resilientRestClient.ExecuteRequestAsync( + HttpMethod.Get, + url, + null, + null, + basicAuth: (chefsApplicationFormGuid, decryptedApiKey) + ); + + if (!response.IsSuccessStatusCode) { - throw new IntegrationServiceException($"Error with integrating with request resource"); + var content = await response.Content.ReadAsStringAsync(); + _logger.LogError( + "Request to {Url} failed with status {StatusCode} ({Reason}). Response: {Content}", + url, + response.StatusCode, + response.ReasonPhrase, + content + ); } + + return response; + } + + private static async Task ParseJsonResponseAsync(HttpResponseMessage response) + { + var content = await response.Content.ReadAsStringAsync(); + return string.IsNullOrWhiteSpace(content) ? null : JsonConvert.DeserializeObject(content); } } } - diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/SubmissionsApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/SubmissionsApiService.cs deleted file mode 100644 index fba7ad473..000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/SubmissionsApiService.cs +++ /dev/null @@ -1,62 +0,0 @@ -using Newtonsoft.Json; -using RestSharp; -using RestSharp.Authenticators; -using System; -using System.Linq; -using System.Threading.Tasks; -using Unity.GrantManager.Applications; -using Unity.GrantManager.Integration.Chefs; -using Unity.GrantManager.Integrations.Exceptions; -using Unity.GrantManager.Integrations.Http; -using Volo.Abp; -using Volo.Abp.Security.Encryption; - -namespace Unity.GrantManager.Integrations.Chefs -{ - [IntegrationService] - public class SubmissionsApiService : GrantManagerAppService, ISubmissionsApiService - { - private readonly IResilientHttpRequest _resilientRestClient; - private readonly IApplicationFormRepository _applicationFormRepository; - private readonly IStringEncryptionService _stringEncryptionService; - - public SubmissionsApiService(IResilientHttpRequest resilientRestClient, - IApplicationFormRepository applicationFormRepository, - IStringEncryptionService stringEncryptionService) - { - _resilientRestClient = resilientRestClient; - _applicationFormRepository = applicationFormRepository; - _stringEncryptionService = stringEncryptionService; - } - - public async Task GetSubmissionDataAsync(Guid chefsFormId, Guid submissionId) - { - var applicationForm = (await _applicationFormRepository - .GetQueryableAsync()) - .Where(s => s.ChefsApplicationFormGuid == chefsFormId.ToString()) - .OrderBy(s => s.CreationTime) - .FirstOrDefault(); - - if (applicationForm == null) return null; - - var response = await _resilientRestClient - .HttpAsync(Method.Get, $"/submissions/{submissionId}", - null, - null, - new HttpBasicAuthenticator(applicationForm.ChefsApplicationFormGuid!, _stringEncryptionService.Decrypt(applicationForm.ApiKey!) ?? string.Empty)); - - if (response != null - && response.Content != null - && response.IsSuccessStatusCode) - { - string content = response.Content; - return JsonConvert.DeserializeObject(content)!; - } - else - { - throw new IntegrationServiceException($"Error with integrating with request resource"); - } - } - } -} - diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Css/CssApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Css/CssApiService.cs index 3a8aa178d..80b456049 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Css/CssApiService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Css/CssApiService.cs @@ -1,135 +1,116 @@ -using RestSharp; -using RestSharp.Authenticators; -using System.Collections.Generic; -using System.Net; -using System.Threading.Tasks; -using Unity.GrantManager.Integrations.Http; -using Volo.Abp.Application.Services; -using System.Text.Json; -using System; -using Volo.Abp.Caching; -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using System.Collections.Generic; using System.Linq; +using System; +using Unity.GrantManager.Integrations.Css; using Volo.Abp; +using Volo.Abp.Application.Services; using Volo.Abp.DependencyInjection; -using Unity.GrantManager.Integration.Css; +using Volo.Abp.Caching; +using Unity.Modules.Shared.Http; namespace Unity.GrantManager.Integrations.Sso { [IntegrationService] [ExposeServices(typeof(CssApiService), typeof(ICssUsersApiService))] - public class CssApiService(IResilientHttpRequest resilientHttpRequest, - IDistributedCache accessTokenCache, - IOptions cssApiOptions, - RestClient restClient) : ApplicationService, ICssUsersApiService + public class CssApiService : ApplicationService, ICssUsersApiService { + private readonly IResilientHttpRequest _resilientHttpRequest; + private readonly IDistributedCache _accessTokenCache; + private readonly CssApiOptions _cssApiOptions; private const string CSS_API_KEY = "CssApiKey"; - public async Task FindUserAsync(string directory, string guid) + public CssApiService( + IResilientHttpRequest resilientHttpRequest, + IDistributedCache accessTokenCache, + IOptions cssApiOptions) { - var paramDictionary = new Dictionary(); - - if (guid != null) - { - paramDictionary.Add(nameof(guid), guid); - } + _resilientHttpRequest = resilientHttpRequest; + _accessTokenCache = accessTokenCache; + _cssApiOptions = cssApiOptions.Value; + } - return await SearchSsoAsync(directory, paramDictionary); + public async Task FindUserAsync(string directory, string guid) + { + var parameters = new Dictionary { { nameof(guid), guid } }; + return await SearchSsoAsync(directory, parameters); } public async Task SearchUsersAsync(string directory, string? firstName = null, string? lastName = null) - { - var paramDictionary = new Dictionary(); + { + var parameters = new Dictionary(); - if (firstName != null && firstName.Length >= 2) - { - paramDictionary.Add(nameof(firstName), firstName); - } + if (!string.IsNullOrWhiteSpace(firstName) && firstName.Length >= 2) + parameters.Add(nameof(firstName), firstName); - if (lastName != null && lastName.Length >= 2) - { - paramDictionary.Add(nameof(lastName), lastName); - } + if (!string.IsNullOrWhiteSpace(lastName) && lastName.Length >= 2) + parameters.Add(nameof(lastName), lastName); - return await SearchSsoAsync(directory, paramDictionary); + return await SearchSsoAsync(directory, parameters); } - private async Task SearchSsoAsync(string directory, Dictionary paramDictionary) + private async Task SearchSsoAsync(string directory, Dictionary parameters) { var tokenResponse = await GetAccessTokenAsync(); + var baseUrl = $"{_cssApiOptions.Url}/{_cssApiOptions.Env}/{directory}/users"; + var url = BuildUrlWithQuery(baseUrl, parameters); + var response = await _resilientHttpRequest.ExecuteRequestAsync(HttpMethod.Get, url, null, tokenResponse.AccessToken); - var resource = BuildUrlWithQueryStringUsingUriBuilder($"{cssApiOptions.Value.Url}/{cssApiOptions.Value.Env}/{directory}/users?", paramDictionary); - - var authHeaders = new Dictionary + if (response != null && response.IsSuccessStatusCode && response.Content != null) { - { "Authorization", $"Bearer {tokenResponse.AccessToken}" } - }; - - var response = await resilientHttpRequest.HttpAsync(Method.Get, resource, authHeaders); - - if (response != null - && response.Content != null - && response.IsSuccessStatusCode) - { - string content = response.Content; - var result = JsonSerializer.Deserialize(content) ?? throw new UserFriendlyException("SearchSsoAsync -> Could not Deserialize"); - + var json = await response.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize(json) ?? throw new UserFriendlyException("Could not deserialize user search result."); result.Success = true; return result; } - else + + return new UserSearchResult { - return new UserSearchResult() { Error = "", Success = false, Data = Array.Empty() }; - } + Success = false, + Error = "Failed to search users", + Data = Array.Empty() + }; } - private static string BuildUrlWithQueryStringUsingUriBuilder(string basePath, Dictionary queryParams) + private static string BuildUrlWithQuery(string basePath, Dictionary queryParams) { - var uriBuilder = new UriBuilder(basePath) - { - Query = string.Join("&", queryParams.Select(kvp => $"{kvp.Key}={kvp.Value}")) - }; - return uriBuilder.Uri.AbsoluteUri; + var query = string.Join("&", queryParams.Select(kv => $"{kv.Key}={Uri.EscapeDataString(kv.Value)}")); + return string.IsNullOrWhiteSpace(query) ? basePath : $"{basePath}?{query}"; } private async Task GetAccessTokenAsync() { - var cachedTokenResponse = await accessTokenCache.GetAsync(CSS_API_KEY); + var cachedToken = await _accessTokenCache.GetAsync(CSS_API_KEY); + if (cachedToken != null) + return cachedToken; - if (cachedTokenResponse != null) - { - return cachedTokenResponse; - } + var client = new HttpClient(); + var request = new HttpRequestMessage(HttpMethod.Post, _cssApiOptions.TokenUrl); - var grantType = "client_credentials"; + var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{_cssApiOptions.ClientId}:{_cssApiOptions.ClientSecret}")); + request.Headers.Authorization = new AuthenticationHeaderValue("Basic", credentials); + request.Content = new StringContent("grant_type=client_credentials", Encoding.UTF8, "application/x-www-form-urlencoded"); - var request = new RestRequest($"{cssApiOptions.Value.TokenUrl}") - { - Authenticator = new HttpBasicAuthenticator(cssApiOptions.Value.ClientId, cssApiOptions.Value.ClientSecret) - }; - request.AddHeader("content-type", "application/x-www-form-urlencoded"); - request.AddParameter("application/x-www-form-urlencoded", $"grant_type={grantType}", ParameterType.RequestBody); - - var response = await restClient.ExecuteAsync(request, Method.Post); + var response = await client.SendAsync(request); - if (response.Content == null) + if (!response.IsSuccessStatusCode || response.Content == null) { - throw new UserFriendlyException($"Error fetching Css API token - content empty {response.StatusCode} {response.ErrorMessage}"); + var errorContent = response.Content != null ? await response.Content.ReadAsStringAsync() : "No content"; + Logger.LogError("Failed to fetch CSS API token. Status: {StatusCode}, Content: {ErrorContent}", response.StatusCode, errorContent); + throw new UserFriendlyException($"Error fetching token: {response.StatusCode}"); } - if (response.StatusCode != HttpStatusCode.OK) - { - Logger.LogError(response.ErrorException, "Error fetching Css API token {StatusCode} {ErrorMessage} {ErrorException}", response.StatusCode, response.ErrorMessage, response.ErrorException); - - if (response.StatusCode == HttpStatusCode.Unauthorized) - { - throw new UnauthorizedAccessException(response.ErrorMessage); - } - } + var content = await response.Content.ReadAsStringAsync(); + var tokenResponse = JsonSerializer.Deserialize(content) ?? throw new UserFriendlyException("Could not parse token response."); - var tokenResponse = JsonSerializer.Deserialize(response.Content) ?? throw new UserFriendlyException($"Error deserializing token response {response.StatusCode} {response.ErrorMessage}"); - await accessTokenCache.SetAsync(CSS_API_KEY, tokenResponse, new Microsoft.Extensions.Caching.Distributed.DistributedCacheEntryOptions() + await _accessTokenCache.SetAsync(CSS_API_KEY, tokenResponse, new DistributedCacheEntryOptions { AbsoluteExpiration = DateTimeOffset.UtcNow.AddSeconds(tokenResponse.ExpiresIn) }); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs new file mode 100644 index 000000000..bc46be57c --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading.Tasks; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; + +namespace Unity.GrantManager.Integrations.Endpoints +{ + [ExposeServices(typeof(EndpointManagementAppService), typeof(IEndpointManagementAppService))] + public class EndpointManagementAppService(IRepository repository) : + CrudAppService< + DynamicUrl, + DynamicUrlDto, + Guid, + PagedAndSortedResultRequestDto, + CreateUpdateDynamicUrlDto>(repository), + IEndpointManagementAppService + { + public async Task GetChefsApiBaseUrlAsync() + { + var url = await GetUrlByKeyNameAsync(DynamicUrlKeyNames.INTAKE_API_BASE); + if (string.IsNullOrWhiteSpace(url)) throw new Exception("CHEFS API base URL not configured."); + return url; + } + + + public async Task GetUrlByKeyNameAsync(string keyName) + { + var dynamicUrl = await Repository.FirstOrDefaultAsync(x => x.KeyName == keyName); + return dynamicUrl?.Url ?? string.Empty; + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Geocoder/ResultMapper.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Geocoder/ResultMapper.cs index 29b5b3614..dcb09459c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Geocoder/ResultMapper.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Geocoder/ResultMapper.cs @@ -1,6 +1,4 @@ -using Unity.GrantManager.Integration.Geocoder; - -namespace Unity.GrantManager.Integrations.Geocoder +namespace Unity.GrantManager.Integrations.Geocoder { internal static class ResultMapper { @@ -50,4 +48,4 @@ internal static class ResultMapper }; } } -} +} \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Http/IResilientHttpRequest.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Http/IResilientHttpRequest.cs deleted file mode 100644 index 899311a98..000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Http/IResilientHttpRequest.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Polly.CircuitBreaker; -using Polly.Retry; -using RestSharp; -using RestSharp.Authenticators; -using System.Collections.Generic; -using System.Threading.Tasks; -using Volo.Abp.Application.Services; - -namespace Unity.GrantManager.Integrations.Http -{ - public interface IResilientHttpRequest : IApplicationService - { - Task HttpAsync(Method httpVerb, string resource, Dictionary? headers = null, object? requestObject = null, IAuthenticator? authenticator = null); - Task HttpAsync(Method httpVerb, string resource, AsyncRetryPolicy retryPolicy, Dictionary? headers = null, object? requestObject = null, IAuthenticator? authenticator = null); - Task HttpAsync(Method httpVerb, string resource, AsyncRetryPolicy retryPolicy, AsyncCircuitBreakerPolicy circuitBreakerPolicy, Dictionary? headers = null, object? requestObject = null, IAuthenticator? authenticator = null); - } -} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Http/ResilientHttpRequest.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Http/ResilientHttpRequest.cs deleted file mode 100644 index 8e281ee60..000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Http/ResilientHttpRequest.cs +++ /dev/null @@ -1,108 +0,0 @@ -using Microsoft.Extensions.Logging; -using Polly; -using Polly.CircuitBreaker; -using Polly.Retry; -using RestSharp; -using RestSharp.Authenticators; -using System; -using System.Collections.Generic; -using System.Net; -using System.Threading.Tasks; -using Volo.Abp; - -namespace Unity.GrantManager.Integrations.Http -{ - [IntegrationService] - public class ResilientHttpRequest : GrantManagerAppService, IResilientHttpRequest - { - private readonly RestClient _restClient; - private static int _maxRetryAttempts = 3; - private static TimeSpan _pauseBetweenFailures = TimeSpan.FromSeconds(10); - - public ResilientHttpRequest(RestClient restClient) - { - _restClient = restClient; - } - - public static void SetMaxRetryAttemptsAndPauseBetweenFailures(int maxRetryAttempts, TimeSpan pauseBetweenFailures) - { - _maxRetryAttempts = maxRetryAttempts; - _pauseBetweenFailures = pauseBetweenFailures; - } - - public async Task HttpAsync(Method httpVerb, string resource, Dictionary? headers = null, object? requestObject = null, IAuthenticator? authenticator = null) - { - return await ExecuteRequestAsync(httpVerb, resource, headers, requestObject, null, null, authenticator); - } - - public async Task HttpAsync(Method httpVerb, string resource, AsyncRetryPolicy retryPolicy, Dictionary? headers = null, object? requestObject = null, IAuthenticator? authenticator = null) - { - return await ExecuteRequestAsync(httpVerb, resource, headers, requestObject, retryPolicy, null, authenticator); - } - - public async Task HttpAsync(Method httpVerb, string resource, AsyncRetryPolicy retryPolicy, AsyncCircuitBreakerPolicy circuitBreakerPolicy, Dictionary? headers = null, object? requestObject = null, IAuthenticator? authenticator = null) - { - return await ExecuteRequestAsync(httpVerb, resource, headers, requestObject, retryPolicy, circuitBreakerPolicy, authenticator); - } - - private async Task ExecuteRequestAsync(Method httpVerb, string resource, Dictionary? headers, object? requestObject, AsyncRetryPolicy? retryPolicy = null, AsyncCircuitBreakerPolicy? circuitBreakerPolicy = null, IAuthenticator? authenticator = null) - { - RestResponse? restResponse; - - try - { - var restRequest = new RestRequest(resource, httpVerb); - if (authenticator != null) - { - restRequest.Authenticator = authenticator; - } - restRequest.AddHeader("cache-control", "no-cache"); - if (headers != null && headers.Count > 0) - foreach (var header in headers) - restRequest.AddHeader(header.Key, header.Value); - - if (httpVerb != Method.Get && requestObject != null) - { - restRequest.RequestFormat = DataFormat.Json; - restRequest.AddJsonBody(requestObject); - } - restResponse = await RestResponseWithPolicyAsync(restRequest, retryPolicy, circuitBreakerPolicy); - } - catch (Exception ex) - { - restResponse = new RestResponse - { - Content = ex.Message, - ErrorMessage = ex.Message, - ResponseStatus = ResponseStatus.TimedOut, - StatusCode = HttpStatusCode.ServiceUnavailable - }; - } - - return await Task.FromResult(restResponse); - } - - private async Task RestResponseWithPolicyAsync(RestRequest restRequest, AsyncRetryPolicy? retryPolicy = null, AsyncCircuitBreakerPolicy? circuitBreakerPolicy = null) - { - retryPolicy ??= Policy - .HandleResult(x => !x.IsSuccessful) - .WaitAndRetryAsync(_maxRetryAttempts, x => _pauseBetweenFailures, async (iRestResponse, timeSpan, retryCount, context) => - { - await Task.Run(() => Logger.LogError("The request failed. HttpStatusCode={statusCode}. Waiting {timeSpan} seconds before retry. Number attempt {retryCount}. Uri={responseUri}; RequestResponse={responseContent}", iRestResponse.Result.StatusCode, timeSpan, retryCount, iRestResponse.Result.ResponseUri, iRestResponse.Result.Content)); - }); - - circuitBreakerPolicy ??= Policy - .HandleResult(x => x.StatusCode == HttpStatusCode.ServiceUnavailable || x.StatusCode == HttpStatusCode.TooManyRequests) - .CircuitBreakerAsync(1, TimeSpan.FromSeconds(30), onBreak: async (iRestResponse, timespan, context) => - { - await Task.Run(() => Logger.LogError("Circuit went into a fault state. Reason: {resultContent}", iRestResponse.Result.Content)); - }, - onReset: async (context) => - { - await Task.Run(() => Logger.LogError($"Circuit left the fault state.")); - }); - - return await retryPolicy.WrapAsync(circuitBreakerPolicy).ExecuteAsync(async () => await _restClient.ExecuteAsync(restRequest)); - } - } -} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/OrgBook/OrgBookService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/OrgBook/OrgBookService.cs index 75d05d31c..d46e9aad2 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/OrgBook/OrgBookService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/OrgBook/OrgBookService.cs @@ -1,12 +1,11 @@ using Newtonsoft.Json; -using RestSharp; using System.Text.Json; using System.Threading.Tasks; -using Unity.GrantManager.Integration.Orgbook; using Unity.GrantManager.Integrations.Exceptions; -using Unity.GrantManager.Integrations.Http; using Volo.Abp.Application.Services; using Volo.Abp.DependencyInjection; +using System.Net.Http; +using Unity.Modules.Shared.Http; namespace Unity.GrantManager.Integrations.Orgbook { @@ -26,11 +25,11 @@ public OrgBookService(IResilientHttpRequest resilientRestClient) { public async Task GetOrgBookQueryAsync(string orgBookQuery) { var response = await _resilientRestClient - .HttpAsync(Method.Get, $"{orgbook_base_api}/v4/search/topic?q={orgBookQuery}&{orgbook_query_match}"); + .ExecuteRequestAsync(HttpMethod.Get, $"{orgbook_base_api}/v4/search/topic?q={orgBookQuery}&{orgbook_query_match}", null, null, null); if (response != null && response.Content != null) { - string content = response.Content; + var content = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject(content)!; } else @@ -47,12 +46,12 @@ public async Task GetOrgBookAutocompleteQueryAsync(string? orgBook } var response = await _resilientRestClient - .HttpAsync(Method.Get, $"{orgbook_base_api}/v3/search/autocomplete?q={orgBookQuery}&revoked=false&inactive="); + .ExecuteRequestAsync(HttpMethod.Get, $"{orgbook_base_api}/v3/search/autocomplete?q={orgBookQuery}&revoked=false&inactive=", null, null, null); if (response != null && response.Content != null) { - - return JsonDocument.Parse(response.Content); + var content = await response.Content.ReadAsStringAsync(); + return JsonDocument.Parse(content); } else { @@ -68,12 +67,12 @@ public async Task GetOrgBookDetailsQueryAsync(string? orgBookId) } var response = await _resilientRestClient - .HttpAsync(Method.Get, $"{orgbook_base_api}/v2/topic/ident/registration.registries.ca/{orgBookId}/formatted"); + .ExecuteRequestAsync(HttpMethod.Get, $"{orgbook_base_api}/v2/topic/ident/registration.registries.ca/{orgBookId}/formatted", null, null, null); if (response != null && response.Content != null) { - - return JsonDocument.Parse(response.Content); + var content = await response.Content.ReadAsStringAsync(); + return JsonDocument.Parse(content); } else { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Emails/EmailAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Norifications/EmailAppService.cs similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/Emails/EmailAppService.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application/Norifications/EmailAppService.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Norifications/NotificationsAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Norifications/NotificationsAppService.cs new file mode 100644 index 000000000..6fecf772b --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Norifications/NotificationsAppService.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Unity.GrantManager.Applications; +using Unity.GrantManager.Integrations; +using Unity.Notifications.TeamsNotifications; +using Volo.Abp; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; + +namespace Unity.GrantManager.Notifications +{ + // This class is responsible for first lookup up the Teams channel URL from the database and then posting notifications to the Teams Service. + [Dependency(ReplaceServices = true)] + [ExposeServices(typeof(NotificationsAppService), typeof(INotificationsAppService))] + public class NotificationsAppService : INotificationsAppService, ITransientDependency + { + private readonly IDynamicUrlRepository _dynamicUrlRepository; + private readonly TeamsNotificationService _teamsNotificationService; + + public NotificationsAppService(IDynamicUrlRepository dynamicUrlRepository) + { + _dynamicUrlRepository = dynamicUrlRepository; + _teamsNotificationService = new TeamsNotificationService(); + } + + public async Task InitializeTeamsChannelAsync(string keyName) + { + DynamicUrl? teamsChannel = await _dynamicUrlRepository.FirstOrDefaultAsync(q => q.KeyName == keyName); + if (teamsChannel?.Url == null) + { + return ""; + } + return teamsChannel.Url; + } + + [RemoteService(false)] + public async Task NotifyChefsEventToTeamsAsync(string factName, string factValue) + { + string teamsChannel = await InitializeTeamsChannelAsync(TeamsNotificationService.TEAMS_NOTIFICATION); + if (teamsChannel.IsNullOrEmpty()) + { + return; + } + + string? envInfo = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); + string activityTitle = "Chefs Submission Event Validation Error"; + string activitySubtitle = "Environment: " + envInfo; + _teamsNotificationService.AddFact(factName, factValue); + await _teamsNotificationService.PostFactsToTeamsAsync(teamsChannel, activityTitle, activitySubtitle); + } + + public async Task PostToTeamsAsync(string activityTitle, string activitySubtitle, List facts) + { + string teamsChannel = await InitializeTeamsChannelAsync(TeamsNotificationService.TEAMS_NOTIFICATION); + if (teamsChannel.IsNullOrEmpty()) + { + return; + } + + string messageCard = TeamsNotificationService.InitializeMessageCard(activityTitle, activitySubtitle, facts); + await TeamsNotificationService.PostToTeamsChannelAsync(teamsChannel, messageCard); + } + + public async Task PostToTeamsAsync(string activityTitle, string activitySubtitle) + { + string teamsChannel = await InitializeTeamsChannelAsync(TeamsNotificationService.TEAMS_NOTIFICATION); + if (teamsChannel.IsNullOrEmpty()) + { + return; + } + List facts = new() { }; + string messageCard = TeamsNotificationService.InitializeMessageCard(activityTitle, activitySubtitle, facts); + await TeamsNotificationService.PostToTeamsChannelAsync(teamsChannel, messageCard); + } + + public async Task PostChefsEventToTeamsAsync(string subscriptionEvent, dynamic form, dynamic chefsFormVersion) + { + string teamsChannel = await InitializeTeamsChannelAsync(TeamsNotificationService.TEAMS_NOTIFICATION_1); + if (teamsChannel.IsNullOrEmpty()) + { + return; + } + await TeamsNotificationService.PostChefsEventToTeamsAsync(teamsChannel, subscriptionEvent, form, chefsFormVersion); + } + } +} \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/NotificationApiUrlHolder.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/NotificationApiUrlHolder.cs new file mode 100644 index 000000000..6dd0e861c --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/NotificationApiUrlHolder.cs @@ -0,0 +1,6 @@ + +namespace Unity.GrantManager; +public class NotificationApiUrlHolder +{ + public string? BaseUrl { get; set; } +} \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json index a8763b123..d8918405d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json @@ -15,6 +15,7 @@ "Menu:Intakes": "Intakes", "Menu:ApplicationForms": "Forms", "Menu:TenantManagement": "Tenants", + "Menu:EndpointManagement": "Endpoints", "Welcome": "Welcome", "LongWelcomeMessage": "Welcome to Unity Grant Manager", diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IDynamicUrlRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IDynamicUrlRepository.cs new file mode 100644 index 000000000..6d8aa1d64 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IDynamicUrlRepository.cs @@ -0,0 +1,10 @@ +using System; +using Unity.GrantManager.Integrations; +using Volo.Abp.Domain.Repositories; + +namespace Unity.GrantManager.Applications; + +public interface IDynamicUrlRepository : IRepository +{ + +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/GrantManagerDataSeederContributor.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/GrantManagerDataSeederContributor.cs index 75c425f5d..9fe38d74e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/GrantManagerDataSeederContributor.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/GrantManagerDataSeederContributor.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Threading.Tasks; using Unity.GrantManager.Applications; using Unity.GrantManager.GrantApplications; @@ -7,138 +8,63 @@ namespace Unity.GrantManager; -public class GrantManagerDataSeederContributor : IDataSeedContributor, ITransientDependency +public class GrantManagerDataSeederContributor( + IApplicationStatusRepository applicationStatusRepository) : IDataSeeder, ITransientDependency { - private readonly IApplicationStatusRepository _applicationStatusRepository; - - public GrantManagerDataSeederContributor(IApplicationStatusRepository applicationStatusRepository) + public static class GrantApplicationStates { - _applicationStatusRepository = applicationStatusRepository; + public const string SUBMITTED = "Submitted"; + public const string ASSIGNED = "Assigned"; + public const string WITHDRAWN = "Withdrawn"; + public const string CLOSED = "Closed"; + public const string UNDER_REVIEW = "Under Review"; + public const string UNDER_INITIAL_REVIEW = "Under Initial Review"; + public const string INITITAL_REVIEW_COMPLETED = "Initial Review Completed"; + public const string UNDER_ASSESSMENT = "Under Assessment"; + public const string ASSESSMENT_COMPLETED = "Assessment Completed"; + public const string GRANT_APPROVED = "Grant Approved"; + public const string DECLINED = "Declined"; + public const string DEFER = "Deferred"; + public const string ON_HOLD = "On Hold"; } public async Task SeedAsync(DataSeedContext context) { - if (context.TenantId != null) // only try seed into a tenant database - { - ApplicationStatus? status1 = await _applicationStatusRepository.FirstOrDefaultAsync(s => s.StatusCode == GrantApplicationState.SUBMITTED); - status1 ??= await _applicationStatusRepository.InsertAsync( - new ApplicationStatus - { - StatusCode = GrantApplicationState.SUBMITTED, - ExternalStatus = "Submitted", - InternalStatus = "Submitted" - } - ); - - ApplicationStatus? status2 = await _applicationStatusRepository.FirstOrDefaultAsync(s => s.StatusCode == GrantApplicationState.ASSIGNED); - status2 ??= await _applicationStatusRepository.InsertAsync( - new ApplicationStatus - { - StatusCode = GrantApplicationState.ASSIGNED, - ExternalStatus = "Under Review", - InternalStatus = "Assigned" - } - ); - - ApplicationStatus? status3 = await _applicationStatusRepository.FirstOrDefaultAsync(s => s.StatusCode == GrantApplicationState.WITHDRAWN); - status3 ??= await _applicationStatusRepository.InsertAsync( - new ApplicationStatus - { - StatusCode = GrantApplicationState.WITHDRAWN, - ExternalStatus = "Withdrawn", - InternalStatus = "Withdrawn" - } - ); - - ApplicationStatus? status4 = await _applicationStatusRepository.FirstOrDefaultAsync(s => s.StatusCode == GrantApplicationState.CLOSED); - status4 ??= await _applicationStatusRepository.InsertAsync( - new ApplicationStatus - { - StatusCode = GrantApplicationState.CLOSED, - ExternalStatus = "Closed", - InternalStatus = "Closed" - } - ); - - ApplicationStatus? status5 = await _applicationStatusRepository.FirstOrDefaultAsync(s => s.StatusCode == GrantApplicationState.UNDER_INITIAL_REVIEW); - status5 ??= await _applicationStatusRepository.InsertAsync( - new ApplicationStatus - { - StatusCode = GrantApplicationState.UNDER_INITIAL_REVIEW, - ExternalStatus = "Under Review", - InternalStatus = "Under Initial Review" - } - ); - - ApplicationStatus? status6 = await _applicationStatusRepository.FirstOrDefaultAsync(s => s.StatusCode == GrantApplicationState.INITITAL_REVIEW_COMPLETED); - status6 ??= await _applicationStatusRepository.InsertAsync( - new ApplicationStatus - { - StatusCode = GrantApplicationState.INITITAL_REVIEW_COMPLETED, - ExternalStatus = "Under Review", - InternalStatus = "Initial Review Completed" - } - ); - ApplicationStatus? status7 = await _applicationStatusRepository.FirstOrDefaultAsync(s => s.StatusCode == GrantApplicationState.UNDER_ASSESSMENT); - status7 ??= await _applicationStatusRepository.InsertAsync( - new ApplicationStatus - { - StatusCode = GrantApplicationState.UNDER_ASSESSMENT, - ExternalStatus = "Under Review", - InternalStatus = "Under Assessment" - } - ); - - ApplicationStatus? status8 = await _applicationStatusRepository.FirstOrDefaultAsync(s => s.StatusCode == GrantApplicationState.ASSESSMENT_COMPLETED); - status8 ??= await _applicationStatusRepository.InsertAsync( - new ApplicationStatus - { - StatusCode = GrantApplicationState.ASSESSMENT_COMPLETED, - ExternalStatus = "Under Review", - InternalStatus = "Assessment Completed" - } - ); - - ApplicationStatus? status9 = await _applicationStatusRepository.FirstOrDefaultAsync(s => s.StatusCode == GrantApplicationState.GRANT_APPROVED); - status9 ??= await _applicationStatusRepository.InsertAsync( - new ApplicationStatus - { - StatusCode = GrantApplicationState.GRANT_APPROVED, - ExternalStatus = "Approved", - InternalStatus = "Approved" - } - ); + if (context.TenantId == null) // only seed into a tenant database + { + return; + } - ApplicationStatus? status10 = await _applicationStatusRepository.FirstOrDefaultAsync(s => s.StatusCode == GrantApplicationState.GRANT_NOT_APPROVED); - status10 ??= await _applicationStatusRepository.InsertAsync( - new ApplicationStatus - { - StatusCode = GrantApplicationState.GRANT_NOT_APPROVED, - ExternalStatus = "Declined", - InternalStatus = "Declined" - } - ); + await SeedApplicationStatusAsync(); + } - ApplicationStatus? status11 = await _applicationStatusRepository.FirstOrDefaultAsync(s => s.StatusCode == GrantApplicationState.DEFER); - status11 ??= await _applicationStatusRepository.InsertAsync( - new ApplicationStatus - { - StatusCode = GrantApplicationState.DEFER, - ExternalStatus = "Deferred", - InternalStatus = "Deferred" - } - ); + + private async Task SeedApplicationStatusAsync() + { + var statuses = new List + { + new() { StatusCode = GrantApplicationState.SUBMITTED, ExternalStatus = GrantApplicationStates.SUBMITTED, InternalStatus = GrantApplicationStates.SUBMITTED }, + new() { StatusCode = GrantApplicationState.ASSIGNED, ExternalStatus = GrantApplicationStates.UNDER_REVIEW, InternalStatus = GrantApplicationStates.ASSIGNED }, + new() { StatusCode = GrantApplicationState.WITHDRAWN, ExternalStatus = GrantApplicationStates.WITHDRAWN, InternalStatus = GrantApplicationStates.WITHDRAWN }, + new() { StatusCode = GrantApplicationState.CLOSED, ExternalStatus = GrantApplicationStates.CLOSED, InternalStatus = GrantApplicationStates.CLOSED }, + new() { StatusCode = GrantApplicationState.UNDER_INITIAL_REVIEW, ExternalStatus = GrantApplicationStates.UNDER_REVIEW, InternalStatus = GrantApplicationStates.UNDER_INITIAL_REVIEW }, + new() { StatusCode = GrantApplicationState.INITITAL_REVIEW_COMPLETED, ExternalStatus = GrantApplicationStates.UNDER_REVIEW, InternalStatus = GrantApplicationStates.INITITAL_REVIEW_COMPLETED }, + new() { StatusCode = GrantApplicationState.UNDER_ASSESSMENT, ExternalStatus = GrantApplicationStates.UNDER_REVIEW, InternalStatus = GrantApplicationStates.UNDER_ASSESSMENT }, + new() { StatusCode = GrantApplicationState.ASSESSMENT_COMPLETED, ExternalStatus = GrantApplicationStates.UNDER_REVIEW, InternalStatus = GrantApplicationStates.ASSESSMENT_COMPLETED }, + new() { StatusCode = GrantApplicationState.GRANT_APPROVED, ExternalStatus = GrantApplicationStates.GRANT_APPROVED, InternalStatus = GrantApplicationStates.GRANT_APPROVED }, + new() { StatusCode = GrantApplicationState.GRANT_NOT_APPROVED, ExternalStatus = GrantApplicationStates.DECLINED, InternalStatus = GrantApplicationStates.DECLINED }, + new() { StatusCode = GrantApplicationState.DEFER, ExternalStatus = GrantApplicationStates.DEFER, InternalStatus = GrantApplicationStates.DEFER }, + new() { StatusCode = GrantApplicationState.ON_HOLD, ExternalStatus = GrantApplicationStates.ON_HOLD, InternalStatus = GrantApplicationStates.ON_HOLD }, + }; - ApplicationStatus? status12 = await _applicationStatusRepository.FirstOrDefaultAsync(s => s.StatusCode == GrantApplicationState.ON_HOLD); - status12 ??= await _applicationStatusRepository.InsertAsync( - new ApplicationStatus - { - StatusCode = GrantApplicationState.ON_HOLD, - ExternalStatus = "On Hold", - InternalStatus = "On Hold" - } - ); + foreach (var status in statuses) + { + var existing = await applicationStatusRepository.FirstOrDefaultAsync(s => s.StatusCode == status.StatusCode); + if (existing == null) + { + await applicationStatusRepository.InsertAsync(status); + } } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrl.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrl.cs new file mode 100644 index 000000000..3efcb76eb --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrl.cs @@ -0,0 +1,11 @@ +using System; +using Volo.Abp.Domain.Entities.Auditing; + +namespace Unity.GrantManager.Integrations; + +public class DynamicUrl : AuditedAggregateRoot +{ + public string KeyName { get; set; } = string.Empty; + public string Url { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlDataSeeder.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlDataSeeder.cs new file mode 100644 index 000000000..998de130e --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlDataSeeder.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Unity.GrantManager.Applications; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; + +namespace Unity.GrantManager.Integrations +{ + [Dependency(ReplaceServices = true)] + [ExposeServices(typeof(DynamicUrlDataSeeder), typeof(IDataSeedContributor))] + public class DynamicUrlDataSeeder(IDynamicUrlRepository DynamicUrlRepository) : IDataSeedContributor, ITransientDependency + { + + public async Task SeedAsync(DataSeedContext context) + { + await SeedDynamicUrlAsync(); + } + + public static class DynamicUrls + { + public const string PROTOCOL = "https://"; + public const string CHEFS_PROD_URL = $"{PROTOCOL}submit.digital.gov.bc.ca/app/api/v1"; + public const string CAS_PROD_URL = ""; // Not entered for security reasons + public const string CHES_PROD_URL = $"{PROTOCOL}ches.api.gov.bc.ca/api/v1"; + public const string CHES_PROD_AUTH = $"{PROTOCOL}loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token"; + } + + private async Task SeedDynamicUrlAsync() + { + int messageIndex = 0; + int webhookIndex = 0; + var dynamicUrls = new List + { + new() { KeyName = DynamicUrlKeyNames.PAYMENT_API_BASE, Url = DynamicUrls.CAS_PROD_URL, Description = "BC Corporate Accounting Services API" }, + new() { KeyName = DynamicUrlKeyNames.INTAKE_API_BASE, Url = DynamicUrls.CHEFS_PROD_URL, Description = "Common Hosted Forms Service API" }, + new() { KeyName = DynamicUrlKeyNames.NOTFICATION_API_BASE, Url = DynamicUrls.CHES_PROD_URL, Description = "Common Hosted Email Service API" }, + new() { KeyName = DynamicUrlKeyNames.NOTFICATION_AUTH, Url = DynamicUrls.CHES_PROD_AUTH, Description = "Common Hosted Email Service OAUTH" }, + new() { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{messageIndex++}", Url = "", Description = $"Direct message webhook {messageIndex}" }, + new() { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{messageIndex++}", Url = "", Description = $"Direct message webhook {messageIndex}" }, + new() { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{messageIndex++}", Url = "", Description = $"Direct message webhook {messageIndex}" }, + new() { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{webhookIndex++}", Url = "", Description = $"Webhook {webhookIndex}" }, + new() { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{webhookIndex++}", Url = "", Description = $"Webhook {webhookIndex}" }, + new() { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{webhookIndex++}", Url = "", Description = $"Webhook {webhookIndex}" }, + }; + + foreach (var dynamicUrl in dynamicUrls) + { + var existing = await DynamicUrlRepository.FirstOrDefaultAsync(s => s.KeyName == dynamicUrl.KeyName); + if (existing == null) + { + await DynamicUrlRepository.InsertAsync(dynamicUrl); + } + } + } + + + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlKeyNames.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlKeyNames.cs new file mode 100644 index 000000000..9912148a0 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlKeyNames.cs @@ -0,0 +1,13 @@ +namespace Unity.GrantManager.Integrations; + +public static class DynamicUrlKeyNames +{ + public const string INTAKE_API_BASE = "INTAKE_API_BASE"; + public const string PAYMENT_API_BASE = "PAYMENT_API_BASE"; + public const string NOTFICATION_API_BASE = "NOTFICATION_API_BASE"; + public const string NOTFICATION_AUTH = "NOTFICATION_AUTH"; + public const string DIRECT_MESSAGE_KEY_PREFIX = "DIRECT_MESSAGE_"; // Teams Direct Message URL Weebhook- Dynamically incremented + public const string WEBHOOK_KEY_PREFIX = "WEBHOOK_"; // General Webhook URL - Dynamically incremented +} + + diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantManagerDbContext.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantManagerDbContext.cs index 47fbb5143..ed8b28f03 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantManagerDbContext.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantManagerDbContext.cs @@ -17,6 +17,7 @@ using Volo.Abp.TenantManagement.EntityFrameworkCore; using AppAny.Quartz.EntityFrameworkCore.Migrations; using AppAny.Quartz.EntityFrameworkCore.Migrations.PostgreSQL; +using Unity.GrantManager.Integrations; namespace Unity.GrantManager.EntityFrameworkCore; @@ -30,6 +31,7 @@ public class GrantManagerDbContext : { /* Add DbSet properties for your Aggregate Roots / Entities here. */ + public DbSet DynamicUrls { get; set; } public DbSet Sectors { get; set; } public DbSet SubSectors { get; set; } public DbSet EconomicRegion { get; set; } @@ -90,6 +92,16 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.AddQuartz(builder => builder.UsePostgreSql("qrtz_", null)); /* Configure your own tables/entities inside here */ + modelBuilder.Entity(b => + { + b.ToTable(GrantManagerConsts.DbTablePrefix + "DynamicUrls", + GrantManagerConsts.DbSchema); + b.HasKey(x => x.Id); + b.Property(x => x.KeyName).IsRequired().HasMaxLength(128); + b.Property(x => x.Url).IsRequired(); + b.Property(x => x.Description).HasMaxLength(256); + b.ConfigureByConvention(); + }); modelBuilder.Entity(b => { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20250514165300_DynamicUrls.Designer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20250514165300_DynamicUrls.Designer.cs new file mode 100644 index 000000000..afdb96491 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20250514165300_DynamicUrls.Designer.cs @@ -0,0 +1,2537 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Unity.GrantManager.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +#nullable disable + +namespace Unity.GrantManager.Migrations.HostMigrations +{ + [DbContext(typeof(GrantManagerDbContext))] + [Migration("20250514165300_DynamicUrls")] + partial class DynamicUrls + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.PostgreSql) + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_blob_triggers", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("qrtz_calendars", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_cron_triggers", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_j_req_recovery"); + + b.ToTable("qrtz_job_details", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("qrtz_locks", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("qrtz_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("qrtz_scheduler_state", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_simple_triggers", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("smallint") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_qrtz_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_qrtz_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_qrtz_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("qrtz_triggers", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Intakes.ChefsMissedSubmission", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ChefsApplicationFormGuid") + .HasColumnType("text"); + + b.Property("ChefsSubmissionGuids") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("ChefsMissedSubmissions", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Locality.Community", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegionalDistrictCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Communities", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Locality.EconomicRegion", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("EconomicRegionCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("EconomicRegionName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.HasKey("Id"); + + b.ToTable("EconomicRegions", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Locality.ElectoralDistrict", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ElectoralDistrictCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ElectoralDistrictName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.HasKey("Id"); + + b.ToTable("ElectoralDistricts", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Locality.RegionalDistrict", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("EconomicRegionCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("RegionalDistrictCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegionalDistrictName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("RegionalDistricts", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Locality.Sector", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SectorCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SectorName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Sectors", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Locality.SubSector", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SectorId") + .HasColumnType("uuid"); + + b.Property("SubSectorCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SubSectorName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("SectorId"); + + b.ToTable("SubSectors", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Tokens.TenantToken", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("TenantTokens", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApplicationName") + .HasMaxLength(96) + .HasColumnType("character varying(96)") + .HasColumnName("ApplicationName"); + + b.Property("BrowserInfo") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("BrowserInfo"); + + b.Property("ClientId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("ClientId"); + + b.Property("ClientIpAddress") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("ClientIpAddress"); + + b.Property("ClientName") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("ClientName"); + + b.Property("Comments") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("Comments"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("CorrelationId"); + + b.Property("Exceptions") + .HasColumnType("text"); + + b.Property("ExecutionDuration") + .HasColumnType("integer") + .HasColumnName("ExecutionDuration"); + + b.Property("ExecutionTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("HttpMethod") + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasColumnName("HttpMethod"); + + b.Property("HttpStatusCode") + .HasColumnType("integer") + .HasColumnName("HttpStatusCode"); + + b.Property("ImpersonatorTenantId") + .HasColumnType("uuid") + .HasColumnName("ImpersonatorTenantId"); + + b.Property("ImpersonatorTenantName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("ImpersonatorTenantName"); + + b.Property("ImpersonatorUserId") + .HasColumnType("uuid") + .HasColumnName("ImpersonatorUserId"); + + b.Property("ImpersonatorUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("ImpersonatorUserName"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TenantName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("TenantName"); + + b.Property("Url") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("Url"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("UserName"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ExecutionTime"); + + b.HasIndex("TenantId", "UserId", "ExecutionTime"); + + b.ToTable("AuditLogs", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.AuditLogAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuditLogId") + .HasColumnType("uuid") + .HasColumnName("AuditLogId"); + + b.Property("ExecutionDuration") + .HasColumnType("integer") + .HasColumnName("ExecutionDuration"); + + b.Property("ExecutionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("ExecutionTime"); + + b.Property("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("MethodName") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("MethodName"); + + b.Property("Parameters") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("Parameters"); + + b.Property("ServiceName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("ServiceName"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AuditLogId"); + + b.HasIndex("TenantId", "ServiceName", "MethodName", "ExecutionTime"); + + b.ToTable("AuditLogActions", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.EntityChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuditLogId") + .HasColumnType("uuid") + .HasColumnName("AuditLogId"); + + b.Property("ChangeTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("ChangeTime"); + + b.Property("ChangeType") + .HasColumnType("smallint") + .HasColumnName("ChangeType"); + + b.Property("EntityId") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("EntityId"); + + b.Property("EntityTenantId") + .HasColumnType("uuid"); + + b.Property("EntityTypeFullName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("EntityTypeFullName"); + + b.Property("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AuditLogId"); + + b.HasIndex("TenantId", "EntityTypeFullName", "EntityId"); + + b.ToTable("EntityChanges", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.EntityPropertyChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EntityChangeId") + .HasColumnType("uuid"); + + b.Property("NewValue") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("NewValue"); + + b.Property("OriginalValue") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("OriginalValue"); + + b.Property("PropertyName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("PropertyName"); + + b.Property("PropertyTypeFullName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("PropertyTypeFullName"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("EntityChangeId"); + + b.ToTable("EntityPropertyChanges", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.BackgroundJobs.BackgroundJobRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsAbandoned") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JobArgs") + .IsRequired() + .HasMaxLength(1048576) + .HasColumnType("character varying(1048576)"); + + b.Property("JobName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("LastTryTime") + .HasColumnType("timestamp without time zone"); + + b.Property("NextTryTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Priority") + .ValueGeneratedOnAdd() + .HasColumnType("smallint") + .HasDefaultValue((byte)15); + + b.Property("TryCount") + .ValueGeneratedOnAdd() + .HasColumnType("smallint") + .HasDefaultValue((short)0); + + b.HasKey("Id"); + + b.HasIndex("IsAbandoned", "NextTryTime"); + + b.ToTable("BackgroundJobs", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.FeatureManagement.FeatureDefinitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AllowedProviders") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DefaultValue") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("IsAvailableToHost") + .HasColumnType("boolean"); + + b.Property("IsVisibleToClients") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ParentName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ValueType") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.HasKey("Id"); + + b.HasIndex("GroupName"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Features", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.FeatureManagement.FeatureGroupDefinitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("FeatureGroups", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.FeatureManagement.FeatureValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ProviderKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ProviderName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.HasKey("Id"); + + b.HasIndex("Name", "ProviderName", "ProviderKey") + .IsUnique(); + + b.ToTable("FeatureValues", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityClaimType", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsStatic") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Regex") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("RegexDescription") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Required") + .HasColumnType("boolean"); + + b.Property("ValueType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ClaimTypes", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityLinkUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("SourceTenantId") + .HasColumnType("uuid"); + + b.Property("SourceUserId") + .HasColumnType("uuid"); + + b.Property("TargetTenantId") + .HasColumnType("uuid"); + + b.Property("TargetUserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("SourceUserId", "SourceTenantId", "TargetUserId", "TargetTenantId") + .IsUnique(); + + b.ToTable("LinkUsers", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("EntityVersion") + .HasColumnType("integer"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDefault") + .HasColumnType("boolean") + .HasColumnName("IsDefault"); + + b.Property("IsPublic") + .HasColumnType("boolean") + .HasColumnName("IsPublic"); + + b.Property("IsStatic") + .HasColumnType("boolean") + .HasColumnName("IsStatic"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName"); + + b.ToTable("Roles", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClaimType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ClaimValue") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("RoleClaims", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentitySecurityLog", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Action") + .HasMaxLength(96) + .HasColumnType("character varying(96)"); + + b.Property("ApplicationName") + .HasMaxLength(96) + .HasColumnType("character varying(96)"); + + b.Property("BrowserInfo") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("ClientId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ClientIpAddress") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("Identity") + .HasMaxLength(96) + .HasColumnType("character varying(96)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TenantName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Action"); + + b.HasIndex("TenantId", "ApplicationName"); + + b.HasIndex("TenantId", "Identity"); + + b.HasIndex("TenantId", "UserId"); + + b.ToTable("SecurityLogs", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentitySession", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClientId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Device") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("DeviceInfo") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("IpAddresses") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LastAccessed") + .HasColumnType("timestamp without time zone"); + + b.Property("SessionId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("SignedIn") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Device"); + + b.HasIndex("SessionId"); + + b.HasIndex("TenantId", "UserId"); + + b.ToTable("Sessions", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessFailedCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("Email"); + + b.Property("EmailConfirmed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("EmailConfirmed"); + + b.Property("EntityVersion") + .HasColumnType("integer"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("IsActive"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsExternal") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsExternal"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastPasswordChangeTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LockoutEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("LockoutEnabled"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("Name"); + + b.Property("NormalizedEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("NormalizedEmail"); + + b.Property("NormalizedUserName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("NormalizedUserName"); + + b.Property("OidcSub") + .HasColumnType("text"); + + b.Property("PasswordHash") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("PasswordHash"); + + b.Property("PhoneNumber") + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasColumnName("PhoneNumber"); + + b.Property("PhoneNumberConfirmed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("PhoneNumberConfirmed"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("SecurityStamp"); + + b.Property("ShouldChangePasswordOnNextLogin") + .HasColumnType("boolean"); + + b.Property("Surname") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("Surname"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TwoFactorEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("TwoFactorEnabled"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("UserName"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("NormalizedEmail"); + + b.HasIndex("NormalizedUserName"); + + b.HasIndex("UserName"); + + b.ToTable("Users", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClaimType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ClaimValue") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserDelegation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("EndTime") + .HasColumnType("timestamp without time zone"); + + b.Property("SourceUserId") + .HasColumnType("uuid"); + + b.Property("StartTime") + .HasColumnType("timestamp without time zone"); + + b.Property("TargetUserId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("UserDelegations", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserLogin", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("LoginProvider") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ProviderDisplayName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ProviderKey") + .IsRequired() + .HasMaxLength(196) + .HasColumnType("character varying(196)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("UserId", "LoginProvider"); + + b.HasIndex("LoginProvider", "ProviderKey"); + + b.ToTable("UserLogins", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserOrganizationUnit", b => + { + b.Property("OrganizationUnitId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("OrganizationUnitId", "UserId"); + + b.HasIndex("UserId", "OrganizationUnitId"); + + b.ToTable("UserOrganizationUnits", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId", "UserId"); + + b.ToTable("UserRoles", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("LoginProvider") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Name") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("UserTokens", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnit", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(95) + .HasColumnType("character varying(95)") + .HasColumnName("Code"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("DisplayName"); + + b.Property("EntityVersion") + .HasColumnType("integer"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ParentId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("Code"); + + b.HasIndex("ParentId"); + + b.ToTable("OrganizationUnits", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnitRole", b => + { + b.Property("OrganizationUnitId") + .HasColumnType("uuid"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("OrganizationUnitId", "RoleId"); + + b.HasIndex("RoleId", "OrganizationUnitId"); + + b.ToTable("OrganizationUnitRoles", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.PermissionManagement.PermissionDefinitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("MultiTenancySide") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ParentName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Providers") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("StateCheckers") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("GroupName"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Permissions", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.PermissionManagement.PermissionGrant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ProviderKey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ProviderName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Name", "ProviderName", "ProviderKey") + .IsUnique(); + + b.ToTable("PermissionGrants", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.PermissionManagement.PermissionGroupDefinitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("PermissionGroups", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.SettingManagement.Setting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ProviderKey") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ProviderName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.HasKey("Id"); + + b.HasIndex("Name", "ProviderName", "ProviderKey") + .IsUnique(); + + b.ToTable("Settings", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.SettingManagement.SettingDefinitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DefaultValue") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsEncrypted") + .HasColumnType("boolean"); + + b.Property("IsInherited") + .HasColumnType("boolean"); + + b.Property("IsVisibleToClients") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Providers") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("SettingDefinitions", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.TenantManagement.Tenant", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EntityVersion") + .HasColumnType("integer"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.HasIndex("NormalizedName"); + + b.ToTable("Tenants", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.TenantManagement.TenantConnectionString", b => + { + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Name") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("TenantId", "Name"); + + b.ToTable("TenantConnectionStrings", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("Unity.GrantManager.Locality.SubSector", b => + { + b.HasOne("Unity.GrantManager.Locality.Sector", "Sector") + .WithMany("SubSectors") + .HasForeignKey("SectorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Sector"); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.AuditLogAction", b => + { + b.HasOne("Volo.Abp.AuditLogging.AuditLog", null) + .WithMany("Actions") + .HasForeignKey("AuditLogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.EntityChange", b => + { + b.HasOne("Volo.Abp.AuditLogging.AuditLog", null) + .WithMany("EntityChanges") + .HasForeignKey("AuditLogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.EntityPropertyChange", b => + { + b.HasOne("Volo.Abp.AuditLogging.EntityChange", null) + .WithMany("PropertyChanges") + .HasForeignKey("EntityChangeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityRoleClaim", b => + { + b.HasOne("Volo.Abp.Identity.IdentityRole", null) + .WithMany("Claims") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserClaim", b => + { + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("Claims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserLogin", b => + { + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("Logins") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserOrganizationUnit", b => + { + b.HasOne("Volo.Abp.Identity.OrganizationUnit", null) + .WithMany() + .HasForeignKey("OrganizationUnitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("OrganizationUnits") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserRole", b => + { + b.HasOne("Volo.Abp.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("Roles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserToken", b => + { + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("Tokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnit", b => + { + b.HasOne("Volo.Abp.Identity.OrganizationUnit", null) + .WithMany() + .HasForeignKey("ParentId"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnitRole", b => + { + b.HasOne("Volo.Abp.Identity.OrganizationUnit", null) + .WithMany("Roles") + .HasForeignKey("OrganizationUnitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Volo.Abp.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.TenantManagement.TenantConnectionString", b => + { + b.HasOne("Volo.Abp.TenantManagement.Tenant", null) + .WithMany("ConnectionStrings") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); + + modelBuilder.Entity("Unity.GrantManager.Locality.Sector", b => + { + b.Navigation("SubSectors"); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.AuditLog", b => + { + b.Navigation("Actions"); + + b.Navigation("EntityChanges"); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.EntityChange", b => + { + b.Navigation("PropertyChanges"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityRole", b => + { + b.Navigation("Claims"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUser", b => + { + b.Navigation("Claims"); + + b.Navigation("Logins"); + + b.Navigation("OrganizationUnits"); + + b.Navigation("Roles"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnit", b => + { + b.Navigation("Roles"); + }); + + modelBuilder.Entity("Volo.Abp.TenantManagement.Tenant", b => + { + b.Navigation("ConnectionStrings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20250514165300_DynamicUrls.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20250514165300_DynamicUrls.cs new file mode 100644 index 000000000..407fef6b7 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20250514165300_DynamicUrls.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Unity.GrantManager.Migrations.HostMigrations +{ + /// + public partial class DynamicUrls : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "DynamicUrls", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + KeyName = table.Column(type: "text", nullable: false), + Url = table.Column(type: "text", nullable: false), + Description = table.Column(type: "text", nullable: false), + TenantId = table.Column(type: "uuid", nullable: true), + ExtraProperties = table.Column(type: "text", nullable: false), + ConcurrencyStamp = table.Column(type: "character varying(40)", maxLength: 40, nullable: false), + CreationTime = table.Column(type: "timestamp without time zone", nullable: false), + CreatorId = table.Column(type: "uuid", nullable: true), + LastModificationTime = table.Column(type: "timestamp without time zone", nullable: true), + LastModifierId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_DynamicUrls", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "DynamicUrls"); + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/GrantManagerDbContextModelSnapshot.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/GrantManagerDbContextModelSnapshot.cs index f75f52d54..fbaa4e0de 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/GrantManagerDbContextModelSnapshot.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/GrantManagerDbContextModelSnapshot.cs @@ -706,6 +706,55 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Sectors", (string)null); }); + modelBuilder.Entity("Unity.GrantManager.Locality.DynamicUrl", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.Property("KeyName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("DynamicUrls", (string)null); + }); + modelBuilder.Entity("Unity.GrantManager.Locality.SubSector", b => { b.Property("Id") diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/GrantTenantDbContextModelSnapshot.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/GrantTenantDbContextModelSnapshot.cs index fe61ccb52..f70d317b2 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/GrantTenantDbContextModelSnapshot.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/GrantTenantDbContextModelSnapshot.cs @@ -3103,6 +3103,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + b.HasKey("Id"); b.ToTable("AccountCodings", "Payments"); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicationRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicationRepository.cs index 39e673a1c..2928e5ffc 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicationRepository.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicationRepository.cs @@ -191,4 +191,4 @@ public override async Task> WithDetailsAsync() .AsNoTracking() // read?only; drop this line if you need tracking .FirstOrDefaultAsync(a => a.Id == id); } -} +} \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/DynamicUrlRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/DynamicUrlRepository.cs new file mode 100644 index 000000000..e5f4c24d8 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/DynamicUrlRepository.cs @@ -0,0 +1,19 @@ +using System; +using Unity.GrantManager.Applications; +using Unity.GrantManager.EntityFrameworkCore; +using Unity.GrantManager.Integrations; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +namespace Unity.GrantManager.Repositories +{ + [Dependency(ReplaceServices = true)] + [ExposeServices(typeof(IDynamicUrlRepository))] + public class DynamicUrlRepository : EfCoreRepository, IDynamicUrlRepository + { + public DynamicUrlRepository(IDbContextProvider dbContextProvider) : base(dbContextProvider) + { + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/FormController.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/FormController.cs index e1888f74d..04888a55b 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/FormController.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/FormController.cs @@ -5,36 +5,28 @@ using System.Threading.Tasks; using Unity.GrantManager.ApplicationForms; using Unity.GrantManager.Applications; -using Unity.GrantManager.Integration.Chefs; using Volo.Abp; using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.Domain.Entities; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Unity.GrantManager.Integrations.Chefs; namespace Unity.GrantManager.Controllers { [Route("/api/app")] - public partial class FormController : AbpController + public partial class FormController( + IApplicationFormRepository applicationFormRepository, + IApplicationFormSubmissionRepository applicationFormSubmissionRepository, + IApplicationFormVersionAppService applicationFormVersionAppService, + IFormsApiService formsApiService) : AbpController { - private readonly IApplicationFormRepository _applicationFormRepository; - private readonly IApplicationFormVersionAppService _applicationFormVersionAppService; - private readonly IApplicationFormSubmissionRepository _applicationFormSubmissionRepository; - private readonly IFormsApiService _formsApiService; - - protected ILogger logger => LazyServiceProvider.LazyGetService(provider => LoggerFactory?.CreateLogger(GetType().FullName!) ?? NullLogger.Instance); - - public FormController( - IApplicationFormRepository applicationFormRepository, - IApplicationFormSubmissionRepository applicationFormSubmissionRepository, - IApplicationFormVersionAppService applicationFormVersionAppService, - IFormsApiService formsApiService) - { - _applicationFormSubmissionRepository = applicationFormSubmissionRepository; - _applicationFormRepository = applicationFormRepository; - _applicationFormVersionAppService = applicationFormVersionAppService; - _formsApiService = formsApiService; - } + private readonly IApplicationFormRepository _applicationFormRepository = applicationFormRepository; + private readonly IApplicationFormVersionAppService _applicationFormVersionAppService = applicationFormVersionAppService; + private readonly IApplicationFormSubmissionRepository _applicationFormSubmissionRepository = applicationFormSubmissionRepository; + private readonly IFormsApiService _formsApiService = formsApiService; + + protected ILogger Logger => LazyServiceProvider.LazyGetService(provider => LoggerFactory?.CreateLogger(GetType().FullName!) ?? NullLogger.Instance); [HttpPost("form/{formId}/version/{formVersionId}")] public async Task SynchronizeChefsAvailableFields(string formId, string formVersionId) @@ -62,11 +54,13 @@ public async Task SynchronizeChefsAvailableFields(string formId, throw new BusinessException("Application Form API Key is Required"); } - var chefsFormVersion = await _formsApiService.GetFormDataAsync(formId, formVersionId); - + var chefsFormVersion = await _formsApiService.GetFormDataAsync(formId, formVersionId) + ?? throw new BusinessException("Chefs Form Version data could not be retrieved."); + + var result = await _applicationFormVersionAppService - .UpdateOrCreateApplicationFormVersion(formId, formVersionId, applicationForm.Id, chefsFormVersion); - + .UpdateOrCreateApplicationFormVersion(formId, formVersionId, applicationForm.Id, chefsFormVersion); + return Ok(result); } catch (EntityNotFoundException ex) @@ -80,7 +74,7 @@ public async Task SynchronizeChefsAvailableFields(string formId, catch (Exception ex) { string ExceptionMessage = ex.Message; - logger.LogError(ex, "FormController->SynchronizeChefsAvailableFields: {ExceptionMessage}", ExceptionMessage); + Logger.LogError(ex, "FormController->SynchronizeChefsAvailableFields: {ExceptionMessage}", ExceptionMessage); return StatusCode(500, "An error occurred while processing your request."); } } @@ -99,7 +93,7 @@ public async Task StoreSubmissionHtml([FromBody] ApplicationSubmi { if (!ModelState.IsValid) { - logger.LogWarning("Invalid model state for StoreSubmissionHtml"); + Logger.LogWarning("Invalid model state for StoreSubmissionHtml"); return BadRequest(ModelState); } @@ -121,7 +115,7 @@ public async Task StoreSubmissionHtml([FromBody] ApplicationSubmi catch (Exception ex) { string ExceptionMessage = ex.Message; - logger.LogError(ex, "FormController->StoreSubmissionHtml: {ExceptionMessage}", ExceptionMessage); + Logger.LogError(ex, "FormController->StoreSubmissionHtml: {ExceptionMessage}", ExceptionMessage); return StatusCode(500, "An error occurred while processing your request."); } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebModule.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebModule.cs index d23c2f83e..7b8e31497 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebModule.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebModule.cs @@ -75,7 +75,6 @@ using Unity.Modules.Shared.Utils; using Unity.Notifications.Web.Views.Settings; using Unity.Notifications.Web.Bundling; -using Unity.Reporting.Web; using Unity.GrantManager.Web.Views.Settings; namespace Unity.GrantManager.Web; @@ -99,8 +98,7 @@ namespace Unity.GrantManager.Web; typeof(PaymentsWebModule), typeof(AbpBlobStoringModule), typeof(NotificationsWebModule), - typeof(FlexWebModule), - typeof(ReportingWebModule) + typeof(FlexWebModule) )] public class GrantManagerWebModule : AbpModule diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenuContributor.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenuContributor.cs index 00ab97c38..bb14098f0 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenuContributor.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenuContributor.cs @@ -2,6 +2,7 @@ using Unity.GrantManager.Localization; using Unity.GrantManager.Permissions; using Unity.Identity.Web.Navigation; +using Unity.Modules.Shared.Permissions; using Unity.TenantManagement; using Unity.TenantManagement.Web.Navigation; using Volo.Abp.Identity; @@ -92,6 +93,9 @@ private static Task ConfigureMainMenuAsync(MenuConfigurationContext context) ) ); + + // ******************** + // Admin - Tenant Management context.Menu.AddItem( new ApplicationMenuItem( TenantManagementMenuNames.Tenants, @@ -103,6 +107,15 @@ private static Task ConfigureMainMenuAsync(MenuConfigurationContext context) ) ); + context.Menu.AddItem( + new ApplicationMenuItem( + GrantManagerMenus.EndpointManagement, + displayName: "Endpoints", + "~/EndpointManagement/Endpoints", + requiredPermissionName: IdentityConsts.ITAdminPermissionName + ) + ); + // End Admin ******************** #pragma warning disable S125 // Sections of code should not be commented out /* - will complete later after fixing ui sub menu issue */ //var administration = context.Menu.GetAdministration(); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenus.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenus.cs index 07b33690b..ff26a1487 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenus.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenus.cs @@ -16,4 +16,5 @@ public static class GrantManagerMenus public const string Welcome = Prefix + ".Welcome"; public const string Intakes = Prefix + ".Intakes"; public const string ApplicationForms = Prefix + ".ApplicationForms"; + public const string EndpointManagement = Prefix + ".EndpointManagement"; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/Mapping.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/Mapping.cshtml.cs index 54f447565..5f9d1ecee 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/Mapping.cshtml.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/Mapping.cshtml.cs @@ -77,7 +77,7 @@ public async Task OnGetAsync() { ApplicationFormDto = await _applicationFormAppService.GetAsync(ApplicationId); ScoresheetId = ApplicationFormDto.ScoresheetId; - ApplicationFormVersionDtoList = (List?)await _applicationFormAppService.GetVersionsAsync(ApplicationFormDto.Id); + ApplicationFormVersionDtoList = (List?) await _applicationFormAppService.GetVersionsAsync(ApplicationFormDto.Id); FlexEnabled = await _featureChecker.IsEnabledAsync("Unity.Flex"); if (ApplicationFormVersionDtoList != null) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/wwwroot/teams/MessageCard.json b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/wwwroot/teams/MessageCard.json index 2bdc644ae..a79b2c501 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/wwwroot/teams/MessageCard.json +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/wwwroot/teams/MessageCard.json @@ -6,7 +6,7 @@ "sections": [{ "activityTitle": "", "activitySubtitle": "", - "activityImage": "https://unity.gov.bc.ca/images/logo/bcgov/BC_Logo_Light.png", + "activityImage": "https://bcgov.github.io/unity-docs/images/UnityLogo.png", "facts": [], "markdown": true }] diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Events/ChefsEventSubscriptionServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Events/ChefsEventSubscriptionServiceTests.cs index e76667ef7..32ba5366f 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Events/ChefsEventSubscriptionServiceTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Events/ChefsEventSubscriptionServiceTests.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json.Linq; using NSubstitute; using Shouldly; using System; @@ -6,7 +7,7 @@ using System.Threading.Tasks; using Unity.GrantManager.Applications; using Unity.GrantManager.Intakes; -using Unity.GrantManager.Integration.Chefs; +using Unity.GrantManager.Integrations.Chefs; using Volo.Abp.Domain.Repositories; using Volo.Abp.Uow; using Volo.Abp.Users; @@ -88,6 +89,21 @@ public Task GetForm(Guid? formId, string chefsApplicationFormGuid, strin { throw new NotImplementedException(); } + + public Task GetSubmissionDataAsync(Guid chefsFormId, Guid submissionId) + { + throw new NotImplementedException(); + } + + Task IFormsApiService.GetForm(Guid? formId, string chefsApplicationFormGuid, string encryptedApiKey) + { + throw new NotImplementedException(); + } + + Task IFormsApiService.GetFormDataAsync(string chefsFormId, string chefsFormVersionId) + { + throw new NotImplementedException(); + } } public class IntakeFormSubmissionMapperMock : IIntakeFormSubmissionMapper From 2d01305ffdc219f6123be361363d62a942285c79 Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Wed, 20 Aug 2025 16:47:20 -0700 Subject: [PATCH 06/55] feature/AB#26441-DynamicUrls-Initial --- .../Events/EmailNotificationHandler.cs | 1 - .../Integrations/Ches/ChesClientOptions.cs | 2 - .../Integrations/Ches/ChesClientService.cs | 58 +++++----- .../TeamsNotificationService.cs | 49 +++------ .../Integrations/Cas/CasClientOptions.cs | 1 - .../Integrations/Cas/CasTokenService.cs | 5 +- .../Integrations/Cas/InvoiceService.cs | 13 ++- .../Integrations/Cas/SupplierService.cs | 98 ++++++++++++----- .../PaymentRequestAppService.cs | 26 ++--- .../UpdatePaymentRequestStatus.cshtml.cs | 18 +--- .../Menus/ReportingMenuContributor.cs | 9 +- .../Http/IResilientHttpRequest.cs | 21 ++-- .../Http/ResilientHttpRequest.cs | 84 ++++++++------- .../Permissions/IdentityConsts.cs | 5 + .../Notifications/INotificationsAppService.cs | 2 +- .../ApplicationFormAppService.cs | 72 +++++-------- .../ApplicationFormVersionAppService.cs | 80 ++++++-------- .../ConfigureIntakeClientOptions.cs | 30 +++--- .../GrantManagerApplicationModule.cs | 1 + .../Intakes/IntakeSubmissionAppService.cs | 8 +- .../Integrations/Chefs/FormsApiService.cs | 50 +++------ .../Integrations/Css/CssApiOptions.cs | 4 +- .../Integrations/Css/CssApiService.cs | 34 +++--- .../Geocoder/GeocoderApiService.cs | 28 ++--- .../Integrations/OrgBook/OrgBookService.cs | 101 +++++++++--------- .../Norifications/NotificationsAppService.cs | 6 +- .../Integrations/DynamicUrl.cs | 0 .../Integrations/DynamicUrlKeyNames.cs | 9 +- .../Integrations/DynamicUrlDataSeeder.cs | 44 ++++---- .../Identity/CurrentUser.cs | 2 +- .../Identity/PolicyRegistrant.cs | 7 ++ .../Components/EmailsWidget/Default.css | 1 - 32 files changed, 416 insertions(+), 453 deletions(-) rename applications/Unity.GrantManager/src/{Unity.GrantManager.Domain => Unity.GrantManager.Domain.Shared}/Integrations/DynamicUrl.cs (100%) rename applications/Unity.GrantManager/src/{Unity.GrantManager.Domain => Unity.GrantManager.Domain.Shared}/Integrations/DynamicUrlKeyNames.cs (63%) 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 b53c3c860..46157df30 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 @@ -15,7 +15,6 @@ internal class EmailNotificationHandler( IEmailNotificationService emailNotificationService, IFeatureChecker featureChecker) : ILocalEventHandler, ITransientDependency { - private const string GRANT_APPLICATION_UPDATE_SUBJECT = "Grant Application Update"; private const string FAILED_PAYMENTS_SUBJECT = "CAS Payment Failure Notification"; public async Task HandleEventAsync(EmailNotificationEvent eventData) diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Integrations/Ches/ChesClientOptions.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Integrations/Ches/ChesClientOptions.cs index 3c8a3f3a3..1ee0f3f6a 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Integrations/Ches/ChesClientOptions.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Integrations/Ches/ChesClientOptions.cs @@ -2,8 +2,6 @@ { public class ChesClientOptions { - public string ChesUrl { get; set; } = string.Empty; - public string ChesTokenUrl { get; set; } = string.Empty; public string ChesClientId { get; set; } = string.Empty; public string ChesClientSecret { get; set; } = string.Empty; } diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Integrations/Ches/ChesClientService.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Integrations/Ches/ChesClientService.cs index 6bfd0bc1a..35e62a834 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Integrations/Ches/ChesClientService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Integrations/Ches/ChesClientService.cs @@ -1,6 +1,5 @@ using Volo.Abp; using System.Threading.Tasks; -using System.Text.Json; using Unity.Modules.Shared.Integrations; using Unity.Modules.Shared.Http; using Volo.Abp.Application.Services; @@ -8,47 +7,52 @@ using Volo.Abp.DependencyInjection; using System.Net.Http; using Volo.Abp.Caching; +using Unity.GrantManager.Integrations; namespace Unity.Notifications.Integrations.Ches { [IntegrationService] [RemoteService(false, Name = "Ches")] [ExposeServices(typeof(ChesClientService), typeof(IChesClientService))] - public class ChesClientService : ApplicationService, IChesClientService + public class ChesClientService( + IDistributedCache chesTokenCache, + IResilientHttpRequest resilientHttpRequest, + IEndpointManagementAppService endpointManagementAppService, + IHttpClientFactory httpClientFactory, + IOptions chesClientOptions + ) : ApplicationService, IChesClientService { - private readonly IHttpClientFactory _httpClientFactory; - private readonly IResilientHttpRequest _resilientRestClient; - private readonly IOptions _chesClientOptions; - private readonly IDistributedCache _chesTokenCache; - - public ChesClientService( - IDistributedCache chesTokenCache, - IResilientHttpRequest resilientHttpRequest, - IHttpClientFactory httpClientFactory, - IOptions chesClientOptions) + public async Task SendAsync(object emailRequest) { - _resilientRestClient = resilientHttpRequest; - _chesClientOptions = chesClientOptions; - _httpClientFactory = httpClientFactory; - _chesTokenCache = chesTokenCache; + string authToken = await GetAuthTokenAsync(); + string notificationsApiUrl = await endpointManagementAppService.GetUrlByKeyNameAsync(DynamicUrlKeyNames.NOTFICATION_API_BASE); + var resource = $"{notificationsApiUrl}/email"; + + // Pass the object directly; ResilientHttpRequest will serialize it to JSON + var response = await resilientHttpRequest.HttpAsync( + HttpMethod.Post, + resource, + emailRequest, + authToken + ); + + return response; } - public async Task SendAsync(object emailRequest) + private async Task GetAuthTokenAsync() { - ClientOptions clientOptions = new ClientOptions + string notificationsAuthUrl = await endpointManagementAppService.GetUrlByKeyNameAsync(DynamicUrlKeyNames.NOTFICATION_AUTH); + + ClientOptions clientOptions = new() { - Url = _chesClientOptions.Value.ChesTokenUrl, - ClientId = _chesClientOptions.Value.ChesClientId, - ClientSecret = _chesClientOptions.Value.ChesClientSecret, + Url = notificationsAuthUrl, + ClientId = chesClientOptions.Value.ChesClientId, + ClientSecret = chesClientOptions.Value.ChesClientSecret, ApiKey = "ChesApiKey" }; - TokenService tokenService = new(_httpClientFactory, _chesTokenCache, Logger); - var authToken = await tokenService.GetAuthTokenAsync(clientOptions); - var resource = $"{_chesClientOptions.Value.ChesUrl}/email"; - string jsonString = JsonSerializer.Serialize(emailRequest); - var response = await _resilientRestClient.HttpAsyncWithBody(HttpMethod.Post, resource, jsonString, authToken); - return response; + TokenService tokenService = new(httpClientFactory, chesTokenCache, Logger); + return await tokenService.GetAuthTokenAsync(clientOptions); } } } diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/TeamsNotifications/TeamsNotificationService.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/TeamsNotifications/TeamsNotificationService.cs index 84159fd7a..f4d319906 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/TeamsNotifications/TeamsNotificationService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/TeamsNotifications/TeamsNotificationService.cs @@ -15,12 +15,12 @@ public class TeamsNotificationService public TeamsNotificationService() : base() { } public const string DIRECT_MESSAGE_KEY_PREFIX = "DIRECT_MESSAGE_"; - public const string TEAMS_NOTIFICATION = $"{DIRECT_MESSAGE_KEY_PREFIX}0"; - public const string TEAMS_NOTIFICATION_1 = $"{DIRECT_MESSAGE_KEY_PREFIX}1"; + public const string TEAMS_ALERT = $"{DIRECT_MESSAGE_KEY_PREFIX}0"; + public const string TEAMS_NOTIFICATION = $"{DIRECT_MESSAGE_KEY_PREFIX}1"; public static string TeamsChannel { get; set; } = string.Empty; - private readonly List _facts = new List(); + private readonly List _facts = []; public async Task PostFactsToTeamsAsync(string teamsChannel, string activityTitle, string activitySubtitle) { @@ -118,39 +118,16 @@ public static async Task PostChefsEventToTeamsAsync(string teamsChannel, string string activitySubtitle = "Form Name: " + formName?.ToString(); - List facts = new() - { - new Fact - { - Name = "Form Version: ", - Value = version?.ToString() ?? string.Empty - }, - new Fact - { - Name = "Published: ", - Value = published?.ToString() ?? string.Empty - }, - new Fact - { - Name = "Updated By: ", - Value = updatedBy?.ToString() ?? string.Empty - }, - new Fact - { - Name = "Updated At: ", - Value = updatedAt?.ToString() + " UTC" - }, - new Fact - { - Name = "Created By: ", - Value = createdBy?.ToString() ?? string.Empty - }, - new Fact - { - Name = "Created At: ", - Value = createdAt?.ToString() + " UTC" - }, - }; + // Fix for IDE0028: Simplify collection initialization + List facts = + [ + new Fact { Name = "Form Version: ", Value = version?.ToString() ?? string.Empty }, + new Fact { Name = "Published: ", Value = published?.ToString() ?? string.Empty }, + new Fact { Name = "Updated By: ", Value = updatedBy?.ToString() ?? string.Empty }, + new Fact { Name = "Updated At: ", Value = updatedAt?.ToString() + " UTC" }, + new Fact { Name = "Created By: ", Value = createdBy?.ToString() ?? string.Empty }, + new Fact { Name = "Created At: ", Value = createdAt?.ToString() + " UTC" } + ]; await PostToTeamsAsync(teamsChannel, activityTitle, activitySubtitle, facts); } diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/CasClientOptions.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/CasClientOptions.cs index ea0fb984a..b1e0715b9 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/CasClientOptions.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/CasClientOptions.cs @@ -2,7 +2,6 @@ { public class CasClientOptions { - public string CasBaseUrl { get; set; } = string.Empty; public string CasClientId { get; set; } = string.Empty; public string CasClientSecret { get; set; } = string.Empty; } diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/CasTokenService.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/CasTokenService.cs index 7cd54851c..46e3cd452 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/CasTokenService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/CasTokenService.cs @@ -1,6 +1,7 @@ using System.Net.Http; using System.Threading.Tasks; using Microsoft.Extensions.Options; +using Unity.GrantManager.Integrations; using Unity.Modules.Shared.Integrations; using Volo.Abp; using Volo.Abp.Application.Services; @@ -12,6 +13,7 @@ namespace Unity.Payments.Integrations.Cas [IntegrationService] [ExposeServices(typeof(CasTokenService), typeof(ICasTokenService))] public class CasTokenService( + IEndpointManagementAppService endpointManagementAppService, IOptions casClientOptions, IHttpClientFactory httpClientFactory, IDistributedCache chesTokenCache @@ -22,9 +24,10 @@ IDistributedCache chesTokenCache public async Task GetAuthTokenAsync() { + string caseBaseUrl = await endpointManagementAppService.GetUrlByKeyNameAsync(DynamicUrlKeyNames.PAYMENT_API_BASE); ClientOptions clientOptions = new ClientOptions { - Url = $"{casClientOptions.Value.CasBaseUrl}/{OAUTH_PATH}", + Url = $"{caseBaseUrl}/{OAUTH_PATH}", ClientId = casClientOptions.Value.CasClientId, ClientSecret = casClientOptions.Value.CasClientSecret, ApiKey = CAS_API_KEY, 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 5fc82be56..20a22b7a6 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 @@ -18,6 +18,7 @@ using Unity.Modules.Shared.Http; using Unity.Payments.PaymentConfigurations; using Unity.Payments.Domain.AccountCodings; +using Unity.GrantManager.Integrations; namespace Unity.Payments.Integrations.Cas { @@ -26,6 +27,7 @@ namespace Unity.Payments.Integrations.Cas [ExposeServices(typeof(InvoiceService), typeof(IInvoiceService))] #pragma warning disable S107 // Methods should not have too many parameters public class InvoiceService( + IEndpointManagementAppService endpointManagementAppService, ICasTokenService iTokenService, IAccountCodingRepository accountCodingRepository, PaymentConfigurationAppService paymentConfigurationAppService, @@ -165,8 +167,9 @@ public async Task CreateInvoiceAsync(Invoice casAPInvoice) { string jsonString = JsonSerializer.Serialize(casAPInvoice); var authToken = await iTokenService.GetAuthTokenAsync(); - var resource = $"{casClientOptions.Value.CasBaseUrl}/{CFS_APINVOICE}/"; - var response = await resilientHttpRequest.HttpAsyncWithBody(HttpMethod.Post, resource, jsonString, authToken); + string casBaseUrl = await endpointManagementAppService.GetUrlByKeyNameAsync(DynamicUrlKeyNames.PAYMENT_API_BASE); + var resource = $"{casBaseUrl}/{CFS_APINVOICE}/"; + var response = await resilientHttpRequest.HttpAsync(HttpMethod.Post, resource, jsonString, authToken); if (response != null) { @@ -196,7 +199,8 @@ public async Task CreateInvoiceAsync(Invoice casAPInvoice) public async Task GetCasInvoiceAsync(string invoiceNumber, string supplierNumber, string supplierSiteCode) { var authToken = await iTokenService.GetAuthTokenAsync(); - var resource = $"{casClientOptions.Value.CasBaseUrl}/{CFS_APINVOICE}/{invoiceNumber}/{supplierNumber}/{supplierSiteCode}"; + var casBaseUrl = await endpointManagementAppService.GetUrlByKeyNameAsync(DynamicUrlKeyNames.PAYMENT_API_BASE); + var resource = $"{casBaseUrl}/{CFS_APINVOICE}/{invoiceNumber}/{supplierNumber}/{supplierSiteCode}"; var response = await resilientHttpRequest.HttpAsync(HttpMethod.Get, resource, authToken); if (response != null @@ -216,7 +220,8 @@ public async Task GetCasInvoiceAsync(string invoiceNumbe public async Task GetCasPaymentAsync(string invoiceNumber, string supplierNumber, string siteNumber) { var authToken = await iTokenService.GetAuthTokenAsync(); - var resource = $"{casClientOptions.Value.CasBaseUrl}/{CFS_APINVOICE}/{invoiceNumber}/{supplierNumber}/{siteNumber}"; + var casBaseUrl = await endpointManagementAppService.GetUrlByKeyNameAsync(DynamicUrlKeyNames.PAYMENT_API_BASE); + var resource = $"{casBaseUrl}/{CFS_APINVOICE}/{invoiceNumber}/{supplierNumber}/{siteNumber}"; var response = await resilientHttpRequest.HttpAsync(HttpMethod.Get, resource, authToken); CasPaymentSearchResult casPaymentSearchResult = new(); diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/SupplierService.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/SupplierService.cs index cbaff113e..d5ac569df 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/SupplierService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/SupplierService.cs @@ -13,27 +13,48 @@ using Unity.Modules.Shared.Correlation; using System; using System.Collections.Generic; -using Newtonsoft.Json.Linq; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Unity.GrantManager.Integrations; namespace Unity.Payments.Integrations.Cas { [IntegrationService] [ExposeServices(typeof(SupplierService), typeof(ISupplierService))] - public class SupplierService(ILocalEventBus localEventBus, - IResilientHttpRequest resilientHttpRequest, - IOptions casClientOptions, - ICasTokenService iTokenService) : ApplicationService, ISupplierService + public class SupplierService : ApplicationService, ISupplierService { protected new ILogger Logger => LazyServiceProvider.LazyGetService(provider => LoggerFactory?.CreateLogger(GetType().FullName!) ?? NullLogger.Instance); - private const string CFS_SUPPLIER = "cfs/supplier"; + private readonly Task casBaseApiTask; + private readonly ILocalEventBus localEventBus; + private readonly IResilientHttpRequest resilientHttpRequest; + private readonly IOptions casClientOptions; + private readonly ICasTokenService iTokenService; + public SupplierService (ILocalEventBus localEventBus, + IEndpointManagementAppService endpointManagementAppService, + IResilientHttpRequest resilientHttpRequest, + IOptions casClientOptions, + ICasTokenService iTokenService) + { + this.localEventBus = localEventBus; + this.resilientHttpRequest = resilientHttpRequest; + this.casClientOptions = casClientOptions; + this.iTokenService = iTokenService; + + // Initialize the base API URL once during construction + casBaseApiTask = InitializeBaseApiAsync(endpointManagementAppService); + } + private static async Task InitializeBaseApiAsync(IEndpointManagementAppService endpointManagementAppService) + { + var url = await endpointManagementAppService.GetUrlByKeyNameAsync(DynamicUrlKeyNames.PAYMENT_API_BASE); + return url ?? throw new UserFriendlyException("Payment API base URL is not configured."); + } + public virtual async Task UpdateApplicantSupplierInfo(string? supplierNumber, Guid applicantId) { Logger.LogInformation("SupplierService->UpdateApplicantSupplierInfo: {SupplierNumber}, {ApplicantId}", supplierNumber, applicantId); - + // Integrate with payments module to update / insert supplier if (await FeatureChecker.IsEnabledAsync(PaymentConsts.UnityPaymentsFeature) && !string.IsNullOrEmpty(supplierNumber)) @@ -69,7 +90,7 @@ public async Task UpdateApplicantSupplierInfoByBn9(string? bn9, Guid ap catch (Exception ex) { Logger.LogError(ex, "An exception occurred updating the supplier for BN9: {ExceptionMessage}", ex.Message); - casSupplierResponse = "An exception occurred updating the supplier for BN9: " + ex.Message; + casSupplierResponse = "An exception occurred updating the supplier for BN9: " + ex.Message; } return casSupplierResponse; @@ -77,36 +98,51 @@ public async Task UpdateApplicantSupplierInfoByBn9(string? bn9, Guid ap private async Task UpdateSupplierInfo(dynamic casSupplierResponse, Guid applicantId) { - try { - UpsertSupplierEto supplierEto = GetEventDtoFromCasResponse(casSupplierResponse); + try + { + if (casSupplierResponse.TryGetProperty("code", out JsonElement codeProp) && codeProp.GetString() == "Unauthorized") + throw new UserFriendlyException("Unauthorized access to CAS supplier information."); + var casSupplierJson = casSupplierResponse is string str ? str : casSupplierResponse.ToString(); + using var doc = JsonDocument.Parse(casSupplierJson); + UpsertSupplierEto supplierEto = GetEventDtoFromCasResponse(doc.RootElement); supplierEto.CorrelationId = applicantId; supplierEto.CorrelationProvider = CorrelationConsts.Applicant; await localEventBus.PublishAsync(supplierEto); - } catch (Exception ex) + } + catch (Exception ex) { Logger.LogError(ex, "An exception occurred updating the supplier: {ExceptionMessage}", ex.Message); - throw new UserFriendlyException("An exception occurred updating the supplier."); + throw new UserFriendlyException("An exception occurred updating the supplier: " + ex.Message); } } - protected virtual UpsertSupplierEto GetEventDtoFromCasResponse(dynamic casSupplierResponse) + protected virtual UpsertSupplierEto GetEventDtoFromCasResponse(JsonElement casSupplierResponse) { - string lastUpdated = casSupplierResponse.GetProperty("lastupdated").ToString(); - string suppliernumber = casSupplierResponse.GetProperty("suppliernumber").ToString(); - string suppliername = casSupplierResponse.GetProperty("suppliername").ToString(); - string subcategory = casSupplierResponse.GetProperty("subcategory").ToString(); - string providerid = casSupplierResponse.GetProperty("providerid").ToString(); - string businessnumber = casSupplierResponse.GetProperty("businessnumber").ToString(); - string status = casSupplierResponse.GetProperty("status").ToString(); - string supplierprotected = casSupplierResponse.GetProperty("supplierprotected").ToString(); - string standardindustryclassification = casSupplierResponse.GetProperty("standardindustryclassification").ToString(); + string GetProp(string name) => + casSupplierResponse.TryGetProperty(name, out var prop) && prop.ValueKind != JsonValueKind.Null + ? prop.ToString() + : string.Empty; + + string lastUpdated = GetProp("lastupdated"); + string suppliernumber = GetProp("suppliernumber"); + string suppliername = GetProp("suppliername"); + string subcategory = GetProp("subcategory"); + string providerid = GetProp("providerid"); + string businessnumber = GetProp("businessnumber"); + string status = GetProp("status"); + string supplierprotected = GetProp("supplierprotected"); + string standardindustryclassification = GetProp("standardindustryclassification"); _ = DateTime.TryParse(lastUpdated, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out DateTime lastUpdatedDate); - List siteEtos = new List(); - JArray siteArray = Newtonsoft.Json.JsonConvert.DeserializeObject(casSupplierResponse.GetProperty("supplieraddress").ToString()); - foreach (dynamic site in siteArray) + + var siteEtos = new List(); + if (casSupplierResponse.TryGetProperty("supplieraddress", out var sitesJson) && + sitesJson.ValueKind == JsonValueKind.Array) { - siteEtos.Add(GetSiteEto(site)); + foreach (var site in sitesJson.EnumerateArray()) + { + siteEtos.Add(GetSiteEto(site)); + } } return new UpsertSupplierEto @@ -124,6 +160,7 @@ protected virtual UpsertSupplierEto GetEventDtoFromCasResponse(dynamic casSuppli }; } + protected static SiteEto GetSiteEto(dynamic site) { string supplierSiteCode = site["suppliersitecode"].ToString(); @@ -169,7 +206,8 @@ public async Task GetCasSupplierInformationAsync(string? supplierNumber { if (!string.IsNullOrEmpty(supplierNumber)) { - var resource = $"{casClientOptions.Value.CasBaseUrl}/{CFS_SUPPLIER}/{supplierNumber}"; + var casBaseApi = await casBaseApiTask; + var resource = $"{casBaseApi}/{CFS_SUPPLIER}/{supplierNumber}"; return await GetCasSupplierInformationByResourceAsync(resource); } else @@ -182,7 +220,8 @@ public async Task GetCasSupplierInformationByBn9Async(string? bn9) { if (!string.IsNullOrEmpty(bn9)) { - var resource = $"{casClientOptions.Value.CasBaseUrl}/{CFS_SUPPLIER}/{bn9}/businessnumber"; + var casBaseApi = await casBaseApiTask; + var resource = $"{casBaseApi}/{CFS_SUPPLIER}/{bn9}/businessnumber"; return await GetCasSupplierInformationByResourceAsync(resource); } else @@ -199,7 +238,8 @@ private async Task GetCasSupplierInformationByResourceAsync(string? res var authToken = await iTokenService.GetAuthTokenAsync(); try { - using (var response = await resilientHttpRequest.HttpAsync(HttpMethod.Get, resource, authToken)) { + using (var response = await resilientHttpRequest.HttpAsync(HttpMethod.Get, resource, authToken)) + { if (response != null) { if (response.Content != null && response.StatusCode != HttpStatusCode.NotFound) 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 b54839aa6..39115ece0 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 @@ -143,7 +143,7 @@ private static string GenerateReferenceNumberAsync(string referenceNumber, strin private static string GenerateSequenceNumberAsync(int sequenceNumber, int index) { - sequenceNumber = sequenceNumber + index; + sequenceNumber += index; return sequenceNumber.ToString("D4"); } @@ -282,13 +282,7 @@ private async Task GetLevel2ApprovalActionAsync(UpdatePay { 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."); - } - + .FirstOrDefaultAsync(a => a.Id == applicationId) ?? throw new BusinessException($"Application with Id {applicationId} not found."); var appForm = application.ApplicationForm ?? (application.ApplicationFormId != Guid.Empty ? await applicationFormRepository.GetAsync(application.ApplicationFormId) @@ -306,13 +300,13 @@ private async Task GetLevel2ApprovalActionAsync(UpdatePay private async Task CanPerformLevel1ActionAsync(PaymentRequestStatus status) { - List level1Approvals = new() { PaymentRequestStatus.L1Pending, PaymentRequestStatus.L1Declined }; + List level1Approvals = [PaymentRequestStatus.L1Pending, PaymentRequestStatus.L1Declined]; return await permissionChecker.IsGrantedAsync(PaymentsPermissions.Payments.L1ApproveOrDecline) && level1Approvals.Contains(status); } private async Task CanPerformLevel2ActionAsync(PaymentRequest payment, bool IsApprove) { - List level2Approvals = new() { PaymentRequestStatus.L2Pending, PaymentRequestStatus.L2Declined }; + List level2Approvals = [PaymentRequestStatus.L2Pending, PaymentRequestStatus.L2Declined]; // Rule AB#26693: Reject Payment Request update if violates L1 and L2 separation of duties var IsSameApprover = CurrentUser.Id == payment.ExpenseApprovals.FirstOrDefault(x => x.Type == ExpenseApprovalType.Level1)?.DecisionUserId; @@ -327,7 +321,7 @@ private async Task CanPerformLevel2ActionAsync(PaymentRequest payment, boo private async Task CanPerformLevel3ActionAsync(PaymentRequestStatus status) { - List level3Approvals = new() { PaymentRequestStatus.L3Pending, PaymentRequestStatus.L3Declined }; + List level3Approvals = [PaymentRequestStatus.L3Pending, PaymentRequestStatus.L3Declined]; return await permissionChecker.IsGrantedAsync(PaymentsPermissions.Payments.L3ApproveOrDecline) && level3Approvals.Contains(status); } @@ -395,19 +389,17 @@ protected internal async Task> MapToDtoAndLoadDetailsAsy var paymentDtos = ObjectMapper.Map, List>(paymentsList); // Flatten all DecisionUserIds from ExpenseApprovals across all PaymentRequestDtos - List paymentRequesterIds = paymentDtos + List paymentRequesterIds = [.. paymentDtos .Select(payment => payment.CreatorId) .OfType() - .Distinct() - .ToList(); + .Distinct()]; - List expenseApprovalCreatorIds = paymentDtos + List expenseApprovalCreatorIds = [.. paymentDtos .SelectMany(payment => payment.ExpenseApprovals) .Where(expenseApproval => expenseApproval.Status != ExpenseApprovalStatus.Requested) .Select(expenseApproval => expenseApproval.DecisionUserId) .OfType() - .Distinct() - .ToList(); + .Distinct()]; // Call external lookup for each distinct User Id and store in a dictionary. var userDictionary = new Dictionary(); diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentApprovals/UpdatePaymentRequestStatus.cshtml.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentApprovals/UpdatePaymentRequestStatus.cshtml.cs index fc55717f5..09a677ea0 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentApprovals/UpdatePaymentRequestStatus.cshtml.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentApprovals/UpdatePaymentRequestStatus.cshtml.cs @@ -21,7 +21,7 @@ public class PaymentGrouping { public int GroupId { get; set; } public PaymentRequestStatus ToStatus { get; set; } - public List Items { get; set; } = new(); + public List Items { get; set; } = []; } public class UpdatePaymentRequestStatus( @@ -32,7 +32,7 @@ public class UpdatePaymentRequestStatus( IPaymentConfigurationAppService paymentConfigurationAppService, IPermissionCheckerService permissionCheckerService) : AbpPageModel { - [BindProperty] public List PaymentGroupings { get; set; } = new(); + [BindProperty] public List PaymentGroupings { get; set; } = []; [BindProperty] public decimal? UserPaymentThreshold { get; set; } [BindProperty] public decimal PaymentThreshold { get; set; } [BindProperty] public bool DisableSubmit { get; set; } @@ -44,7 +44,7 @@ public class UpdatePaymentRequestStatus( [BindProperty] public string? Note { get; set; } = string.Empty; - public List SelectedPaymentIds { get; set; } = new(); + public List SelectedPaymentIds { get; set; } = []; public string FromStatusText { get; set; } = string.Empty; public async Task OnGetAsync(string paymentIds, bool isApprove) @@ -53,15 +53,7 @@ public async Task OnGetAsync(string paymentIds, bool isApprove) var payments = await paymentRequestAppService.GetListByPaymentIdsAsync(SelectedPaymentIds); var paymentApprovals = await BuildPaymentApprovalsAsync(payments); - PaymentGroupings = paymentApprovals - .GroupBy(item => item.ToStatus) - .Select((group, index) => new PaymentGrouping - { - GroupId = index, - ToStatus = group.Key, - Items = group.ToList() - }) - .ToList(); + PaymentGroupings = [.. paymentApprovals.GroupBy(item => item.ToStatus).Select((group, index) => new PaymentGrouping { GroupId = index, ToStatus = group.Key, Items = [.. group] })]; DisableSubmit = paymentApprovals.Count == 0 || !ModelState.IsValid; } @@ -70,7 +62,7 @@ private async Task InitializeStateAsync(string paymentIds, bool isApprove) { await GetFromStateForUserAsync(); IsApproval = isApprove; - SelectedPaymentIds = JsonSerializer.Deserialize>(paymentIds) ?? new(); + SelectedPaymentIds = JsonSerializer.Deserialize>(paymentIds) ?? []; UserPaymentThreshold = await paymentRequestAppService.GetUserPaymentThresholdAsync(); HasPaymentConfiguration = await paymentConfigurationAppService.GetAsync() != null; } diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Menus/ReportingMenuContributor.cs b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Menus/ReportingMenuContributor.cs index c299201fe..c3b47eb67 100644 --- a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Menus/ReportingMenuContributor.cs +++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Menus/ReportingMenuContributor.cs @@ -13,14 +13,7 @@ public async Task ConfigureMenuAsync(MenuConfigurationContext context) private static Task ConfigureReportingMenuAsync(MenuConfigurationContext context) { - //Add main menu items. - context.Menu.AddItem( - new ApplicationMenuItem( - ReportingMenus.Prefix, - displayName: "Reporting Admin", - "~/ReportingAdmin", - requiredPermissionName: IdentityConsts.ITAdminPermissionName - )); + context.Menu.AddItem( new ApplicationMenuItem( ReportingMenus.Prefix, diff --git a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/IResilientHttpRequest.cs b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/IResilientHttpRequest.cs index 3cfa35156..c572b1cfe 100644 --- a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/IResilientHttpRequest.cs +++ b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/IResilientHttpRequest.cs @@ -1,4 +1,5 @@ using System.Net.Http; +using System.Threading; using System.Threading.Tasks; using Volo.Abp; @@ -6,15 +7,21 @@ namespace Unity.Modules.Shared.Http { public interface IResilientHttpRequest : IRemoteService { - Task HttpAsyncWithBody(HttpMethod httpVerb, string resource, string? body = null, string? authToken = null); - Task HttpAsync(HttpMethod httpVerb, string resource, string? authToken = null); - Task ExecuteRequestAsync( + /// + /// Send an HTTP request with optional JSON body, authentication, and resilience policies. + /// If body is an object, it will be automatically serialized to JSON. + /// + Task HttpAsync( HttpMethod httpVerb, string resource, - string? body, - string? authToken, - (string username, string password)? basicAuth = null); + object? body = null, + string? authToken = null, + (string username, string password)? basicAuth = null, + CancellationToken cancellationToken = default); + /// + /// Set a base URL to be used for relative request paths. + /// void SetBaseUrl(string baseUrl); } -} +} \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/ResilientHttpRequest.cs b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/ResilientHttpRequest.cs index 76499540d..cfacccfd1 100644 --- a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/ResilientHttpRequest.cs +++ b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/ResilientHttpRequest.cs @@ -4,8 +4,11 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Text; +using System.Threading; using System.Threading.Tasks; using Volo.Abp; +using Newtonsoft.Json; namespace Unity.Modules.Shared.Http { @@ -23,6 +26,15 @@ public class ResilientHttpRequest(HttpClient httpClient) : IResilientHttpRequest private string? _baseUrl; private readonly HttpClient _httpClient = httpClient; + private static readonly HttpStatusCode[] RetryableStatusCodes = + [ + HttpStatusCode.TooManyRequests, + HttpStatusCode.InternalServerError, + HttpStatusCode.BadGateway, + HttpStatusCode.ServiceUnavailable, + HttpStatusCode.GatewayTimeout + ]; + public static void SetPipelineOptions( int maxRetryAttempts, TimeSpan pauseBetweenFailures, @@ -62,41 +74,19 @@ public void SetBaseUrl(string baseUrl) _baseUrl = baseUrl.TrimEnd('/'); } - private static bool ShouldRetry(HttpStatusCode statusCode) - { - var retryableStatusCodes = new[] - { - HttpStatusCode.TooManyRequests, - HttpStatusCode.InternalServerError, - HttpStatusCode.BadGateway, - HttpStatusCode.ServiceUnavailable, - HttpStatusCode.GatewayTimeout - }; - - return retryableStatusCodes.Contains(statusCode); - } - - public async Task HttpAsync(HttpMethod httpVerb, string resource, string? authToken = null) - { - return await ExecuteRequestAsync(httpVerb, resource, null, authToken); - } - - public async Task HttpAsyncWithBody(HttpMethod httpVerb, string resource, string? body = null, string? authToken = null) - { - return await ExecuteRequestAsync(httpVerb, resource, body, authToken); - } - - public static async Task ContentToStringAsync(HttpContent httpContent) - { - return await httpContent.ReadAsStringAsync(); - } - - public async Task ExecuteRequestAsync( - HttpMethod httpVerb, - string resource, - string? body, - string? authToken, - (string username, string password)? basicAuth = null) + private static bool ShouldRetry(HttpStatusCode statusCode) => + RetryableStatusCodes.Contains(statusCode); + + /// + /// Send an HTTP request with resilience policies applied. + /// + public async Task HttpAsync( + HttpMethod httpVerb, + string resource, + object? body = null, + string? authToken = null, + (string username, string password)? basicAuth = null, + CancellationToken cancellationToken = default) { // Determine final URL if (!Uri.TryCreate(resource, UriKind.Absolute, out Uri? fullUrl)) @@ -113,7 +103,7 @@ public async Task ExecuteRequestAsync( { using var requestMessage = new HttpRequestMessage(httpVerb, fullUrl) { - Version = new Version(3, 0) + Version = HttpVersion.Version20 // safer default, negotiates automatically }; // Headers are per-request, not global @@ -128,19 +118,33 @@ public async Task ExecuteRequestAsync( else if (basicAuth.HasValue) { var credentials = Convert.ToBase64String( - System.Text.Encoding.ASCII.GetBytes($"{basicAuth.Value.username}:{basicAuth.Value.password}") + Encoding.ASCII.GetBytes($"{basicAuth.Value.username}:{basicAuth.Value.password}") ); requestMessage.Headers.Remove(AuthorizationHeader); requestMessage.Headers.Add(AuthorizationHeader, $"Basic {credentials}"); } - if (!string.IsNullOrWhiteSpace(body)) + // Handle body if present + if (body != null) { - requestMessage.Content = new StringContent(body); + string bodyString = body is string s + ? s + : JsonConvert.SerializeObject(body); // allow passing objects directly + + requestMessage.Content = new StringContent( + bodyString, + Encoding.UTF8, + "application/json" + ); } return await _httpClient.SendAsync(requestMessage, ct); - }); + }, cancellationToken); + } + + public static async Task ContentToStringAsync(HttpContent httpContent) + { + return await httpContent.ReadAsStringAsync(); } } } diff --git a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Permissions/IdentityConsts.cs b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Permissions/IdentityConsts.cs index 46ce155c9..1a4b1583e 100644 --- a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Permissions/IdentityConsts.cs +++ b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Permissions/IdentityConsts.cs @@ -5,5 +5,10 @@ public static class IdentityConsts public const string ITAdminPolicyName = "ITAdministrator"; public const string ITAdminRoleName = "ITAdministrator"; public const string ITAdminPermissionName = "ITAdministrator"; + + public const string ITOperationsPolicyName = "ITOperations"; + public const string ITOperationsRoleName = "ITOperations"; + public const string ITOperationsPermissionName = "ITOperations"; + } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/INotificationsAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/INotificationsAppService.cs index 74c164345..24b50ac1c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/INotificationsAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/INotificationsAppService.cs @@ -6,7 +6,7 @@ namespace Unity.GrantManager.Notifications { public interface INotificationsAppService { - Task NotifyChefsEventToTeamsAsync(string factName, string factValue); + Task NotifyChefsEventToTeamsAsync(string factName, string factValue, bool alert = false); Task PostChefsEventToTeamsAsync(string subscriptionEvent, dynamic form, dynamic chefsFormVersion); Task PostToTeamsAsync(string activityTitle, string activitySubtitle); Task PostToTeamsAsync(string activityTitle, string activitySubtitle, List facts); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormAppService.cs index a72cf471e..d31d7400a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormAppService.cs @@ -19,42 +19,24 @@ namespace Unity.GrantManager.ApplicationForms; [Authorize] -public class ApplicationFormAppService : +public class ApplicationFormAppService(IRepository repository, + IStringEncryptionService stringEncryptionService, + IApplicationFormVersionAppService applicationFormVersionAppService, + IApplicationFormVersionRepository applicationFormVersionRepository, + IGrantApplicationAppService applicationService, + IFormsApiService formsApiService) : CrudAppService< ApplicationForm, ApplicationFormDto, Guid, PagedAndSortedResultRequestDto, - CreateUpdateApplicationFormDto>, + CreateUpdateApplicationFormDto>(repository), IApplicationFormAppService { - private readonly IStringEncryptionService _stringEncryptionService; - private readonly IFormsApiService _formsApiService; - private readonly IApplicationFormVersionAppService _applicationFormVersionAppService; - private readonly IApplicationFormVersionRepository _applicationFormVersionRepository; - private readonly IGrantApplicationAppService _applicationService; - private readonly IRepository _applicationFormRepository; - - public ApplicationFormAppService(IRepository repository, - IStringEncryptionService stringEncryptionService, - IApplicationFormVersionAppService applicationFormVersionAppService, - IApplicationFormVersionRepository applicationFormVersionRepository, - IGrantApplicationAppService applicationService, - IFormsApiService formsApiService) - : base(repository) - { - _stringEncryptionService = stringEncryptionService; - _applicationFormVersionAppService = applicationFormVersionAppService; - _formsApiService = formsApiService; - _applicationFormVersionRepository = applicationFormVersionRepository; - _applicationFormRepository = repository; - _applicationService = applicationService; - } - [Authorize(GrantManagerPermissions.ApplicationForms.Default)] public override async Task CreateAsync(CreateUpdateApplicationFormDto input) { - input.ApiKey = _stringEncryptionService.Encrypt(input.ApiKey); + input.ApiKey = stringEncryptionService.Encrypt(input.ApiKey); ApplicationFormDto applicationFormDto = await base.CreateAsync(input); return await InitializeFormVersion(applicationFormDto.Id, input); } @@ -63,7 +45,7 @@ public override async Task CreateAsync(CreateUpdateApplicati public override async Task UpdateAsync(Guid id, CreateUpdateApplicationFormDto input) { var existingForm = await Repository.GetAsync(id); - input.ApiKey = _stringEncryptionService.Encrypt(input.ApiKey); + input.ApiKey = stringEncryptionService.Encrypt(input.ApiKey); bool hasFormGuidChanged = existingForm.ChefsApplicationFormGuid != input.ChefsApplicationFormGuid; bool hasFormApiKeyChanged = existingForm.ApiKey != input.ApiKey; @@ -87,7 +69,7 @@ private async Task InitializeFormVersion(Guid id, CreateUpda { if (input.ChefsApplicationFormGuid != null && input.ApiKey != null) { - dynamic form = await _formsApiService.GetForm(Guid.Parse(input.ChefsApplicationFormGuid), input.ChefsApplicationFormGuid.ToString(), input.ApiKey); + dynamic form = await formsApiService.GetForm(Guid.Parse(input.ChefsApplicationFormGuid), input.ChefsApplicationFormGuid.ToString(), input.ApiKey); if (form != null) { JObject formObject = JObject.Parse(form.ToString()); @@ -98,7 +80,7 @@ private async Task InitializeFormVersion(Guid id, CreateUpda applicationFormDto = await base.UpdateAsync(id, input); } bool initializePublishedOnly = false; - await _applicationFormVersionAppService.InitializePublishedFormVersion(form, id, initializePublishedOnly); + await applicationFormVersionAppService.InitializePublishedFormVersion(form, id, initializePublishedOnly); } } return applicationFormDto; @@ -114,8 +96,8 @@ private async Task InitializeFormVersion(Guid id, CreateUpda public override async Task GetAsync(Guid id) { var dto = await base.GetAsync(id); - dto.ApiKey = _stringEncryptionService.Decrypt(dto.ApiKey); - dto.ApiToken = _stringEncryptionService.Decrypt(dto.ApiToken); + dto.ApiKey = stringEncryptionService.Decrypt(dto.ApiKey); + dto.ApiToken = stringEncryptionService.Decrypt(dto.ApiToken); return dto; } @@ -129,64 +111,64 @@ public async Task GetElectoralDistrictAddressTypeAsync(Guid id) [Authorize(GrantManagerPermissions.ApplicationForms.Default)] public async Task> GetPublishedVersionsAsync(Guid id) { - IQueryable queryableFormVersions = _applicationFormVersionRepository.GetQueryableAsync().Result; + IQueryable queryableFormVersions = applicationFormVersionRepository.GetQueryableAsync().Result; var formVersions = queryableFormVersions.Where(c => c.ApplicationFormId.Equals(id) && c.Published.Equals(true)).ToList(); - return await Task.FromResult>(ObjectMapper.Map, List>(formVersions.OrderByDescending(s => s.Version).ToList())); + return await Task.FromResult>(ObjectMapper.Map, List>([.. formVersions.OrderByDescending(s => s.Version)])); } [Authorize(GrantManagerPermissions.ApplicationForms.Default)] public async Task> GetVersionsAsync(Guid id) { - IQueryable queryableFormVersions = _applicationFormVersionRepository.GetQueryableAsync().Result; + IQueryable queryableFormVersions = applicationFormVersionRepository.GetQueryableAsync().Result; var formVersions = queryableFormVersions.Where(c => c.ApplicationFormId.Equals(id)).ToList(); - return await Task.FromResult>(ObjectMapper.Map, List>(formVersions.OrderByDescending(s => s.Version).ToList())); + return await Task.FromResult>(ObjectMapper.Map, List>([.. formVersions.OrderByDescending(s => s.Version)])); } [Authorize(GrantManagerPermissions.ApplicationForms.Default)] public async Task SaveApplicationFormScoresheet(FormScoresheetDto dto) { - var appForm = await _applicationFormRepository.GetAsync(dto.ApplicationFormId); + var appForm = await repository.GetAsync(dto.ApplicationFormId); appForm.ScoresheetId = dto.ScoresheetId; - await _applicationFormRepository.UpdateAsync(appForm); + await repository.UpdateAsync(appForm); } [Authorize(GrantManagerPermissions.ApplicationForms.Default)] public async Task PatchOtherConfig(Guid id, OtherConfigDto config) { - var form = await _applicationFormRepository.GetAsync(id); + var form = await repository.GetAsync(id); form.IsDirectApproval = config.IsDirectApproval; form.ElectoralDistrictAddressType = config.ElectoralDistrictAddressType; - await _applicationFormRepository.UpdateAsync(form); + await repository.UpdateAsync(form); } [Authorize(PaymentsPermissions.Payments.EditFormPaymentConfiguration)] public async Task GetFormPreventPaymentStatusByApplicationId(Guid applicationId) { // Get the payment threshold for the application - GrantApplicationDto grantApplicationDto = await _applicationService.GetAsync(applicationId); + GrantApplicationDto grantApplicationDto = await applicationService.GetAsync(applicationId); Guid formId = grantApplicationDto.ApplicationForm.Id; - ApplicationForm appForm = await _applicationFormRepository.GetAsync(formId); + ApplicationForm appForm = await repository.GetAsync(formId); return appForm.PreventPayment; } public async Task SavePaymentConfiguration(FormPaymentConfigurationDto dto) { - ApplicationForm appForm = await _applicationFormRepository.GetAsync(dto.ApplicationFormId); + ApplicationForm appForm = await repository.GetAsync(dto.ApplicationFormId); appForm.AccountCodingId = dto.AccountCodingId; appForm.Payable = dto.Payable; appForm.PreventPayment = dto.PreventPayment; appForm.PaymentApprovalThreshold = dto.PaymentApprovalThreshold; - await _applicationFormRepository.UpdateAsync(appForm); + await repository.UpdateAsync(appForm); } public async Task GetFormPaymentApprovalThresholdByApplicationIdAsync(Guid applicationId) { // Get the payment threshold for the application - GrantApplicationDto application = await _applicationService.GetAsync(applicationId); + GrantApplicationDto application = await applicationService.GetAsync(applicationId); Guid formId = application.ApplicationForm.Id; - ApplicationForm appForm = await _applicationFormRepository.GetAsync(formId); + ApplicationForm appForm = await repository.GetAsync(formId); return appForm.PaymentApprovalThreshold; } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormVersionAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormVersionAppService.cs index 1fd73c1cd..dc0b776b1 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormVersionAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormVersionAppService.cs @@ -21,43 +21,23 @@ namespace Unity.GrantManager.ApplicationForms { - public class ApplicationFormVersionAppService : + public class ApplicationFormVersionAppService( + IRepository repository, + IIntakeFormSubmissionMapper formSubmissionMapper, + IUnitOfWorkManager unitOfWorkManager, + IFormsApiService formsApiService, + IApplicationFormVersionRepository formVersionRepository, + IApplicationFormSubmissionRepository formSubmissionRepository, + IReportingFieldsGeneratorService reportingFieldsGeneratorService, + IFeatureChecker featureChecker) : CrudAppService< ApplicationFormVersion, ApplicationFormVersionDto, Guid, PagedAndSortedResultRequestDto, - CreateUpdateApplicationFormVersionDto>, + CreateUpdateApplicationFormVersionDto>(repository), IApplicationFormVersionAppService { - private readonly IApplicationFormVersionRepository _formVersionRepository; - private readonly IIntakeFormSubmissionMapper _formSubmissionMapper; - private readonly IUnitOfWorkManager _unitOfWorkManager; - private readonly IFormsApiService _formsApiService; - private readonly IApplicationFormSubmissionRepository _formSubmissionRepository; - private readonly IReportingFieldsGeneratorService _reportingFieldsGeneratorService; - private readonly IFeatureChecker _featureChecker; - - public ApplicationFormVersionAppService( - IRepository repository, - IIntakeFormSubmissionMapper formSubmissionMapper, - IUnitOfWorkManager unitOfWorkManager, - IFormsApiService formsApiService, - IApplicationFormVersionRepository formVersionRepository, - IApplicationFormSubmissionRepository formSubmissionRepository, - IReportingFieldsGeneratorService reportingFieldsGeneratorService, - IFeatureChecker featureChecker) - : base(repository) - { - _formVersionRepository = formVersionRepository; - _formSubmissionMapper = formSubmissionMapper; - _unitOfWorkManager = unitOfWorkManager; - _formsApiService = formsApiService; - _formSubmissionRepository = formSubmissionRepository; - _reportingFieldsGeneratorService = reportingFieldsGeneratorService; - _featureChecker = featureChecker; - } - public override async Task CreateAsync(CreateUpdateApplicationFormVersionDto input) => await base.CreateAsync(input); @@ -143,14 +123,14 @@ private async Task FormVersionDoesNotExist(string formVersionId) => ChefsFormVersionGuid = formVersionId }; - var formVersion = await _formsApiService.GetFormDataAsync(formId, formVersionId); + var formVersion = await formsApiService.GetFormDataAsync(formId, formVersionId); if (formVersion == null) // Ensure formVersion is not null { Logger.LogWarning("Form version data is null for formId: {FormId}, formVersionId: {FormVersionId}", formId, formVersionId); return null; } - applicationFormVersion.AvailableChefsFields = _formSubmissionMapper.InitializeAvailableFormFields(formVersion); + applicationFormVersion.AvailableChefsFields = formSubmissionMapper.InitializeAvailableFormFields(formVersion); if (formVersion is JObject formVersionObject) { @@ -170,19 +150,19 @@ private async Task FormVersionDoesNotExist(string formVersionId) => private async Task InsertApplicationFormVersion(ApplicationFormVersionDto applicationFormVersionDto) { var applicationFormVersion = ObjectMapper.Map(applicationFormVersionDto); - await _formVersionRepository.InsertAsync(applicationFormVersion); + await formVersionRepository.InsertAsync(applicationFormVersion); } public async Task GetFormVersionSubmissionMapping(string chefsFormVersionId) { - var applicationFormVersion = (await _formVersionRepository.GetQueryableAsync()) + var applicationFormVersion = (await formVersionRepository.GetQueryableAsync()) .FirstOrDefault(s => s.ChefsFormVersionGuid == chefsFormVersionId); return applicationFormVersion?.SubmissionHeaderMapping; } private async Task GetApplicationFormVersion(string chefsFormVersionId) => - (await _formVersionRepository.GetQueryableAsync()) + (await formVersionRepository.GetQueryableAsync()) .FirstOrDefault(s => s.ChefsFormVersionGuid == chefsFormVersionId); public async Task FormVersionExists(string chefsFormVersionId) => @@ -190,14 +170,14 @@ public async Task FormVersionExists(string chefsFormVersionId) => private async Task UnPublishFormVersions(Guid applicationFormId, string chefsFormVersionId) { - using var uow = _unitOfWorkManager.Begin(); - var applicationFormVersion = (await _formVersionRepository.GetQueryableAsync()) + using var uow = unitOfWorkManager.Begin(); + var applicationFormVersion = (await formVersionRepository.GetQueryableAsync()) .FirstOrDefault(s => s.ChefsFormVersionGuid != chefsFormVersionId && s.ApplicationFormId == applicationFormId); if (applicationFormVersion != null) { applicationFormVersion.Published = false; - await _formVersionRepository.UpdateAsync(applicationFormVersion); + await formVersionRepository.UpdateAsync(applicationFormVersion); await uow.SaveChangesAsync(); return true; } @@ -214,10 +194,10 @@ public async Task UpdateOrCreateApplicationFormVersio var applicationFormVersion = await GetOrCreateApplicationFormVersion(chefsFormId, chefsFormVersionId, applicationFormId); await UpdateApplicationFormVersionFields(applicationFormVersion, chefsFormVersion, applicationFormId, chefsFormVersionId); - if (await _featureChecker.IsEnabledAsync(FeatureConsts.Reporting) && + if (await featureChecker.IsEnabledAsync(FeatureConsts.Reporting) && string.IsNullOrEmpty(applicationFormVersion.ReportViewName)) { - await _reportingFieldsGeneratorService.GenerateAndSetAsync(applicationFormVersion); + await reportingFieldsGeneratorService.GenerateAndSetAsync(applicationFormVersion); } return ObjectMapper.Map(applicationFormVersion); @@ -226,7 +206,7 @@ public async Task UpdateOrCreateApplicationFormVersio private async Task GetOrCreateApplicationFormVersion(string chefsFormId, string chefsFormVersionId, Guid applicationFormId) { var applicationFormVersion = await GetApplicationFormVersion(chefsFormVersionId) ?? - (await _formVersionRepository.GetQueryableAsync()) + (await formVersionRepository.GetQueryableAsync()) .FirstOrDefault(s => s.ChefsApplicationFormGuid == chefsFormId && s.ChefsFormVersionGuid == null) ?? new ApplicationFormVersion { @@ -247,7 +227,7 @@ private async Task UpdateApplicationFormVersionFields(ApplicationFormVersion app var published = ((JObject)chefsFormVersion).SelectToken("published")?.ToString(); var schema = ((JObject)chefsFormVersion).SelectToken("schema")?.ToString(); - applicationFormVersion.AvailableChefsFields = _formSubmissionMapper.InitializeAvailableFormFields(chefsFormVersion); + applicationFormVersion.AvailableChefsFields = formSubmissionMapper.InitializeAvailableFormFields(chefsFormVersion); applicationFormVersion.FormSchema = schema != null ? ChefsFormIOReplacement.ReplaceAdvancedFormIoControls(schema) ?? string.Empty : string.Empty; if (version != null) @@ -262,20 +242,20 @@ private async Task UpdateApplicationFormVersionFields(ApplicationFormVersion app } if (applicationFormVersion.Id == Guid.Empty) - await _formVersionRepository.InsertAsync(applicationFormVersion, true); + await formVersionRepository.InsertAsync(applicationFormVersion, true); else - await _formVersionRepository.UpdateAsync(applicationFormVersion, true); + await formVersionRepository.UpdateAsync(applicationFormVersion, true); } public async Task GetByChefsFormVersionId(Guid chefsFormVersionId) { - var applicationFormVersion = await _formVersionRepository.GetByChefsFormVersionAsync(chefsFormVersionId); + var applicationFormVersion = await formVersionRepository.GetByChefsFormVersionAsync(chefsFormVersionId); return applicationFormVersion == null ? null : ObjectMapper.Map(applicationFormVersion); } public async Task GetFormVersionByApplicationIdAsync(Guid applicationId) { - var formSubmission = await _formSubmissionRepository.GetByApplicationAsync(applicationId); + var formSubmission = await formSubmissionRepository.GetByApplicationAsync(applicationId); if (formSubmission.FormVersionId == null) { @@ -306,7 +286,7 @@ private async Task HandleEmptyFormVersionIdAsync(ApplicationFormSubmission var formVersionId = Guid.Parse(formVersionIdString); formSubmission.FormVersionId = formVersionId; - await _formSubmissionRepository.UpdateAsync(formSubmission); + await formSubmissionRepository.UpdateAsync(formSubmission); return await GetVersion(formVersionId); } catch @@ -317,17 +297,17 @@ private async Task HandleEmptyFormVersionIdAsync(ApplicationFormSubmission public async Task DeleteWorkSheetMappingByFormName(string formName, Guid formVersionId) { - var applicationFormVersion = await _formVersionRepository.GetAsync(formVersionId); + var applicationFormVersion = await formVersionRepository.GetAsync(formVersionId); if (applicationFormVersion?.SubmissionHeaderMapping == null) return; var pattern = $"(,\\s*\\\"{formName}.*\\\")|(\\\"{formName}.*\\\",)"; applicationFormVersion.SubmissionHeaderMapping = Regex.Replace(applicationFormVersion.SubmissionHeaderMapping, pattern, "", RegexOptions.None, TimeSpan.FromSeconds(30)); - await _formVersionRepository.UpdateAsync(applicationFormVersion); + await formVersionRepository.UpdateAsync(applicationFormVersion); } private async Task GetVersion(Guid formVersionId) { - var formVersion = await _formVersionRepository.GetByChefsFormVersionAsync(formVersionId); + var formVersion = await formVersionRepository.GetByChefsFormVersionAsync(formVersionId); return formVersion?.Version ?? 0; } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ConfigureIntakeClientOptions.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ConfigureIntakeClientOptions.cs index da6124905..21b592681 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ConfigureIntakeClientOptions.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ConfigureIntakeClientOptions.cs @@ -1,28 +1,26 @@ +using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; using Unity.GrantManager.Intake; +using Unity.GrantManager.Integrations; namespace Unity.GrantManager; -public class ConfigureIntakeClientOptions : IConfigureOptions -{ - private readonly IConfiguration _configuration; - const string PROTOCOL = "https://"; - const string DefaultBaseUri = $"{PROTOCOL}submit.digital.gov.bc.ca/app/api/v1"; - - - public ConfigureIntakeClientOptions(IConfiguration configuration) - { - _configuration = configuration; - } - +public class ConfigureIntakeClientOptions( + IConfiguration configuration, + IEndpointManagementAppService endpointManagementAppService) : IConfigureOptions +{ public void Configure(IntakeClientOptions options) { - options.BaseUri = DefaultBaseUri; - options.BearerTokenPlaceholder = _configuration["Intake:BearerTokenPlaceholder"] ?? ""; - options.UseBearerToken = _configuration.GetValue("Intake:UseBearerToken"); - options.AllowUnregisteredVersions = _configuration.GetValue("Intake:AllowUnregisteredVersions"); + // Note: GetUrlByKeyNameAsync is async, but IConfigureOptions.Configure must be sync. + // If possible, use a sync alternative or ensure the value is available synchronously. + // Here, we block on the async call (not ideal, but sometimes necessary in options pattern). + var intakeBaseUri = endpointManagementAppService.GetUrlByKeyNameAsync(DynamicUrlKeyNames.INTAKE_API_BASE).GetAwaiter().GetResult(); + options.BaseUri = intakeBaseUri; + options.BearerTokenPlaceholder = configuration["Intake:BearerTokenPlaceholder"] ?? ""; + options.UseBearerToken = configuration.GetValue("Intake:UseBearerToken"); + options.AllowUnregisteredVersions = configuration.GetValue("Intake:AllowUnregisteredVersions"); } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs index 8b77662ca..5483dc4b9 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs @@ -129,6 +129,7 @@ public override void ConfigureServices(ServiceConfigurationContext context) context.Services.AddTransient, ConfigureIntakeClientOptions>(); context.Services.AddTransient(); context.Services.AddTransient(); + context.Services.Configure(configuration.GetSection("Payments")); context.Services.Configure(configuration.GetSection("CssApi")); context.Services.Configure(configuration.GetSection("Notifications")); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/IntakeSubmissionAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/IntakeSubmissionAppService.cs index bc044b31e..43dc33266 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/IntakeSubmissionAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/IntakeSubmissionAppService.cs @@ -58,7 +58,7 @@ private async Task ValidateSubmission(EventSubscriptionDto eventSubscripti if (tokenDraft != null && tokenDraft.ToString() == "True") { string factName = "A draft submission was submitted and should not have been"; string factValue = $"FormId: {eventSubscriptionDto.FormId} SubmissionID: {eventSubscriptionDto.SubmissionId}"; - await notificationsAppService.NotifyChefsEventToTeamsAsync(factName, factValue); + await notificationsAppService.NotifyChefsEventToTeamsAsync(factName, factValue, true); return false; } @@ -66,7 +66,7 @@ private async Task ValidateSubmission(EventSubscriptionDto eventSubscripti if (tokenDeleted != null && tokenDeleted.ToString() == "True") { string factName = "A deleted submission was submitted - user navigated back and got a success message from chefs"; string factValue = $"FormId: {eventSubscriptionDto.FormId} SubmissionID: {eventSubscriptionDto.SubmissionId}"; - await notificationsAppService.NotifyChefsEventToTeamsAsync(factName, factValue); + await notificationsAppService.NotifyChefsEventToTeamsAsync(factName, factValue, false); } // If there are no mappings initialize the available @@ -81,7 +81,7 @@ private async Task ValidateSubmission(EventSubscriptionDto eventSubscripti { string factName = "Application Form Version Not Registered - Unknown Version"; string factValue = $"FormId: {eventSubscriptionDto.FormId} FormVersion: {eventSubscriptionDto.FormVersion}"; - await notificationsAppService.NotifyChefsEventToTeamsAsync(factName, factValue); + await notificationsAppService.NotifyChefsEventToTeamsAsync(factName, factValue, true); return false; } else //if(!intakeClientOptions.Value.AllowUnregisteredVersions) { @@ -89,7 +89,7 @@ private async Task ValidateSubmission(EventSubscriptionDto eventSubscripti var published = ((JObject)formVersion!).SelectToken("published"); string factName = "Application Form Version Not Registered - Unknown Version"; string factValue = $"Application Form Version Not Registerd - Version: {version} Published: {published}"; - await notificationsAppService.NotifyChefsEventToTeamsAsync(factName, factValue); + await notificationsAppService.NotifyChefsEventToTeamsAsync(factName, factValue, true); return false; } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs index b3f80eab7..058ac8b3f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs @@ -17,49 +17,31 @@ namespace Unity.GrantManager.Integrations.Chefs { [IntegrationService] [ExposeServices(typeof(IFormsApiService))] - public class FormsApiService : GrantManagerAppService, IFormsApiService + public class FormsApiService( + IEndpointManagementAppService endpointManagementAppService, + IApplicationFormRepository applicationFormRepository, + IStringEncryptionService stringEncryptionService, + IResilientHttpRequest resilientRestClient, + ILogger logger) : GrantManagerAppService, IFormsApiService { - private readonly IEndpointManagementAppService _endpointManagementAppService; - private readonly IApplicationFormRepository _applicationFormRepository; - private readonly IStringEncryptionService _stringEncryptionService; - private readonly IResilientHttpRequest _resilientRestClient; - private readonly ILogger _logger; - private string? _chefsApiBaseUrl; - public FormsApiService( - IEndpointManagementAppService endpointManagementAppService, - IApplicationFormRepository applicationFormRepository, - IStringEncryptionService stringEncryptionService, - IResilientHttpRequest resilientRestClient, - ILogger logger) - { - _endpointManagementAppService = endpointManagementAppService; - _applicationFormRepository = applicationFormRepository; - _stringEncryptionService = stringEncryptionService; - _resilientRestClient = resilientRestClient; - _logger = logger; - } - private async Task GetChefsApiBaseUrlAsync() { - if (_chefsApiBaseUrl == null) - { - _chefsApiBaseUrl = await _endpointManagementAppService.GetChefsApiBaseUrlAsync(); - } + _chefsApiBaseUrl ??= await endpointManagementAppService.GetChefsApiBaseUrlAsync(); return _chefsApiBaseUrl; } public async Task GetFormDataAsync(string chefsFormId, string chefsFormVersionId) { - var applicationForm = await (await _applicationFormRepository.GetQueryableAsync()) + var applicationForm = await (await applicationFormRepository.GetQueryableAsync()) .Where(s => s.ChefsApplicationFormGuid == chefsFormId) .OrderBy(s => s.CreationTime) .FirstOrDefaultAsync(); if (applicationForm == null) { - _logger.LogWarning("No application form found for FormId: {FormId}", chefsFormId); + logger.LogWarning("No application form found for FormId: {FormId}", chefsFormId); return null; } @@ -81,19 +63,19 @@ public async Task GetForm(Guid? formId, string chefsApplicationFormGuid string url = $"{chefsApi}/forms/{formId}"; var response = await GetRequestAsync(url, chefsApplicationFormGuid, encryptedApiKey); - return await ParseJsonResponseAsync(response) ?? new JObject(); + return await ParseJsonResponseAsync(response) ?? []; } public async Task GetSubmissionDataAsync(Guid chefsFormId, Guid submissionId) { - var applicationForm = await (await _applicationFormRepository.GetQueryableAsync()) + var applicationForm = await (await applicationFormRepository.GetQueryableAsync()) .Where(s => s.ChefsApplicationFormGuid == chefsFormId.ToString()) .OrderBy(s => s.CreationTime) .FirstOrDefaultAsync(); if (applicationForm == null) { - _logger.LogWarning("No application form found for SubmissionId: {SubmissionId}", submissionId); + logger.LogWarning("No application form found for SubmissionId: {SubmissionId}", submissionId); return null; } @@ -109,14 +91,14 @@ private async Task GetRequestAsync(string url, string chefs if (string.IsNullOrWhiteSpace(encryptedApiKey)) throw new ArgumentException("API key is missing or empty"); - var decryptedApiKey = _stringEncryptionService.Decrypt(encryptedApiKey) ?? string.Empty; - _logger.LogInformation( + var decryptedApiKey = stringEncryptionService.Decrypt(encryptedApiKey) ?? string.Empty; + logger.LogInformation( "Sending GET request to {Url} using basic auth with FormGuid: {FormGuid}", url, chefsApplicationFormGuid ); - var response = await _resilientRestClient.ExecuteRequestAsync( + var response = await resilientRestClient.HttpAsync( HttpMethod.Get, url, null, @@ -127,7 +109,7 @@ private async Task GetRequestAsync(string url, string chefs if (!response.IsSuccessStatusCode) { var content = await response.Content.ReadAsStringAsync(); - _logger.LogError( + logger.LogError( "Request to {Url} failed with status {StatusCode} ({Reason}). Response: {Content}", url, response.StatusCode, diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Css/CssApiOptions.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Css/CssApiOptions.cs index 939a6d72d..a007e08f8 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Css/CssApiOptions.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Css/CssApiOptions.cs @@ -1,11 +1,9 @@ namespace Unity.GrantManager.Integrations.Sso { public class CssApiOptions - { - public string TokenUrl { get; set; } = string.Empty; + { public string ClientId { get; set; } = string.Empty; public string ClientSecret { get; set; } = string.Empty; - public string Url { get; set; } = string.Empty; public string Env { get; set; } = string.Empty; } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Css/CssApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Css/CssApiService.cs index 80b456049..b4d147d3d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Css/CssApiService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Css/CssApiService.cs @@ -20,23 +20,15 @@ namespace Unity.GrantManager.Integrations.Sso { [IntegrationService] [ExposeServices(typeof(CssApiService), typeof(ICssUsersApiService))] - public class CssApiService : ApplicationService, ICssUsersApiService + public class CssApiService( + IEndpointManagementAppService endpointManagementAppService, + IResilientHttpRequest resilientHttpRequest, + IDistributedCache accessTokenCache, + IOptions cssApiOptions) : ApplicationService, ICssUsersApiService { - private readonly IResilientHttpRequest _resilientHttpRequest; - private readonly IDistributedCache _accessTokenCache; - private readonly CssApiOptions _cssApiOptions; + private readonly CssApiOptions _cssApiOptions = cssApiOptions.Value; private const string CSS_API_KEY = "CssApiKey"; - public CssApiService( - IResilientHttpRequest resilientHttpRequest, - IDistributedCache accessTokenCache, - IOptions cssApiOptions) - { - _resilientHttpRequest = resilientHttpRequest; - _accessTokenCache = accessTokenCache; - _cssApiOptions = cssApiOptions.Value; - } - public async Task FindUserAsync(string directory, string guid) { var parameters = new Dictionary { { nameof(guid), guid } }; @@ -58,11 +50,12 @@ public async Task SearchUsersAsync(string directory, string? f private async Task SearchSsoAsync(string directory, Dictionary parameters) { + var cssApiUrl = await endpointManagementAppService.GetUrlByKeyNameAsync(DynamicUrlKeyNames.CSS_API_BASE); var tokenResponse = await GetAccessTokenAsync(); - var baseUrl = $"{_cssApiOptions.Url}/{_cssApiOptions.Env}/{directory}/users"; + var baseUrl = $"{cssApiUrl}/{_cssApiOptions.Env}/{directory}/users"; var url = BuildUrlWithQuery(baseUrl, parameters); - var response = await _resilientHttpRequest.ExecuteRequestAsync(HttpMethod.Get, url, null, tokenResponse.AccessToken); + var response = await resilientHttpRequest.HttpAsync(HttpMethod.Get, url, null, tokenResponse.AccessToken); if (response != null && response.IsSuccessStatusCode && response.Content != null) { var json = await response.Content.ReadAsStringAsync(); @@ -75,7 +68,7 @@ private async Task SearchSsoAsync(string directory, Dictionary { Success = false, Error = "Failed to search users", - Data = Array.Empty() + Data = [] }; } @@ -87,12 +80,13 @@ private static string BuildUrlWithQuery(string basePath, Dictionary GetAccessTokenAsync() { - var cachedToken = await _accessTokenCache.GetAsync(CSS_API_KEY); + var cachedToken = await accessTokenCache.GetAsync(CSS_API_KEY); if (cachedToken != null) return cachedToken; var client = new HttpClient(); - var request = new HttpRequestMessage(HttpMethod.Post, _cssApiOptions.TokenUrl); + var cssTokenApiUrl = await endpointManagementAppService.GetUrlByKeyNameAsync(DynamicUrlKeyNames.CSS_TOKEN_API_BASE); + var request = new HttpRequestMessage(HttpMethod.Post, cssTokenApiUrl); var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{_cssApiOptions.ClientId}:{_cssApiOptions.ClientSecret}")); request.Headers.Authorization = new AuthenticationHeaderValue("Basic", credentials); @@ -110,7 +104,7 @@ private async Task GetAccessTokenAsync() var content = await response.Content.ReadAsStringAsync(); var tokenResponse = JsonSerializer.Deserialize(content) ?? throw new UserFriendlyException("Could not parse token response."); - await _accessTokenCache.SetAsync(CSS_API_KEY, tokenResponse, new DistributedCacheEntryOptions + await accessTokenCache.SetAsync(CSS_API_KEY, tokenResponse, new DistributedCacheEntryOptions { AbsoluteExpiration = DateTimeOffset.UtcNow.AddSeconds(tokenResponse.ExpiresIn) }); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Geocoder/GeocoderApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Geocoder/GeocoderApiService.cs index 4c3d6c3a4..39d9ab5d9 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Geocoder/GeocoderApiService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Geocoder/GeocoderApiService.cs @@ -1,30 +1,27 @@ -using Microsoft.AspNetCore.Authorization; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Configuration; using Newtonsoft.Json; -using RestSharp; -using System.Threading.Tasks; -using Unity.GrantManager.Integration.Geocoder; using Unity.GrantManager.Integrations.Exceptions; -using Unity.GrantManager.Integrations.Http; +using Unity.Modules.Shared.Http; using Volo.Abp.Application.Services; namespace Unity.GrantManager.Integrations.Geocoder { - //[IntegrationService] - //[ExposeServices(typeof(GeocoderApiService), typeof(IGeocoderApiService))] [AllowAnonymous] - public class GeocoderApiService(IResilientHttpRequest resilientRestClient, IConfiguration configuration) : ApplicationService, IGeocoderApiService + public class GeocoderApiService(IResilientHttpRequest resilientRestClient, IConfiguration configuration, IEndpointManagementAppService endpointManagementAppService) : ApplicationService { public async Task GetAddressDetailsAsync(string address) { var resource = $"{configuration["Geocoder:LocationDetails:BaseUri"]}/addresses.json?outputSRS=3005&addressString={address}"; - return ResultMapper.MapToLocation(await GetGeoCodeDataSegmentAsync(resource)); } public async Task GetElectoralDistrictAsync(LocationCoordinates locationCoordinates) { - var resource = $"{configuration["Geocoder:BaseUri"]}" + + var geoCoderBaseUri = await endpointManagementAppService.GetUrlByKeyNameAsync(DynamicUrlKeyNames.GEOCODER_API_BASE); + var resource = $"{geoCoderBaseUri}" + $"{configuration["Geocoder:ElectoralDistrict:feature"]}" + $"&srsname=EPSG:4326" + $"&propertyName={configuration["Geocoder:ElectoralDistrict:property"]}" + @@ -63,20 +60,17 @@ public async Task GetRegionalDistrictAsync(LocationCoordina private async Task GetGeoCodeDataSegmentAsync(string resource) { - var response = await resilientRestClient.HttpAsync(Method.Get, resource); + var response = await resilientRestClient.HttpAsync(HttpMethod.Get, resource, null, null); - if (response != null - && response.Content != null - && response.IsSuccessStatusCode) + if (response != null && response.IsSuccessStatusCode && response.Content != null) { - string content = response.Content; + var content = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject(content)!; } else { - throw new IntegrationServiceException($"Error with integrating with request resource"); + throw new IntegrationServiceException($"Error integrating with resource: {resource}. Status: {response?.StatusCode}"); } } } } - diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/OrgBook/OrgBookService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/OrgBook/OrgBookService.cs index d46e9aad2..71890ce75 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/OrgBook/OrgBookService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/OrgBook/OrgBookService.cs @@ -6,79 +6,78 @@ using Volo.Abp.DependencyInjection; using System.Net.Http; using Unity.Modules.Shared.Http; +using System; namespace Unity.GrantManager.Integrations.Orgbook { - [ExposeServices(typeof(OrgBookService), typeof(IOrgBookService))] public class OrgBookService : ApplicationService, IOrgBookService { - private readonly IResilientHttpRequest _resilientRestClient; + private readonly IResilientHttpRequest resilientRestClient; + private readonly Task orgbookBaseApiTask; + + private const string OrgbookQueryMatch = "inactive=any&latest=any&revoked=any&ordering=-score"; + + public OrgBookService( + IResilientHttpRequest resilientRestClient, + IEndpointManagementAppService endpointManagementAppService) + { + this.resilientRestClient = resilientRestClient; - private readonly string orgbook_base_api = "https://orgbook.gov.bc.ca/api"; - private readonly string orgbook_query_match = "inactive=any&latest=any&revoked=any&ordering=-score"; + // Initialize the base API URL once during construction + orgbookBaseApiTask = InitializeBaseApiAsync(endpointManagementAppService); + } - public OrgBookService(IResilientHttpRequest resilientRestClient) { - _resilientRestClient = resilientRestClient; + private static async Task InitializeBaseApiAsync(IEndpointManagementAppService endpointManagementAppService) + { + var url = await endpointManagementAppService.GetUrlByKeyNameAsync(DynamicUrlKeyNames.ORGBOOK_API_BASE); + return url ?? throw new IntegrationServiceException("OrgBook API base URL is not configured."); } public async Task GetOrgBookQueryAsync(string orgBookQuery) { - var response = await _resilientRestClient - .ExecuteRequestAsync(HttpMethod.Get, $"{orgbook_base_api}/v4/search/topic?q={orgBookQuery}&{orgbook_query_match}", null, null, null); - - if (response != null && response.Content != null) - { - var content = await response.Content.ReadAsStringAsync(); - return JsonConvert.DeserializeObject(content)!; - } - else - { - throw new IntegrationServiceException("GetOrgBookByNumberAsync -> No Response"); - } + var baseApi = await orgbookBaseApiTask; + var queryParams = $"q={Uri.EscapeDataString(orgBookQuery)}&{OrgbookQueryMatch}"; + var response = await resilientRestClient.HttpAsync( + HttpMethod.Get, + $"{baseApi}/v4/search/topic?{queryParams}", + null, null, null); + + if (response?.Content == null) + throw new IntegrationServiceException("OrgBook query request returned no response."); + + var content = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(content)!; } public async Task GetOrgBookAutocompleteQueryAsync(string? orgBookQuery) { - if (orgBookQuery == null) - { + if (string.IsNullOrWhiteSpace(orgBookQuery)) return JsonDocument.Parse("{}"); - } - - var response = await _resilientRestClient - .ExecuteRequestAsync(HttpMethod.Get, $"{orgbook_base_api}/v3/search/autocomplete?q={orgBookQuery}&revoked=false&inactive=", null, null, null); - - if (response != null && response.Content != null) - { - var content = await response.Content.ReadAsStringAsync(); - return JsonDocument.Parse(content); - } - else - { - throw new IntegrationServiceException("Failed to connect to Org Book"); - } + + var baseApi = await orgbookBaseApiTask; + var url = $"{baseApi}/v3/search/autocomplete?q={Uri.EscapeDataString(orgBookQuery)}&revoked=false&inactive="; + var response = await resilientRestClient.HttpAsync(HttpMethod.Get, url, null, null, null); + + if (response?.Content == null) + throw new IntegrationServiceException("OrgBook autocomplete request returned no response."); + + return JsonDocument.Parse(await response.Content.ReadAsStringAsync()); } public async Task GetOrgBookDetailsQueryAsync(string? orgBookId) { - if (orgBookId == null) - { + if (string.IsNullOrWhiteSpace(orgBookId)) return JsonDocument.Parse("{}"); - } - - var response = await _resilientRestClient - .ExecuteRequestAsync(HttpMethod.Get, $"{orgbook_base_api}/v2/topic/ident/registration.registries.ca/{orgBookId}/formatted", null, null, null); - - if (response != null && response.Content != null) - { - var content = await response.Content.ReadAsStringAsync(); - return JsonDocument.Parse(content); - } - else - { - throw new IntegrationServiceException("Failed to connect to Org Book"); - } + + var baseApi = await orgbookBaseApiTask; + var url = $"{baseApi}/v2/topic/ident/registration.registries.ca/{Uri.EscapeDataString(orgBookId)}/formatted"; + var response = await resilientRestClient.HttpAsync(HttpMethod.Get, url, null, null, null); + + if (response?.Content == null) + throw new IntegrationServiceException("OrgBook details request returned no response."); + + return JsonDocument.Parse(await response.Content.ReadAsStringAsync()); } } } - diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Norifications/NotificationsAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Norifications/NotificationsAppService.cs index 6fecf772b..b01b2881d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Norifications/NotificationsAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Norifications/NotificationsAppService.cs @@ -35,9 +35,9 @@ public async Task InitializeTeamsChannelAsync(string keyName) } [RemoteService(false)] - public async Task NotifyChefsEventToTeamsAsync(string factName, string factValue) + public async Task NotifyChefsEventToTeamsAsync(string factName, string factValue, bool alert = false) { - string teamsChannel = await InitializeTeamsChannelAsync(TeamsNotificationService.TEAMS_NOTIFICATION); + string teamsChannel = await InitializeTeamsChannelAsync(alert ? TeamsNotificationService.TEAMS_ALERT : TeamsNotificationService.TEAMS_NOTIFICATION); if (teamsChannel.IsNullOrEmpty()) { return; @@ -76,7 +76,7 @@ public async Task PostToTeamsAsync(string activityTitle, string activitySubtitle public async Task PostChefsEventToTeamsAsync(string subscriptionEvent, dynamic form, dynamic chefsFormVersion) { - string teamsChannel = await InitializeTeamsChannelAsync(TeamsNotificationService.TEAMS_NOTIFICATION_1); + string teamsChannel = await InitializeTeamsChannelAsync(TeamsNotificationService.TEAMS_NOTIFICATION); if (teamsChannel.IsNullOrEmpty()) { return; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrl.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Integrations/DynamicUrl.cs similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrl.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Integrations/DynamicUrl.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlKeyNames.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Integrations/DynamicUrlKeyNames.cs similarity index 63% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlKeyNames.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Integrations/DynamicUrlKeyNames.cs index 9912148a0..658f05c77 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlKeyNames.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Integrations/DynamicUrlKeyNames.cs @@ -2,12 +2,15 @@ namespace Unity.GrantManager.Integrations; public static class DynamicUrlKeyNames { + public const string CSS_API_BASE = "CSS_API_BASE"; + public const string CSS_TOKEN_API_BASE = "CSS_TOKEN_API_BASE"; public const string INTAKE_API_BASE = "INTAKE_API_BASE"; public const string PAYMENT_API_BASE = "PAYMENT_API_BASE"; + public const string ORGBOOK_API_BASE = "ORGBOOK_API_BASE"; public const string NOTFICATION_API_BASE = "NOTFICATION_API_BASE"; public const string NOTFICATION_AUTH = "NOTFICATION_AUTH"; public const string DIRECT_MESSAGE_KEY_PREFIX = "DIRECT_MESSAGE_"; // Teams Direct Message URL Weebhook- Dynamically incremented public const string WEBHOOK_KEY_PREFIX = "WEBHOOK_"; // General Webhook URL - Dynamically incremented -} - - + public const string GEOCODER_API_BASE = "GEOCODER_API_BASE"; + public const string GEOCODER_LOCATION_API_BASE = "GEOCODER_LOCATION_API_BASE"; +} \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlDataSeeder.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlDataSeeder.cs index 998de130e..d7018d68a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlDataSeeder.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlDataSeeder.cs @@ -19,11 +19,16 @@ public async Task SeedAsync(DataSeedContext context) public static class DynamicUrls { - public const string PROTOCOL = "https://"; - public const string CHEFS_PROD_URL = $"{PROTOCOL}submit.digital.gov.bc.ca/app/api/v1"; - public const string CAS_PROD_URL = ""; // Not entered for security reasons - public const string CHES_PROD_URL = $"{PROTOCOL}ches.api.gov.bc.ca/api/v1"; - public const string CHES_PROD_AUTH = $"{PROTOCOL}loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token"; + public const string PROTOCOL = "https:"; + public const string CHEFS_PROD_URL = $"{PROTOCOL}//submit.digital.gov.bc.ca/app/api/v1"; + public const string CAS_PROD_URL = $"{PROTOCOL}//cfs-systws.cas.gov.bc.ca:7026/ords/cas"; // Not entered for security reasons + public const string CHES_PROD_URL = $"{PROTOCOL}//ches.api.gov.bc.ca/api/v1"; + public const string CHES_PROD_AUTH = $"{PROTOCOL}//loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token"; + public const string ORGBOOK_PROD_URL = $"{PROTOCOL}//orgbook.gov.bc.ca/api"; + public const string CSS_API_BASE_URL = $"{PROTOCOL}//api.loginproxy.gov.bc.ca/api/v1"; + public const string CSS_TOKEN_API_BASE_URL = $"{PROTOCOL}//loginproxy.gov.bc.ca/auth/realms/standard/protocol/openid-connect/token"; + public const string GEOCODER_BASE_URL = $"{PROTOCOL}//openmaps.gov.bc.ca/geo/pub/ows?service=WFS&version=1.0.0&request=GetFeature&typeName="; + public const string GEOCODER_LOCATION_BASE_URL = $"{PROTOCOL}//geocoder.api.gov.bc.ca"; } private async Task SeedDynamicUrlAsync() @@ -31,17 +36,22 @@ private async Task SeedDynamicUrlAsync() int messageIndex = 0; int webhookIndex = 0; var dynamicUrls = new List - { - new() { KeyName = DynamicUrlKeyNames.PAYMENT_API_BASE, Url = DynamicUrls.CAS_PROD_URL, Description = "BC Corporate Accounting Services API" }, - new() { KeyName = DynamicUrlKeyNames.INTAKE_API_BASE, Url = DynamicUrls.CHEFS_PROD_URL, Description = "Common Hosted Forms Service API" }, - new() { KeyName = DynamicUrlKeyNames.NOTFICATION_API_BASE, Url = DynamicUrls.CHES_PROD_URL, Description = "Common Hosted Email Service API" }, - new() { KeyName = DynamicUrlKeyNames.NOTFICATION_AUTH, Url = DynamicUrls.CHES_PROD_AUTH, Description = "Common Hosted Email Service OAUTH" }, - new() { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{messageIndex++}", Url = "", Description = $"Direct message webhook {messageIndex}" }, - new() { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{messageIndex++}", Url = "", Description = $"Direct message webhook {messageIndex}" }, - new() { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{messageIndex++}", Url = "", Description = $"Direct message webhook {messageIndex}" }, - new() { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{webhookIndex++}", Url = "", Description = $"Webhook {webhookIndex}" }, - new() { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{webhookIndex++}", Url = "", Description = $"Webhook {webhookIndex}" }, - new() { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{webhookIndex++}", Url = "", Description = $"Webhook {webhookIndex}" }, + { + new() { KeyName = DynamicUrlKeyNames.GEOCODER_API_BASE, Url = DynamicUrls.GEOCODER_BASE_URL, Description = "Geocoder API Base" }, + new() { KeyName = DynamicUrlKeyNames.GEOCODER_LOCATION_API_BASE, Url = DynamicUrls.GEOCODER_LOCATION_BASE_URL, Description = "Geocoder Location API Base" }, + new() { KeyName = DynamicUrlKeyNames.CSS_API_BASE, Url = DynamicUrls.CSS_API_BASE_URL, Description = "Common Single Sign-on Services API" }, + new() { KeyName = DynamicUrlKeyNames.CSS_TOKEN_API_BASE, Url = DynamicUrls.CSS_TOKEN_API_BASE_URL, Description = "Common Single Sign-on Token API" }, + new() { KeyName = DynamicUrlKeyNames.PAYMENT_API_BASE, Url = DynamicUrls.CAS_PROD_URL, Description = "BC Corporate Accounting Services API" }, + new() { KeyName = DynamicUrlKeyNames.ORGBOOK_API_BASE, Url = DynamicUrls.ORGBOOK_PROD_URL, Description = "OrgBook Services API" }, + new() { KeyName = DynamicUrlKeyNames.INTAKE_API_BASE, Url = DynamicUrls.CHEFS_PROD_URL, Description = "Common Hosted Forms Service API" }, + new() { KeyName = DynamicUrlKeyNames.NOTFICATION_API_BASE, Url = DynamicUrls.CHES_PROD_URL, Description = "Common Hosted Email Service API" }, + new() { KeyName = DynamicUrlKeyNames.NOTFICATION_AUTH, Url = DynamicUrls.CHES_PROD_AUTH, Description = "Common Hosted Email Service OAUTH" }, + new() { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{messageIndex++}", Url = "", Description = $"Direct message webhook {messageIndex}" }, + new() { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{messageIndex++}", Url = "", Description = $"Direct message webhook {messageIndex}" }, + new() { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{messageIndex++}", Url = "", Description = $"Direct message webhook {messageIndex}" }, + new() { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{webhookIndex++}", Url = "", Description = $"Webhook {webhookIndex}" }, + new() { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{webhookIndex++}", Url = "", Description = $"Webhook {webhookIndex}" }, + new() { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{webhookIndex++}", Url = "", Description = $"Webhook {webhookIndex}" }, }; foreach (var dynamicUrl in dynamicUrls) @@ -53,7 +63,5 @@ private async Task SeedDynamicUrlAsync() } } } - - } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/CurrentUser.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/CurrentUser.cs index 9886f8e7b..6fa6a08d6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/CurrentUser.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/CurrentUser.cs @@ -12,7 +12,7 @@ namespace Unity.GrantManager.Web.Identity [ExposeServices(typeof(ICurrentUser))] public class CurrentUser : ICurrentUser, ITransientDependency { - private static readonly Claim[] EmptyClaimsArray = Array.Empty(); + private static readonly Claim[] EmptyClaimsArray = []; public virtual bool IsAuthenticated => Id.HasValue; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/PolicyRegistrant.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/PolicyRegistrant.cs index e712e4019..ae96974c4 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/PolicyRegistrant.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/PolicyRegistrant.cs @@ -193,6 +193,13 @@ internal static void Register(ServiceConfigurationContext context) context.User.HasClaim(c => c.Type == PermissionConstant && c.Value == IdentityConsts.ITAdminPermissionName) )); + // IT Operations Policies + authorizationBuilder.AddPolicy(IdentityConsts.ITOperationsPolicyName, + policy => policy.RequireAssertion(context => + context.User.IsInRole(IdentityConsts.ITOperationsRoleName) || + context.User.HasClaim(c => c.Type == PermissionConstant && c.Value == IdentityConsts.ITOperationsPermissionName) + )); + // Project Info Policies authorizationBuilder.AddPolicy(UnitySelector.Project.Default, policy => policy.RequireClaim(PermissionConstant, UnitySelector.Project.Default)); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.css index df9a7d7ca..8d852c770 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.css +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.css @@ -122,7 +122,6 @@ max-width: fit-content; margin-left: auto; margin-right: auto; - padding-right: 114px; color: inherit; } From b7a4a474b59a61caaeb9627ecd06ca6506373511 Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Fri, 22 Aug 2025 16:50:24 -0700 Subject: [PATCH 07/55] feature/AB#26441-DynamicUrls-Initial-caching --- .../Integrations/Ches/ChesClientService.cs | 4 +- .../Settings/DynamicUrl.cs | 4 +- .../Integrations/Cas/CasTokenService.cs | 2 +- .../Integrations/Cas/InvoiceService.cs | 6 +- .../Integrations/Cas/SupplierService.cs | 47 +++++------ .../TenantManagementPermissions.cs | 4 +- .../Endpoints/CreateModal.cshtml.cs | 3 + .../Endpoints/UpdateModal.cshtml.cs | 1 + .../IEndpointManagementAppService.cs | 4 +- .../ConfigureIntakeClientOptions.cs | 6 +- .../Integrations/Css/CssApiService.cs | 4 +- .../Endpoints/EndpointManagementAppService.cs | 83 +++++++++++++++++-- .../Geocoder/GeocoderApiService.cs | 2 +- .../Integrations/OrgBook/OrgBookService.cs | 2 +- .../Integrations/DynamicUrl.cs | 6 +- .../Permissions/GrantManagerPermissions.cs | 10 ++- .../Unity.GrantManager.Domain.Shared.csproj | 2 +- .../Integrations/DynamicUrlDataSeeder.cs | 54 ++++++------ .../20250514165300_DynamicUrls.cs | 9 +- .../GrantManagerWebModule.cs | 4 +- .../Identity/CurrentUser.cs | 2 +- .../IdentityProfileLoginAdminHandler.cs | 2 +- .../IdentityProfileLoginUserHandler.cs | 31 +++++-- .../Menus/GrantManagerMenuContributor.cs | 13 ++- 24 files changed, 211 insertions(+), 94 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Integrations/Ches/ChesClientService.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Integrations/Ches/ChesClientService.cs index 35e62a834..378f0387d 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Integrations/Ches/ChesClientService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Integrations/Ches/ChesClientService.cs @@ -25,7 +25,7 @@ IOptions chesClientOptions public async Task SendAsync(object emailRequest) { string authToken = await GetAuthTokenAsync(); - string notificationsApiUrl = await endpointManagementAppService.GetUrlByKeyNameAsync(DynamicUrlKeyNames.NOTFICATION_API_BASE); + string notificationsApiUrl = await endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.NOTFICATION_API_BASE); var resource = $"{notificationsApiUrl}/email"; // Pass the object directly; ResilientHttpRequest will serialize it to JSON @@ -41,7 +41,7 @@ IOptions chesClientOptions private async Task GetAuthTokenAsync() { - string notificationsAuthUrl = await endpointManagementAppService.GetUrlByKeyNameAsync(DynamicUrlKeyNames.NOTFICATION_AUTH); + string notificationsAuthUrl = await endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.NOTFICATION_AUTH); ClientOptions clientOptions = new() { diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Settings/DynamicUrl.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Settings/DynamicUrl.cs index 436936cc8..35b5e15fa 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Settings/DynamicUrl.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Settings/DynamicUrl.cs @@ -1,9 +1,10 @@ using System; using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; namespace Unity.GrantManager.Notifications.Settings; -public class DynamicUrl : AuditedAggregateRoot +public class DynamicUrl : FullAuditedEntity, IMultiTenant { public string KeyName { get; set; } = string.Empty; @@ -11,4 +12,5 @@ public class DynamicUrl : AuditedAggregateRoot public string Description { get; set; } = string.Empty; + public Guid? TenantId { get; set; } } diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/CasTokenService.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/CasTokenService.cs index 46e3cd452..b2b5cfe03 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/CasTokenService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/CasTokenService.cs @@ -24,7 +24,7 @@ IDistributedCache chesTokenCache public async Task GetAuthTokenAsync() { - string caseBaseUrl = await endpointManagementAppService.GetUrlByKeyNameAsync(DynamicUrlKeyNames.PAYMENT_API_BASE); + string caseBaseUrl = await endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.PAYMENT_API_BASE); ClientOptions clientOptions = new ClientOptions { Url = $"{caseBaseUrl}/{OAUTH_PATH}", 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 20a22b7a6..9c1b77b47 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 @@ -167,7 +167,7 @@ public async Task CreateInvoiceAsync(Invoice casAPInvoice) { string jsonString = JsonSerializer.Serialize(casAPInvoice); var authToken = await iTokenService.GetAuthTokenAsync(); - string casBaseUrl = await endpointManagementAppService.GetUrlByKeyNameAsync(DynamicUrlKeyNames.PAYMENT_API_BASE); + string casBaseUrl = await endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.PAYMENT_API_BASE); var resource = $"{casBaseUrl}/{CFS_APINVOICE}/"; var response = await resilientHttpRequest.HttpAsync(HttpMethod.Post, resource, jsonString, authToken); @@ -199,7 +199,7 @@ public async Task CreateInvoiceAsync(Invoice casAPInvoice) public async Task GetCasInvoiceAsync(string invoiceNumber, string supplierNumber, string supplierSiteCode) { var authToken = await iTokenService.GetAuthTokenAsync(); - var casBaseUrl = await endpointManagementAppService.GetUrlByKeyNameAsync(DynamicUrlKeyNames.PAYMENT_API_BASE); + 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, authToken); @@ -220,7 +220,7 @@ public async Task GetCasInvoiceAsync(string invoiceNumbe public async Task GetCasPaymentAsync(string invoiceNumber, string supplierNumber, string siteNumber) { var authToken = await iTokenService.GetAuthTokenAsync(); - var casBaseUrl = await endpointManagementAppService.GetUrlByKeyNameAsync(DynamicUrlKeyNames.PAYMENT_API_BASE); + var casBaseUrl = await endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.PAYMENT_API_BASE); var resource = $"{casBaseUrl}/{CFS_APINVOICE}/{invoiceNumber}/{supplierNumber}/{siteNumber}"; var response = await resilientHttpRequest.HttpAsync(HttpMethod.Get, resource, authToken); CasPaymentSearchResult casPaymentSearchResult = new(); diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/SupplierService.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/SupplierService.cs index d5ac569df..006bba43b 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/SupplierService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/SupplierService.cs @@ -28,17 +28,14 @@ public class SupplierService : ApplicationService, ISupplierService private readonly Task casBaseApiTask; private readonly ILocalEventBus localEventBus; private readonly IResilientHttpRequest resilientHttpRequest; - private readonly IOptions casClientOptions; private readonly ICasTokenService iTokenService; public SupplierService (ILocalEventBus localEventBus, IEndpointManagementAppService endpointManagementAppService, IResilientHttpRequest resilientHttpRequest, - IOptions casClientOptions, ICasTokenService iTokenService) { this.localEventBus = localEventBus; this.resilientHttpRequest = resilientHttpRequest; - this.casClientOptions = casClientOptions; this.iTokenService = iTokenService; // Initialize the base API URL once during construction @@ -46,7 +43,7 @@ public SupplierService (ILocalEventBus localEventBus, } private static async Task InitializeBaseApiAsync(IEndpointManagementAppService endpointManagementAppService) { - var url = await endpointManagementAppService.GetUrlByKeyNameAsync(DynamicUrlKeyNames.PAYMENT_API_BASE); + var url = await endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.PAYMENT_API_BASE); return url ?? throw new UserFriendlyException("Payment API base URL is not configured."); } @@ -238,35 +235,33 @@ private async Task GetCasSupplierInformationByResourceAsync(string? res var authToken = await iTokenService.GetAuthTokenAsync(); try { - using (var response = await resilientHttpRequest.HttpAsync(HttpMethod.Get, resource, authToken)) + using var response = await resilientHttpRequest.HttpAsync(HttpMethod.Get, resource, authToken); + if (response != null) { - if (response != null) + if (response.Content != null && response.StatusCode != HttpStatusCode.NotFound) { - if (response.Content != null && response.StatusCode != HttpStatusCode.NotFound) - { - var contentString = await response.Content.ReadAsStringAsync(); - var result = JsonSerializer.Deserialize(contentString) - ?? throw new UserFriendlyException("CAS SupplierService GetCasSupplierInformationAsync: " + response); - return result; - } - else if (response.StatusCode == HttpStatusCode.NotFound) - { - throw new UserFriendlyException("Supplier not Found."); - } - else if (response.StatusCode != HttpStatusCode.OK) - { - throw new UserFriendlyException("CAS SupplierService GetCasSupplierInformationAsync Status Code: " + response.StatusCode); - } - else - { - throw new UserFriendlyException("The CAS Supplier Number was not found."); - } + var contentString = await response.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize(contentString) + ?? throw new UserFriendlyException("CAS SupplierService GetCasSupplierInformationAsync: " + response); + return result; + } + else if (response.StatusCode == HttpStatusCode.NotFound) + { + throw new UserFriendlyException("Supplier not Found."); + } + else if (response.StatusCode != HttpStatusCode.OK) + { + throw new UserFriendlyException("CAS SupplierService GetCasSupplierInformationAsync Status Code: " + response.StatusCode); } else { - throw new UserFriendlyException("CAS SupplierService GetCasSupplierInformationAsync: Null response"); + throw new UserFriendlyException("The CAS Supplier Number was not found."); } } + else + { + throw new UserFriendlyException("CAS SupplierService GetCasSupplierInformationAsync: Null response"); + } } catch (Exception ex) { diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Application.Contracts/TenantManagementPermissions.cs b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Application.Contracts/TenantManagementPermissions.cs index b9764f49c..4b10adc48 100644 --- a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Application.Contracts/TenantManagementPermissions.cs +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Application.Contracts/TenantManagementPermissions.cs @@ -12,10 +12,10 @@ public static class Tenants public const string Default = GroupName + ".Tenants"; public const string Create = Default + ".Create"; public const string Update = Default + ".Update"; - public const string Delete = Default + ".Delete"; + public const string Delete = Default + ".Delete"; public const string ManageConnectionStrings = Default + ".ManageConnectionStrings"; - public const string ManageFeatures = AbpGroupName + ".Tenants" + ".ManageFeatures"; + public const string ManageEndpoints = AbpGroupName + ".Tenants" + ".ManageEndpoints"; } public static string[] GetAll() diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/CreateModal.cshtml.cs b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/CreateModal.cshtml.cs index 984318707..349fd9ff8 100644 --- a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/CreateModal.cshtml.cs +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/CreateModal.cshtml.cs @@ -1,10 +1,13 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using System.Threading.Tasks; using Unity.GrantManager.Integrations; +using Unity.Modules.Shared.Permissions; using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; namespace Unity.GrantManager.Web.Pages.EndpointManagement; +[Authorize(IdentityConsts.ITOperationsPolicyName)] public class CreateModalModel(IEndpointManagementAppService endpointManagementAppService) : AbpPageModel { [BindProperty] diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/UpdateModal.cshtml.cs b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/UpdateModal.cshtml.cs index 1425980b1..bfa892eeb 100644 --- a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/UpdateModal.cshtml.cs +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/UpdateModal.cshtml.cs @@ -23,6 +23,7 @@ public async Task OnGetAsync() public async Task OnPostAsync() { await endpointManagementAppService.UpdateAsync(Id, Endpoint!); + endpointManagementAppService.ClearCache(); return NoContent(); } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/IEndpointManagementAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/IEndpointManagementAppService.cs index ef26c8994..0b84bc847 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/IEndpointManagementAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/IEndpointManagementAppService.cs @@ -4,7 +4,7 @@ using Volo.Abp.Application.Services; namespace Unity.GrantManager.Integrations; - + public interface IEndpointManagementAppService : ICrudAppService< DynamicUrlDto, @@ -15,4 +15,6 @@ public interface IEndpointManagementAppService : ICrudAppService< { Task GetChefsApiBaseUrlAsync(); Task GetUrlByKeyNameAsync(string keyName); + Task GetUgmUrlByKeyNameAsync(string keyName); + void ClearCache(); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ConfigureIntakeClientOptions.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ConfigureIntakeClientOptions.cs index 21b592681..69524f131 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ConfigureIntakeClientOptions.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ConfigureIntakeClientOptions.cs @@ -1,5 +1,3 @@ - -using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; using Unity.GrantManager.Intake; @@ -13,10 +11,10 @@ public class ConfigureIntakeClientOptions( { public void Configure(IntakeClientOptions options) { - // Note: GetUrlByKeyNameAsync is async, but IConfigureOptions.Configure must be sync. + // Note: GetUgmUrlByKeyNameAsync is async, but IConfigureOptions.Configure must be sync. // If possible, use a sync alternative or ensure the value is available synchronously. // Here, we block on the async call (not ideal, but sometimes necessary in options pattern). - var intakeBaseUri = endpointManagementAppService.GetUrlByKeyNameAsync(DynamicUrlKeyNames.INTAKE_API_BASE).GetAwaiter().GetResult(); + var intakeBaseUri = endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.INTAKE_API_BASE).GetAwaiter().GetResult(); options.BaseUri = intakeBaseUri; options.BearerTokenPlaceholder = configuration["Intake:BearerTokenPlaceholder"] ?? ""; options.UseBearerToken = configuration.GetValue("Intake:UseBearerToken"); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Css/CssApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Css/CssApiService.cs index b4d147d3d..07aeb4817 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Css/CssApiService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Css/CssApiService.cs @@ -50,7 +50,7 @@ public async Task SearchUsersAsync(string directory, string? f private async Task SearchSsoAsync(string directory, Dictionary parameters) { - var cssApiUrl = await endpointManagementAppService.GetUrlByKeyNameAsync(DynamicUrlKeyNames.CSS_API_BASE); + var cssApiUrl = await endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.CSS_API_BASE); var tokenResponse = await GetAccessTokenAsync(); var baseUrl = $"{cssApiUrl}/{_cssApiOptions.Env}/{directory}/users"; var url = BuildUrlWithQuery(baseUrl, parameters); @@ -85,7 +85,7 @@ private async Task GetAccessTokenAsync() return cachedToken; var client = new HttpClient(); - var cssTokenApiUrl = await endpointManagementAppService.GetUrlByKeyNameAsync(DynamicUrlKeyNames.CSS_TOKEN_API_BASE); + var cssTokenApiUrl = await endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.CSS_TOKEN_API_BASE); var request = new HttpRequestMessage(HttpMethod.Post, cssTokenApiUrl); var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{_cssApiOptions.ClientId}:{_cssApiOptions.ClientSecret}")); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs index bc46be57c..6f7c18470 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs @@ -1,34 +1,103 @@ using System; +using System.Collections.Concurrent; using System.Threading.Tasks; +using Volo.Abp; using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories; +using Volo.Abp.Uow; namespace Unity.GrantManager.Integrations.Endpoints { [ExposeServices(typeof(EndpointManagementAppService), typeof(IEndpointManagementAppService))] public class EndpointManagementAppService(IRepository repository) : - CrudAppService< + CrudAppService< DynamicUrl, DynamicUrlDto, Guid, PagedAndSortedResultRequestDto, CreateUpdateDynamicUrlDto>(repository), - IEndpointManagementAppService + IEndpointManagementAppService { + // Key: (keyName, tenantSpecific, tenantId), Value: url + private static readonly ConcurrentDictionary<(string keyName, bool tenantSpecific, Guid? tenantId), string> _urlCache + = new(); + + private string? _chefsApiBaseUrl; // Lazy initialized + + [UnitOfWork] public async Task GetChefsApiBaseUrlAsync() { - var url = await GetUrlByKeyNameAsync(DynamicUrlKeyNames.INTAKE_API_BASE); - if (string.IsNullOrWhiteSpace(url)) throw new Exception("CHEFS API base URL not configured."); + if (_chefsApiBaseUrl == null) + { + _chefsApiBaseUrl = await GetUrlByKeyNameInternalAsync(DynamicUrlKeyNames.INTAKE_API_BASE, tenantSpecific: false); + + if (string.IsNullOrWhiteSpace(_chefsApiBaseUrl)) + throw new UserFriendlyException("CHEFS API base URL not configured."); + } + + return _chefsApiBaseUrl; + } + + [UnitOfWork] + public Task GetUgmUrlByKeyNameAsync(string keyName) + { + return GetUrlByKeyNameInternalAsync(keyName, tenantSpecific: false); + } + + public Task GetUrlByKeyNameAsync(string keyName) + { + return GetUrlByKeyNameInternalAsync(keyName, tenantSpecific: true); + } + + private async Task GetUrlByKeyNameInternalAsync(string keyName, bool tenantSpecific) + { + Guid? tenantId = tenantSpecific ? CurrentTenant.Id : null; + var cacheKey = (keyName, tenantSpecific, tenantId); + + // O(1) cache lookup + if (_urlCache.TryGetValue(cacheKey, out var cachedUrl)) + { + return cachedUrl; + } + + // Cache miss: fetch from DB + DynamicUrl? dynamicUrl; + if (tenantSpecific) + { + dynamicUrl = await Repository.FirstOrDefaultAsync(x => x.KeyName == keyName); + } + else + { + using (CurrentTenant.Change(null)) + { + dynamicUrl = await Repository.FirstOrDefaultAsync(x => x.KeyName == keyName && x.TenantId == null); + } + } + + var url = dynamicUrl?.Url ?? string.Empty; + + // Cache only if not empty + if (!string.IsNullOrWhiteSpace(url)) + { + _urlCache[cacheKey] = url; + } + return url; } + // Optional: explicit cache invalidation for a key + public static void InvalidateCache(string keyName, bool tenantSpecific, Guid? tenantId) + { + var cacheKey = (keyName, tenantSpecific, tenantId); + _urlCache.TryRemove(cacheKey, out _); + } - public async Task GetUrlByKeyNameAsync(string keyName) + // Optional: clear entire cache + public void ClearCache() { - var dynamicUrl = await Repository.FirstOrDefaultAsync(x => x.KeyName == keyName); - return dynamicUrl?.Url ?? string.Empty; + _urlCache.Clear(); } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Geocoder/GeocoderApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Geocoder/GeocoderApiService.cs index 39d9ab5d9..47b65c629 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Geocoder/GeocoderApiService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Geocoder/GeocoderApiService.cs @@ -20,7 +20,7 @@ public async Task GetAddressDetailsAsync(string address) public async Task GetElectoralDistrictAsync(LocationCoordinates locationCoordinates) { - var geoCoderBaseUri = await endpointManagementAppService.GetUrlByKeyNameAsync(DynamicUrlKeyNames.GEOCODER_API_BASE); + var geoCoderBaseUri = await endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.GEOCODER_API_BASE); var resource = $"{geoCoderBaseUri}" + $"{configuration["Geocoder:ElectoralDistrict:feature"]}" + $"&srsname=EPSG:4326" + diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/OrgBook/OrgBookService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/OrgBook/OrgBookService.cs index 71890ce75..cf8d50a2c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/OrgBook/OrgBookService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/OrgBook/OrgBookService.cs @@ -30,7 +30,7 @@ public OrgBookService( private static async Task InitializeBaseApiAsync(IEndpointManagementAppService endpointManagementAppService) { - var url = await endpointManagementAppService.GetUrlByKeyNameAsync(DynamicUrlKeyNames.ORGBOOK_API_BASE); + var url = await endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.ORGBOOK_API_BASE); return url ?? throw new IntegrationServiceException("OrgBook API base URL is not configured."); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Integrations/DynamicUrl.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Integrations/DynamicUrl.cs index 3efcb76eb..99ef7797b 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Integrations/DynamicUrl.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Integrations/DynamicUrl.cs @@ -1,11 +1,15 @@ using System; +using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; namespace Unity.GrantManager.Integrations; -public class DynamicUrl : AuditedAggregateRoot +public class DynamicUrl : FullAuditedEntity, IMultiTenant, IHasConcurrencyStamp { + public string ConcurrencyStamp { get; set; } = string.Empty; public string KeyName { get; set; } = string.Empty; public string Url { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; + public Guid? TenantId { get; set; } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/GrantManagerPermissions.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/GrantManagerPermissions.cs index 612268927..991948f97 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/GrantManagerPermissions.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/GrantManagerPermissions.cs @@ -3,8 +3,8 @@ #pragma warning disable S3218 // Inner class members should not shadow outer class "static" or type members public static class GrantManagerPermissions { - public const string GroupName = "GrantManagerManagement"; - + public const string GroupName = "GrantManagerManagement"; + public const string Default = GroupName + ".Default"; public static class Organizations @@ -23,5 +23,11 @@ public static class ApplicationForms { public const string Default = GroupName + ".ApplicationForms"; } + + public static class Endpoints + { + public const string Default = GroupName + ".Endpoints"; + public const string ManageEndpoints = Default + ".ManageEndpoints"; + } } #pragma warning restore S3218 // Inner class members should not shadow outer class "static" or type members \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Unity.GrantManager.Domain.Shared.csproj b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Unity.GrantManager.Domain.Shared.csproj index a6dc6a7f5..38791bfc9 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Unity.GrantManager.Domain.Shared.csproj +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Unity.GrantManager.Domain.Shared.csproj @@ -26,7 +26,7 @@ - + diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlDataSeeder.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlDataSeeder.cs index d7018d68a..3cd5a8cbb 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlDataSeeder.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlDataSeeder.cs @@ -4,12 +4,13 @@ using Volo.Abp.Data; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories; +using Volo.Abp.MultiTenancy; namespace Unity.GrantManager.Integrations { [Dependency(ReplaceServices = true)] [ExposeServices(typeof(DynamicUrlDataSeeder), typeof(IDataSeedContributor))] - public class DynamicUrlDataSeeder(IDynamicUrlRepository DynamicUrlRepository) : IDataSeedContributor, ITransientDependency + public class DynamicUrlDataSeeder(IDynamicUrlRepository DynamicUrlRepository, ICurrentTenant currentTenant) : IDataSeedContributor, ITransientDependency { public async Task SeedAsync(DataSeedContext context) @@ -33,33 +34,36 @@ public static class DynamicUrls private async Task SeedDynamicUrlAsync() { - int messageIndex = 0; - int webhookIndex = 0; - var dynamicUrls = new List + if (currentTenant == null || currentTenant.Id == null) { - new() { KeyName = DynamicUrlKeyNames.GEOCODER_API_BASE, Url = DynamicUrls.GEOCODER_BASE_URL, Description = "Geocoder API Base" }, - new() { KeyName = DynamicUrlKeyNames.GEOCODER_LOCATION_API_BASE, Url = DynamicUrls.GEOCODER_LOCATION_BASE_URL, Description = "Geocoder Location API Base" }, - new() { KeyName = DynamicUrlKeyNames.CSS_API_BASE, Url = DynamicUrls.CSS_API_BASE_URL, Description = "Common Single Sign-on Services API" }, - new() { KeyName = DynamicUrlKeyNames.CSS_TOKEN_API_BASE, Url = DynamicUrls.CSS_TOKEN_API_BASE_URL, Description = "Common Single Sign-on Token API" }, - new() { KeyName = DynamicUrlKeyNames.PAYMENT_API_BASE, Url = DynamicUrls.CAS_PROD_URL, Description = "BC Corporate Accounting Services API" }, - new() { KeyName = DynamicUrlKeyNames.ORGBOOK_API_BASE, Url = DynamicUrls.ORGBOOK_PROD_URL, Description = "OrgBook Services API" }, - new() { KeyName = DynamicUrlKeyNames.INTAKE_API_BASE, Url = DynamicUrls.CHEFS_PROD_URL, Description = "Common Hosted Forms Service API" }, - new() { KeyName = DynamicUrlKeyNames.NOTFICATION_API_BASE, Url = DynamicUrls.CHES_PROD_URL, Description = "Common Hosted Email Service API" }, - new() { KeyName = DynamicUrlKeyNames.NOTFICATION_AUTH, Url = DynamicUrls.CHES_PROD_AUTH, Description = "Common Hosted Email Service OAUTH" }, - new() { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{messageIndex++}", Url = "", Description = $"Direct message webhook {messageIndex}" }, - new() { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{messageIndex++}", Url = "", Description = $"Direct message webhook {messageIndex}" }, - new() { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{messageIndex++}", Url = "", Description = $"Direct message webhook {messageIndex}" }, - new() { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{webhookIndex++}", Url = "", Description = $"Webhook {webhookIndex}" }, - new() { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{webhookIndex++}", Url = "", Description = $"Webhook {webhookIndex}" }, - new() { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{webhookIndex++}", Url = "", Description = $"Webhook {webhookIndex}" }, - }; + int messageIndex = 0; + int webhookIndex = 0; + var dynamicUrls = new List + { + new() { KeyName = DynamicUrlKeyNames.GEOCODER_API_BASE, Url = DynamicUrls.GEOCODER_BASE_URL, Description = "Geocoder API Base" }, + new() { KeyName = DynamicUrlKeyNames.GEOCODER_LOCATION_API_BASE, Url = DynamicUrls.GEOCODER_LOCATION_BASE_URL, Description = "Geocoder Location API Base" }, + new() { KeyName = DynamicUrlKeyNames.CSS_API_BASE, Url = DynamicUrls.CSS_API_BASE_URL, Description = "Common Single Sign-on Services API" }, + new() { KeyName = DynamicUrlKeyNames.CSS_TOKEN_API_BASE, Url = DynamicUrls.CSS_TOKEN_API_BASE_URL, Description = "Common Single Sign-on Token API" }, + new() { KeyName = DynamicUrlKeyNames.PAYMENT_API_BASE, Url = DynamicUrls.CAS_PROD_URL, Description = "BC Corporate Accounting Services API" }, + new() { KeyName = DynamicUrlKeyNames.ORGBOOK_API_BASE, Url = DynamicUrls.ORGBOOK_PROD_URL, Description = "OrgBook Services API" }, + new() { KeyName = DynamicUrlKeyNames.INTAKE_API_BASE, Url = DynamicUrls.CHEFS_PROD_URL, Description = "Common Hosted Forms Service API" }, + new() { KeyName = DynamicUrlKeyNames.NOTFICATION_API_BASE, Url = DynamicUrls.CHES_PROD_URL, Description = "Common Hosted Email Service API" }, + new() { KeyName = DynamicUrlKeyNames.NOTFICATION_AUTH, Url = DynamicUrls.CHES_PROD_AUTH, Description = "Common Hosted Email Service OAUTH" }, + new() { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{messageIndex++}", Url = "", Description = $"Direct message webhook {messageIndex}" }, + new() { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{messageIndex++}", Url = "", Description = $"Direct message webhook {messageIndex}" }, + new() { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{messageIndex++}", Url = "", Description = $"Direct message webhook {messageIndex}" }, + new() { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{webhookIndex++}", Url = "", Description = $"Webhook {webhookIndex}" }, + new() { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{webhookIndex++}", Url = "", Description = $"Webhook {webhookIndex}" }, + new() { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{webhookIndex++}", Url = "", Description = $"Webhook {webhookIndex}" }, + }; - foreach (var dynamicUrl in dynamicUrls) - { - var existing = await DynamicUrlRepository.FirstOrDefaultAsync(s => s.KeyName == dynamicUrl.KeyName); - if (existing == null) + foreach (var dynamicUrl in dynamicUrls) { - await DynamicUrlRepository.InsertAsync(dynamicUrl); + var existing = await DynamicUrlRepository.FirstOrDefaultAsync(s => s.KeyName == dynamicUrl.KeyName); + if (existing == null) + { + await DynamicUrlRepository.InsertAsync(dynamicUrl); + } } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20250514165300_DynamicUrls.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20250514165300_DynamicUrls.cs index 407fef6b7..0fe143de2 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20250514165300_DynamicUrls.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20250514165300_DynamicUrls.cs @@ -19,13 +19,16 @@ protected override void Up(MigrationBuilder migrationBuilder) KeyName = table.Column(type: "text", nullable: false), Url = table.Column(type: "text", nullable: false), Description = table.Column(type: "text", nullable: false), - TenantId = table.Column(type: "uuid", nullable: true), - ExtraProperties = table.Column(type: "text", nullable: false), + TenantId = table.Column(type: "uuid", nullable: true), + ExtraProperties = table.Column(type: "text", nullable: false, defaultValue: "{}"), ConcurrencyStamp = table.Column(type: "character varying(40)", maxLength: 40, nullable: false), CreationTime = table.Column(type: "timestamp without time zone", nullable: false), CreatorId = table.Column(type: "uuid", nullable: true), LastModificationTime = table.Column(type: "timestamp without time zone", nullable: true), - LastModifierId = table.Column(type: "uuid", nullable: true) + LastModifierId = table.Column(type: "uuid", nullable: true), + IsDeleted = table.Column(type: "boolean", nullable: false, defaultValue: false), + DeleterId = table.Column(type: "uuid", nullable: true), + DeletionTime = table.Column(type: "timestamp without time zone", nullable: true) }, constraints: table => { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebModule.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebModule.cs index 7b8e31497..d23c2f83e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebModule.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebModule.cs @@ -75,6 +75,7 @@ using Unity.Modules.Shared.Utils; using Unity.Notifications.Web.Views.Settings; using Unity.Notifications.Web.Bundling; +using Unity.Reporting.Web; using Unity.GrantManager.Web.Views.Settings; namespace Unity.GrantManager.Web; @@ -98,7 +99,8 @@ namespace Unity.GrantManager.Web; typeof(PaymentsWebModule), typeof(AbpBlobStoringModule), typeof(NotificationsWebModule), - typeof(FlexWebModule) + typeof(FlexWebModule), + typeof(ReportingWebModule) )] public class GrantManagerWebModule : AbpModule diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/CurrentUser.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/CurrentUser.cs index 6fa6a08d6..ab17dea52 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/CurrentUser.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/CurrentUser.cs @@ -38,7 +38,7 @@ public class CurrentUser : ICurrentUser, ITransientDependency private string[] FindRoleClaims() { - return FindClaims(UnityClaimsTypes.Role).Select(c => c.Value).Distinct().ToArray(); + return [.. FindClaims(UnityClaimsTypes.Role).Select(c => c.Value).Distinct()]; } private readonly ICurrentPrincipalAccessor _principalAccessor; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginAdminHandler.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginAdminHandler.cs index 8bb88ea44..c80469dbd 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginAdminHandler.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginAdminHandler.cs @@ -27,7 +27,7 @@ internal class IdentityProfileLoginAdminHandler : IdentityProfileLoginBase IdentityPermissions.UserLookup.Default, IdentityConsts.ITAdminPermissionName ); - + internal async Task Handle(TokenValidatedContext validatedTokenContext, IList userTenantAccounts, string? idp) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginUserHandler.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginUserHandler.cs index 875934589..a13314a84 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginUserHandler.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginUserHandler.cs @@ -11,6 +11,7 @@ using Unity.GrantManager.Identity; using Unity.GrantManager.Permissions; using Unity.GrantManager.Web.Exceptions; +using Unity.Modules.Shared.Permissions; using Volo.Abp.Identity; using Volo.Abp.PermissionManagement; using Volo.Abp.Security.Claims; @@ -19,8 +20,15 @@ namespace Unity.GrantManager.Web.Identity.LoginHandlers { internal class IdentityProfileLoginUserHandler : IdentityProfileLoginBase { - internal readonly ImmutableArray _userPermissions = ImmutableArray - .Create(GrantManagerPermissions.Default, IdentityPermissions.UserLookup.Default); + internal readonly ImmutableArray _userPermissions = [ + GrantManagerPermissions.Default, + IdentityPermissions.UserLookup.Default + ]; + + internal readonly ImmutableArray _itOperationsPermissions = [ + GrantManagerPermissions.Endpoints.ManageEndpoints, + IdentityConsts.ITOperationsPermissionName + ]; internal async Task Handle(TokenValidatedContext validatedTokenContext, IList? userTenantAccounts, @@ -48,6 +56,11 @@ internal async Task Handle(TokenValidatedContext validated throw new NoGrantProgramsLinkedException("User is not linked to any grant programs"); } } + + if (validatedTokenContext.Principal != null && validatedTokenContext.Principal.IsInRole(IdentityConsts.ITOperationsRoleName)) + { + AssignITOperationsPermissions(validatedTokenContext.Principal); + } UserTenantAccountDto? userTenantAccount = null; var setTenant = validatedTokenContext.Request.Cookies["set_tenant"]; @@ -76,12 +89,11 @@ internal async Task Handle(TokenValidatedContext validated var userPermissions = (await PermissionManager.GetAllForUserAsync(userTenantAccount.Id)).Where(s => s.IsGranted); - foreach (var permissionName in userPermissions.Select(s => s.Name)) + foreach (var permissionName in userPermissions + .Select(s => s.Name) + .Where(permissionName => !principal.HasClaim(UnityClaimsTypes.Permission, permissionName))) { - if (!principal.HasClaim(UnityClaimsTypes.Permission, permissionName)) - { - principal.AddClaim(UnityClaimsTypes.Permission, permissionName); - } + principal.AddClaim(UnityClaimsTypes.Permission, permissionName); } } @@ -93,6 +105,11 @@ internal async Task Handle(TokenValidatedContext validated return userTenantAccount; } + private void AssignITOperationsPermissions(ClaimsPrincipal claimsPrincipal) + { + claimsPrincipal.AddPermissions(_itOperationsPermissions); + } + private async Task> AutoRegisterUserWithDefaultAsync(string userIdentifier, string username, string firstName, diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenuContributor.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenuContributor.cs index bb14098f0..0811ad138 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenuContributor.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenuContributor.cs @@ -92,6 +92,15 @@ private static Task ConfigureMainMenuAsync(MenuConfigurationContext context) requiredPermissionName: GrantApplicationPermissions.Dashboard.Default ) ); + // Displayed in the Grant Manager - Used at Tenant Level if the user in the IT Operations role + context.Menu.AddItem( + new ApplicationMenuItem( + GrantManagerMenus.EndpointManagement, + displayName: "Endpoints", + "~/EndpointManagement/Endpoints", + requiredPermissionName: IdentityConsts.ITOperationsPermissionName + ) + ); // ******************** @@ -107,14 +116,16 @@ private static Task ConfigureMainMenuAsync(MenuConfigurationContext context) ) ); + // Displayed on the Tenant Managment area if the user has the ITAdministrator Role context.Menu.AddItem( new ApplicationMenuItem( GrantManagerMenus.EndpointManagement, displayName: "Endpoints", "~/EndpointManagement/Endpoints", - requiredPermissionName: IdentityConsts.ITAdminPermissionName + requiredPermissionName: TenantManagementPermissions.Tenants.Default ) ); + // End Admin ******************** #pragma warning disable S125 // Sections of code should not be commented out /* - will complete later after fixing ui sub menu issue */ From 083b91a34513cc3807ea74f11e15a438d896fa40 Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Tue, 19 Aug 2025 17:41:57 -0700 Subject: [PATCH 08/55] AB#23904: Update EF - Add LinkType Column --- .../GrantApplications/ApplicationLinksDto.cs | 2 + .../ApplicationLinksInfoDto.cs | 2 + .../ApplicationLinksAppService.cs | 6 +- .../Applications/ApplicationLinkType.cs | 11 + .../Applications/ApplicationLink.cs | 1 + .../GrantTenantDbContext.cs | 5 + ...0819224936_FixSnapshotAgain_V3.Designer.cs | 4233 ++++++++++++++++ .../20250819224936_FixSnapshotAgain_V3.cs | 23 + ...dd_ApplicationLinksType_Column.Designer.cs | 4239 +++++++++++++++++ ...9225154_Add_ApplicationLinksType_Column.cs | 29 + .../GrantTenantDbContextModelSnapshot.cs | 6 + 11 files changed, 8555 insertions(+), 2 deletions(-) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Applications/ApplicationLinkType.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20250819224936_FixSnapshotAgain_V3.Designer.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20250819224936_FixSnapshotAgain_V3.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20250819225154_Add_ApplicationLinksType_Column.Designer.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20250819225154_Add_ApplicationLinksType_Column.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/ApplicationLinksDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/ApplicationLinksDto.cs index ce68555ce..c9f708b7d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/ApplicationLinksDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/ApplicationLinksDto.cs @@ -1,4 +1,5 @@ using System; +using Unity.GrantManager.Applications; using Volo.Abp.Application.Dtos; namespace Unity.GrantManager.GrantApplications; @@ -8,5 +9,6 @@ public class ApplicationLinksDto : EntityDto { public Guid ApplicationId { get; set; } public Guid LinkedApplicationId { get; set; } + public ApplicationLinkType LinkType { get; set; } = ApplicationLinkType.Related; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/ApplicationLinksInfoDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/ApplicationLinksInfoDto.cs index a9b90c716..5abf53492 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/ApplicationLinksInfoDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/ApplicationLinksInfoDto.cs @@ -1,4 +1,5 @@ using System; +using Unity.GrantManager.Applications; using Volo.Abp.Application.Dtos; namespace Unity.GrantManager.GrantApplications; @@ -11,5 +12,6 @@ public class ApplicationLinksInfoDto : EntityDto public String ApplicationStatus { get; set; } = String.Empty; public String Category { get; set; } = String.Empty; public string ProjectName { get; set; } = string.Empty; + public ApplicationLinkType LinkType { get; set; } = ApplicationLinkType.Related; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationLinksAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationLinksAppService.cs index c3ad567cb..a74b12eaf 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationLinksAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationLinksAppService.cs @@ -44,7 +44,8 @@ join appForm in applicationFormsQuery on application.ApplicationFormId equals ap ApplicationStatus = application.ApplicationStatus.InternalStatus, ReferenceNumber = application.ReferenceNo, Category = appForm.Category ?? "Unknown", // Handle potential nulls - ProjectName = application.ProjectName + ProjectName = application.ProjectName, + LinkType = applicationLinks.LinkType }; return await combinedQuery.ToListAsync(); @@ -69,7 +70,8 @@ join appForm in applicationFormsQuery on application.ApplicationFormId equals ap ApplicationStatus = application.ApplicationStatus.InternalStatus, ReferenceNumber = application.ReferenceNo, Category = appForm.Category ?? "Unknown", // Handle potential nulls - ProjectName = application.ProjectName + ProjectName = application.ProjectName, + LinkType = applicationLinks.LinkType }; return await combinedQuery.SingleAsync(); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Applications/ApplicationLinkType.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Applications/ApplicationLinkType.cs new file mode 100644 index 000000000..84a274070 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Applications/ApplicationLinkType.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace Unity.GrantManager.Applications; + +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum ApplicationLinkType +{ + Related, + Parent, + Child +} \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationLink.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationLink.cs index 0b90c67a0..e0bdb81e0 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationLink.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationLink.cs @@ -9,4 +9,5 @@ public class ApplicationLink : AuditedAggregateRoot, IMultiTenant public Guid ApplicationId { get; set; } public Guid LinkedApplicationId { get; set; } public Guid? TenantId { get; set; } + public ApplicationLinkType LinkType { get; set; } = ApplicationLinkType.Related; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantTenantDbContext.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantTenantDbContext.cs index 41dc770a6..2f978759b 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantTenantDbContext.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantTenantDbContext.cs @@ -284,6 +284,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.ConfigureByConvention(); b.HasOne().WithMany().HasForeignKey(x => x.ApplicationId).IsRequired(); + + b.Property(x => x.LinkType) + .IsRequired() + .HasDefaultValue(ApplicationLinkType.Related) + .HasConversion(new EnumToStringConverter()); }); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20250819224936_FixSnapshotAgain_V3.Designer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20250819224936_FixSnapshotAgain_V3.Designer.cs new file mode 100644 index 000000000..1752c6509 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20250819224936_FixSnapshotAgain_V3.Designer.cs @@ -0,0 +1,4233 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Unity.GrantManager.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + [DbContext(typeof(GrantTenantDbContext))] + [Migration("20250819224936_FixSnapshotAgain_V3")] + partial class FixSnapshotAgain_V3 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.PostgreSql) + .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScoresheetId"); + + b.ToTable("ScoresheetInstances", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Answer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("QuestionId") + .HasColumnType("uuid"); + + b.Property("ScoresheetInstanceId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("QuestionId"); + + b.HasIndex("ScoresheetInstanceId"); + + b.ToTable("Answers", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("SectionId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("Questions", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Scoresheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Scoresheets", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ScoresheetId"); + + b.ToTable("ScoresheetSections", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.CustomFieldValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CustomFieldId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("WorksheetInstanceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetInstanceId"); + + b.ToTable("CustomFieldValues", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UiAnchor") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetCorrelationId") + .HasColumnType("uuid"); + + b.Property("WorksheetCorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("WorksheetInstances", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetLinks.WorksheetLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UiAnchor") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetId"); + + b.ToTable("WorksheetLinks", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.CustomField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("SectionId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("CustomFields", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.Worksheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Worksheets", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetId"); + + b.ToTable("WorksheetSections", "Flex"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Applicant", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantName") + .IsRequired() + .HasMaxLength(600) + .HasColumnType("character varying(600)"); + + b.Property("ApproxNumberOfEmployees") + .HasColumnType("text"); + + b.Property("BusinessNumber") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ElectoralDistrict") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FiscalDay") + .HasColumnType("integer"); + + b.Property("FiscalMonth") + .HasColumnType("text"); + + b.Property("IndigenousOrgInd") + .HasColumnType("text"); + + b.Property("IsDuplicated") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MatchPercentage") + .HasColumnType("numeric"); + + b.Property("NonRegOrgName") + .HasColumnType("text"); + + b.Property("NonRegisteredBusinessName") + .HasColumnType("text"); + + b.Property("OrgName") + .HasColumnType("text"); + + b.Property("OrgNumber") + .HasColumnType("text"); + + b.Property("OrgStatus") + .HasColumnType("text"); + + b.Property("OrganizationSize") + .HasColumnType("text"); + + b.Property("OrganizationType") + .HasColumnType("text"); + + b.Property("RedStop") + .HasColumnType("boolean"); + + b.Property("Sector") + .HasColumnType("text"); + + b.Property("SectorSubSectorIndustryDesc") + .HasColumnType("text"); + + b.Property("SiteId") + .HasColumnType("uuid"); + + b.Property("StartedOperatingDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("SubSector") + .HasColumnType("text"); + + b.Property("SupplierId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UnityApplicantId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantName"); + + b.ToTable("Applicants", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAddress", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AddressType") + .HasColumnType("integer"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Postal") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("Street") + .HasColumnType("text"); + + b.Property("Street2") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Unit") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicantAddresses", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAgent", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("BceidBusinessGuid") + .HasColumnType("uuid"); + + b.Property("BceidBusinessName") + .HasColumnType("text"); + + b.Property("BceidUserGuid") + .HasColumnType("uuid"); + + b.Property("BceidUserName") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactOrder") + .HasColumnType("integer"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IdentityEmail") + .HasColumnType("text"); + + b.Property("IdentityName") + .HasColumnType("text"); + + b.Property("IdentityProvider") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsConfirmed") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OidcSubUser") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("Phone2") + .HasColumnType("text"); + + b.Property("Phone2Extension") + .HasColumnType("text"); + + b.Property("PhoneExtension") + .HasColumnType("text"); + + b.Property("RoleForApplicant") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationId") + .IsUnique(); + + b.HasIndex("OidcSubUser"); + + b.ToTable("ApplicantAgents", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Acquisition") + .HasColumnType("text"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("ApplicationStatusId") + .HasColumnType("uuid"); + + b.Property("ApprovedAmount") + .HasColumnType("numeric"); + + b.Property("AssessmentResultDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AssessmentResultStatus") + .HasColumnType("text"); + + b.Property("AssessmentStartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Community") + .HasColumnType("text"); + + b.Property("CommunityPopulation") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContractExecutionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ContractNumber") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeclineRational") + .HasColumnType("text"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("DueDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DueDiligenceStatus") + .HasColumnType("text"); + + b.Property("EconomicRegion") + .HasColumnType("text"); + + b.Property("ElectoralDistrict") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FinalDecisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Forestry") + .HasColumnType("text"); + + b.Property("ForestryFocus") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LikelihoodOfFunding") + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("NotificationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Payload") + .HasColumnType("jsonb"); + + b.Property("PercentageTotalProjectBudget") + .HasColumnType("double precision"); + + b.Property("Place") + .HasColumnType("text"); + + b.Property("ProjectEndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ProjectFundingTotal") + .HasColumnType("numeric"); + + b.Property("ProjectName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProjectStartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ProjectSummary") + .HasColumnType("text"); + + b.Property("ProposalDate") + .HasColumnType("timestamp without time zone"); + + b.Property("RecommendedAmount") + .HasColumnType("numeric"); + + b.Property("ReferenceNo") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegionalDistrict") + .HasColumnType("text"); + + b.Property("RequestedAmount") + .HasColumnType("numeric"); + + b.Property("RiskRanking") + .HasColumnType("text"); + + b.Property("SigningAuthorityBusinessPhone") + .HasColumnType("text"); + + b.Property("SigningAuthorityCellPhone") + .HasColumnType("text"); + + b.Property("SigningAuthorityEmail") + .HasColumnType("text"); + + b.Property("SigningAuthorityFullName") + .HasColumnType("text"); + + b.Property("SigningAuthorityTitle") + .HasColumnType("text"); + + b.Property("SubStatus") + .HasColumnType("text"); + + b.Property("SubmissionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TotalProjectBudget") + .HasColumnType("numeric"); + + b.Property("TotalScore") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationFormId"); + + b.HasIndex("ApplicationStatusId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Applications", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAssignment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AssigneeId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Duty") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("AssigneeId"); + + b.ToTable("ApplicationAssignments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationChefsFileAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ChefsFileId") + .HasColumnType("text"); + + b.Property("ChefsSumbissionId") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationChefsFileAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationContact", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactEmail") + .HasColumnType("text"); + + b.Property("ContactFullName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactMobilePhone") + .HasColumnType("text"); + + b.Property("ContactTitle") + .HasColumnType("text"); + + b.Property("ContactType") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactWorkPhone") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationContact", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationForm", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountCodingId") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .HasColumnType("text"); + + b.Property("ApplicationFormDescription") + .HasColumnType("text"); + + b.Property("ApplicationFormName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AttemptedConnectionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AvailableChefsFields") + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("text"); + + b.Property("ChefsApplicationFormGuid") + .HasColumnType("text"); + + b.Property("ChefsCriteriaFormGuid") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ConnectionHttpStatus") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ElectoralDistrictAddressType") + .HasColumnType("integer"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IntakeId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsDirectApproval") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Payable") + .HasColumnType("boolean"); + + b.Property("PaymentApprovalThreshold") + .HasColumnType("numeric"); + + b.Property("PreventPayment") + .HasColumnType("boolean"); + + b.Property("RenderFormIoToHtml") + .HasColumnType("boolean"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IntakeId"); + + b.ToTable("ApplicationForms", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormSubmission", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormVersionId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ChefsSubmissionGuid") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormVersionId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OidcSub") + .IsRequired() + .HasColumnType("text"); + + b.Property("RenderedHTML") + .HasColumnType("text"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Submission") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormSubmissions", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormVersion", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("AvailableChefsFields") + .HasColumnType("text"); + + b.Property("ChefsApplicationFormGuid") + .HasColumnType("text"); + + b.Property("ChefsFormVersionGuid") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormSchema") + .HasColumnType("jsonb"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SubmissionHeaderMapping") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormVersion", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationLink", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LinkedApplicationId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationLinks", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationStatus", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExternalStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("InternalStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("StatusCode") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("StatusCode") + .IsUnique(); + + b.ToTable("ApplicationStatuses", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationTags", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TagId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("TagId"); + + b.ToTable("ApplicationTags", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AssessmentAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AssessmentId"); + + b.ToTable("AssessmentAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Assessments.Assessment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ApprovalRecommended") + .HasColumnType("boolean"); + + b.Property("AssessorId") + .HasColumnType("uuid"); + + b.Property("CleanGrowth") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("EconomicImpact") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FinancialAnalysis") + .HasColumnType("integer"); + + b.Property("InclusiveGrowth") + .HasColumnType("integer"); + + b.Property("IsComplete") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("AssessorId"); + + b.ToTable("Assessments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicationComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("CommenterId"); + + b.ToTable("ApplicationComments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.AssessmentComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AssessmentId"); + + b.HasIndex("CommenterId"); + + b.ToTable("AssessmentComments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.GlobalTag.Tag", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Tags", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Identity.Person", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Badge") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FullName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OidcDisplayName") + .IsRequired() + .HasColumnType("text"); + + b.Property("OidcSub") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("OidcSub"); + + b.ToTable("Persons", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Intakes.Intake", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Budget") + .HasColumnType("double precision"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IntakeName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("StartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Intakes", (string)null); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EmailGroups", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroupUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.ToTable("EmailGroupUsers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("BCC") + .IsRequired() + .HasColumnType("text"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CC") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChesMsgId") + .HasColumnType("uuid"); + + b.Property("ChesResponse") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChesStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FromAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Priority") + .IsRequired() + .HasColumnType("text"); + + b.Property("RetryAttempts") + .HasColumnType("integer"); + + b.Property("SendOnDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("SentDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("text"); + + b.Property("TemplateName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("ToAddress") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EmailLogs", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.EmailTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BodyHTML") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyText") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SendFrom") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("EmailTemplates", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.Subscriber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Subscribers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("SubscriptionGroups", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroupSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SubscriberId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.HasIndex("SubscriberId"); + + b.ToTable("SubscriptionGroupSubscribers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TemplateVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MapTo") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("TemplateVariables", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.Trigger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("InternalName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Triggers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TriggerSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SubscriptionGroupId") + .HasColumnType("uuid"); + + b.Property("TemplateId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TriggerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionGroupId"); + + b.HasIndex("TemplateId"); + + b.HasIndex("TriggerId"); + + b.ToTable("TriggerSubscriptions", "Notifications"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.AccountCodings.AccountCoding", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MinistryClient") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("Responsibility") + .IsRequired() + .HasColumnType("text"); + + b.Property("ServiceLine") + .IsRequired() + .HasColumnType("text"); + + b.Property("Stob") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("AccountCodings", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentConfigurations.PaymentConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DefaultAccountCodingId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentIdPrefix") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("PaymentConfigurations", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.ExpenseApproval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DecisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DecisionUserId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PaymentRequestId"); + + b.ToTable("ExpenseApprovals", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountCodingId") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("BatchName") + .IsRequired() + .HasColumnType("text"); + + b.Property("BatchNumber") + .HasColumnType("numeric"); + + b.Property("CasHttpStatusCode") + .HasColumnType("integer"); + + b.Property("CasResponse") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContractNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("InvoiceNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("InvoiceStatus") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsRecon") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("PayeeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentDate") + .HasColumnType("text"); + + b.Property("PaymentNumber") + .HasColumnType("text"); + + b.Property("PaymentStatus") + .HasColumnType("text"); + + b.Property("ReferenceNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("RequesterName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SiteId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("SubmissionConfirmationCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SupplierName") + .HasColumnType("text"); + + b.Property("SupplierNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AccountCodingId"); + + b.HasIndex("ReferenceNumber") + .IsUnique(); + + b.HasIndex("SiteId"); + + b.ToTable("PaymentRequests", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentTags.PaymentTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestId") + .HasColumnType("uuid"); + + b.Property("TagId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("PaymentRequestId"); + + b.HasIndex("TagId"); + + b.ToTable("PaymentTags", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentThresholds.PaymentThreshold", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Threshold") + .HasColumnType("numeric"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("PaymentThresholds", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Site", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddressLine1") + .HasColumnType("text"); + + b.Property("AddressLine2") + .HasColumnType("text"); + + b.Property("AddressLine3") + .HasColumnType("text"); + + b.Property("BankAccount") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EFTAdvicePref") + .HasColumnType("text"); + + b.Property("EmailAddress") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastUpdatedInCas") + .HasColumnType("timestamp without time zone"); + + b.Property("MarkDeletedInUse") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentGroup") + .HasColumnType("integer"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("SiteProtected") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("SupplierId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("SupplierId"); + + b.ToTable("Sites", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Supplier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BusinessNumber") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastUpdatedInCAS") + .HasColumnType("timestamp without time zone"); + + b.Property("MailingAddress") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("text"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("SIN") + .HasColumnType("text"); + + b.Property("StandardIndustryClassification") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("Subcategory") + .HasColumnType("text"); + + b.Property("SupplierProtected") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Suppliers", "Payments"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Scoresheet", "Scoresheet") + .WithMany("Instances") + .HasForeignKey("ScoresheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scoresheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Answer", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Question", "Question") + .WithMany("Answers") + .HasForeignKey("QuestionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", null) + .WithMany("Answers") + .HasForeignKey("ScoresheetInstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Question"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.ScoresheetSection", "Section") + .WithMany("Fields") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Scoresheet", "Scoresheet") + .WithMany("Sections") + .HasForeignKey("ScoresheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scoresheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.CustomFieldValue", b => + { + b.HasOne("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", null) + .WithMany("Values") + .HasForeignKey("WorksheetInstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetLinks.WorksheetLink", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.Worksheet", "Worksheet") + .WithMany("Links") + .HasForeignKey("WorksheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Worksheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.CustomField", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.WorksheetSection", "Section") + .WithMany("Fields") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.Worksheet", "Worksheet") + .WithMany("Sections") + .HasForeignKey("WorksheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Worksheet"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAddress", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", "Applicant") + .WithMany("ApplicantAddresses") + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicantAddresses") + .HasForeignKey("ApplicationId"); + + b.Navigation("Applicant"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAgent", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithOne("ApplicantAgent") + .HasForeignKey("Unity.GrantManager.Applications.ApplicantAgent", "ApplicationId"); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("OidcSubUser") + .HasPrincipalKey("OidcSub"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", "Applicant") + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", "ApplicationForm") + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationStatus", "ApplicationStatus") + .WithMany("Applications") + .HasForeignKey("ApplicationStatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Applicant"); + + b.Navigation("ApplicationForm"); + + b.Navigation("ApplicationStatus"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAssignment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicationAssignments") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", "Assignee") + .WithMany() + .HasForeignKey("AssigneeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Assignee"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAttachment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationChefsFileAttachment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationContact", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationForm", b => + { + b.HasOne("Unity.GrantManager.Intakes.Intake", null) + .WithMany() + .HasForeignKey("IntakeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormSubmission", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormVersion", b => + { + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationLink", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationTags", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicationTags") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.GlobalTag.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AssessmentAttachment", b => + { + b.HasOne("Unity.GrantManager.Assessments.Assessment", null) + .WithMany() + .HasForeignKey("AssessmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Assessments.Assessment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("Assessments") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("AssessorId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicationComment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.AssessmentComment", b => + { + b.HasOne("Unity.GrantManager.Assessments.Assessment", null) + .WithMany() + .HasForeignKey("AssessmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroupUser", b => + { + b.HasOne("Unity.Notifications.EmailGroups.EmailGroup", null) + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroupSubscription", b => + { + b.HasOne("Unity.Notifications.Templates.SubscriptionGroup", "SubscriptionGroup") + .WithMany() + .HasForeignKey("GroupId"); + + b.HasOne("Unity.Notifications.Templates.Subscriber", "Subscriber") + .WithMany() + .HasForeignKey("SubscriberId"); + + b.Navigation("Subscriber"); + + b.Navigation("SubscriptionGroup"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TriggerSubscription", b => + { + b.HasOne("Unity.Notifications.Templates.SubscriptionGroup", "SubscriptionGroup") + .WithMany() + .HasForeignKey("SubscriptionGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Notifications.Templates.EmailTemplate", "EmailTemplate") + .WithMany() + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Notifications.Templates.Trigger", "Trigger") + .WithMany() + .HasForeignKey("TriggerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmailTemplate"); + + b.Navigation("SubscriptionGroup"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.ExpenseApproval", b => + { + b.HasOne("Unity.Payments.Domain.PaymentRequests.PaymentRequest", "PaymentRequest") + .WithMany("ExpenseApprovals") + .HasForeignKey("PaymentRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PaymentRequest"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.HasOne("Unity.Payments.Domain.AccountCodings.AccountCoding", "AccountCoding") + .WithMany() + .HasForeignKey("AccountCodingId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Unity.Payments.Domain.Suppliers.Site", "Site") + .WithMany() + .HasForeignKey("SiteId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("AccountCoding"); + + b.Navigation("Site"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentTags.PaymentTag", b => + { + b.HasOne("Unity.Payments.Domain.PaymentRequests.PaymentRequest", null) + .WithMany("PaymentTags") + .HasForeignKey("PaymentRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.GlobalTag.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Site", b => + { + b.HasOne("Unity.Payments.Domain.Suppliers.Supplier", "Supplier") + .WithMany("Sites") + .HasForeignKey("SupplierId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.Navigation("Answers"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.Navigation("Answers"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Scoresheet", b => + { + b.Navigation("Instances"); + + b.Navigation("Sections"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.Navigation("Fields"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", b => + { + b.Navigation("Values"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.Worksheet", b => + { + b.Navigation("Links"); + + b.Navigation("Sections"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.Navigation("Fields"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Applicant", b => + { + b.Navigation("ApplicantAddresses"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.Navigation("ApplicantAddresses"); + + b.Navigation("ApplicantAgent"); + + b.Navigation("ApplicationAssignments"); + + b.Navigation("ApplicationTags"); + + b.Navigation("Assessments"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationStatus", b => + { + b.Navigation("Applications"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.Navigation("ExpenseApprovals"); + + b.Navigation("PaymentTags"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Supplier", b => + { + b.Navigation("Sites"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20250819224936_FixSnapshotAgain_V3.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20250819224936_FixSnapshotAgain_V3.cs new file mode 100644 index 000000000..69c852ccc --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20250819224936_FixSnapshotAgain_V3.cs @@ -0,0 +1,23 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + /// + public partial class FixSnapshotAgain_V3 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20250819225154_Add_ApplicationLinksType_Column.Designer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20250819225154_Add_ApplicationLinksType_Column.Designer.cs new file mode 100644 index 000000000..1d542b825 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20250819225154_Add_ApplicationLinksType_Column.Designer.cs @@ -0,0 +1,4239 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Unity.GrantManager.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + [DbContext(typeof(GrantTenantDbContext))] + [Migration("20250819225154_Add_ApplicationLinksType_Column")] + partial class Add_ApplicationLinksType_Column + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.PostgreSql) + .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScoresheetId"); + + b.ToTable("ScoresheetInstances", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Answer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("QuestionId") + .HasColumnType("uuid"); + + b.Property("ScoresheetInstanceId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("QuestionId"); + + b.HasIndex("ScoresheetInstanceId"); + + b.ToTable("Answers", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("SectionId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("Questions", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Scoresheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Scoresheets", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ScoresheetId"); + + b.ToTable("ScoresheetSections", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.CustomFieldValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CustomFieldId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("WorksheetInstanceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetInstanceId"); + + b.ToTable("CustomFieldValues", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UiAnchor") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetCorrelationId") + .HasColumnType("uuid"); + + b.Property("WorksheetCorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("WorksheetInstances", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetLinks.WorksheetLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UiAnchor") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetId"); + + b.ToTable("WorksheetLinks", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.CustomField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("SectionId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("CustomFields", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.Worksheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Worksheets", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetId"); + + b.ToTable("WorksheetSections", "Flex"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Applicant", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantName") + .IsRequired() + .HasMaxLength(600) + .HasColumnType("character varying(600)"); + + b.Property("ApproxNumberOfEmployees") + .HasColumnType("text"); + + b.Property("BusinessNumber") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ElectoralDistrict") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FiscalDay") + .HasColumnType("integer"); + + b.Property("FiscalMonth") + .HasColumnType("text"); + + b.Property("IndigenousOrgInd") + .HasColumnType("text"); + + b.Property("IsDuplicated") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MatchPercentage") + .HasColumnType("numeric"); + + b.Property("NonRegOrgName") + .HasColumnType("text"); + + b.Property("NonRegisteredBusinessName") + .HasColumnType("text"); + + b.Property("OrgName") + .HasColumnType("text"); + + b.Property("OrgNumber") + .HasColumnType("text"); + + b.Property("OrgStatus") + .HasColumnType("text"); + + b.Property("OrganizationSize") + .HasColumnType("text"); + + b.Property("OrganizationType") + .HasColumnType("text"); + + b.Property("RedStop") + .HasColumnType("boolean"); + + b.Property("Sector") + .HasColumnType("text"); + + b.Property("SectorSubSectorIndustryDesc") + .HasColumnType("text"); + + b.Property("SiteId") + .HasColumnType("uuid"); + + b.Property("StartedOperatingDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("SubSector") + .HasColumnType("text"); + + b.Property("SupplierId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UnityApplicantId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantName"); + + b.ToTable("Applicants", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAddress", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AddressType") + .HasColumnType("integer"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Postal") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("Street") + .HasColumnType("text"); + + b.Property("Street2") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Unit") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicantAddresses", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAgent", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("BceidBusinessGuid") + .HasColumnType("uuid"); + + b.Property("BceidBusinessName") + .HasColumnType("text"); + + b.Property("BceidUserGuid") + .HasColumnType("uuid"); + + b.Property("BceidUserName") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactOrder") + .HasColumnType("integer"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IdentityEmail") + .HasColumnType("text"); + + b.Property("IdentityName") + .HasColumnType("text"); + + b.Property("IdentityProvider") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsConfirmed") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OidcSubUser") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("Phone2") + .HasColumnType("text"); + + b.Property("Phone2Extension") + .HasColumnType("text"); + + b.Property("PhoneExtension") + .HasColumnType("text"); + + b.Property("RoleForApplicant") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationId") + .IsUnique(); + + b.HasIndex("OidcSubUser"); + + b.ToTable("ApplicantAgents", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Acquisition") + .HasColumnType("text"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("ApplicationStatusId") + .HasColumnType("uuid"); + + b.Property("ApprovedAmount") + .HasColumnType("numeric"); + + b.Property("AssessmentResultDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AssessmentResultStatus") + .HasColumnType("text"); + + b.Property("AssessmentStartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Community") + .HasColumnType("text"); + + b.Property("CommunityPopulation") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContractExecutionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ContractNumber") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeclineRational") + .HasColumnType("text"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("DueDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DueDiligenceStatus") + .HasColumnType("text"); + + b.Property("EconomicRegion") + .HasColumnType("text"); + + b.Property("ElectoralDistrict") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FinalDecisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Forestry") + .HasColumnType("text"); + + b.Property("ForestryFocus") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LikelihoodOfFunding") + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("NotificationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Payload") + .HasColumnType("jsonb"); + + b.Property("PercentageTotalProjectBudget") + .HasColumnType("double precision"); + + b.Property("Place") + .HasColumnType("text"); + + b.Property("ProjectEndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ProjectFundingTotal") + .HasColumnType("numeric"); + + b.Property("ProjectName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProjectStartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ProjectSummary") + .HasColumnType("text"); + + b.Property("ProposalDate") + .HasColumnType("timestamp without time zone"); + + b.Property("RecommendedAmount") + .HasColumnType("numeric"); + + b.Property("ReferenceNo") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegionalDistrict") + .HasColumnType("text"); + + b.Property("RequestedAmount") + .HasColumnType("numeric"); + + b.Property("RiskRanking") + .HasColumnType("text"); + + b.Property("SigningAuthorityBusinessPhone") + .HasColumnType("text"); + + b.Property("SigningAuthorityCellPhone") + .HasColumnType("text"); + + b.Property("SigningAuthorityEmail") + .HasColumnType("text"); + + b.Property("SigningAuthorityFullName") + .HasColumnType("text"); + + b.Property("SigningAuthorityTitle") + .HasColumnType("text"); + + b.Property("SubStatus") + .HasColumnType("text"); + + b.Property("SubmissionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TotalProjectBudget") + .HasColumnType("numeric"); + + b.Property("TotalScore") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationFormId"); + + b.HasIndex("ApplicationStatusId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Applications", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAssignment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AssigneeId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Duty") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("AssigneeId"); + + b.ToTable("ApplicationAssignments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationChefsFileAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ChefsFileId") + .HasColumnType("text"); + + b.Property("ChefsSumbissionId") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationChefsFileAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationContact", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactEmail") + .HasColumnType("text"); + + b.Property("ContactFullName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactMobilePhone") + .HasColumnType("text"); + + b.Property("ContactTitle") + .HasColumnType("text"); + + b.Property("ContactType") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactWorkPhone") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationContact", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationForm", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountCodingId") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .HasColumnType("text"); + + b.Property("ApplicationFormDescription") + .HasColumnType("text"); + + b.Property("ApplicationFormName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AttemptedConnectionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AvailableChefsFields") + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("text"); + + b.Property("ChefsApplicationFormGuid") + .HasColumnType("text"); + + b.Property("ChefsCriteriaFormGuid") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ConnectionHttpStatus") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ElectoralDistrictAddressType") + .HasColumnType("integer"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IntakeId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsDirectApproval") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Payable") + .HasColumnType("boolean"); + + b.Property("PaymentApprovalThreshold") + .HasColumnType("numeric"); + + b.Property("PreventPayment") + .HasColumnType("boolean"); + + b.Property("RenderFormIoToHtml") + .HasColumnType("boolean"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IntakeId"); + + b.ToTable("ApplicationForms", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormSubmission", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormVersionId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ChefsSubmissionGuid") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormVersionId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OidcSub") + .IsRequired() + .HasColumnType("text"); + + b.Property("RenderedHTML") + .HasColumnType("text"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Submission") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormSubmissions", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormVersion", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("AvailableChefsFields") + .HasColumnType("text"); + + b.Property("ChefsApplicationFormGuid") + .HasColumnType("text"); + + b.Property("ChefsFormVersionGuid") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormSchema") + .HasColumnType("jsonb"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SubmissionHeaderMapping") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormVersion", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationLink", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LinkType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("Related"); + + b.Property("LinkedApplicationId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationLinks", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationStatus", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExternalStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("InternalStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("StatusCode") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("StatusCode") + .IsUnique(); + + b.ToTable("ApplicationStatuses", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationTags", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TagId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("TagId"); + + b.ToTable("ApplicationTags", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AssessmentAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AssessmentId"); + + b.ToTable("AssessmentAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Assessments.Assessment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ApprovalRecommended") + .HasColumnType("boolean"); + + b.Property("AssessorId") + .HasColumnType("uuid"); + + b.Property("CleanGrowth") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("EconomicImpact") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FinancialAnalysis") + .HasColumnType("integer"); + + b.Property("InclusiveGrowth") + .HasColumnType("integer"); + + b.Property("IsComplete") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("AssessorId"); + + b.ToTable("Assessments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicationComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("CommenterId"); + + b.ToTable("ApplicationComments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.AssessmentComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AssessmentId"); + + b.HasIndex("CommenterId"); + + b.ToTable("AssessmentComments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.GlobalTag.Tag", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Tags", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Identity.Person", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Badge") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FullName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OidcDisplayName") + .IsRequired() + .HasColumnType("text"); + + b.Property("OidcSub") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("OidcSub"); + + b.ToTable("Persons", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Intakes.Intake", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Budget") + .HasColumnType("double precision"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IntakeName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("StartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Intakes", (string)null); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EmailGroups", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroupUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.ToTable("EmailGroupUsers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("BCC") + .IsRequired() + .HasColumnType("text"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CC") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChesMsgId") + .HasColumnType("uuid"); + + b.Property("ChesResponse") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChesStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FromAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Priority") + .IsRequired() + .HasColumnType("text"); + + b.Property("RetryAttempts") + .HasColumnType("integer"); + + b.Property("SendOnDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("SentDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("text"); + + b.Property("TemplateName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("ToAddress") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EmailLogs", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.EmailTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BodyHTML") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyText") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SendFrom") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("EmailTemplates", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.Subscriber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Subscribers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("SubscriptionGroups", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroupSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SubscriberId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.HasIndex("SubscriberId"); + + b.ToTable("SubscriptionGroupSubscribers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TemplateVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MapTo") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("TemplateVariables", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.Trigger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("InternalName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Triggers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TriggerSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SubscriptionGroupId") + .HasColumnType("uuid"); + + b.Property("TemplateId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TriggerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionGroupId"); + + b.HasIndex("TemplateId"); + + b.HasIndex("TriggerId"); + + b.ToTable("TriggerSubscriptions", "Notifications"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.AccountCodings.AccountCoding", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MinistryClient") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("Responsibility") + .IsRequired() + .HasColumnType("text"); + + b.Property("ServiceLine") + .IsRequired() + .HasColumnType("text"); + + b.Property("Stob") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("AccountCodings", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentConfigurations.PaymentConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DefaultAccountCodingId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentIdPrefix") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("PaymentConfigurations", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.ExpenseApproval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DecisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DecisionUserId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PaymentRequestId"); + + b.ToTable("ExpenseApprovals", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountCodingId") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("BatchName") + .IsRequired() + .HasColumnType("text"); + + b.Property("BatchNumber") + .HasColumnType("numeric"); + + b.Property("CasHttpStatusCode") + .HasColumnType("integer"); + + b.Property("CasResponse") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContractNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("InvoiceNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("InvoiceStatus") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsRecon") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("PayeeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentDate") + .HasColumnType("text"); + + b.Property("PaymentNumber") + .HasColumnType("text"); + + b.Property("PaymentStatus") + .HasColumnType("text"); + + b.Property("ReferenceNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("RequesterName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SiteId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("SubmissionConfirmationCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SupplierName") + .HasColumnType("text"); + + b.Property("SupplierNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AccountCodingId"); + + b.HasIndex("ReferenceNumber") + .IsUnique(); + + b.HasIndex("SiteId"); + + b.ToTable("PaymentRequests", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentTags.PaymentTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestId") + .HasColumnType("uuid"); + + b.Property("TagId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("PaymentRequestId"); + + b.HasIndex("TagId"); + + b.ToTable("PaymentTags", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentThresholds.PaymentThreshold", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Threshold") + .HasColumnType("numeric"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("PaymentThresholds", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Site", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddressLine1") + .HasColumnType("text"); + + b.Property("AddressLine2") + .HasColumnType("text"); + + b.Property("AddressLine3") + .HasColumnType("text"); + + b.Property("BankAccount") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EFTAdvicePref") + .HasColumnType("text"); + + b.Property("EmailAddress") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastUpdatedInCas") + .HasColumnType("timestamp without time zone"); + + b.Property("MarkDeletedInUse") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentGroup") + .HasColumnType("integer"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("SiteProtected") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("SupplierId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("SupplierId"); + + b.ToTable("Sites", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Supplier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BusinessNumber") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastUpdatedInCAS") + .HasColumnType("timestamp without time zone"); + + b.Property("MailingAddress") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("text"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("SIN") + .HasColumnType("text"); + + b.Property("StandardIndustryClassification") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("Subcategory") + .HasColumnType("text"); + + b.Property("SupplierProtected") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Suppliers", "Payments"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Scoresheet", "Scoresheet") + .WithMany("Instances") + .HasForeignKey("ScoresheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scoresheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Answer", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Question", "Question") + .WithMany("Answers") + .HasForeignKey("QuestionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", null) + .WithMany("Answers") + .HasForeignKey("ScoresheetInstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Question"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.ScoresheetSection", "Section") + .WithMany("Fields") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Scoresheet", "Scoresheet") + .WithMany("Sections") + .HasForeignKey("ScoresheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scoresheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.CustomFieldValue", b => + { + b.HasOne("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", null) + .WithMany("Values") + .HasForeignKey("WorksheetInstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetLinks.WorksheetLink", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.Worksheet", "Worksheet") + .WithMany("Links") + .HasForeignKey("WorksheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Worksheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.CustomField", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.WorksheetSection", "Section") + .WithMany("Fields") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.Worksheet", "Worksheet") + .WithMany("Sections") + .HasForeignKey("WorksheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Worksheet"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAddress", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", "Applicant") + .WithMany("ApplicantAddresses") + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicantAddresses") + .HasForeignKey("ApplicationId"); + + b.Navigation("Applicant"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAgent", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithOne("ApplicantAgent") + .HasForeignKey("Unity.GrantManager.Applications.ApplicantAgent", "ApplicationId"); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("OidcSubUser") + .HasPrincipalKey("OidcSub"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", "Applicant") + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", "ApplicationForm") + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationStatus", "ApplicationStatus") + .WithMany("Applications") + .HasForeignKey("ApplicationStatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Applicant"); + + b.Navigation("ApplicationForm"); + + b.Navigation("ApplicationStatus"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAssignment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicationAssignments") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", "Assignee") + .WithMany() + .HasForeignKey("AssigneeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Assignee"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAttachment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationChefsFileAttachment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationContact", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationForm", b => + { + b.HasOne("Unity.GrantManager.Intakes.Intake", null) + .WithMany() + .HasForeignKey("IntakeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormSubmission", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormVersion", b => + { + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationLink", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationTags", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicationTags") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.GlobalTag.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AssessmentAttachment", b => + { + b.HasOne("Unity.GrantManager.Assessments.Assessment", null) + .WithMany() + .HasForeignKey("AssessmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Assessments.Assessment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("Assessments") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("AssessorId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicationComment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.AssessmentComment", b => + { + b.HasOne("Unity.GrantManager.Assessments.Assessment", null) + .WithMany() + .HasForeignKey("AssessmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroupUser", b => + { + b.HasOne("Unity.Notifications.EmailGroups.EmailGroup", null) + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroupSubscription", b => + { + b.HasOne("Unity.Notifications.Templates.SubscriptionGroup", "SubscriptionGroup") + .WithMany() + .HasForeignKey("GroupId"); + + b.HasOne("Unity.Notifications.Templates.Subscriber", "Subscriber") + .WithMany() + .HasForeignKey("SubscriberId"); + + b.Navigation("Subscriber"); + + b.Navigation("SubscriptionGroup"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TriggerSubscription", b => + { + b.HasOne("Unity.Notifications.Templates.SubscriptionGroup", "SubscriptionGroup") + .WithMany() + .HasForeignKey("SubscriptionGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Notifications.Templates.EmailTemplate", "EmailTemplate") + .WithMany() + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Notifications.Templates.Trigger", "Trigger") + .WithMany() + .HasForeignKey("TriggerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmailTemplate"); + + b.Navigation("SubscriptionGroup"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.ExpenseApproval", b => + { + b.HasOne("Unity.Payments.Domain.PaymentRequests.PaymentRequest", "PaymentRequest") + .WithMany("ExpenseApprovals") + .HasForeignKey("PaymentRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PaymentRequest"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.HasOne("Unity.Payments.Domain.AccountCodings.AccountCoding", "AccountCoding") + .WithMany() + .HasForeignKey("AccountCodingId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Unity.Payments.Domain.Suppliers.Site", "Site") + .WithMany() + .HasForeignKey("SiteId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("AccountCoding"); + + b.Navigation("Site"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentTags.PaymentTag", b => + { + b.HasOne("Unity.Payments.Domain.PaymentRequests.PaymentRequest", null) + .WithMany("PaymentTags") + .HasForeignKey("PaymentRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.GlobalTag.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Site", b => + { + b.HasOne("Unity.Payments.Domain.Suppliers.Supplier", "Supplier") + .WithMany("Sites") + .HasForeignKey("SupplierId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.Navigation("Answers"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.Navigation("Answers"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Scoresheet", b => + { + b.Navigation("Instances"); + + b.Navigation("Sections"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.Navigation("Fields"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", b => + { + b.Navigation("Values"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.Worksheet", b => + { + b.Navigation("Links"); + + b.Navigation("Sections"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.Navigation("Fields"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Applicant", b => + { + b.Navigation("ApplicantAddresses"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.Navigation("ApplicantAddresses"); + + b.Navigation("ApplicantAgent"); + + b.Navigation("ApplicationAssignments"); + + b.Navigation("ApplicationTags"); + + b.Navigation("Assessments"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationStatus", b => + { + b.Navigation("Applications"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.Navigation("ExpenseApprovals"); + + b.Navigation("PaymentTags"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Supplier", b => + { + b.Navigation("Sites"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20250819225154_Add_ApplicationLinksType_Column.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20250819225154_Add_ApplicationLinksType_Column.cs new file mode 100644 index 000000000..ce7c2e8be --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20250819225154_Add_ApplicationLinksType_Column.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + /// + public partial class Add_ApplicationLinksType_Column : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LinkType", + table: "ApplicationLinks", + type: "text", + nullable: false, + defaultValue: "Related"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LinkType", + table: "ApplicationLinks"); + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/GrantTenantDbContextModelSnapshot.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/GrantTenantDbContextModelSnapshot.cs index 0f0cc39bc..e339ff024 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/GrantTenantDbContextModelSnapshot.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/GrantTenantDbContextModelSnapshot.cs @@ -1866,6 +1866,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid") .HasColumnName("LastModifierId"); + b.Property("LinkType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("Related"); + b.Property("LinkedApplicationId") .HasColumnType("uuid"); From 4d8e7129ceeaea8d1a51e5d7c3a0d7f51e72c667 Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Wed, 20 Aug 2025 18:07:07 -0700 Subject: [PATCH 09/55] AB#23904: Change links table to DataTable --- .../IApplicationLinksService.cs | 1 + .../ApplicationLinksAppService.cs | 20 +++ .../Localization/GrantManager/en.json | 7 +- .../ApplicationLinksModal.cshtml.cs | 7 +- .../Pages/GrantApplications/Details.js | 4 + .../ApplicationLinksWidgetViewComponent.cs | 5 +- .../ApplicationLinksWidget/Default.cshtml | 30 +---- .../ApplicationLinksWidget/Default.css | 57 +++++++-- .../ApplicationLinksWidget/Default.js | 114 ++++++++++++++++++ 9 files changed, 208 insertions(+), 37 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationLinksService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationLinksService.cs index a3a82e1e4..275d2c27c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationLinksService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationLinksService.cs @@ -11,5 +11,6 @@ public interface IApplicationLinksService : ICrudAppService< { Task> GetListByApplicationAsync(Guid applicationId); Task GetLinkedApplicationAsync(Guid currentApplicationId, Guid linkedApplicationId); + Task DeleteWithPairAsync(Guid applicationLinkId); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationLinksAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationLinksAppService.cs index a74b12eaf..6998622e2 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationLinksAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationLinksAppService.cs @@ -76,4 +76,24 @@ join appForm in applicationFormsQuery on application.ApplicationFormId equals ap return await combinedQuery.SingleAsync(); } + + public async Task DeleteWithPairAsync(Guid applicationLinkId) + { + // Get the link to find the paired record + var link = await Repository.GetAsync(applicationLinkId); + + // Find the paired link (reverse direction) + var applicationLinksQuery = await ApplicationLinksRepository.GetQueryableAsync(); + var pairedLink = await applicationLinksQuery + .Where(x => x.ApplicationId == link.LinkedApplicationId && x.LinkedApplicationId == link.ApplicationId) + .FirstOrDefaultAsync(); + + // Delete both links + await Repository.DeleteAsync(applicationLinkId); + + if (pairedLink != null) + { + await Repository.DeleteAsync(pairedLink.Id); + } + } } \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json index a8763b123..96cee198c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json @@ -455,6 +455,11 @@ "ApplicationBatchApprovalRequest:InvalidPermissions": "Invalid permissions", "ApplicationBatchApprovalRequest:InvalidApprovedAmount": "Invalid Approved Amount, it must be greater than 0.00", "ApplicationBatchApprovalRequest:MaxCountExceeded": "You have exceeded the maximum number of items for bulk approval. Please reduce the number to {0} or fewer", - "ApplicationBatchApprovalRequest:InvalidRecommendedAmount": "Invalid Recommended Amount, it must be greater than 0.00" + "ApplicationBatchApprovalRequest:InvalidRecommendedAmount": "Invalid Recommended Amount, it must be greater than 0.00", + + "ApplicationLinks:Category": "Category", + "ApplicationLinks:ID": "ID", + "ApplicationLinks:Status": "Status", + "ApplicationLinks:LinkType": "Link Type" } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinksModal.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinksModal.cshtml.cs index ff444e604..b1b648945 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinksModal.cshtml.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinksModal.cshtml.cs @@ -6,6 +6,7 @@ using System.ComponentModel; using System.Linq; using System.Threading.Tasks; +using Unity.GrantManager.Applications; using Unity.GrantManager.GrantApplications; using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; @@ -107,14 +108,16 @@ public async Task OnPostAsync() //For CurrentApplication await _applicationLinksService.CreateAsync(new ApplicationLinksDto{ ApplicationId = CurrentApplicationId ?? Guid.Empty, - LinkedApplicationId = linkedApplicationId + LinkedApplicationId = linkedApplicationId, + LinkType = ApplicationLinkType.Related }); //For LinkedApplication await _applicationLinksService.CreateAsync(new ApplicationLinksDto { ApplicationId = linkedApplicationId, - LinkedApplicationId = CurrentApplicationId ?? Guid.Empty + LinkedApplicationId = CurrentApplicationId ?? Guid.Empty, + LinkType = ApplicationLinkType.Related }); } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js index 9306c08d6..62dff016e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js @@ -24,6 +24,10 @@ $(function () { initCommentsWidget(); initEmailsWidget(); updateLinksCounters(); + // Update links counter again after a delay to catch AJAX-loaded data + setTimeout(() => { + updateLinksCounters(); + }, 1000); renderSubmission(); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationLinksWidget/ApplicationLinksWidgetViewComponent.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationLinksWidget/ApplicationLinksWidgetViewComponent.cs index 9c355d760..756b6706d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationLinksWidget/ApplicationLinksWidgetViewComponent.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationLinksWidget/ApplicationLinksWidgetViewComponent.cs @@ -26,10 +26,9 @@ public ApplicationLinksWidgetViewComponent(IApplicationLinksService applicationL public async Task InvokeAsync(Guid applicationId) { - var applicationList = await _applicationLinksService.GetListByApplicationAsync(applicationId); - List applicationLinks = applicationList.Where(item => item.ApplicationId != applicationId).ToList(); + // DataTables will load the data via AJAX, so we don't need to pre-load it here ApplicationLinksWidgetViewModel model = new() { - ApplicationLinks = applicationLinks, + ApplicationLinks = new List(), // Empty list since DataTables will load the data ApplicationId = applicationId }; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationLinksWidget/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationLinksWidget/Default.cshtml index b3c026aa9..7cf9629a8 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationLinksWidget/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationLinksWidget/Default.cshtml @@ -4,33 +4,15 @@ @{ Layout = null; } - ${escapedLinkType} From 01b13f6a1ac784c42a6f7d9dce298356fe59e0ab Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Thu, 28 Aug 2025 10:04:30 -0700 Subject: [PATCH 22/55] feature/AB#26441-DynamicUrls-Sonar --- .../EndpointManagement/Endpoints/Index.cshtml | 3 - .../ApplicationFormAppService.cs | 140 +++++++++++------- .../GrantManagerApplicationModule.cs | 24 --- .../Intakes/IntakeSubmissionAppService.cs | 9 +- .../Controllers/FormController.cs | 3 - 5 files changed, 85 insertions(+), 94 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.cshtml b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.cshtml index ff13f26ce..66071335e 100644 --- a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.cshtml @@ -3,9 +3,6 @@ @using Microsoft.AspNetCore.Mvc.Localization; @using Unity.TenantManagement.Web.Navigation; @using Volo.Abp.AspNetCore.Mvc.UI.Layout; -@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Pages.Shared.Components.AbpPageToolbar; -@using Volo.Abp.FeatureManagement; -@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; @using Volo.Abp.TenantManagement.Localization; @using Unity.TenantManagement.Web.Pages.TenantManagement.Tenants; @model IndexModel diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormAppService.cs index d31d7400a..d8901abcc 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormAppService.cs @@ -19,25 +19,42 @@ namespace Unity.GrantManager.ApplicationForms; [Authorize] -public class ApplicationFormAppService(IRepository repository, - IStringEncryptionService stringEncryptionService, - IApplicationFormVersionAppService applicationFormVersionAppService, - IApplicationFormVersionRepository applicationFormVersionRepository, - IGrantApplicationAppService applicationService, - IFormsApiService formsApiService) : -CrudAppService< - ApplicationForm, - ApplicationFormDto, - Guid, - PagedAndSortedResultRequestDto, - CreateUpdateApplicationFormDto>(repository), - IApplicationFormAppService +public class ApplicationFormAppService + : CrudAppService< + ApplicationForm, + ApplicationFormDto, + Guid, + PagedAndSortedResultRequestDto, + CreateUpdateApplicationFormDto>, + IApplicationFormAppService { + private readonly IStringEncryptionService _stringEncryptionService; + private readonly IApplicationFormVersionAppService _applicationFormVersionAppService; + private readonly IApplicationFormVersionRepository _applicationFormVersionRepository; + private readonly IGrantApplicationAppService _applicationService; + private readonly IFormsApiService _formsApiService; + + public ApplicationFormAppService( + IRepository repository, + IStringEncryptionService stringEncryptionService, + IApplicationFormVersionAppService applicationFormVersionAppService, + IApplicationFormVersionRepository applicationFormVersionRepository, + IGrantApplicationAppService applicationService, + IFormsApiService formsApiService) + : base(repository) + { + _stringEncryptionService = stringEncryptionService; + _applicationFormVersionAppService = applicationFormVersionAppService; + _applicationFormVersionRepository = applicationFormVersionRepository; + _applicationService = applicationService; + _formsApiService = formsApiService; + } + [Authorize(GrantManagerPermissions.ApplicationForms.Default)] public override async Task CreateAsync(CreateUpdateApplicationFormDto input) { - input.ApiKey = stringEncryptionService.Encrypt(input.ApiKey); - ApplicationFormDto applicationFormDto = await base.CreateAsync(input); + input.ApiKey = _stringEncryptionService.Encrypt(input.ApiKey); + var applicationFormDto = await base.CreateAsync(input); return await InitializeFormVersion(applicationFormDto.Id, input); } @@ -45,59 +62,63 @@ public override async Task CreateAsync(CreateUpdateApplicati public override async Task UpdateAsync(Guid id, CreateUpdateApplicationFormDto input) { var existingForm = await Repository.GetAsync(id); - input.ApiKey = stringEncryptionService.Encrypt(input.ApiKey); + input.ApiKey = _stringEncryptionService.Encrypt(input.ApiKey); bool hasFormGuidChanged = existingForm.ChefsApplicationFormGuid != input.ChefsApplicationFormGuid; bool hasFormApiKeyChanged = existingForm.ApiKey != input.ApiKey; - // Only initialize form version if changes are made to form connection details if (hasFormGuidChanged || hasFormApiKeyChanged) { return await InitializeFormVersion(id, input); } - else - { - return await base.UpdateAsync(id, input); - } + + return await base.UpdateAsync(id, input); } [Authorize(GrantManagerPermissions.ApplicationForms.Default)] private async Task InitializeFormVersion(Guid id, CreateUpdateApplicationFormDto input) { var applicationFormDto = new ApplicationFormDto(); + try { - if (input.ChefsApplicationFormGuid != null && input.ApiKey != null) + if (!string.IsNullOrWhiteSpace(input.ChefsApplicationFormGuid) && !string.IsNullOrWhiteSpace(input.ApiKey)) { - dynamic form = await formsApiService.GetForm(Guid.Parse(input.ChefsApplicationFormGuid), input.ChefsApplicationFormGuid.ToString(), input.ApiKey); - if (form != null) + var form = await _formsApiService.GetForm( + Guid.Parse(input.ChefsApplicationFormGuid), + input.ChefsApplicationFormGuid, + input.ApiKey); + + if (form is JObject formObject) { - JObject formObject = JObject.Parse(form.ToString()); - var formName = formObject.SelectToken("name"); - if (formName != null) + var formName = formObject.SelectToken("name")?.ToString(); + if (!string.IsNullOrWhiteSpace(formName)) { - input.ApplicationFormName = formName.ToString(); + input.ApplicationFormName = formName; applicationFormDto = await base.UpdateAsync(id, input); } - bool initializePublishedOnly = false; - await applicationFormVersionAppService.InitializePublishedFormVersion(form, id, initializePublishedOnly); + + await _applicationFormVersionAppService.InitializePublishedFormVersion(formObject, id, initializePublishedOnly: false); } } + return applicationFormDto; } catch (Exception ex) { - throw new UserFriendlyException("Exception: " + ex.Message + "\n\r Please check the CHEFS Form ID and CHEFS Form API Key"); + throw new UserFriendlyException( + "Error initializing CHEFS form. Please check the CHEFS: Form ID and API Key.", + innerException: ex + ); } - } [Authorize(GrantManagerPermissions.ApplicationForms.Default)] public override async Task GetAsync(Guid id) { var dto = await base.GetAsync(id); - dto.ApiKey = stringEncryptionService.Decrypt(dto.ApiKey); - dto.ApiToken = stringEncryptionService.Decrypt(dto.ApiToken); + dto.ApiKey = _stringEncryptionService.Decrypt(dto.ApiKey); + dto.ApiToken = _stringEncryptionService.Decrypt(dto.ApiToken); return dto; } @@ -111,64 +132,69 @@ public async Task GetElectoralDistrictAddressTypeAsync(Guid id) [Authorize(GrantManagerPermissions.ApplicationForms.Default)] public async Task> GetPublishedVersionsAsync(Guid id) { - IQueryable queryableFormVersions = applicationFormVersionRepository.GetQueryableAsync().Result; - var formVersions = queryableFormVersions.Where(c => c.ApplicationFormId.Equals(id) && c.Published.Equals(true)).ToList(); - return await Task.FromResult>(ObjectMapper.Map, List>([.. formVersions.OrderByDescending(s => s.Version)])); + var queryableFormVersions = await _applicationFormVersionRepository.GetQueryableAsync(); + var formVersions = queryableFormVersions + .Where(c => c.ApplicationFormId == id && c.Published) + .OrderByDescending(s => s.Version) + .ToList(); + + return ObjectMapper.Map, List>(formVersions); } [Authorize(GrantManagerPermissions.ApplicationForms.Default)] public async Task> GetVersionsAsync(Guid id) { - IQueryable queryableFormVersions = applicationFormVersionRepository.GetQueryableAsync().Result; - var formVersions = queryableFormVersions.Where(c => c.ApplicationFormId.Equals(id)).ToList(); - return await Task.FromResult>(ObjectMapper.Map, List>([.. formVersions.OrderByDescending(s => s.Version)])); + var queryableFormVersions = await _applicationFormVersionRepository.GetQueryableAsync(); + var formVersions = queryableFormVersions + .Where(c => c.ApplicationFormId == id) + .OrderByDescending(s => s.Version) + .ToList(); + + return ObjectMapper.Map, List>(formVersions); } [Authorize(GrantManagerPermissions.ApplicationForms.Default)] public async Task SaveApplicationFormScoresheet(FormScoresheetDto dto) { - var appForm = await repository.GetAsync(dto.ApplicationFormId); + var appForm = await Repository.GetAsync(dto.ApplicationFormId); appForm.ScoresheetId = dto.ScoresheetId; - await repository.UpdateAsync(appForm); + await Repository.UpdateAsync(appForm); } [Authorize(GrantManagerPermissions.ApplicationForms.Default)] public async Task PatchOtherConfig(Guid id, OtherConfigDto config) { - var form = await repository.GetAsync(id); - + var form = await Repository.GetAsync(id); form.IsDirectApproval = config.IsDirectApproval; form.ElectoralDistrictAddressType = config.ElectoralDistrictAddressType; - - await repository.UpdateAsync(form); + await Repository.UpdateAsync(form); } [Authorize(PaymentsPermissions.Payments.EditFormPaymentConfiguration)] public async Task GetFormPreventPaymentStatusByApplicationId(Guid applicationId) { - // Get the payment threshold for the application - GrantApplicationDto grantApplicationDto = await applicationService.GetAsync(applicationId); - Guid formId = grantApplicationDto.ApplicationForm.Id; - ApplicationForm appForm = await repository.GetAsync(formId); + var grantApplicationDto = await _applicationService.GetAsync(applicationId); + var formId = grantApplicationDto.ApplicationForm.Id; + var appForm = await Repository.GetAsync(formId); return appForm.PreventPayment; } + [Authorize(PaymentsPermissions.Payments.EditFormPaymentConfiguration)] public async Task SavePaymentConfiguration(FormPaymentConfigurationDto dto) { - ApplicationForm appForm = await repository.GetAsync(dto.ApplicationFormId); + var appForm = await Repository.GetAsync(dto.ApplicationFormId); appForm.AccountCodingId = dto.AccountCodingId; appForm.Payable = dto.Payable; appForm.PreventPayment = dto.PreventPayment; appForm.PaymentApprovalThreshold = dto.PaymentApprovalThreshold; - await repository.UpdateAsync(appForm); + await Repository.UpdateAsync(appForm); } - + public async Task GetFormPaymentApprovalThresholdByApplicationIdAsync(Guid applicationId) { - // Get the payment threshold for the application - GrantApplicationDto application = await applicationService.GetAsync(applicationId); - Guid formId = application.ApplicationForm.Id; - ApplicationForm appForm = await repository.GetAsync(formId); + var application = await _applicationService.GetAsync(applicationId); + var formId = application.ApplicationForm.Id; + var appForm = await Repository.GetAsync(formId); return appForm.PaymentApprovalThreshold; } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs index 5483dc4b9..ea8d5a8b1 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs @@ -37,7 +37,6 @@ using Unity.GrantManager.Infrastructure; using Medallion.Threading; using Unity.GrantManager.Locks; -using Volo.Abp; using JsonSerializerOptions = System.Text.Json.JsonSerializerOptions; using Unity.GrantManager.Integrations.Chefs; using Unity.Modules.Shared.Http; @@ -200,27 +199,4 @@ private void ConfigureBackgroundServices(IConfiguration configuration) options.Quartz.IsAutoRegisterEnabled = configuration.GetValue("BackgroundJobs:Quartz:IsAutoRegisterEnabled"); }); } - - private void ConfigureDistributedCache(ServiceConfigurationContext context, IConfiguration configuration) - { - if (!Convert.ToBoolean(configuration["Redis:IsEnabled"])) - return; - - context.Services.AddStackExchangeRedisCache(options => - { - options.InstanceName = configuration["Redis:InstanceName"]; - options.Configuration = $"{configuration["Redis:Host"]}:{configuration["Redis:Port"]},password={configuration["Redis:Password"]}"; - }); - - Configure(options => - { - options.InstanceName = configuration["Redis:InstanceName"]; - options.Configuration = $"{configuration["Redis:Host"]}:{configuration["Redis:Port"]},password={configuration["Redis:Password"]}"; - }); - - Configure(options => - { - options.KeyPrefix = configuration["Redis:KeyPrefix"] ?? "unity"; - }); - } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/IntakeSubmissionAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/IntakeSubmissionAppService.cs index 43dc33266..e095bdb7c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/IntakeSubmissionAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/IntakeSubmissionAppService.cs @@ -7,8 +7,6 @@ using Unity.GrantManager.Applications; using Unity.GrantManager.Events; using Unity.GrantManager.Exceptions; -using Unity.GrantManager.Intake; -using Unity.GrantManager.Integrations; using Unity.GrantManager.Integrations.Chefs; using Unity.GrantManager.Notifications; @@ -19,11 +17,9 @@ namespace Unity.GrantManager.Intakes [RemoteService(false)] public class IntakeSubmissionAppService(INotificationsAppService notificationsAppService, IIntakeFormSubmissionManager intakeFormSubmissionManager, - IEndpointManagementAppService endpointManagementAppService, IFormsApiService formsApiService, IApplicationFormRepository applicationFormRepository, - IApplicationFormVersionAppService applicationFormVersionAppService, - IOptions intakeClientOptions) : GrantManagerAppService, IIntakeSubmissionAppService + IApplicationFormVersionAppService applicationFormVersionAppService) : GrantManagerAppService, IIntakeSubmissionAppService { public async Task CreateIntakeSubmissionAsync(EventSubscriptionDto eventSubscriptionDto) @@ -83,8 +79,7 @@ private async Task ValidateSubmission(EventSubscriptionDto eventSubscripti string factValue = $"FormId: {eventSubscriptionDto.FormId} FormVersion: {eventSubscriptionDto.FormVersion}"; await notificationsAppService.NotifyChefsEventToTeamsAsync(factName, factValue, true); return false; - } else //if(!intakeClientOptions.Value.AllowUnregisteredVersions) - { + } else { var version = ((JObject)formVersion!).SelectToken("version"); var published = ((JObject)formVersion!).SelectToken("published"); string factName = "Application Form Version Not Registered - Unknown Version"; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/FormController.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/FormController.cs index 04888a55b..bf90e9424 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/FormController.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/FormController.cs @@ -9,7 +9,6 @@ using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.Domain.Entities; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; using Unity.GrantManager.Integrations.Chefs; namespace Unity.GrantManager.Controllers @@ -26,8 +25,6 @@ public partial class FormController( private readonly IApplicationFormSubmissionRepository _applicationFormSubmissionRepository = applicationFormSubmissionRepository; private readonly IFormsApiService _formsApiService = formsApiService; - protected ILogger Logger => LazyServiceProvider.LazyGetService(provider => LoggerFactory?.CreateLogger(GetType().FullName!) ?? NullLogger.Instance); - [HttpPost("form/{formId}/version/{formVersionId}")] public async Task SynchronizeChefsAvailableFields(string formId, string formVersionId) { From 69e7d71adcecaedac81c23c7c7972c36df9326e5 Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Thu, 28 Aug 2025 10:10:11 -0700 Subject: [PATCH 23/55] feature/AB#26441-DynamicUrls-Sonar --- .../Intakes/IntakeSubmissionAppService.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/IntakeSubmissionAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/IntakeSubmissionAppService.cs index e095bdb7c..aaa3f5a84 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/IntakeSubmissionAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/IntakeSubmissionAppService.cs @@ -1,5 +1,4 @@ -using Microsoft.Extensions.Options; -using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Linq; using System; using System.Linq; using System.Threading.Tasks; From de1ea96b65903b9416d6949ae97b892d2d4f5e80 Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Thu, 28 Aug 2025 10:13:09 -0700 Subject: [PATCH 24/55] feature/AB#26441-DynamicUrls-remove urls from app settings --- .../src/Unity.GrantManager.Web/appsettings.json | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.json b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.json index 6e876953f..768232398 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.json +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.json @@ -29,15 +29,10 @@ "CasClientSecret": "" }, "Notifications": { - "TeamsNotificationsWebhook": "", - "ChesUrl": "https://ches-dev.api.gov.bc.ca/api/v1", - "ChesTokenUrl": "https://dev.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", "ChesClientId": "", - "ChesClientSecret": "", - "ChesFromEmail": "unity@gov.bc.ca" + "ChesClientSecret": "" }, "Intake": { - "BaseUri": "https://chefs-dev.apps.silver.devops.gov.bc.ca/app/api/v1", "FormId": "", "ApiKey": "", "BearerTokenPlaceholder": "", @@ -59,10 +54,8 @@ "IsBehindTlsTerminationProxy": false }, "CssApi": { - "TokenUrl": "https://loginproxy.gov.bc.ca/auth/realms/standard/protocol/openid-connect/token", "ClientId": "service-account-team-1552-4984", "ClientSecret": "", - "Url": "https://api.loginproxy.gov.bc.ca/api/v1", "Env": "dev" }, "S3": { @@ -76,8 +69,7 @@ "MaxFileSize": 25 }, "Geocoder": { - "BaseUri": "https://openmaps.gov.bc.ca/geo/pub/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=", - "ElectoralDistrict": { + "ElectoralDistrict": { "feature": "pub:WHSE_ADMIN_BOUNDARIES.EBC_PROV_ELECTORAL_DIST_SVW", "property": "ED_NAME", "querytype": "SHAPE" @@ -91,10 +83,8 @@ "feature": "pub:WHSE_LEGAL_ADMIN_BOUNDARIES.ABMS_REGIONAL_DISTRICTS_SP", "property": "ADMIN_AREA_NAME", "querytype": "SHAPE" - }, - "LocationDetails": { - "BaseUri": "https://geocoder.api.gov.bc.ca" } + }, "Redis": { "IsEnabled": false, From 768b7438beddfab1eaebdc7beb7a6ef7bbd78538 Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Thu, 28 Aug 2025 11:27:31 -0700 Subject: [PATCH 25/55] feature/AB#26441-DynamicUrls-Sonar --- .../GrantManagerApplicationModule.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs index ea8d5a8b1..bf9b32415 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs @@ -40,8 +40,6 @@ using JsonSerializerOptions = System.Text.Json.JsonSerializerOptions; using Unity.GrantManager.Integrations.Chefs; using Unity.Modules.Shared.Http; -using Volo.Abp.Caching; -using Microsoft.Extensions.Caching.StackExchangeRedis; namespace Unity.GrantManager; From 3cfc659d95407dc9e94a92903616feaa8c395333 Mon Sep 17 00:00:00 2001 From: James Pasta <129337673+JamesPasta@users.noreply.github.com> Date: Thu, 28 Aug 2025 12:55:12 -0700 Subject: [PATCH 26/55] Update applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Integrations/Chefs/FormsApiService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs index 058ac8b3f..4abeb638d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs @@ -63,7 +63,7 @@ public async Task GetForm(Guid? formId, string chefsApplicationFormGuid string url = $"{chefsApi}/forms/{formId}"; var response = await GetRequestAsync(url, chefsApplicationFormGuid, encryptedApiKey); - return await ParseJsonResponseAsync(response) ?? []; + return await ParseJsonResponseAsync(response) ?? new JObject(); } public async Task GetSubmissionDataAsync(Guid chefsFormId, Guid submissionId) From ccc2709ef286ab1079c77fec15f62228d4b25ad8 Mon Sep 17 00:00:00 2001 From: James Pasta <129337673+JamesPasta@users.noreply.github.com> Date: Thu, 28 Aug 2025 12:58:00 -0700 Subject: [PATCH 27/55] Update applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js index bd6d38039..a24c4d86d 100644 --- a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js +++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js @@ -335,7 +335,7 @@ function initializeDataTable(options) { ); // On the ITAdministrator pages, bootstrap popover is not loaded and causes the js to fail - if($('#btn-toggle-filter').popover+"" != "undefined") { + if (typeof $('#btn-toggle-filter').popover !== "undefined") { initializeFilterButtonPopover(iDt); } From 47f0eec7246813df629b4a352d51f3dcd3d1b9d4 Mon Sep 17 00:00:00 2001 From: James Pasta <129337673+JamesPasta@users.noreply.github.com> Date: Thu, 28 Aug 2025 12:58:19 -0700 Subject: [PATCH 28/55] Update applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Integrations/DynamicUrlKeyNames.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Integrations/DynamicUrlKeyNames.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Integrations/DynamicUrlKeyNames.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Integrations/DynamicUrlKeyNames.cs index 658f05c77..8e1c9cce8 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Integrations/DynamicUrlKeyNames.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Integrations/DynamicUrlKeyNames.cs @@ -7,8 +7,8 @@ public static class DynamicUrlKeyNames public const string INTAKE_API_BASE = "INTAKE_API_BASE"; public const string PAYMENT_API_BASE = "PAYMENT_API_BASE"; public const string ORGBOOK_API_BASE = "ORGBOOK_API_BASE"; - public const string NOTFICATION_API_BASE = "NOTFICATION_API_BASE"; - public const string NOTFICATION_AUTH = "NOTFICATION_AUTH"; + public const string NOTIFICATION_API_BASE = "NOTIFICATION_API_BASE"; + public const string NOTIFICATION_AUTH = "NOTIFICATION_AUTH"; public const string DIRECT_MESSAGE_KEY_PREFIX = "DIRECT_MESSAGE_"; // Teams Direct Message URL Weebhook- Dynamically incremented public const string WEBHOOK_KEY_PREFIX = "WEBHOOK_"; // General Webhook URL - Dynamically incremented public const string GEOCODER_API_BASE = "GEOCODER_API_BASE"; From 8aa6e2d73fe7ac8cb9b93dc314fda6df6f888fd8 Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Thu, 28 Aug 2025 13:00:15 -0700 Subject: [PATCH 29/55] feature/AB#26441-DynamicUrls-Copilot --- .../Integrations/Ches/ChesClientService.cs | 4 ++-- .../Integrations/DynamicUrlKeyNames.cs | 4 ++-- .../Integrations/DynamicUrlDataSeeder.cs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Integrations/Ches/ChesClientService.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Integrations/Ches/ChesClientService.cs index 378f0387d..1939807c1 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Integrations/Ches/ChesClientService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Integrations/Ches/ChesClientService.cs @@ -25,7 +25,7 @@ IOptions chesClientOptions public async Task SendAsync(object emailRequest) { string authToken = await GetAuthTokenAsync(); - string notificationsApiUrl = await endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.NOTFICATION_API_BASE); + string notificationsApiUrl = await endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.NOTIFICATION_API_BASE); var resource = $"{notificationsApiUrl}/email"; // Pass the object directly; ResilientHttpRequest will serialize it to JSON @@ -41,7 +41,7 @@ IOptions chesClientOptions private async Task GetAuthTokenAsync() { - string notificationsAuthUrl = await endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.NOTFICATION_AUTH); + string notificationsAuthUrl = await endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.NOTIFICATION_AUTH); ClientOptions clientOptions = new() { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Integrations/DynamicUrlKeyNames.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Integrations/DynamicUrlKeyNames.cs index 658f05c77..8e1c9cce8 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Integrations/DynamicUrlKeyNames.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Integrations/DynamicUrlKeyNames.cs @@ -7,8 +7,8 @@ public static class DynamicUrlKeyNames public const string INTAKE_API_BASE = "INTAKE_API_BASE"; public const string PAYMENT_API_BASE = "PAYMENT_API_BASE"; public const string ORGBOOK_API_BASE = "ORGBOOK_API_BASE"; - public const string NOTFICATION_API_BASE = "NOTFICATION_API_BASE"; - public const string NOTFICATION_AUTH = "NOTFICATION_AUTH"; + public const string NOTIFICATION_API_BASE = "NOTIFICATION_API_BASE"; + public const string NOTIFICATION_AUTH = "NOTIFICATION_AUTH"; public const string DIRECT_MESSAGE_KEY_PREFIX = "DIRECT_MESSAGE_"; // Teams Direct Message URL Weebhook- Dynamically incremented public const string WEBHOOK_KEY_PREFIX = "WEBHOOK_"; // General Webhook URL - Dynamically incremented public const string GEOCODER_API_BASE = "GEOCODER_API_BASE"; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlDataSeeder.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlDataSeeder.cs index 3cd5a8cbb..a1c43db3e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlDataSeeder.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlDataSeeder.cs @@ -47,8 +47,8 @@ private async Task SeedDynamicUrlAsync() new() { KeyName = DynamicUrlKeyNames.PAYMENT_API_BASE, Url = DynamicUrls.CAS_PROD_URL, Description = "BC Corporate Accounting Services API" }, new() { KeyName = DynamicUrlKeyNames.ORGBOOK_API_BASE, Url = DynamicUrls.ORGBOOK_PROD_URL, Description = "OrgBook Services API" }, new() { KeyName = DynamicUrlKeyNames.INTAKE_API_BASE, Url = DynamicUrls.CHEFS_PROD_URL, Description = "Common Hosted Forms Service API" }, - new() { KeyName = DynamicUrlKeyNames.NOTFICATION_API_BASE, Url = DynamicUrls.CHES_PROD_URL, Description = "Common Hosted Email Service API" }, - new() { KeyName = DynamicUrlKeyNames.NOTFICATION_AUTH, Url = DynamicUrls.CHES_PROD_AUTH, Description = "Common Hosted Email Service OAUTH" }, + new() { KeyName = DynamicUrlKeyNames.NOTIFICATION_API_BASE, Url = DynamicUrls.CHES_PROD_URL, Description = "Common Hosted Email Service API" }, + new() { KeyName = DynamicUrlKeyNames.NOTIFICATION_AUTH, Url = DynamicUrls.CHES_PROD_AUTH, Description = "Common Hosted Email Service OAUTH" }, new() { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{messageIndex++}", Url = "", Description = $"Direct message webhook {messageIndex}" }, new() { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{messageIndex++}", Url = "", Description = $"Direct message webhook {messageIndex}" }, new() { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{messageIndex++}", Url = "", Description = $"Direct message webhook {messageIndex}" }, From 15f2a065286023f880947bccf56d6aea92bffb9c Mon Sep 17 00:00:00 2001 From: James Pasta <129337673+JamesPasta@users.noreply.github.com> Date: Thu, 28 Aug 2025 13:05:07 -0700 Subject: [PATCH 30/55] Update applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/SupplierService.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Integrations/Cas/SupplierService.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/SupplierService.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/SupplierService.cs index 006bba43b..1227d82bc 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/SupplierService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/SupplierService.cs @@ -97,11 +97,12 @@ private async Task UpdateSupplierInfo(dynamic casSupplierResponse, Guid applican { try { - if (casSupplierResponse.TryGetProperty("code", out JsonElement codeProp) && codeProp.GetString() == "Unauthorized") - throw new UserFriendlyException("Unauthorized access to CAS supplier information."); var casSupplierJson = casSupplierResponse is string str ? str : casSupplierResponse.ToString(); using var doc = JsonDocument.Parse(casSupplierJson); - UpsertSupplierEto supplierEto = GetEventDtoFromCasResponse(doc.RootElement); + var rootElement = doc.RootElement; + if (rootElement.TryGetProperty("code", out JsonElement codeProp) && codeProp.GetString() == "Unauthorized") + throw new UserFriendlyException("Unauthorized access to CAS supplier information."); + UpsertSupplierEto supplierEto = GetEventDtoFromCasResponse(rootElement); supplierEto.CorrelationId = applicantId; supplierEto.CorrelationProvider = CorrelationConsts.Applicant; await localEventBus.PublishAsync(supplierEto); From e5ef8d7b6c96a725a71d3c6b6d2b98d6000a1c65 Mon Sep 17 00:00:00 2001 From: James Pasta <129337673+JamesPasta@users.noreply.github.com> Date: Thu, 28 Aug 2025 13:05:21 -0700 Subject: [PATCH 31/55] Potential fix for code scanning alert no. 49: Log entries created from user input Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../Integrations/Chefs/FormsApiService.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs index 4abeb638d..46d02eff1 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs @@ -41,7 +41,8 @@ private async Task GetChefsApiBaseUrlAsync() if (applicationForm == null) { - logger.LogWarning("No application form found for FormId: {FormId}", chefsFormId); + string sanitizedFormId = chefsFormId.Replace("\n", "").Replace("\r", ""); + logger.LogWarning("No application form found for FormId: {FormId}", sanitizedFormId); return null; } From e2c1d4db3d0cf4fc60ba1380222d15bef1b7d301 Mon Sep 17 00:00:00 2001 From: James Pasta <129337673+JamesPasta@users.noreply.github.com> Date: Thu, 28 Aug 2025 13:06:33 -0700 Subject: [PATCH 32/55] Update applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20250514165300_DynamicUrls.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Migrations/HostMigrations/20250514165300_DynamicUrls.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20250514165300_DynamicUrls.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20250514165300_DynamicUrls.cs index 0fe143de2..dd6924e54 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20250514165300_DynamicUrls.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20250514165300_DynamicUrls.cs @@ -18,7 +18,7 @@ protected override void Up(MigrationBuilder migrationBuilder) Id = table.Column(type: "uuid", nullable: false), KeyName = table.Column(type: "text", nullable: false), Url = table.Column(type: "text", nullable: false), - Description = table.Column(type: "text", nullable: false), + Description = table.Column(type: "text", nullable: true), TenantId = table.Column(type: "uuid", nullable: true), ExtraProperties = table.Column(type: "text", nullable: false, defaultValue: "{}"), ConcurrencyStamp = table.Column(type: "character varying(40)", maxLength: 40, nullable: false), From 867bc1e8aa09021c0033b9fba318cd0e551e863a Mon Sep 17 00:00:00 2001 From: James Pasta <129337673+JamesPasta@users.noreply.github.com> Date: Thu, 28 Aug 2025 13:10:28 -0700 Subject: [PATCH 33/55] Update applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlDataSeeder.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Integrations/DynamicUrlDataSeeder.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlDataSeeder.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlDataSeeder.cs index a1c43db3e..8291ab412 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlDataSeeder.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlDataSeeder.cs @@ -49,12 +49,12 @@ private async Task SeedDynamicUrlAsync() new() { KeyName = DynamicUrlKeyNames.INTAKE_API_BASE, Url = DynamicUrls.CHEFS_PROD_URL, Description = "Common Hosted Forms Service API" }, new() { KeyName = DynamicUrlKeyNames.NOTIFICATION_API_BASE, Url = DynamicUrls.CHES_PROD_URL, Description = "Common Hosted Email Service API" }, new() { KeyName = DynamicUrlKeyNames.NOTIFICATION_AUTH, Url = DynamicUrls.CHES_PROD_AUTH, Description = "Common Hosted Email Service OAUTH" }, - new() { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{messageIndex++}", Url = "", Description = $"Direct message webhook {messageIndex}" }, - new() { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{messageIndex++}", Url = "", Description = $"Direct message webhook {messageIndex}" }, - new() { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{messageIndex++}", Url = "", Description = $"Direct message webhook {messageIndex}" }, - new() { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{webhookIndex++}", Url = "", Description = $"Webhook {webhookIndex}" }, - new() { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{webhookIndex++}", Url = "", Description = $"Webhook {webhookIndex}" }, - new() { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{webhookIndex++}", Url = "", Description = $"Webhook {webhookIndex}" }, + (() => { var currentMessageIndex = messageIndex++; return new DynamicUrl { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{currentMessageIndex}", Url = "", Description = $"Direct message webhook {currentMessageIndex}" }; })(), + (() => { var currentMessageIndex = messageIndex++; return new DynamicUrl { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{currentMessageIndex}", Url = "", Description = $"Direct message webhook {currentMessageIndex}" }; })(), + (() => { var currentMessageIndex = messageIndex++; return new DynamicUrl { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{currentMessageIndex}", Url = "", Description = $"Direct message webhook {currentMessageIndex}" }; })(), + (() => { var currentWebhookIndex = webhookIndex++; return new DynamicUrl { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{currentWebhookIndex}", Url = "", Description = $"Webhook {currentWebhookIndex}" }; })(), + (() => { var currentWebhookIndex = webhookIndex++; return new DynamicUrl { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{currentWebhookIndex}", Url = "", Description = $"Webhook {currentWebhookIndex}" }; })(), + (() => { var currentWebhookIndex = webhookIndex++; return new DynamicUrl { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{currentWebhookIndex}", Url = "", Description = $"Webhook {currentWebhookIndex}" }; })(), }; foreach (var dynamicUrl in dynamicUrls) From e0594fd8da3a1398e9f94c2c5d85df9a0203b8bc Mon Sep 17 00:00:00 2001 From: James Pasta <129337673+JamesPasta@users.noreply.github.com> Date: Thu, 28 Aug 2025 13:11:56 -0700 Subject: [PATCH 34/55] Update applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Integrations/Chefs/FormsApiService.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs index 46d02eff1..4abeb638d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs @@ -41,8 +41,7 @@ private async Task GetChefsApiBaseUrlAsync() if (applicationForm == null) { - string sanitizedFormId = chefsFormId.Replace("\n", "").Replace("\r", ""); - logger.LogWarning("No application form found for FormId: {FormId}", sanitizedFormId); + logger.LogWarning("No application form found for FormId: {FormId}", chefsFormId); return null; } From d6e9d24ed86f13fd56ad4532095eb28fe3d49bd2 Mon Sep 17 00:00:00 2001 From: James Pasta <129337673+JamesPasta@users.noreply.github.com> Date: Thu, 28 Aug 2025 13:12:50 -0700 Subject: [PATCH 35/55] Potential fix for code scanning alert no. 51: Log entries created from user input Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../Integrations/Chefs/FormsApiService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs index 4abeb638d..86b88847c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs @@ -111,7 +111,7 @@ private async Task GetRequestAsync(string url, string chefs var content = await response.Content.ReadAsStringAsync(); logger.LogError( "Request to {Url} failed with status {StatusCode} ({Reason}). Response: {Content}", - url, + url.Replace(Environment.NewLine, "").Replace("\n", "").Replace("\r", ""), response.StatusCode, response.ReasonPhrase, content From 33eb5ac1da2216f613330dbd099a08d487d78ffd Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Thu, 28 Aug 2025 13:50:53 -0700 Subject: [PATCH 36/55] feature/AB#26441-DynamicUrls-Copilot --- .../Endpoints/UpdateModal.cshtml.cs | 2 +- .../Integration/Endpoints/DynamicUrlDto.cs | 1 + .../IEndpointManagementAppService.cs | 2 +- .../Endpoints/EndpointManagementAppService.cs | 134 ++++++++++++------ .../Integrations/DynamicUrlDataSeeder.cs | 12 +- 5 files changed, 102 insertions(+), 49 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/UpdateModal.cshtml.cs b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/UpdateModal.cshtml.cs index bfa892eeb..0f8a17190 100644 --- a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/UpdateModal.cshtml.cs +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/UpdateModal.cshtml.cs @@ -23,7 +23,7 @@ public async Task OnGetAsync() public async Task OnPostAsync() { await endpointManagementAppService.UpdateAsync(Id, Endpoint!); - endpointManagementAppService.ClearCache(); + await endpointManagementAppService.ClearCacheAsync(); return NoContent(); } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/DynamicUrlDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/DynamicUrlDto.cs index 06a99169d..db995751d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/DynamicUrlDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/DynamicUrlDto.cs @@ -10,5 +10,6 @@ public class DynamicUrlDto : AuditedEntityDto public string Url { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; + public Guid TenantId { get; set; } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/IEndpointManagementAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/IEndpointManagementAppService.cs index 0b84bc847..7c233192c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/IEndpointManagementAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Integration/Endpoints/IEndpointManagementAppService.cs @@ -16,5 +16,5 @@ public interface IEndpointManagementAppService : ICrudAppService< Task GetChefsApiBaseUrlAsync(); Task GetUrlByKeyNameAsync(string keyName); Task GetUgmUrlByKeyNameAsync(string keyName); - void ClearCache(); + Task ClearCacheAsync(Guid? tenantId = null); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs index 6f7c18470..4dfbe9626 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs @@ -1,72 +1,81 @@ -using System; -using System.Collections.Concurrent; +using Microsoft.Extensions.Caching.Distributed; +using StackExchange.Redis; +using System; using System.Threading.Tasks; using Volo.Abp; using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; -using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories; using Volo.Abp.Uow; namespace Unity.GrantManager.Integrations.Endpoints { - [ExposeServices(typeof(EndpointManagementAppService), typeof(IEndpointManagementAppService))] - public class EndpointManagementAppService(IRepository repository) : + public class EndpointManagementAppService : CrudAppService< DynamicUrl, DynamicUrlDto, Guid, PagedAndSortedResultRequestDto, - CreateUpdateDynamicUrlDto>(repository), + CreateUpdateDynamicUrlDto>, IEndpointManagementAppService { - // Key: (keyName, tenantSpecific, tenantId), Value: url - private static readonly ConcurrentDictionary<(string keyName, bool tenantSpecific, Guid? tenantId), string> _urlCache - = new(); + private readonly IDistributedCache _cache; + private readonly IConnectionMultiplexer _redis; // for Redis-specific ops - private string? _chefsApiBaseUrl; // Lazy initialized + public EndpointManagementAppService( + IRepository repository, + IDistributedCache cache, + IConnectionMultiplexer redis) : base(repository) + { + _cache = cache; + _redis = redis; + } + + private static string BuildCacheKey(string keyName, bool tenantSpecific, Guid? tenantId) + => $"DynamicUrl:{tenantSpecific}:{tenantId ?? Guid.Empty}:{keyName}"; [UnitOfWork] public async Task GetChefsApiBaseUrlAsync() { - if (_chefsApiBaseUrl == null) - { - _chefsApiBaseUrl = await GetUrlByKeyNameInternalAsync(DynamicUrlKeyNames.INTAKE_API_BASE, tenantSpecific: false); + var url = await GetUrlByKeyNameInternalAsync(DynamicUrlKeyNames.INTAKE_API_BASE, tenantSpecific: false); - if (string.IsNullOrWhiteSpace(_chefsApiBaseUrl)) - throw new UserFriendlyException("CHEFS API base URL not configured."); - } + if (string.IsNullOrWhiteSpace(url)) + throw new UserFriendlyException("CHEFS API base URL not configured."); - return _chefsApiBaseUrl; + return url!; } [UnitOfWork] - public Task GetUgmUrlByKeyNameAsync(string keyName) + public async Task GetUgmUrlByKeyNameAsync(string keyName) { - return GetUrlByKeyNameInternalAsync(keyName, tenantSpecific: false); + var url = await GetUrlByKeyNameInternalAsync(keyName, tenantSpecific: false); + if (url == null) + throw new UserFriendlyException($"URL for key '{keyName}' not configured."); + return url; } - public Task GetUrlByKeyNameAsync(string keyName) + public async Task GetUrlByKeyNameAsync(string keyName) { - return GetUrlByKeyNameInternalAsync(keyName, tenantSpecific: true); + var url = await GetUrlByKeyNameInternalAsync(keyName, tenantSpecific: true); + if (url == null) + throw new UserFriendlyException($"URL for key '{keyName}' not configured."); + return url; } - private async Task GetUrlByKeyNameInternalAsync(string keyName, bool tenantSpecific) + private async Task GetUrlByKeyNameInternalAsync(string keyName, bool tenantSpecific) { - Guid? tenantId = tenantSpecific ? CurrentTenant.Id : null; - var cacheKey = (keyName, tenantSpecific, tenantId); + var tenantId = tenantSpecific ? CurrentTenant.Id : null; + var cacheKey = BuildCacheKey(keyName, tenantSpecific, tenantId); - // O(1) cache lookup - if (_urlCache.TryGetValue(cacheKey, out var cachedUrl)) - { - return cachedUrl; - } + // try cache first + var cached = await _cache.GetStringAsync(cacheKey); + if (cached != null) + return cached; - // Cache miss: fetch from DB DynamicUrl? dynamicUrl; if (tenantSpecific) { - dynamicUrl = await Repository.FirstOrDefaultAsync(x => x.KeyName == keyName); + dynamicUrl = await Repository.FirstOrDefaultAsync(x => x.KeyName == keyName && x.TenantId == tenantId); } else { @@ -76,28 +85,71 @@ private async Task GetUrlByKeyNameInternalAsync(string keyName, bool ten } } - var url = dynamicUrl?.Url ?? string.Empty; + var url = dynamicUrl?.Url; - // Cache only if not empty if (!string.IsNullOrWhiteSpace(url)) { - _urlCache[cacheKey] = url; + await _cache.SetStringAsync( + cacheKey, + url, + new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1) // configurable + }); } return url; } - // Optional: explicit cache invalidation for a key - public static void InvalidateCache(string keyName, bool tenantSpecific, Guid? tenantId) + // ------------------------------ + // Cache invalidation methods + // ------------------------------ + public async Task InvalidateCacheAsync(string keyName, bool tenantSpecific, Guid? tenantId) + { + var cacheKey = BuildCacheKey(keyName, tenantSpecific, tenantId); + await _cache.RemoveAsync(cacheKey); + } + + /// + /// Clears all DynamicUrl cache entries from Redis efficiently using SCAN. + /// If tenantId is specified, only clears that tenant's entries. + /// + public async Task ClearCacheAsync(Guid? tenantId = null) { - var cacheKey = (keyName, tenantSpecific, tenantId); - _urlCache.TryRemove(cacheKey, out _); + var server = _redis.GetServer(_redis.GetEndPoints()[0]); + var pattern = tenantId == null + ? "DynamicUrl:*" // all tenants + : $"DynamicUrl:*:{tenantId}:*"; // scoped to one tenant + + foreach (var key in server.Keys(pattern: pattern)) + { + await _redis.GetDatabase().KeyDeleteAsync(key); + } } - // Optional: clear entire cache - public void ClearCache() + // ------------------------------ + // CRUD overrides for invalidation + // ------------------------------ + public override async Task CreateAsync(CreateUpdateDynamicUrlDto input) { - _urlCache.Clear(); + var result = await base.CreateAsync(input); + await InvalidateCacheAsync(result.KeyName, result.TenantId != Guid.Empty, result.TenantId); + return result; } + + public override async Task UpdateAsync(Guid id, CreateUpdateDynamicUrlDto input) + { + var result = await base.UpdateAsync(id, input); + await InvalidateCacheAsync(result.KeyName, result.TenantId != Guid.Empty, result.TenantId); + return result; + } + + public override async Task DeleteAsync(Guid id) + { + var entity = await Repository.GetAsync(id); + await base.DeleteAsync(id); + await InvalidateCacheAsync(entity.KeyName, entity.TenantId != Guid.Empty, entity.TenantId); + } + } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlDataSeeder.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlDataSeeder.cs index 8291ab412..a1c43db3e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlDataSeeder.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlDataSeeder.cs @@ -49,12 +49,12 @@ private async Task SeedDynamicUrlAsync() new() { KeyName = DynamicUrlKeyNames.INTAKE_API_BASE, Url = DynamicUrls.CHEFS_PROD_URL, Description = "Common Hosted Forms Service API" }, new() { KeyName = DynamicUrlKeyNames.NOTIFICATION_API_BASE, Url = DynamicUrls.CHES_PROD_URL, Description = "Common Hosted Email Service API" }, new() { KeyName = DynamicUrlKeyNames.NOTIFICATION_AUTH, Url = DynamicUrls.CHES_PROD_AUTH, Description = "Common Hosted Email Service OAUTH" }, - (() => { var currentMessageIndex = messageIndex++; return new DynamicUrl { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{currentMessageIndex}", Url = "", Description = $"Direct message webhook {currentMessageIndex}" }; })(), - (() => { var currentMessageIndex = messageIndex++; return new DynamicUrl { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{currentMessageIndex}", Url = "", Description = $"Direct message webhook {currentMessageIndex}" }; })(), - (() => { var currentMessageIndex = messageIndex++; return new DynamicUrl { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{currentMessageIndex}", Url = "", Description = $"Direct message webhook {currentMessageIndex}" }; })(), - (() => { var currentWebhookIndex = webhookIndex++; return new DynamicUrl { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{currentWebhookIndex}", Url = "", Description = $"Webhook {currentWebhookIndex}" }; })(), - (() => { var currentWebhookIndex = webhookIndex++; return new DynamicUrl { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{currentWebhookIndex}", Url = "", Description = $"Webhook {currentWebhookIndex}" }; })(), - (() => { var currentWebhookIndex = webhookIndex++; return new DynamicUrl { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{currentWebhookIndex}", Url = "", Description = $"Webhook {currentWebhookIndex}" }; })(), + new() { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{messageIndex++}", Url = "", Description = $"Direct message webhook {messageIndex}" }, + new() { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{messageIndex++}", Url = "", Description = $"Direct message webhook {messageIndex}" }, + new() { KeyName = $"{DynamicUrlKeyNames.DIRECT_MESSAGE_KEY_PREFIX}{messageIndex++}", Url = "", Description = $"Direct message webhook {messageIndex}" }, + new() { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{webhookIndex++}", Url = "", Description = $"Webhook {webhookIndex}" }, + new() { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{webhookIndex++}", Url = "", Description = $"Webhook {webhookIndex}" }, + new() { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{webhookIndex++}", Url = "", Description = $"Webhook {webhookIndex}" }, }; foreach (var dynamicUrl in dynamicUrls) From fb2e1a6c779f197b0d554a53ff462e7cb58c6dbb Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Thu, 28 Aug 2025 14:42:22 -0700 Subject: [PATCH 37/55] feature/AB#26441-DynamicUrls-Copilot --- .../Endpoints/EndpointManagementAppService.cs | 93 +++++++++++++------ 1 file changed, 67 insertions(+), 26 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs index 4dfbe9626..23e4f0419 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs @@ -1,7 +1,8 @@ -using Microsoft.Extensions.Caching.Distributed; -using StackExchange.Redis; -using System; +using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Distributed; using Volo.Abp; using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; @@ -10,29 +11,59 @@ namespace Unity.GrantManager.Integrations.Endpoints { - public class EndpointManagementAppService : + public class EndpointManagementAppService( + IRepository repository, + IDistributedCache cache) : CrudAppService< DynamicUrl, DynamicUrlDto, Guid, PagedAndSortedResultRequestDto, - CreateUpdateDynamicUrlDto>, + CreateUpdateDynamicUrlDto>(repository), IEndpointManagementAppService { - private readonly IDistributedCache _cache; - private readonly IConnectionMultiplexer _redis; // for Redis-specific ops + private readonly IDistributedCache _cache = cache; + private const string CACHE_KEY_SET_PREFIX = "DynamicUrl:KeySet"; - public EndpointManagementAppService( - IRepository repository, - IDistributedCache cache, - IConnectionMultiplexer redis) : base(repository) + private static string BuildCacheKey(string keyName, bool tenantSpecific, Guid? tenantId) + => $"DynamicUrl:{tenantSpecific}:{tenantId ?? Guid.Empty}:{keyName}"; + + private static string BuildCacheKeySetKey(Guid? tenantId) + => $"{CACHE_KEY_SET_PREFIX}:{tenantId ?? Guid.Empty}"; + + private async Task AddToKeySetAsync(string cacheKey, Guid? tenantId) { - _cache = cache; - _redis = redis; + var keySetKey = BuildCacheKeySetKey(tenantId); + var existing = await _cache.GetStringAsync(keySetKey); + var keySet = string.IsNullOrEmpty(existing) + ? new HashSet() + : System.Text.Json.JsonSerializer.Deserialize>(existing) ?? new HashSet(); + + keySet.Add(cacheKey); + var serialized = System.Text.Json.JsonSerializer.Serialize(keySet); + + await _cache.SetStringAsync(keySetKey, serialized, new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24) // longer than individual entries + }); } - private static string BuildCacheKey(string keyName, bool tenantSpecific, Guid? tenantId) - => $"DynamicUrl:{tenantSpecific}:{tenantId ?? Guid.Empty}:{keyName}"; + private async Task RemoveFromKeySetAsync(string cacheKey, Guid? tenantId) + { + var keySetKey = BuildCacheKeySetKey(tenantId); + var existing = await _cache.GetStringAsync(keySetKey); + if (string.IsNullOrEmpty(existing)) return; + + var keySet = System.Text.Json.JsonSerializer.Deserialize>(existing); + if (keySet?.Remove(cacheKey) == true) + { + var serialized = System.Text.Json.JsonSerializer.Serialize(keySet); + await _cache.SetStringAsync(keySetKey, serialized, new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24) + }); + } + } [UnitOfWork] public async Task GetChefsApiBaseUrlAsync() @@ -94,8 +125,11 @@ await _cache.SetStringAsync( url, new DistributedCacheEntryOptions { - AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1) // configurable + AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1) }); + + // Track this key + await AddToKeySetAsync(cacheKey, tenantId); } return url; @@ -108,23 +142,31 @@ public async Task InvalidateCacheAsync(string keyName, bool tenantSpecific, Guid { var cacheKey = BuildCacheKey(keyName, tenantSpecific, tenantId); await _cache.RemoveAsync(cacheKey); + await RemoveFromKeySetAsync(cacheKey, tenantId); } /// - /// Clears all DynamicUrl cache entries from Redis efficiently using SCAN. - /// If tenantId is specified, only clears that tenant's entries. + /// Clears all DynamicUrl cache entries for the specified tenant (or all tenants if null). + /// Uses tracked cache keys for efficient bulk clearing. /// public async Task ClearCacheAsync(Guid? tenantId = null) { - var server = _redis.GetServer(_redis.GetEndPoints()[0]); - var pattern = tenantId == null - ? "DynamicUrl:*" // all tenants - : $"DynamicUrl:*:{tenantId}:*"; // scoped to one tenant + var keySetKey = BuildCacheKeySetKey(tenantId); + var existing = await _cache.GetStringAsync(keySetKey); - foreach (var key in server.Keys(pattern: pattern)) + if (!string.IsNullOrEmpty(existing)) { - await _redis.GetDatabase().KeyDeleteAsync(key); + var keySet = System.Text.Json.JsonSerializer.Deserialize>(existing); + if (keySet != null) + { + // Remove all tracked cache entries + var tasks = keySet.Select(key => _cache.RemoveAsync(key)); + await Task.WhenAll(tasks); + } } + + // Clear the key set itself + await _cache.RemoveAsync(keySetKey); } // ------------------------------ @@ -150,6 +192,5 @@ public override async Task DeleteAsync(Guid id) await base.DeleteAsync(id); await InvalidateCacheAsync(entity.KeyName, entity.TenantId != Guid.Empty, entity.TenantId); } - } -} +} \ No newline at end of file From 721db33ff355deb36f69141d16ea230995e1274f Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Thu, 28 Aug 2025 14:48:59 -0700 Subject: [PATCH 38/55] feature/AB#26441-DynamicUrls-Copilot --- .../Integrations/Css/CssApiService.cs | 189 ++++++++++++++---- 1 file changed, 147 insertions(+), 42 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Css/CssApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Css/CssApiService.cs index 07aeb4817..2eb035b6c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Css/CssApiService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Css/CssApiService.cs @@ -23,6 +23,7 @@ namespace Unity.GrantManager.Integrations.Sso public class CssApiService( IEndpointManagementAppService endpointManagementAppService, IResilientHttpRequest resilientHttpRequest, + IHttpClientFactory httpClientFactory, // Add this for token requests IDistributedCache accessTokenCache, IOptions cssApiOptions) : ApplicationService, ICssUsersApiService { @@ -50,66 +51,170 @@ public async Task SearchUsersAsync(string directory, string? f private async Task SearchSsoAsync(string directory, Dictionary parameters) { - var cssApiUrl = await endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.CSS_API_BASE); - var tokenResponse = await GetAccessTokenAsync(); - var baseUrl = $"{cssApiUrl}/{_cssApiOptions.Env}/{directory}/users"; - var url = BuildUrlWithQuery(baseUrl, parameters); - - var response = await resilientHttpRequest.HttpAsync(HttpMethod.Get, url, null, tokenResponse.AccessToken); - if (response != null && response.IsSuccessStatusCode && response.Content != null) + try { - var json = await response.Content.ReadAsStringAsync(); - var result = JsonSerializer.Deserialize(json) ?? throw new UserFriendlyException("Could not deserialize user search result."); - result.Success = true; - return result; + var cssApiUrl = await endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.CSS_API_BASE); + var tokenResponse = await GetAccessTokenAsync(); + var baseUrl = $"{cssApiUrl}/{_cssApiOptions.Env}/{directory}/users"; + var url = BuildUrlWithQuery(baseUrl, parameters); + + var response = await resilientHttpRequest.HttpAsync(HttpMethod.Get, url, null, tokenResponse.AccessToken); + + if (response != null && response.IsSuccessStatusCode && response.Content != null) + { + var json = await response.Content.ReadAsStringAsync(); + + if (string.IsNullOrWhiteSpace(json)) + { + Logger.LogWarning("Empty response received from CSS API for directory {Directory}", directory); + return CreateErrorResult("Empty response from CSS API"); + } + + try + { + var result = JsonSerializer.Deserialize(json, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + + if (result != null) + { + result.Success = true; + return result; + } + } + catch (JsonException ex) + { + Logger.LogError(ex, "Failed to deserialize user search result. JSON: {Json}", json); + return CreateErrorResult("Failed to parse response from CSS API"); + } + } + + var statusCode = response?.StatusCode.ToString() ?? "Unknown"; + var errorContent = response?.Content != null ? await response.Content.ReadAsStringAsync() : "No response"; + Logger.LogWarning("CSS API request failed. Status: {StatusCode}, Content: {Content}, URL: {Url}", + statusCode, errorContent, url); + + return CreateErrorResult($"CSS API request failed with status {statusCode}"); } - - return new UserSearchResult + catch (Exception ex) { - Success = false, - Error = "Failed to search users", - Data = [] - }; + Logger.LogError(ex, "Unexpected error during CSS API search for directory {Directory}", directory); + return CreateErrorResult("Unexpected error occurred while searching users"); + } } + private static UserSearchResult CreateErrorResult(string error) => new() + { + Success = false, + Error = error, + Data = [] + }; + private static string BuildUrlWithQuery(string basePath, Dictionary queryParams) { + if (!queryParams.Any()) + return basePath; + var query = string.Join("&", queryParams.Select(kv => $"{kv.Key}={Uri.EscapeDataString(kv.Value)}")); - return string.IsNullOrWhiteSpace(query) ? basePath : $"{basePath}?{query}"; + return $"{basePath}?{query}"; } private async Task GetAccessTokenAsync() { + // Check cache first var cachedToken = await accessTokenCache.GetAsync(CSS_API_KEY); - if (cachedToken != null) + if (cachedToken != null && !IsTokenExpiringSoon(cachedToken)) + { return cachedToken; + } - var client = new HttpClient(); - var cssTokenApiUrl = await endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.CSS_TOKEN_API_BASE); - var request = new HttpRequestMessage(HttpMethod.Post, cssTokenApiUrl); - - var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{_cssApiOptions.ClientId}:{_cssApiOptions.ClientSecret}")); - request.Headers.Authorization = new AuthenticationHeaderValue("Basic", credentials); - request.Content = new StringContent("grant_type=client_credentials", Encoding.UTF8, "application/x-www-form-urlencoded"); - - var response = await client.SendAsync(request); - - if (!response.IsSuccessStatusCode || response.Content == null) + try { - var errorContent = response.Content != null ? await response.Content.ReadAsStringAsync() : "No content"; - Logger.LogError("Failed to fetch CSS API token. Status: {StatusCode}, Content: {ErrorContent}", response.StatusCode, errorContent); - throw new UserFriendlyException($"Error fetching token: {response.StatusCode}"); + var cssTokenApiUrl = await endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.CSS_TOKEN_API_BASE); + + // Use HttpClientFactory instead of new HttpClient() + using var client = httpClientFactory.CreateClient(); + + var request = new HttpRequestMessage(HttpMethod.Post, cssTokenApiUrl); + var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{_cssApiOptions.ClientId}:{_cssApiOptions.ClientSecret}")); + + request.Headers.Authorization = new AuthenticationHeaderValue("Basic", credentials); + request.Content = new StringContent("grant_type=client_credentials", Encoding.UTF8, "application/x-www-form-urlencoded"); + + var response = await client.SendAsync(request); + + if (!response.IsSuccessStatusCode) + { + var errorContent = response.Content != null ? await response.Content.ReadAsStringAsync() : "No content"; + Logger.LogError("Failed to fetch CSS API token. Status: {StatusCode}, Content: {ErrorContent}, URL: {Url}", + response.StatusCode, errorContent, cssTokenApiUrl); + throw new UserFriendlyException($"Failed to authenticate with CSS API: {response.StatusCode}"); + } + + if (response.Content == null) + { + Logger.LogError("CSS token API returned success but no content"); + throw new UserFriendlyException("Invalid response from CSS token API"); + } + + var content = await response.Content.ReadAsStringAsync(); + + if (string.IsNullOrWhiteSpace(content)) + { + Logger.LogError("CSS token API returned empty content"); + throw new UserFriendlyException("Empty response from CSS token API"); + } + + TokenValidationResponse tokenResponse; + try + { + tokenResponse = JsonSerializer.Deserialize(content, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + })!; + + if (tokenResponse == null) + { + throw new UserFriendlyException("Invalid token response format"); + } + } + catch (JsonException ex) + { + Logger.LogError(ex, "Failed to deserialize token response. Content: {Content}", content); + throw new UserFriendlyException("Failed to parse token response"); + } + + // Cache with buffer time to avoid expiration edge cases + var cacheExpiration = DateTimeOffset.UtcNow.AddSeconds(tokenResponse.ExpiresIn - 60); // 60 second buffer + await accessTokenCache.SetAsync(CSS_API_KEY, tokenResponse, new DistributedCacheEntryOptions + { + AbsoluteExpiration = cacheExpiration + }); + + Logger.LogDebug("Successfully cached new CSS API token, expires at {ExpirationTime}", cacheExpiration); + return tokenResponse; } - - var content = await response.Content.ReadAsStringAsync(); - var tokenResponse = JsonSerializer.Deserialize(content) ?? throw new UserFriendlyException("Could not parse token response."); - - await accessTokenCache.SetAsync(CSS_API_KEY, tokenResponse, new DistributedCacheEntryOptions + catch (UserFriendlyException) + { + throw; // Re-throw user-friendly exceptions as-is + } + catch (Exception ex) { - AbsoluteExpiration = DateTimeOffset.UtcNow.AddSeconds(tokenResponse.ExpiresIn) - }); + Logger.LogError(ex, "Unexpected error while fetching CSS API access token"); + throw new UserFriendlyException("Failed to authenticate with CSS API"); + } + } - return tokenResponse; + /// + /// Check if token is expiring within the next 5 minutes + /// + private static bool IsTokenExpiringSoon(TokenValidationResponse token) + { + if (token.ExpiresIn <= 0) return true; + + // Consider token expiring if it has less than 5 minutes left + return token.ExpiresIn < 300; } } -} +} \ No newline at end of file From 4bfd5b09942189629c66d6f50ce7ae955907dd29 Mon Sep 17 00:00:00 2001 From: James Pasta <129337673+JamesPasta@users.noreply.github.com> Date: Thu, 28 Aug 2025 14:50:52 -0700 Subject: [PATCH 39/55] Update applications/Unity.GrantManager/src/Unity.GrantManager.Application/ConfigureIntakeClientOptions.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../ConfigureIntakeClientOptions.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ConfigureIntakeClientOptions.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ConfigureIntakeClientOptions.cs index 69524f131..0292f3822 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ConfigureIntakeClientOptions.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ConfigureIntakeClientOptions.cs @@ -2,19 +2,18 @@ using Microsoft.Extensions.Options; using Unity.GrantManager.Intake; using Unity.GrantManager.Integrations; +using System.Threading; +using System.Threading.Tasks; namespace Unity.GrantManager; public class ConfigureIntakeClientOptions( IConfiguration configuration, - IEndpointManagementAppService endpointManagementAppService) : IConfigureOptions + IEndpointManagementAppService endpointManagementAppService) : IAsyncConfigureOptions { - public void Configure(IntakeClientOptions options) + public async Task ConfigureAsync(IntakeClientOptions options, CancellationToken cancellationToken = default) { - // Note: GetUgmUrlByKeyNameAsync is async, but IConfigureOptions.Configure must be sync. - // If possible, use a sync alternative or ensure the value is available synchronously. - // Here, we block on the async call (not ideal, but sometimes necessary in options pattern). - var intakeBaseUri = endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.INTAKE_API_BASE).GetAwaiter().GetResult(); + var intakeBaseUri = await endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.INTAKE_API_BASE); options.BaseUri = intakeBaseUri; options.BearerTokenPlaceholder = configuration["Intake:BearerTokenPlaceholder"] ?? ""; options.UseBearerToken = configuration.GetValue("Intake:UseBearerToken"); From 9c1e0c48c958e17753985f525636e908ee7a0c36 Mon Sep 17 00:00:00 2001 From: James Pasta <129337673+JamesPasta@users.noreply.github.com> Date: Thu, 28 Aug 2025 14:52:12 -0700 Subject: [PATCH 40/55] Potential fix for code scanning alert no. 50: Log entries created from user input Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../Integrations/Chefs/FormsApiService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs index 86b88847c..070de5eda 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Chefs/FormsApiService.cs @@ -94,7 +94,7 @@ private async Task GetRequestAsync(string url, string chefs var decryptedApiKey = stringEncryptionService.Decrypt(encryptedApiKey) ?? string.Empty; logger.LogInformation( "Sending GET request to {Url} using basic auth with FormGuid: {FormGuid}", - url, + url.Replace(Environment.NewLine, "").Replace("\n", "").Replace("\r", ""), chefsApplicationFormGuid ); From 75825e3544d1b88aa49fb3e4f955db5ec4439e12 Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Thu, 28 Aug 2025 15:06:35 -0700 Subject: [PATCH 41/55] feature/AB#26441-DynamicUrls-Copilot --- .../ConfigureIntakeClientOptions.cs | 5 ++--- .../GrantManagerApplicationModule.cs | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ConfigureIntakeClientOptions.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ConfigureIntakeClientOptions.cs index 0292f3822..ddaecd402 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ConfigureIntakeClientOptions.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ConfigureIntakeClientOptions.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Options; using Unity.GrantManager.Intake; using Unity.GrantManager.Integrations; using System.Threading; @@ -9,8 +8,8 @@ namespace Unity.GrantManager; public class ConfigureIntakeClientOptions( IConfiguration configuration, - IEndpointManagementAppService endpointManagementAppService) : IAsyncConfigureOptions -{ + IEndpointManagementAppService endpointManagementAppService) +{ public async Task ConfigureAsync(IntakeClientOptions options, CancellationToken cancellationToken = default) { var intakeBaseUri = await endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.INTAKE_API_BASE); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs index bf9b32415..c5936eab6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs @@ -123,7 +123,6 @@ public override void ConfigureServices(ServiceConfigurationContext context) }); context.Services.AddSingleton(); - context.Services.AddTransient, ConfigureIntakeClientOptions>(); context.Services.AddTransient(); context.Services.AddTransient(); From 6732a0460b38080ae0a4463175b93acfe84bcd6e Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Thu, 28 Aug 2025 15:13:09 -0700 Subject: [PATCH 42/55] feature/AB#26441-DynamicUrls-Copilot --- .../Endpoints/EndpointManagementAppService.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs index 23e4f0419..000395ce0 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs @@ -170,19 +170,23 @@ public async Task ClearCacheAsync(Guid? tenantId = null) } // ------------------------------ - // CRUD overrides for invalidation + // CRUD overrides for invalidation (FIXED) // ------------------------------ public override async Task CreateAsync(CreateUpdateDynamicUrlDto input) { var result = await base.CreateAsync(input); - await InvalidateCacheAsync(result.KeyName, result.TenantId != Guid.Empty, result.TenantId); + // Fix: When TenantId is Guid.Empty, treat as null for tenant-agnostic URLs + var tenantId = result.TenantId == Guid.Empty ? null : (Guid?)result.TenantId; + await InvalidateCacheAsync(result.KeyName, tenantId.HasValue, tenantId); return result; } public override async Task UpdateAsync(Guid id, CreateUpdateDynamicUrlDto input) { var result = await base.UpdateAsync(id, input); - await InvalidateCacheAsync(result.KeyName, result.TenantId != Guid.Empty, result.TenantId); + // Fix: When TenantId is Guid.Empty, treat as null for tenant-agnostic URLs + var tenantId = result.TenantId == Guid.Empty ? null : (Guid?)result.TenantId; + await InvalidateCacheAsync(result.KeyName, tenantId.HasValue, tenantId); return result; } @@ -190,7 +194,9 @@ public override async Task DeleteAsync(Guid id) { var entity = await Repository.GetAsync(id); await base.DeleteAsync(id); - await InvalidateCacheAsync(entity.KeyName, entity.TenantId != Guid.Empty, entity.TenantId); + // Fix: When TenantId is Guid.Empty, treat as null for tenant-agnostic URLs + var tenantId = entity.TenantId == Guid.Empty ? null : entity.TenantId; + await InvalidateCacheAsync(entity.KeyName, tenantId.HasValue, tenantId); } } } \ No newline at end of file From 022e24b4113938cbe3c277631ae7ba98c17be9c7 Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Thu, 28 Aug 2025 15:17:50 -0700 Subject: [PATCH 43/55] feature/AB#26441-DynamicUrls-Copilot --- .../Endpoints/EndpointManagementAppService.cs | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs index 000395ce0..e507e0526 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs @@ -168,35 +168,5 @@ public async Task ClearCacheAsync(Guid? tenantId = null) // Clear the key set itself await _cache.RemoveAsync(keySetKey); } - - // ------------------------------ - // CRUD overrides for invalidation (FIXED) - // ------------------------------ - public override async Task CreateAsync(CreateUpdateDynamicUrlDto input) - { - var result = await base.CreateAsync(input); - // Fix: When TenantId is Guid.Empty, treat as null for tenant-agnostic URLs - var tenantId = result.TenantId == Guid.Empty ? null : (Guid?)result.TenantId; - await InvalidateCacheAsync(result.KeyName, tenantId.HasValue, tenantId); - return result; - } - - public override async Task UpdateAsync(Guid id, CreateUpdateDynamicUrlDto input) - { - var result = await base.UpdateAsync(id, input); - // Fix: When TenantId is Guid.Empty, treat as null for tenant-agnostic URLs - var tenantId = result.TenantId == Guid.Empty ? null : (Guid?)result.TenantId; - await InvalidateCacheAsync(result.KeyName, tenantId.HasValue, tenantId); - return result; - } - - public override async Task DeleteAsync(Guid id) - { - var entity = await Repository.GetAsync(id); - await base.DeleteAsync(id); - // Fix: When TenantId is Guid.Empty, treat as null for tenant-agnostic URLs - var tenantId = entity.TenantId == Guid.Empty ? null : entity.TenantId; - await InvalidateCacheAsync(entity.KeyName, tenantId.HasValue, tenantId); - } } } \ No newline at end of file From e9c7dd31bc8aab7f9be1a8c1831f875db785e65e Mon Sep 17 00:00:00 2001 From: James Pasta <129337673+JamesPasta@users.noreply.github.com> Date: Thu, 28 Aug 2025 15:28:29 -0700 Subject: [PATCH 44/55] Update applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Integrations/Endpoints/EndpointManagementAppService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs index e507e0526..85c888449 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs @@ -110,7 +110,7 @@ public async Task GetUrlByKeyNameAsync(string keyName) } else { - using (CurrentTenant.Change(null)) + await using (CurrentTenant.Change(null)) { dynamicUrl = await Repository.FirstOrDefaultAsync(x => x.KeyName == keyName && x.TenantId == null); } From dcb28699d9d7b6a0a3d748bd30622d5fa5e37e46 Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Thu, 28 Aug 2025 15:30:23 -0700 Subject: [PATCH 45/55] feature/AB#26441-DynamicUrls-Copilot --- .../modules/Unity.SharedKernel/Http/ResilientHttpRequest.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/ResilientHttpRequest.cs b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/ResilientHttpRequest.cs index cfacccfd1..61e0856e3 100644 --- a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/ResilientHttpRequest.cs +++ b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/ResilientHttpRequest.cs @@ -101,10 +101,7 @@ public async Task HttpAsync( // Execute through resilience pipeline return await _pipeline.ExecuteAsync(async ct => { - using var requestMessage = new HttpRequestMessage(httpVerb, fullUrl) - { - Version = HttpVersion.Version20 // safer default, negotiates automatically - }; + using var requestMessage = new HttpRequestMessage(httpVerb, fullUrl); // Headers are per-request, not global requestMessage.Headers.Accept.Clear(); From c3d105beb1b6ffcdc010598866ac5e77731e58de Mon Sep 17 00:00:00 2001 From: James Pasta <129337673+JamesPasta@users.noreply.github.com> Date: Thu, 28 Aug 2025 15:35:52 -0700 Subject: [PATCH 46/55] Update applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Integrations/Endpoints/EndpointManagementAppService.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs index 85c888449..d38a52315 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs @@ -96,11 +96,7 @@ public async Task GetUrlByKeyNameAsync(string keyName) private async Task GetUrlByKeyNameInternalAsync(string keyName, bool tenantSpecific) { var tenantId = tenantSpecific ? CurrentTenant.Id : null; - var cacheKey = BuildCacheKey(keyName, tenantSpecific, tenantId); - - // try cache first - var cached = await _cache.GetStringAsync(cacheKey); - if (cached != null) + if (!string.IsNullOrEmpty(cached)) return cached; DynamicUrl? dynamicUrl; From 990a7ae73e7a8f217865c5e5b440e5f2fadd8cf8 Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Thu, 28 Aug 2025 16:03:15 -0700 Subject: [PATCH 47/55] feature/AB#26441-DynamicUrls-Copilot --- .../Integrations/Endpoints/EndpointManagementAppService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs index 85c888449..e507e0526 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs @@ -110,7 +110,7 @@ public async Task GetUrlByKeyNameAsync(string keyName) } else { - await using (CurrentTenant.Change(null)) + using (CurrentTenant.Change(null)) { dynamicUrl = await Repository.FirstOrDefaultAsync(x => x.KeyName == keyName && x.TenantId == null); } From 5a014f1cc4a4d1ae82db0cacb0bddbb9ed045f65 Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Thu, 28 Aug 2025 16:08:02 -0700 Subject: [PATCH 48/55] feature/AB#26441-DynamicUrls-Copilot --- .../Integrations/Endpoints/EndpointManagementAppService.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs index e51bf6189..5718fe268 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs @@ -96,6 +96,8 @@ public async Task GetUrlByKeyNameAsync(string keyName) private async Task GetUrlByKeyNameInternalAsync(string keyName, bool tenantSpecific) { var tenantId = tenantSpecific ? CurrentTenant.Id : null; + var cacheKey = BuildCacheKey(keyName, tenantSpecific, tenantId); + var cached = await _cache.GetStringAsync(cacheKey); if (!string.IsNullOrEmpty(cached)) return cached; From a1dd9071a06ba3d1ed5a059050ef7541115c04ab Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Fri, 29 Aug 2025 09:22:07 -0700 Subject: [PATCH 49/55] feature/AB#26441-DynamicUrls-Sonar --- .../Integrations/Css/CssApiService.cs | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Css/CssApiService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Css/CssApiService.cs index 2eb035b6c..24b808e8f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Css/CssApiService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Css/CssApiService.cs @@ -72,10 +72,7 @@ private async Task SearchSsoAsync(string directory, Dictionary try { - var result = JsonSerializer.Deserialize(json, new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true - }); + var result = JsonSerializer.Deserialize(json, _jsonOptions)!; if (result != null) { @@ -113,13 +110,19 @@ private async Task SearchSsoAsync(string directory, Dictionary private static string BuildUrlWithQuery(string basePath, Dictionary queryParams) { - if (!queryParams.Any()) + if (queryParams.Count == 0) return basePath; var query = string.Join("&", queryParams.Select(kv => $"{kv.Key}={Uri.EscapeDataString(kv.Value)}")); return $"{basePath}?{query}"; } + // Define this once (e.g., at the top of your class or as a static field) + private static readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + private async Task GetAccessTokenAsync() { // Check cache first @@ -132,13 +135,13 @@ private async Task GetAccessTokenAsync() try { var cssTokenApiUrl = await endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.CSS_TOKEN_API_BASE); - + // Use HttpClientFactory instead of new HttpClient() using var client = httpClientFactory.CreateClient(); - + var request = new HttpRequestMessage(HttpMethod.Post, cssTokenApiUrl); var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{_cssApiOptions.ClientId}:{_cssApiOptions.ClientSecret}")); - + request.Headers.Authorization = new AuthenticationHeaderValue("Basic", credentials); request.Content = new StringContent("grant_type=client_credentials", Encoding.UTF8, "application/x-www-form-urlencoded"); @@ -147,7 +150,7 @@ private async Task GetAccessTokenAsync() if (!response.IsSuccessStatusCode) { var errorContent = response.Content != null ? await response.Content.ReadAsStringAsync() : "No content"; - Logger.LogError("Failed to fetch CSS API token. Status: {StatusCode}, Content: {ErrorContent}, URL: {Url}", + Logger.LogError("Failed to fetch CSS API token. Status: {StatusCode}, Content: {ErrorContent}, URL: {Url}", response.StatusCode, errorContent, cssTokenApiUrl); throw new UserFriendlyException($"Failed to authenticate with CSS API: {response.StatusCode}"); } @@ -159,7 +162,7 @@ private async Task GetAccessTokenAsync() } var content = await response.Content.ReadAsStringAsync(); - + if (string.IsNullOrWhiteSpace(content)) { Logger.LogError("CSS token API returned empty content"); @@ -169,11 +172,8 @@ private async Task GetAccessTokenAsync() TokenValidationResponse tokenResponse; try { - tokenResponse = JsonSerializer.Deserialize(content, new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true - })!; - + tokenResponse = JsonSerializer.Deserialize(content, _jsonOptions)!; + if (tokenResponse == null) { throw new UserFriendlyException("Invalid token response format"); From 4da293f894ee1e33cf472c73559dd2be528d09fa Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Fri, 29 Aug 2025 09:55:05 -0700 Subject: [PATCH 50/55] AB#23904: Remove CURRENT badge --- .../ApplicationLinks/ApplicationLinksModal.cshtml | 1 - .../ApplicationLinks/ApplicationLinksModal.css | 13 ------------- 2 files changed, 14 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinksModal.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinksModal.cshtml index bf4167543..64aa5e04e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinksModal.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinksModal.cshtml @@ -31,7 +31,6 @@ } @(Model.CurrentApplication?.ApplicationStatus ?? "Status Unavailable") - CURRENT diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinksModal.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinksModal.css index e085e8e58..a26bfed58 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinksModal.css +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinksModal.css @@ -1,18 +1,5 @@ /* ApplicationLinksModal CSS */ -/* Current application badge - styled like application-status but with green colors */ -.current-app-badge { - margin-left: auto; - margin-right: 10px; - padding: 0.125rem 0.5rem; - font-size: 0.8rem; - font-weight: 700; - color: #28a745; - text-transform: uppercase; - border: 3px solid #28a745; - border-radius: 1rem; - background-color: transparent; -} .links-display-area { background-color: #ffffff; From dab77563aa05e72381f58c503688d37545bf945d Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Mon, 1 Sep 2025 17:30:00 -0700 Subject: [PATCH 51/55] AB#23904: Fix sonarqube issues --- .../ApplicationLinksAppService.cs | 98 +++++++++---------- .../GrantApplicationAppService.cs | 3 +- .../GrantManagerConsts.cs | 2 + .../ApplicationLinksModal.cshtml.cs | 2 +- .../ApplicationLinksModal.css | 5 +- .../ApplicationLinksWidget/Default.js | 40 ++++---- 6 files changed, 71 insertions(+), 79 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationLinksAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationLinksAppService.cs index a3a9dd3bc..98e25b47e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationLinksAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationLinksAppService.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Unity.GrantManager; using Unity.GrantManager.Applications; using Volo.Abp.Application.Services; using Volo.Abp.DependencyInjection; @@ -48,9 +49,9 @@ join applicant in applicantsQuery on application.ApplicantId equals applicant.Id ApplicationId = application.Id, ApplicationStatus = application.ApplicationStatus.InternalStatus, ReferenceNumber = application.ReferenceNo, - Category = appForm.Category ?? "Unknown", // Handle potential nulls + Category = appForm.Category ?? GrantManagerConsts.UnknownValue, // Handle potential nulls ProjectName = application.ProjectName, - ApplicantName = applicant.ApplicantName ?? "Unknown", // Handle potential nulls + ApplicantName = applicant.ApplicantName ?? GrantManagerConsts.UnknownValue, // Handle potential nulls LinkType = applicationLinks.LinkType }; @@ -78,9 +79,9 @@ join applicant in applicantsQuery on application.ApplicantId equals applicant.Id ApplicationId = application.Id, ApplicationStatus = application.ApplicationStatus.InternalStatus, ReferenceNumber = application.ReferenceNo, - Category = appForm.Category ?? "Unknown", // Handle potential nulls + Category = appForm.Category ?? GrantManagerConsts.UnknownValue, // Handle potential nulls ProjectName = application.ProjectName, - ApplicantName = applicant.ApplicantName ?? "Unknown", // Handle potential nulls + ApplicantName = applicant.ApplicantName ?? GrantManagerConsts.UnknownValue, // Handle potential nulls LinkType = applicationLinks.LinkType }; @@ -107,18 +108,18 @@ public async Task GetCurrentApplicationInfoAsync(Guid a Id = Guid.Empty, ApplicationId = applicationId, ApplicationStatus = "Not Found", - ReferenceNumber = "Unknown", - Category = "Unknown", - ProjectName = "Unknown", - ApplicantName = "Unknown", + ReferenceNumber = GrantManagerConsts.UnknownValue, + Category = GrantManagerConsts.UnknownValue, + ProjectName = GrantManagerConsts.UnknownValue, + ApplicantName = GrantManagerConsts.UnknownValue, LinkType = ApplicationLinkType.Related }; } // Now try to get related data safely - string category = "Unknown"; - string applicantName = "Unknown"; + string category = GrantManagerConsts.UnknownValue; + string applicantName = GrantManagerConsts.UnknownValue; try { @@ -128,7 +129,7 @@ public async Task GetCurrentApplicationInfoAsync(Guid a .FirstOrDefaultAsync(); if (applicationForm != null) { - category = applicationForm.Category ?? "Unknown"; + category = applicationForm.Category ?? GrantManagerConsts.UnknownValue; } else { @@ -148,7 +149,7 @@ public async Task GetCurrentApplicationInfoAsync(Guid a .FirstOrDefaultAsync(); if (applicant != null) { - applicantName = applicant.ApplicantName ?? "Unknown"; + applicantName = applicant.ApplicantName ?? GrantManagerConsts.UnknownValue; } else { @@ -161,12 +162,12 @@ public async Task GetCurrentApplicationInfoAsync(Guid a } // Get application status (loaded via Include) - string applicationStatus = "Unknown"; + string applicationStatus; try { if (application.ApplicationStatus != null) { - applicationStatus = application.ApplicationStatus.InternalStatus ?? "Unknown"; + applicationStatus = application.ApplicationStatus.InternalStatus ?? GrantManagerConsts.UnknownValue; } else { @@ -185,9 +186,9 @@ public async Task GetCurrentApplicationInfoAsync(Guid a Id = Guid.Empty, ApplicationId = application.Id, ApplicationStatus = applicationStatus, - ReferenceNumber = application.ReferenceNo ?? "Unknown", + ReferenceNumber = application.ReferenceNo ?? GrantManagerConsts.UnknownValue, Category = category, - ProjectName = application.ProjectName ?? "Unknown", + ProjectName = application.ProjectName ?? GrantManagerConsts.UnknownValue, ApplicantName = applicantName, LinkType = ApplicationLinkType.Related }; @@ -204,10 +205,10 @@ public async Task GetCurrentApplicationInfoAsync(Guid a Id = Guid.Empty, ApplicationId = applicationId, ApplicationStatus = "Error Loading", - ReferenceNumber = "Unknown", - Category = "Unknown", - ProjectName = "Unknown", - ApplicantName = "Unknown", + ReferenceNumber = GrantManagerConsts.UnknownValue, + Category = GrantManagerConsts.UnknownValue, + ProjectName = GrantManagerConsts.UnknownValue, + ApplicantName = GrantManagerConsts.UnknownValue, LinkType = ApplicationLinkType.Related }; } @@ -254,16 +255,16 @@ public async Task GetApplicationDetailsByReferenceAsync ApplicationId = Guid.Empty, ApplicationStatus = "Not Found", ReferenceNumber = referenceNumber, - Category = "Unknown", - ProjectName = "Unknown", - ApplicantName = "Unknown", + Category = GrantManagerConsts.UnknownValue, + ProjectName = GrantManagerConsts.UnknownValue, + ApplicantName = GrantManagerConsts.UnknownValue, LinkType = ApplicationLinkType.Related }; } // Get related data safely - string category = "Unknown"; - string applicantName = "Unknown"; + string category = GrantManagerConsts.UnknownValue; + string applicantName = GrantManagerConsts.UnknownValue; try { @@ -273,7 +274,7 @@ public async Task GetApplicationDetailsByReferenceAsync .FirstOrDefaultAsync(); if (applicationForm != null) { - category = applicationForm.Category ?? "Unknown"; + category = applicationForm.Category ?? GrantManagerConsts.UnknownValue; } } catch (Exception ex) @@ -289,7 +290,7 @@ public async Task GetApplicationDetailsByReferenceAsync .FirstOrDefaultAsync(); if (applicant != null) { - applicantName = applicant.ApplicantName ?? "Unknown"; + applicantName = applicant.ApplicantName ?? GrantManagerConsts.UnknownValue; } } catch (Exception ex) @@ -297,10 +298,10 @@ public async Task GetApplicationDetailsByReferenceAsync Logger.LogError(ex, "Error looking up applicant"); } - string applicationStatus = "Unknown"; + string applicationStatus = GrantManagerConsts.UnknownValue; if (application.ApplicationStatus != null) { - applicationStatus = application.ApplicationStatus.InternalStatus ?? "Unknown"; + applicationStatus = application.ApplicationStatus.InternalStatus ?? GrantManagerConsts.UnknownValue; } return new ApplicationLinksInfoDto @@ -310,7 +311,7 @@ public async Task GetApplicationDetailsByReferenceAsync ApplicationStatus = applicationStatus, ReferenceNumber = application.ReferenceNo ?? referenceNumber, Category = category, - ProjectName = application.ProjectName ?? "Unknown", + ProjectName = application.ProjectName ?? GrantManagerConsts.UnknownValue, ApplicantName = applicantName, LinkType = ApplicationLinkType.Related }; @@ -325,9 +326,9 @@ public async Task GetApplicationDetailsByReferenceAsync ApplicationId = Guid.Empty, ApplicationStatus = "Error Loading", ReferenceNumber = referenceNumber, - Category = "Unknown", - ProjectName = "Unknown", - ApplicantName = "Unknown", + Category = GrantManagerConsts.UnknownValue, + ProjectName = GrantManagerConsts.UnknownValue, + ApplicantName = GrantManagerConsts.UnknownValue, LinkType = ApplicationLinkType.Related }; } @@ -336,29 +337,22 @@ public async Task GetApplicationDetailsByReferenceAsync public async Task UpdateLinkTypeAsync(Guid applicationLinkId, ApplicationLinkType newLinkType) { Logger.LogInformation("UpdateLinkTypeAsync called with linkId: {LinkId}, newLinkType: {LinkType}", applicationLinkId, newLinkType); - - try - { - // Get the existing link - var link = await Repository.GetAsync(applicationLinkId); + + // Get the existing link + var link = await Repository.GetAsync(applicationLinkId); - if (link != null) - { - // Update the link type - link.LinkType = newLinkType; - await Repository.UpdateAsync(link); + if (link != null) + { + // Update the link type + link.LinkType = newLinkType; + await Repository.UpdateAsync(link); - Logger.LogInformation("Successfully updated link type for linkId: {LinkId}", applicationLinkId); - } - else - { - Logger.LogWarning("Link not found with ID: {LinkId}", applicationLinkId); - } + Logger.LogInformation("Successfully updated link type for linkId: {LinkId}", applicationLinkId); } - catch (Exception ex) + else { - Logger.LogError(ex, "Error updating link type for linkId: {LinkId}", applicationLinkId); - throw; + Logger.LogWarning("Link not found with ID: {LinkId}", applicationLinkId); } + } } \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs index 8f8c6fb50..cb9b4ce34 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs @@ -12,6 +12,7 @@ using System.Linq; using System.Text.Json; using System.Threading.Tasks; +using Unity.GrantManager; using Unity.Flex.WorksheetInstances; using Unity.Flex.Worksheets; using Unity.GrantManager.Applicants; @@ -1288,7 +1289,7 @@ from applicant in applicantGroup.DefaultIfEmpty() Id = applications.Id, ProjectName = applications.ProjectName, ReferenceNo = applications.ReferenceNo, - ApplicantName = applicant != null ? applicant.ApplicantName : "Unknown" + ApplicantName = applicant != null ? (applicant.ApplicantName ?? GrantManagerConsts.UnknownValue) : GrantManagerConsts.UnknownValue }; return await query.ToListAsync(); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/GrantManagerConsts.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/GrantManagerConsts.cs index 4e1a94625..21788549c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/GrantManagerConsts.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/GrantManagerConsts.cs @@ -16,4 +16,6 @@ public static class GrantManagerConsts public const string DefaultTenantConnectionStringName = "Tenant"; public const string DefaultConnectionStringName = "Default"; + + public const string UnknownValue = "Unknown"; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinksModal.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinksModal.cshtml.cs index bc5ef90b0..cafe5f71c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinksModal.cshtml.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinksModal.cshtml.cs @@ -190,7 +190,7 @@ await _applicationLinksService.CreateAsync(new ApplicationLinksDto return new JsonResult(new { success = true }); } - private ApplicationLinkType GetReverseLinkType(ApplicationLinkType linkType) + private static ApplicationLinkType GetReverseLinkType(ApplicationLinkType linkType) { return linkType switch { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinksModal.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinksModal.css index a26bfed58..b9903cfdd 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinksModal.css +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinksModal.css @@ -217,10 +217,6 @@ } /* New item styling */ -.link-item.new-item { - position: relative; -} - @keyframes slideInFade { 0% { opacity: 0; @@ -233,6 +229,7 @@ } .link-item.new-item { + position: relative; animation: slideInFade 0.3s ease-out; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationLinksWidget/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationLinksWidget/Default.js index d801ac45e..6a4480ccc 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationLinksWidget/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationLinksWidget/Default.js @@ -110,7 +110,7 @@ // Handle delete button clicks using event delegation $('#ApplicationLinksTable').on('click', '.delete-link-btn', function(e) { e.preventDefault(); - var linkId = $(this).data('link-id'); + let linkId = $(this).data('link-id'); abp.message.confirm( 'Are you sure you want to delete this application link?', @@ -342,7 +342,7 @@ const linkType = $('#linkTypeSelect').val() || 'Related'; // Create link with unique ID for safe updates - const uniqueId = Date.now() + '_' + Math.random(); + const uniqueId = Date.now() + '_' + Math.random(); // NOSONAR - Safe: ID only used for client-side DOM element tracking, not for security const newLink = { id: uniqueId, referenceNumber: referenceNumber, @@ -443,6 +443,21 @@ }); } + function removeLinkFromList(link, capturedIndex, currentLinks, deletedLinks) { + if (link.id) { + // Remove by ID for safer deletion + const linkIndex = currentLinks.findIndex(l => l.id === link.id); + if (linkIndex !== -1) { + currentLinks.splice(linkIndex, 1); + updateLinksDisplay(currentLinks, deletedLinks); + } + } else { + // Use the captured index instead of trying to get it from 'this' + currentLinks.splice(capturedIndex, 1); + updateLinksDisplay(currentLinks, deletedLinks); + } + } + function createLinkElement(link, index, currentLinks, deletedLinks) { const linkTypeClass = (link.linkType || 'related').toLowerCase(); @@ -499,7 +514,6 @@ // Handle delete button - use link ID for safer deletion linkElement.find('.link-delete-btn').on('click', function() { // Capture the button element and index BEFORE showing dialog - const deleteButton = $(this); const capturedIndex = index; // Check if this is an existing link (not new) @@ -517,29 +531,13 @@ }); // Remove from display - removeLink(); + removeLinkFromList(link, capturedIndex, currentLinks, deletedLinks); } } ); } else { // New link - delete without confirmation - removeLink(); - } - - function removeLink() { - if (link.id) { - // Remove by ID for safer deletion - const linkIndex = currentLinks.findIndex(l => l.id === link.id); - if (linkIndex !== -1) { - currentLinks.splice(linkIndex, 1); - updateLinksDisplay(currentLinks, deletedLinks); - } - } else { - // Use the captured index instead of trying to get it from 'this' - const indexToRemove = capturedIndex; - currentLinks.splice(indexToRemove, 1); - updateLinksDisplay(currentLinks, deletedLinks); - } + removeLinkFromList(link, capturedIndex, currentLinks, deletedLinks); } }); From 53d60da49246fe56b47886a0928ad36edc37e379 Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Tue, 2 Sep 2025 14:58:53 -0700 Subject: [PATCH 52/55] AB#23904: Fix error on form configuration --- .../Applications/ApplicationFormVersion.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationFormVersion.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationFormVersion.cs index e9cfb1467..f72fba7de 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationFormVersion.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationFormVersion.cs @@ -21,7 +21,7 @@ public class ApplicationFormVersion : AuditedAggregateRoot, IMultiTenant public string ReportKeys { get; set; } = string.Empty; public string ReportViewName { get; set; } = string.Empty; [Column(TypeName = "jsonb")] - public string? FormSchema { get; set; } = string.Empty; + public string? FormSchema { get; set; } = null; /// /// Checks if the submission header mapping contains a specific field. From df2ae46ce28e50af56a30bc9f911944022e35ec9 Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Tue, 2 Sep 2025 15:09:14 -0700 Subject: [PATCH 53/55] Revert "AB#23904: Fix error on form configuration" This reverts commit 53d60da49246fe56b47886a0928ad36edc37e379. --- .../Applications/ApplicationFormVersion.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationFormVersion.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationFormVersion.cs index f72fba7de..e9cfb1467 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationFormVersion.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationFormVersion.cs @@ -21,7 +21,7 @@ public class ApplicationFormVersion : AuditedAggregateRoot, IMultiTenant public string ReportKeys { get; set; } = string.Empty; public string ReportViewName { get; set; } = string.Empty; [Column(TypeName = "jsonb")] - public string? FormSchema { get; set; } = null; + public string? FormSchema { get; set; } = string.Empty; /// /// Checks if the submission header mapping contains a specific field. From 2c0dceb9b7e03e6d86b72f3e3bc440d7a14ed3b3 Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Tue, 2 Sep 2025 15:15:21 -0700 Subject: [PATCH 54/55] AB#29825: Fix error on form configuration when FormSchema is empty --- .../Applications/ApplicationFormVersion.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationFormVersion.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationFormVersion.cs index e9cfb1467..f72fba7de 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationFormVersion.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationFormVersion.cs @@ -21,7 +21,7 @@ public class ApplicationFormVersion : AuditedAggregateRoot, IMultiTenant public string ReportKeys { get; set; } = string.Empty; public string ReportViewName { get; set; } = string.Empty; [Column(TypeName = "jsonb")] - public string? FormSchema { get; set; } = string.Empty; + public string? FormSchema { get; set; } = null; /// /// Checks if the submission header mapping contains a specific field. From b552edb0e767363b464356e2a72fe4ded4a8ae8b Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Wed, 3 Sep 2025 15:55:02 -0700 Subject: [PATCH 55/55] feature/AB#26441-DynamicUrls-FixBaseUrl --- .../GrantManagerApplicationModule.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs index c5936eab6..843b04557 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs @@ -147,7 +147,12 @@ public override void ConfigureServices(ServiceConfigurationContext context) context.Services.AddSingleton(provider => { - var options = provider.GetService>()?.Value; + var options = (provider.GetService>()?.Value) ?? throw new InvalidOperationException("IntakeClientOptions not configured."); + if (options.BaseUri == string.Empty) + { + options.BaseUri = "https://submit.digital.gov.bc.ca/app/api/v1"; + } + var restOptions = options != null ? new RestClientOptions(options.BaseUri) {