diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Automation/IApplicationAIGenerationQueue.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Automation/IApplicationAIGenerationQueue.cs index 3db64559f0..f7837b6c5a 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Automation/IApplicationAIGenerationQueue.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Automation/IApplicationAIGenerationQueue.cs @@ -5,5 +5,8 @@ namespace Unity.AI.Automation; public interface IApplicationAIGenerationQueue { - Task QueueApplicationPipelineAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null); + Task QueueAttachmentSummaryAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null); + Task QueueApplicationAnalysisAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null); + Task QueueApplicationScoringAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null); + Task QueueAllAIStagesAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null); } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationAnalysisAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationAnalysisAppService.cs index c5d8814534..9c1f173d70 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationAnalysisAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationAnalysisAppService.cs @@ -2,17 +2,20 @@ using System; using System.Threading.Tasks; using Unity.AI; -using Unity.AI.Operations; +using Unity.AI.Automation; using Unity.AI.Permissions; using Volo.Abp; using Volo.Abp.Features; +using Volo.Abp.MultiTenancy; namespace Unity.GrantManager.GrantApplications; [Authorize(AIPermissions.Analysis.GenerateApplicationAnalysis)] public class ApplicationAnalysisAppService( - IApplicationAnalysisService applicationAnalysisService, - IFeatureChecker featureChecker) + Unity.AI.Operations.IApplicationAnalysisService applicationAnalysisService, + IApplicationAIGenerationQueue aiGenerationQueue, + IFeatureChecker featureChecker, + ICurrentTenant currentTenant) : AIAppService, IApplicationAnalysisAppService { public virtual async Task GenerateApplicationAnalysisAsync(Guid applicationId, string? promptVersion = null) @@ -22,8 +25,8 @@ public virtual async Task GenerateApplicationAnaly throw new UserFriendlyException("AI application analysis is not enabled."); } - await applicationAnalysisService.RegenerateAndSaveAsync(applicationId, promptVersion); - return new ApplicationAnalysisResultDto { Completed = true }; + await aiGenerationQueue.QueueApplicationAnalysisAsync(applicationId, currentTenant.Id, promptVersion); + return new ApplicationAnalysisResultDto { Completed = false }; } // Internal-only: no HTTP endpoint, no auth check — safe for background job callers diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationContentAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationContentAppService.cs index cea61ecee7..d0fbe4d7dc 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationContentAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationContentAppService.cs @@ -30,7 +30,7 @@ public async Task GenerateContentAsync(Guid applica throw new UserFriendlyException("AI generate all is not enabled."); } - await aiGenerationQueue.QueueApplicationPipelineAsync(applicationId, currentTenant.Id, promptVersion); + await aiGenerationQueue.QueueAllAIStagesAsync(applicationId, currentTenant.Id, promptVersion); return new ApplicationContentResultDto { diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/ICustomFieldAppService.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/ICustomFieldAppService.cs index 0715d4458c..81955ba310 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/ICustomFieldAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/ICustomFieldAppService.cs @@ -8,5 +8,6 @@ public interface ICustomFieldAppService Task GetAsync(Guid id); Task EditAsync(Guid id, EditCustomFieldDto dto); Task DeleteAsync(Guid id); + Task MoveToSectionAsync(Guid fieldId, Guid targetSectionId, uint newIndex); } } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Worksheets/CustomField.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Worksheets/CustomField.cs index 1eca950ceb..aebf75ba85 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Worksheets/CustomField.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Worksheets/CustomField.cs @@ -32,7 +32,7 @@ public virtual WorksheetSection Section get => _section ?? throw new InvalidOperationException("Uninitialized property: " + nameof(Section)); } - public virtual Guid SectionId { get; } + public virtual Guid SectionId { get; internal set; } private WorksheetSection? _section; protected CustomField() diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/CustomFieldAppService.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/CustomFieldAppService.cs index cb8ba2e617..8b9c696cca 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/CustomFieldAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/CustomFieldAppService.cs @@ -5,6 +5,7 @@ using Volo.Abp.Domain.Entities; using System.Linq; using Unity.Flex.Worksheets.Definitions; +using Volo.Abp; namespace Unity.Flex.Worksheets { @@ -39,5 +40,31 @@ public async Task DeleteAsync(Guid id) section.RemoveField(field); } + + public async Task MoveToSectionAsync(Guid fieldId, Guid targetSectionId, uint newIndex) + { + var field = await customFieldRepostitory.GetAsync(fieldId) ?? throw new EntityNotFoundException(); + if (field.SectionId == targetSectionId) return; + + var worksheet = await worksheetRepository.GetBySectionAsync(field.SectionId, true) ?? throw new EntityNotFoundException(); + if (worksheet.Published) throw new UserFriendlyException("Cannot move fields in a published worksheet."); + + var sourceSection = worksheet.Sections.FirstOrDefault(s => s.Id == field.SectionId) ?? throw new EntityNotFoundException(); + var targetSection = worksheet.Sections.FirstOrDefault(s => s.Id == targetSectionId) ?? throw new EntityNotFoundException(); + + // Renumber source section, excluding the moving field + var sourceFields = sourceSection.Fields.Where(f => f.Id != fieldId).OrderBy(f => f.Order).ToList(); + for (int i = 0; i < sourceFields.Count; i++) + sourceFields[i].SetOrder((uint)(i + 1)); + + // Make room in target section at the insertion point + uint insertAt = Math.Min(newIndex + 1, (uint)(targetSection.Fields.Count + 1)); + foreach (var f in targetSection.Fields.Where(f => f.Order >= insertAt)) + f.SetOrder(f.Order + 1); + + // Update FK directly — EF Core change tracker issues a single UPDATE, avoids orphan-deletion + field.SetOrder(insertAt); + field.SectionId = targetSectionId; + } } } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DataGridWidget/DataGridWidget.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DataGridWidget/DataGridWidget.cs index c020c735c7..fd735cc689 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DataGridWidget/DataGridWidget.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DataGridWidget/DataGridWidget.cs @@ -389,7 +389,8 @@ private static DataGridViewSummary GenerateSummary(DataGridColumn[]? dataColumns private static string SumCells(string? key, DataGridViewModelRow[] rows) { decimal sum = 0; - foreach (var cell in rows.Select(row => row.Cells.Find(x => x.Key == key)).Where(cell => cell != null)) + var cells = rows.Select(row => row.Cells.Find(x => x.Key == key)).Where(cell => cell != null); + foreach (var cell in cells) { var preparse = cell!.Value.Replace("$", "").Replace(",", ""); if (decimal.TryParse(preparse, out decimal value)) diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionSelectListDefinitionWidget/QuestionSelectListDefinitionWidget.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionSelectListDefinitionWidget/QuestionSelectListDefinitionWidget.cs index ceaaebcb4b..0e389db4a6 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionSelectListDefinitionWidget/QuestionSelectListDefinitionWidget.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionSelectListDefinitionWidget/QuestionSelectListDefinitionWidget.cs @@ -28,30 +28,27 @@ public class QuestionSelectListDefinitionWidget : AbpViewComponent var seenKeys = new HashSet(); var counter = 1; - foreach (var key in form.Keys) + var optionKeys = form.Keys.Where(key => key.StartsWith("Options[") && key.EndsWith("].Text")); + foreach (var key in optionKeys) { - if (key.StartsWith("Options[") && key.EndsWith("].Text")) - { - var index = key.Split('[')[1].Split(']')[0]; + var index = key.Split('[')[1].Split(']')[0]; + var optionKey = form[key].ToString() ?? string.Empty; + var scoreKey = $"Options[{index}].Score"; + var optionScore = form[scoreKey].ToString() ?? string.Empty; - var optionKey = form[key].ToString() ?? string.Empty; - var scoreKey = $"Options[{index}].Score"; - var optionScore = form[scoreKey].ToString() ?? string.Empty; + // Ensure optionKey is unique and not empty + if (!seenKeys.Contains(optionKey) && !string.IsNullOrEmpty(optionKey)) + { + seenKeys.Add(optionKey); - // Ensure optionKey is unique and not empty - if (!seenKeys.Contains(optionKey) && !string.IsNullOrEmpty(optionKey)) + var questionOption = new QuestionSelectListOption { - seenKeys.Add(optionKey); - - var questionOption = new QuestionSelectListOption - { - Key = "key" + counter++, - NumericValue = long.TryParse(optionScore, out var score) ? score : 0, - Value = optionKey - }; + Key = "key" + counter++, + NumericValue = long.TryParse(optionScore, out var score) ? score : 0, + Value = optionKey + }; - options.Add(questionOption); - } + options.Add(questionOption); } } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js index 1f4a10c4e7..c92a195602 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js @@ -104,7 +104,9 @@ function makeCustomFieldsSortable() { document.querySelectorAll('.custom-fields-wrapper').forEach(function (div) { const wrapper = div.closest('.sections-wrapper-outer'); const isArchived = wrapper?.dataset.isArchived === 'true'; + const worksheetId = wrapper?.dataset.worksheetId; customFieldSortables.push(new Sortable(div, { + group: `custom-fields-${worksheetId}`, animation: 150, disabled: isArchived, onEnd: function (evt) { @@ -138,18 +140,36 @@ function makeSectionsSortable() { } function updateCustomFieldsSequence(evt) { - let sectionId = evt.target.dataset.sectionId; - let oldIndex = evt.oldIndex; - let newIndex = evt.newIndex; - - unity.flex.worksheets.worksheetSection - .resequenceCustomFields(sectionId, oldIndex, newIndex, {}) - .done(function () { - updatePreview(); - abp.notify.success( - 'Custom fields order updated.' - ); - }); + if (evt.from === evt.to) { + // Reorder within the same section + const sectionId = evt.from.dataset.sectionId; + const oldIndex = evt.oldIndex; + const newIndex = evt.newIndex; + + unity.flex.worksheets.worksheetSection + .resequenceCustomFields(sectionId, oldIndex, newIndex, {}) + .done(function () { + updatePreview(); + abp.notify.success('Custom fields order updated.'); + }); + } else { + // Move to a different section + const fieldId = evt.item.dataset.id; + const targetSectionId = evt.to.dataset.sectionId; + const newIndex = evt.newIndex; + + unity.flex.worksheets.customField + .moveToSection(fieldId, targetSectionId, newIndex, {}) + .done(function () { + updatePreview(); + abp.notify.success('Field moved to new section.'); + }) + .fail(function () { + // Revert the DOM move on failure + evt.from.insertBefore(evt.item, evt.from.children[evt.oldIndex] || null); + abp.notify.error('Failed to move field.'); + }); + } } function updateSectionSequence(evt) { diff --git a/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Roles/CreateModal.cshtml b/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Roles/CreateModal.cshtml index eef21fdfd3..416740042e 100644 --- a/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Roles/CreateModal.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Roles/CreateModal.cshtml @@ -25,34 +25,31 @@ - @foreach (var propertyInfo in ObjectExtensionManager.Instance.GetProperties()) + @foreach (ObjectExtensionPropertyInfo propertyInfo in ObjectExtensionManager.Instance.GetProperties().Where(p => !p.Name.EndsWith("_Text"))) { - if (!propertyInfo.Name.EndsWith("_Text")) + if (propertyInfo.Type.IsEnum || !propertyInfo.Lookup.Url.IsNullOrEmpty()) { - if (propertyInfo.Type.IsEnum || !propertyInfo.Lookup.Url.IsNullOrEmpty()) + if (propertyInfo.Type.IsEnum) { - if (propertyInfo.Type.IsEnum) - { - Model.Role.ExtraProperties.ToEnum(propertyInfo.Name, propertyInfo.Type); - } - - } - else - { - + Model.Role.ExtraProperties.ToEnum(propertyInfo.Name, propertyInfo.Type); } + + } + else + { + } } diff --git a/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Roles/EditModal.cshtml b/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Roles/EditModal.cshtml index 4ecd338f17..dc69b9a473 100644 --- a/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Roles/EditModal.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Roles/EditModal.cshtml @@ -36,34 +36,31 @@ - @foreach (var propertyInfo in ObjectExtensionManager.Instance.GetProperties()) + @foreach (ObjectExtensionPropertyInfo propertyInfo in ObjectExtensionManager.Instance.GetProperties().Where(p => !p.Name.EndsWith("_Text"))) { - if (!propertyInfo.Name.EndsWith("_Text")) + if (propertyInfo.Type.IsEnum || !propertyInfo.Lookup.Url.IsNullOrEmpty()) { - if (propertyInfo.Type.IsEnum || !propertyInfo.Lookup.Url.IsNullOrEmpty()) + if (propertyInfo.Type.IsEnum) { - if (propertyInfo.Type.IsEnum) - { - Model.Role.ExtraProperties.ToEnum(propertyInfo.Name, propertyInfo.Type); - } - - } - else - { - + Model.Role.ExtraProperties.ToEnum(propertyInfo.Name, propertyInfo.Type); } + + } + else + { + } } diff --git a/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/EditModal.cshtml b/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/EditModal.cshtml index b3d420df5e..e7fb61ddd3 100644 --- a/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/EditModal.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/EditModal.cshtml @@ -29,34 +29,31 @@ - @foreach (var propertyInfo in ObjectExtensionManager.Instance.GetProperties()) + @foreach (ObjectExtensionPropertyInfo propertyInfo in ObjectExtensionManager.Instance.GetProperties().Where(p => !p.Name.EndsWith("_Text"))) { - if (!propertyInfo.Name.EndsWith("_Text")) + if (propertyInfo.Type.IsEnum || !propertyInfo.Lookup.Url.IsNullOrEmpty()) { - if (propertyInfo.Type.IsEnum || !propertyInfo.Lookup.Url.IsNullOrEmpty()) + if (propertyInfo.Type.IsEnum) { - if (propertyInfo.Type.IsEnum) - { - Model.UserInfo.ExtraProperties.ToEnum(propertyInfo.Name, propertyInfo.Type); - } - + Model.UserInfo.ExtraProperties.ToEnum(propertyInfo.Name, propertyInfo.Type); } - else - { - - } + autocomplete-api-url="@propertyInfo.Lookup.Url" + autocomplete-selected-item-name="@Model.UserInfo.GetProperty(propertyInfo.Name+"_Text")" + autocomplete-selected-item-value="@Model.UserInfo.GetProperty(propertyInfo.Name)" + autocomplete-filter-param-name="@propertyInfo.Lookup.FilterParamName" + autocomplete-items-property-name="@propertyInfo.Lookup.ResultListPropertyName" + autocomplete-display-property-name="@propertyInfo.Lookup.DisplayPropertyName" + autocomplete-value-property-name="@propertyInfo.Lookup.ValuePropertyName"> + } + else + { + } } diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/ReportMappingUtils.cs b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/ReportMappingUtils.cs index 99b0400aae..169350689f 100644 --- a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/ReportMappingUtils.cs +++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/ReportMappingUtils.cs @@ -275,12 +275,7 @@ internal static bool ValidateColumnNamesConformance(UpsertMapRowDto[] rows) private static bool IsValidColumnName(string columnName) { // Column name should only contain letters, numbers, and underscores - foreach (char c in columnName) - { - if (!char.IsLetterOrDigit(c) && c != '_') - return false; - } - return true; + return columnName.All(c => char.IsLetterOrDigit(c) || c == '_'); } /// diff --git a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Constants/UnitySelector.cs b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Constants/UnitySelector.cs index b5d8e44fcb..35e3e629a3 100644 --- a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Constants/UnitySelector.cs +++ b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Constants/UnitySelector.cs @@ -27,8 +27,7 @@ public static partial class Contact { public const string Default = "Unity.GrantManager.ApplicationManagement.Applicant.Contact"; public const string Create = "Unity.GrantManager.ApplicationManagement.Applicant.Contact.Create"; - public const string Update = "Unity.GrantManager.ApplicationManagement.Applicant.Contact.Update"; - public const string Delete = "Unity.GrantManager.ApplicationManagement.Applicant.Contact.Delete"; + public const string Update = "Unity.GrantManager.ApplicationManagement.Applicant.Contact.Update"; } public static partial class AdditionalContact { diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Tenants/AssignManagerModal.cshtml b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Tenants/AssignManagerModal.cshtml index 0369d9d77d..c4d7ecc3f4 100644 --- a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Tenants/AssignManagerModal.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Tenants/AssignManagerModal.cshtml @@ -23,34 +23,31 @@ - @foreach (var propertyInfo in ObjectExtensionManager.Instance.GetProperties()) + @foreach (ObjectExtensionPropertyInfo propertyInfo in ObjectExtensionManager.Instance.GetProperties().Where(p => !p.Name.EndsWith("_Text"))) { - if (!propertyInfo.Name.EndsWith("_Text")) + if (propertyInfo.Type.IsEnum || !propertyInfo.Lookup.Url.IsNullOrEmpty()) { - if (propertyInfo.Type.IsEnum || !propertyInfo.Lookup.Url.IsNullOrEmpty()) + if (propertyInfo.Type.IsEnum) { - if (propertyInfo.Type.IsEnum) - { - Model.Tenant.ExtraProperties.ToEnum(propertyInfo.Name, propertyInfo.Type); - } - - } - else - { - + Model.Tenant.ExtraProperties.ToEnum(propertyInfo.Name, propertyInfo.Type); } + + } + else + { + } } diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Tenants/CreateModal.cshtml b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Tenants/CreateModal.cshtml index b1716aa5dd..81dccb8d65 100644 --- a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Tenants/CreateModal.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Tenants/CreateModal.cshtml @@ -21,34 +21,31 @@ - @foreach (var propertyInfo in ObjectExtensionManager.Instance.GetProperties()) + @foreach (ObjectExtensionPropertyInfo propertyInfo in ObjectExtensionManager.Instance.GetProperties().Where(p => !p.Name.EndsWith("_Text"))) { - if (!propertyInfo.Name.EndsWith("_Text")) + if (propertyInfo.Type.IsEnum || !propertyInfo.Lookup.Url.IsNullOrEmpty()) { - if (propertyInfo.Type.IsEnum || !propertyInfo.Lookup.Url.IsNullOrEmpty()) + if (propertyInfo.Type.IsEnum) { - if (propertyInfo.Type.IsEnum) - { - Model.Tenant.ExtraProperties.ToEnum(propertyInfo.Name, propertyInfo.Type); - } - - } - else - { - + Model.Tenant.ExtraProperties.ToEnum(propertyInfo.Name, propertyInfo.Type); } + + } + else + { + } } diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Tenants/EditModal.cshtml b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Tenants/EditModal.cshtml index 37f061a457..5b2b0a4142 100644 --- a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Tenants/EditModal.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Tenants/EditModal.cshtml @@ -36,34 +36,31 @@ - @foreach (var propertyInfo in ObjectExtensionManager.Instance.GetProperties()) + @foreach (var propertyInfo in ObjectExtensionManager.Instance.GetProperties().Where(p => !p.Name.EndsWith("_Text"))) { - if (!propertyInfo.Name.EndsWith("_Text")) + if (propertyInfo.Type.IsEnum || !propertyInfo.Lookup.Url.IsNullOrEmpty()) { - if (propertyInfo.Type.IsEnum || !propertyInfo.Lookup.Url.IsNullOrEmpty()) + if (propertyInfo.Type.IsEnum) { - if (propertyInfo.Type.IsEnum) - { - Model.Tenant.ExtraProperties.ToEnum(propertyInfo.Name, propertyInfo.Type); - } - - } - else - { - + Model.Tenant.ExtraProperties.ToEnum(propertyInfo.Name, propertyInfo.Type); } + + } + else + { + } } diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Analytics/AnalyticsUrlProvider.cs b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Analytics/AnalyticsUrlProvider.cs new file mode 100644 index 0000000000..45db04a734 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Analytics/AnalyticsUrlProvider.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using Unity.GrantManager.Integrations; +using Volo.Abp.DependencyInjection; + +namespace Unity.AspNetCore.Mvc.UI.Theme.UX2.Analytics +{ + public class AnalyticsUrlProvider(IEndpointManagementAppService endpointManagementAppService) : IAnalyticsUrlProvider, ITransientDependency + { + public async Task GetMatomoUrlAsync() + { + return await endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.ANALYTICS_MATOMO_BASE); + } + } +} \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Analytics/IAnalyticsUrlProvider.cs b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Analytics/IAnalyticsUrlProvider.cs new file mode 100644 index 0000000000..bd2132108d --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Analytics/IAnalyticsUrlProvider.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; + +namespace Unity.AspNetCore.Mvc.UI.Theme.UX2.Analytics; + +/// +/// Provides the Matomo analytics base URL for the current deployment environment. +/// Implement this interface in the host application to supply the URL from any source +/// (e.g. database, configuration). If no implementation is registered the default +/// no-op implementation is used and analytics tracking is disabled. +/// +public interface IAnalyticsUrlProvider +{ + /// + /// Returns the Matomo base URL (no trailing slash), or an empty string / null if analytics are not configured. + /// + Task GetMatomoUrlAsync(); +} diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Analytics/NullAnalyticsUrlProvider.cs b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Analytics/NullAnalyticsUrlProvider.cs new file mode 100644 index 0000000000..87bdca3efa --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Analytics/NullAnalyticsUrlProvider.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace Unity.AspNetCore.Mvc.UI.Theme.UX2.Analytics; + +/// +/// Default no-op implementation. Analytics are silently disabled when the host +/// application does not register a concrete . +/// +[Dependency(TryRegister = true)] +public class NullAnalyticsUrlProvider : IAnalyticsUrlProvider, ITransientDependency +{ + public Task GetMatomoUrlAsync() => Task.FromResult(string.Empty); +} diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Bundling/UnityThemeUX2GlobalScriptContributor.cs b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Bundling/UnityThemeUX2GlobalScriptContributor.cs index 4dea6cfe20..2540f8c393 100644 --- a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Bundling/UnityThemeUX2GlobalScriptContributor.cs +++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Bundling/UnityThemeUX2GlobalScriptContributor.cs @@ -52,5 +52,6 @@ public override void ConfigureBundle(BundleConfigurationContext context) context.Files.Add("/themes/ux2/table-utils.js"); context.Files.Add("/themes/ux2/json-editor.js"); context.Files.Add("/js/DateUtils.js"); + context.Files.Add("/js/AnalyticsUtils.js"); } } diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Components/Topbar/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Components/Topbar/Default.cshtml index bcd69a27dd..29f143e884 100644 --- a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Components/Topbar/Default.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Components/Topbar/Default.cshtml @@ -30,7 +30,7 @@ @{ bool isAuthorizedForTenantSwitch = false; - if (CurrentUser.IsAuthenticated && CurrentUser.FindClaims("tenant").Length > 1) + if (CurrentUser.IsAuthenticated && CurrentUser.FindClaimValue("has_multiple_tenants") == "true") isAuthorizedForTenantSwitch = true; bool isAuthorizedForPaymentConfiguration = await PermissionChecker.IsGrantedAsync("SettingManagement.ConfigurePayments"); diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Layouts/Account.cshtml b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Layouts/Account.cshtml index 797657a223..aa19019208 100644 --- a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Layouts/Account.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Layouts/Account.cshtml @@ -1,4 +1,5 @@ -@using Microsoft.Extensions.Localization +@using Unity.AspNetCore.Mvc.UI.Theme.UX2.Analytics +@using Microsoft.Extensions.Localization @using Microsoft.Extensions.Options @using Unity.AspNetCore.Mvc.UI.Theme.UX2.Bundling @@ -16,6 +17,7 @@ @using Volo.Abp.Ui.Branding @using Volo.Abp.Ui.LayoutHooks +@inject IServiceProvider ServiceProvider @inject IBrandingProvider BrandingProvider @inject IOptions MultiTenancyOptions @inject ICurrentTenant CurrentTenant @@ -107,6 +109,15 @@ @await Component.InvokeAsync(typeof(WidgetScriptsViewComponent)) + @{ + AnalyticsUrlProvider analyticsUrlProvider = ServiceProvider.GetService(typeof(AnalyticsUrlProvider)) as AnalyticsUrlProvider; + string matomoUrl = analyticsUrlProvider != null ? await analyticsUrlProvider.GetMatomoUrlAsync() : null; + } + @if (!string.IsNullOrEmpty(matomoUrl)) + { + + } + @await Component.InvokeLayoutHookAsync(LayoutHooks.Body.Last, StandardLayouts.Account) diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Layouts/Application.cshtml b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Layouts/Application.cshtml index d2f8b2da94..23a1592dd7 100644 --- a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Layouts/Application.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Layouts/Application.cshtml @@ -1,18 +1,20 @@ -@using System.Reflection +@using Unity.GrantManager.Analytics; +@using System.Reflection; @using Unity.AspNetCore.Mvc.UI.Theme.UX2.Bundling; @using Unity.AspNetCore.Mvc.UI.Theme.UX2.Themes.UX2.Components.ContentTitle; @using Unity.AspNetCore.Mvc.UI.Theme.UX2.Themes.UX2.Components.MainNavbar; @using Volo.Abp.AspNetCore.Mvc.AntiForgery; -@using Volo.Abp.AspNetCore.Mvc.UI.Components.LayoutHook -@using Volo.Abp.AspNetCore.Mvc.UI.Layout -@using Volo.Abp.AspNetCore.Mvc.UI.Theming -@using Volo.Abp.AspNetCore.Mvc.UI.Widgets.Components.WidgetScripts -@using Volo.Abp.AspNetCore.Mvc.UI.Widgets.Components.WidgetStyles -@using Volo.Abp.Localization -@using Volo.Abp.Ui.Branding -@using Volo.Abp.Ui.LayoutHooks - +@using Volo.Abp.AspNetCore.Mvc.UI.Components.LayoutHook; +@using Volo.Abp.AspNetCore.Mvc.UI.Layout; +@using Volo.Abp.AspNetCore.Mvc.UI.Theming; +@using Volo.Abp.AspNetCore.Mvc.UI.Widgets.Components.WidgetScripts; +@using Volo.Abp.AspNetCore.Mvc.UI.Widgets.Components.WidgetStyles; +@using Volo.Abp.Localization; +@using Volo.Abp.Ui.Branding; +@using Volo.Abp.Ui.LayoutHooks; + +@inject Unity.GrantManager.Analytics.IAnalyticsUrlProvider AnalyticsUrlProvider @inject IBrandingProvider BrandingProvider @inject IPageLayout PageLayout @inject IAbpAntiForgeryManager AbpAntiForgeryManager @@ -117,6 +119,19 @@ @await RenderSectionAsync("scripts", false) + @{ + string matomoUrl = await AnalyticsUrlProvider.GetMatomoUrlAsync(); + } + + @await Component.InvokeLayoutHookAsync(LayoutHooks.Body.Last, StandardLayouts.Application) diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Unity.AspNetCore.Mvc.UI.Theme.UX2.csproj b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Unity.AspNetCore.Mvc.UI.Theme.UX2.csproj index 92019f78c6..692e021272 100644 --- a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Unity.AspNetCore.Mvc.UI.Theme.UX2.csproj +++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Unity.AspNetCore.Mvc.UI.Theme.UX2.csproj @@ -20,11 +20,6 @@ - - - - - @@ -44,4 +39,7 @@ PreserveNewest + + + diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/js/AnalyticsUtils.js b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/js/AnalyticsUtils.js new file mode 100644 index 0000000000..bc86ff95b8 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/js/AnalyticsUtils.js @@ -0,0 +1,210 @@ +/** + * Analytics utility functions for Grant Manager application + */ +const AnalyticsUtils = (function () { + 'use strict'; + + const DIMENSION_TENANT_NAME = 1; + const DIMENSION_USER_NAME = 2; + + /** + * Safely gets the current tenant name from ABP. + * + * @returns {string|null} Tenant name or null if unavailable. + */ + function getTenantName() { + return globalThis.abp?.currentTenant?.name ?? null; + } + + /** + * Safely gets the current authenticated user ID from ABP. + * + * @returns {string|null} User ID or null if unavailable. + */ + function getUserId() { + return globalThis.abp?.currentUser?.id ? String(globalThis.abp.currentUser.id) : null; + } + + /** + * Safely builds the current user's full name from ABP. + * + * @returns {string|null} Full user name or null if unavailable. + */ + function getUserName() { + const firstName = globalThis.abp?.currentUser?.name ?? ''; + + const lastName = globalThis.abp && + abp.currentUser && + ( + abp.currentUser.surname || + abp.currentUser.surName || + '' + ); + + const fullName = `${firstName} ${lastName}`.trim(); + + return fullName || null; + } + + /** + * Applies common custom dimensions for tenant and user. + * + * @param {Array} trackerQueue - Matomo tracker queue. + */ + function applyCustomDimensions(trackerQueue) { + const tenantName = getTenantName(); + const userName = getUserName(); + + if (tenantName) { + trackerQueue.push([ + 'setCustomDimension', + DIMENSION_TENANT_NAME, + tenantName + ]); + } else { + console.warn( + '[Analytics] Tenant name unavailable. Custom Dimension 1 not set.' + ); + } + + if (userName) { + trackerQueue.push([ + 'setCustomDimension', + DIMENSION_USER_NAME, + userName + ]); + } else { + console.warn( + '[Analytics] User name unavailable. Custom Dimension 2 not set.' + ); + } + } + + /** + * Loads the Matomo tracking script asynchronously. + * + * @param {string} url - Base Matomo URL. + */ + function loadMatomoScript(url) { + const script = document.createElement('script'); + + script.async = true; + script.src = `${url}/matomo.js`; + + script.onerror = function () { + console.error( + '[Analytics] Failed to load matomo.js from:', + script.src + ); + }; + + document.head.appendChild(script); + } + + /** + * Initialises Matomo analytics tracking. + * Disables cookies (GDPR), sets user identity, + * applies custom dimensions, tracks initial page view, + * and enables link tracking. + * + * @param {string} url - Base Matomo URL (no trailing slash). + * @param {string|number} siteId - Matomo site ID. + */ + function initMatomo(url, siteId) { + console.debug( + '[Analytics] initMatomo called — url:', + url, + '| siteId:', + siteId + ); + + if (!url || !siteId) { + console.warn( + '[Analytics] initMatomo aborted: url or siteId is missing.' + ); + return; + } + + const trackerQueue = globalThis._paq = globalThis._paq || []; + + trackerQueue.push( + ['disableCookies'], + ['setTrackerUrl', `${url}/matomo.php`], + ['setSiteId', String(siteId)] + ); + + const userId = getUserId(); + + if (userId) { + trackerQueue.push(['setUserId', userId]); + } + + applyCustomDimensions(trackerQueue); + + /** + * Track initial page view after dimensions are set. + */ + trackerQueue.push( + ['trackPageView'], + ['enableLinkTracking'] + ); + + loadMatomoScript(url); + } + + /** + * Tracks a virtual page view for AJAX-driven navigation + * without a full page reload. + * + * Re-applies dimensions for Action-scoped dimensions. + * + * @param {string} [url] - Virtual URL. + * @param {string} [title] - Virtual page title. + */ + function trackVirtualPageView(url, title) { + const trackerQueue = globalThis._paq; + + if (!trackerQueue) { + return; + } + + applyCustomDimensions(trackerQueue); + + trackerQueue.push( + ['setCustomUrl', url || globalThis.location.href], + ['setDocumentTitle', title || document.title], + ['trackPageView'] + ); + } + + /** + * Tracks internal site search. + * + * @param {string} keyword - Search term entered by the user. + * @param {string|false} [category] - Optional search category. + * @param {number|false} [resultCount] - Optional result count. + */ + function trackSearch(keyword, category, resultCount) { + const trackerQueue = globalThis._paq; + + if (!trackerQueue || !keyword) { + return; + } + + trackerQueue.push([ + 'trackSiteSearch', + keyword, + typeof category === 'string' ? category : false, + typeof resultCount === 'number' ? resultCount : false + ]); + } + + /** + * Public API + */ + return { + initMatomo, + trackVirtualPageView, + trackSearch + }; +})(); \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Analytics/IAnalyticsUrlProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Analytics/IAnalyticsUrlProvider.cs new file mode 100644 index 0000000000..1c1a88abeb --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Analytics/IAnalyticsUrlProvider.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; + +namespace Unity.GrantManager.Analytics; + +/// +/// Provides the Matomo analytics base URL for the current deployment environment. +/// Implement this interface in the host application to supply the URL from any source +/// (e.g. database, configuration). If no implementation is registered the default +/// no-op implementation is used and analytics tracking is disabled. +/// +public interface IAnalyticsUrlProvider +{ + /// + /// Returns the Matomo base URL (no trailing slash), or an empty string / null if analytics are not configured. + /// + Task GetMatomoUrlAsync(); +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Analytics/NullAnalyticsUrlProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Analytics/NullAnalyticsUrlProvider.cs new file mode 100644 index 0000000000..d8a459444e --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Analytics/NullAnalyticsUrlProvider.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace Unity.GrantManager.Analytics; + +/// +/// Default no-op implementation. Analytics are silently disabled when the host +/// application does not register a concrete . +/// +[Dependency(TryRegister = true)] +public class NullAnalyticsUrlProvider : IAnalyticsUrlProvider, ITransientDependency +{ + public Task GetMatomoUrlAsync() => Task.FromResult(string.Empty); +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Contacts/ApplicantContactRoleOptions.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Contacts/ApplicantContactRoleOptions.cs new file mode 100644 index 0000000000..ecc917b600 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Contacts/ApplicantContactRoleOptions.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace Unity.GrantManager.ApplicantProfile; + +/// +/// Canonical set of contact role/type options exposed to the internal +/// Applicant Contacts UI. Mirrors the options defined by the Applicant Portal. +/// +public static class ApplicantContactRoleOptions +{ + /// Machine key used when storing the role on . + public static readonly IReadOnlyList Options = + [ + new("General", "General"), + new("Primary", "Primary Contact"), + new("Financial", "Financial Officer"), + new("SigningAuthority", "Additional Signing Authority"), + new("Executive", "Executive") + ]; +} + +/// Applicant contact role option (code + human-readable label). +/// The role key stored in . +/// The label displayed in the UI. +public record ApplicantContactRoleOption(string Value, string Label); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Contacts/IApplicantContactAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Contacts/IApplicantContactAppService.cs new file mode 100644 index 0000000000..73bf5a52d8 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Contacts/IApplicantContactAppService.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading.Tasks; +using Unity.GrantManager.ApplicantProfile.ProfileData; +using Unity.GrantManager.Contacts; +using Volo.Abp.Application.Services; + +namespace Unity.GrantManager.ApplicantProfile; + +/// +/// Authorized application service that exposes Create / Update / SetPrimary +/// operations for applicant-scoped contacts (those linked to the Applicant aggregate +/// via with RelatedEntityType = "Applicant"), +/// and aggregated read access that combines applicant-linked, application, and +/// applicant-agent contacts into a single view model. +/// +/// This is the HTTP-facing surface used by the internal Applicant Contacts widget. +/// The underlying remains unexposed so that existing +/// Applicant Portal message handlers (which write directly to the repositories) are +/// unaffected by authorization. +/// +/// +public interface IApplicantContactAppService : IApplicationService +{ + /// + /// Retrieves the aggregated contact info for the specified applicant. + /// + Task GetByApplicantIdAsync(Guid applicantId); + + /// + /// Updates an existing applicant-linked contact. + /// + Task UpdateAsync(Guid applicantId, Guid contactId, UpdateApplicantContactDto input); + + /// + /// Flags the specified contact as primary for the given applicant. + /// + Task SetPrimaryAsync(Guid applicantId, Guid contactId); +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileContactService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Contacts/IApplicantContactQueryService.cs similarity index 69% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileContactService.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Contacts/IApplicantContactQueryService.cs index 779b4f7148..c0e7c9b9c6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileContactService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Contacts/IApplicantContactQueryService.cs @@ -7,11 +7,11 @@ namespace Unity.GrantManager.ApplicantProfile; /// /// Provides applicant-profile-specific contact retrieval operations. -/// This service aggregates contacts from three sources: profile-linked contacts, +/// This query service aggregates contacts from three sources: profile-linked contacts, /// application-level contacts matched by OIDC subject, and applicant agent /// contacts derived from the submission login token. /// -public interface IApplicantProfileContactService +public interface IApplicantContactQueryService { /// /// Retrieves contacts linked to the applicant profile by resolving applicant IDs from @@ -39,4 +39,17 @@ public interface IApplicantProfileContactService /// The OIDC subject identifier (e.g. "user@idir"). /// A list of with IsEditable set to false. Task> GetApplicantAgentContactsBySubjectAsync(string subject); + + /// + /// Retrieves the aggregated contact info for the specified applicant, combining + /// applicant-linked contacts ( with + /// RelatedEntityType = "Applicant"), application contacts, and applicant agent contacts + /// for every application owned by the applicant. Applicant-linked contacts are marked editable; + /// application and agent contacts are always read-only. The primary flag is resolved either from + /// an explicit IsPrimary contact link or, if none exists, by falling back to the most + /// recently created contact. + /// + /// The unique identifier of the applicant. + /// A populated . + Task GetByApplicantIdAsync(Guid applicantId); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Contacts/UpdateApplicantContactDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Contacts/UpdateApplicantContactDto.cs new file mode 100644 index 0000000000..da092fda00 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Contacts/UpdateApplicantContactDto.cs @@ -0,0 +1,42 @@ +using System.ComponentModel.DataAnnotations; + +namespace Unity.GrantManager.ApplicantProfile; + +/// +/// Input DTO for updating an existing applicant-scoped contact. +/// +public class UpdateApplicantContactDto +{ + /// Role/type key — see . + [Required] + [MaxLength(100)] + public string? Role { get; set; } + + /// Full name. + [Required] + [MinLength(2)] + [MaxLength(250)] + public string Name { get; set; } = string.Empty; + + /// Job title. + [MaxLength(200)] + public string? Title { get; set; } + + /// Email address. + [EmailAddress] + public string? Email { get; set; } + + /// Mobile phone number. + [RegularExpression(@"^[\+]?[0-9\-\.\(\)\s]*$")] + public string? MobilePhoneNumber { get; set; } + + /// Work phone number. + [RegularExpression(@"^[\+]?[0-9\-\.\(\)\s]*$")] + public string? WorkPhoneNumber { get; set; } + + /// Work phone extension. + public string? WorkPhoneExtension { get; set; } + + /// When true, other primary contact links for the applicant are demoted. + public bool IsPrimary { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/DataProviders/IApplicantProfileDataProvider.cs similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileDataProvider.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/DataProviders/IApplicantProfileDataProvider.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ContactInfoItemDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ContactInfoItemDto.cs index ac0ec9b771..065223bd34 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ContactInfoItemDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ContactInfoItemDto.cs @@ -15,6 +15,7 @@ public class ContactInfoItemDto public string? ContactType { get; set; } public string? Role { get; set; } public bool IsPrimary { get; set; } + public bool IsPrimaryInferred { get; set; } public bool IsEditable { get; set; } public Guid? ApplicationId { get; set; } public string? ReferenceNo { get; set; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ApplicantProfileDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Queries/ApplicantProfileDto.cs similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ApplicantProfileDto.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Queries/ApplicantProfileDto.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ApplicantProfileRequest.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Queries/ApplicantProfileRequest.cs similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ApplicantProfileRequest.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Queries/ApplicantProfileRequest.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Queries/IApplicantProfileQueryService.cs similarity index 50% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileAppService.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Queries/IApplicantProfileQueryService.cs index feb3645823..b678390e7f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Queries/IApplicantProfileQueryService.cs @@ -4,11 +4,14 @@ namespace Unity.GrantManager.ApplicantProfile { - public interface IApplicantProfileAppService + /// + /// Internal query service that aggregates applicant profile data and tenant mappings for the + /// ApplicantProfileController API surface. Not an ABP application service — the + /// controller handles routing, authorization and API exposure directly. + /// + public interface IApplicantProfileQueryService { Task GetApplicantProfileAsync(ApplicantProfileInfoRequest request); Task> GetApplicantTenantsAsync(ApplicantProfileRequest request); - Task<(int Created, int Updated)> ReconcileApplicantTenantMapsAsync(); } } - diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ApplicantListDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ApplicantListDto.cs index ee8b71bd15..117abbfcdb 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ApplicantListDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ApplicantListDto.cs @@ -29,6 +29,9 @@ public class ApplicantListDto : AuditedEntityDto public int? FiscalDay { get; set; } public DateTime? StartedOperatingDate { get; set; } public string? SupplierId { get; set; } + public string? SupplierNumber { get; set; } + public string? SupplierName { get; set; } + public string? SupplierStatus { get; set; } public decimal? MatchPercentage { get; set; } public bool IsDuplicated { get; set; } } \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/UpdateApplicantContactAddressesDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/UpdateApplicantContactAddressesDto.cs index 523fbce1a1..8ddb5b4d81 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/UpdateApplicantContactAddressesDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/UpdateApplicantContactAddressesDto.cs @@ -12,6 +12,7 @@ public class UpdateApplicantContactAddressesDto public class UpdatePrimaryContactDto { public Guid Id { get; set; } + public string? Source { get; set; } public string? FullName { get; set; } public string? Title { get; set; } public string? Email { get; set; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Contacts/IContactAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Contacts/IContactAppService.cs index 4a07057c5d..3aeee4c050 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Contacts/IContactAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Contacts/IContactAppService.cs @@ -27,6 +27,19 @@ public interface IContactAppService /// The created . Task CreateContactAsync(CreateContactLinkDto input); + /// + /// Updates an existing contact and synchronizes the primary / role flags on its link + /// to the specified related entity. If is true, + /// any other active primary links for the same entity are demoted. + /// + /// The type of the related entity. + /// The unique identifier of the related entity. + /// The unique identifier of the contact to update. + /// The updated contact details. + /// The updated . + /// Thrown when no active contact link is found for the given parameters. + Task UpdateContactAsync(string entityType, Guid entityId, Guid contactId, UpdateContactDto input); + /// /// Sets the specified contact as the primary contact for the given entity. /// Only one primary contact is allowed per entity type and entity ID; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Contacts/UpdateContactDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Contacts/UpdateContactDto.cs new file mode 100644 index 0000000000..782c9562e7 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Contacts/UpdateContactDto.cs @@ -0,0 +1,35 @@ +namespace Unity.GrantManager.Contacts; + +/// +/// Input DTO for updating an existing contact and (optionally) the primary/role flags +/// of its link to a related entity. +/// +public class UpdateContactDto +{ + /// The full name of the contact. + public string Name { get; set; } = string.Empty; + + /// The job title of the contact. + public string? Title { get; set; } + + /// The email address of the contact. + public string? Email { get; set; } + + /// The home phone number of the contact. + public string? HomePhoneNumber { get; set; } + + /// The mobile phone number of the contact. + public string? MobilePhoneNumber { get; set; } + + /// The work phone number of the contact. + public string? WorkPhoneNumber { get; set; } + + /// The work phone extension of the contact. + public string? WorkPhoneExtension { get; set; } + + /// The role of the contact within the linked entity context. + public string? Role { get; set; } + + /// Whether this contact should be flagged as the primary contact. When true, other primary links for the same entity are demoted. + public bool IsPrimary { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/AIGenerationRequestDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/AIGenerationRequestDto.cs new file mode 100644 index 0000000000..0dfccaa949 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/AIGenerationRequestDto.cs @@ -0,0 +1,16 @@ +using System; +using Volo.Abp.Application.Dtos; + +namespace Unity.GrantManager.GrantApplications; + +public class AIGenerationRequestDto : EntityDto +{ + public Guid? ApplicationId { get; set; } + public string OperationType { get; set; } = string.Empty; + public string RequestKey { get; set; } = string.Empty; + public AIGenerationRequestStatus Status { get; set; } + public DateTime? StartedAt { get; set; } + public DateTime? CompletedAt { get; set; } + public string? FailureReason { get; set; } + public bool IsActive { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisBackgroundJobArgs.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisBackgroundJobArgs.cs new file mode 100644 index 0000000000..5c0f79d2d2 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisBackgroundJobArgs.cs @@ -0,0 +1,9 @@ +using System; +namespace Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; +public class GenerateApplicationAnalysisBackgroundJobArgs +{ + public Guid ApplicationId { get; set; } + public Guid? TenantId { get; set; } + public string? PromptVersion { get; set; } + public string RequestKey { get; set; } = string.Empty; +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringBackgroundJobArgs.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringBackgroundJobArgs.cs new file mode 100644 index 0000000000..3a1bdd1d5c --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringBackgroundJobArgs.cs @@ -0,0 +1,9 @@ +using System; +namespace Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; +public class GenerateApplicationScoringBackgroundJobArgs +{ + public Guid ApplicationId { get; set; } + public Guid? TenantId { get; set; } + public string? PromptVersion { get; set; } + public string RequestKey { get; set; } = string.Empty; +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryBackgroundJobArgs.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryBackgroundJobArgs.cs new file mode 100644 index 0000000000..b75c5d3e0f --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryBackgroundJobArgs.cs @@ -0,0 +1,9 @@ +using System; +namespace Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; +public class GenerateAttachmentSummaryBackgroundJobArgs +{ + public Guid ApplicationId { get; set; } + public Guid? TenantId { get; set; } + public string? PromptVersion { get; set; } + public string RequestKey { get; set; } = string.Empty; +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJobArgs.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJobArgs.cs index 9cfc51304c..3f5fee5594 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJobArgs.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJobArgs.cs @@ -1,8 +1,11 @@ using System; + namespace Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; + public class RunApplicationAIPipelineJobArgs { public Guid ApplicationId { get; set; } public Guid? TenantId { get; set; } public string? PromptVersion { get; set; } + public string RequestKey { get; set; } = string.Empty; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IAIGenerationStatusAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IAIGenerationStatusAppService.cs new file mode 100644 index 0000000000..008a5a4915 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IAIGenerationStatusAppService.cs @@ -0,0 +1,10 @@ +using System; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace Unity.GrantManager.GrantApplications; + +public interface IAIGenerationStatusAppService : IApplicationService +{ + Task GetLatestAsync(Guid applicationId, string operationType, Guid? tenantId = null); +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IGrantApplicationAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IGrantApplicationAppService.cs index 953a10a65d..c6a3db3767 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IGrantApplicationAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IGrantApplicationAppService.cs @@ -12,15 +12,21 @@ public interface IGrantApplicationAppService Task> GetActions(Guid applicationId, bool includeInternal = false); Task UpdateProjectInfoAsync(Guid id, CreateUpdateProjectInfoDto input); Task UpdatePartialProjectInfoAsync(Guid id, PartialUpdateDto input); - Task UpdateAssessmentResultsAsync(Guid id, CreateUpdateAssessmentResultsDto input); - Task UpdateSupplierNumberAsync(Guid applicationId, string supplierNumber); - Task> GetAllApplicationsAsync(); - Task> GetApplicationDetailsListAsync(List applicationIds); - Task GetAsync(Guid id); - Task TriggerAction(Guid applicationId, GrantApplicationAction triggerAction); - Task GetAccountCodingIdFromFormIdAsync(Guid formId); + Task UpdateAssessmentResultsAsync(Guid id, CreateUpdateAssessmentResultsDto input); + Task UpdateSupplierNumberAsync(Guid applicationId, string supplierNumber); + Task> GetAllApplicationsAsync(); + Task> GetApplicationDetailsListAsync(List applicationIds); + Task GetAsync(Guid id); + Task TriggerAction(Guid applicationId, GrantApplicationAction triggerAction); + Task QueueAIGenerationAsync(Guid applicationId, string? promptVersion = null); + Task QueueApplicationAnalysisAsync(Guid applicationId, string? promptVersion = null); + Task QueueAttachmentSummaryAsync(Guid applicationId, string? promptVersion = null); + Task QueueApplicationScoringAsync(Guid applicationId, string? promptVersion = null); + Task GetAIGenerationStatusAsync(Guid applicationId, string operationType, string? promptVersion = null); + Task QueueAllAIStagesAsync(Guid applicationId, string? promptVersion = null); + Task GetAccountCodingIdFromFormIdAsync(Guid formId); Task DismissAIAnalysisItemAsync(Guid applicationId, string itemId); Task RestoreAIAnalysisItemAsync(Guid applicationId, string itemId); - Task> GetListAsync(GrantApplicationListInputDto input); - Task IsApplicantRedStopAsync(Guid applicationId); -} + Task> GetListAsync(GrantApplicationListInputDto input); + Task IsApplicantRedStopAsync(Guid applicationId); +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantManagerFeaturesDefinitionProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantManagerFeaturesDefinitionProvider.cs index c829cf27ea..ddfdc4bd55 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantManagerFeaturesDefinitionProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantManagerFeaturesDefinitionProvider.cs @@ -62,6 +62,12 @@ public override void Define(IFeatureDefinitionContext context) displayName: LocalizableString .Create("AI Scoring"), valueType: new ToggleStringValueType()); + + myGroup.AddFeature("Unity.Analytics", + defaultValue: defaultValue, + displayName: LocalizableString + .Create("Analytics"), + valueType: new ToggleStringValueType()); } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantApplications/GrantApplicationPermissionDefinitionProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantApplications/GrantApplicationPermissionDefinitionProvider.cs index 5dae52cf3e..1b84ea5227 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantApplications/GrantApplicationPermissionDefinitionProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantApplications/GrantApplicationPermissionDefinitionProvider.cs @@ -172,7 +172,8 @@ public static void AddApplication_ApplicantInfo_Permissions(this PermissionGroup var upx_Applicant_Summary_Update = upx_Applicant_Summary.AddUnityChild(UnitySelector.Applicant.Summary.Update); var upx_Applicant_Contact = upx_Applicant.AddUnityChild(UnitySelector.Applicant.Contact.Default); - var upx_Applicant_Contact_Update = upx_Applicant_Contact.AddUnityChild(UnitySelector.Applicant.Contact.Update); + var upx_Applicant_Contact_Create = upx_Applicant_Contact.AddUnityChild(UnitySelector.Applicant.Contact.Create); + var upx_Applicant_Contact_Update = upx_Applicant_Contact.AddUnityChild(UnitySelector.Applicant.Contact.Update); var upx_Applicant_Authority = upx_Applicant.AddUnityChild(UnitySelector.Applicant.Authority.Default); var upx_Applicant_Authority_Update = upx_Applicant_Authority.AddUnityChild(UnitySelector.Applicant.Authority.Update); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Analytics/MatomoUrlProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Analytics/MatomoUrlProvider.cs new file mode 100644 index 0000000000..8741b2703b --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Analytics/MatomoUrlProvider.cs @@ -0,0 +1,22 @@ +using System.Threading.Tasks; +using Unity.GrantManager.Integrations; + +namespace Unity.GrantManager.Analytics; + +/// +/// Matomo URL provider implementation for Application layer, using IEndpointManagementAppService. +/// +public class MatomoUrlProvider : IAnalyticsUrlProvider +{ + private readonly IEndpointManagementAppService _endpointManagementAppService; + + public MatomoUrlProvider(IEndpointManagementAppService endpointManagementAppService) + { + _endpointManagementAppService = endpointManagementAppService; + } + + public async Task GetMatomoUrlAsync() + { + return await _endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.ANALYTICS_MATOMO_BASE) ?? string.Empty; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/AppServices/ApplicantContactAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/AppServices/ApplicantContactAppService.cs new file mode 100644 index 0000000000..a24bcd3391 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/AppServices/ApplicantContactAppService.cs @@ -0,0 +1,73 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Unity.GrantManager.ApplicantProfile.ProfileData; +using Unity.GrantManager.Contacts; +using Unity.Modules.Shared; + +namespace Unity.GrantManager.ApplicantProfile; + +/// +/// Authorized facade over (reads) and +/// (writes) for the internal Applicant Contacts widget. +/// All writes are scoped to RelatedEntityType = "Applicant". +/// +[Authorize] +public class ApplicantContactAppService( + IApplicantContactQueryService applicantContactQueryService, + IContactManager contactManager) + : GrantManagerAppService, IApplicantContactAppService +{ + private const string ApplicantEntityType = "Applicant"; + + /// + [Authorize(UnitySelector.Applicant.Contact.Default)] + public virtual Task GetByApplicantIdAsync(Guid applicantId) + { + return applicantContactQueryService.GetByApplicantIdAsync(applicantId); + } + + /// + [Authorize(UnitySelector.Applicant.Contact.Update)] + public virtual async Task UpdateAsync(Guid applicantId, Guid contactId, UpdateApplicantContactDto input) + { + ArgumentNullException.ThrowIfNull(input); + + var (contact, link) = await contactManager.UpdateAsync( + ApplicantEntityType, + applicantId, + contactId, + new ContactInput( + input.Name, + input.Title, + input.Email, + HomePhoneNumber: null, + input.MobilePhoneNumber, + input.WorkPhoneNumber, + input.WorkPhoneExtension), + input.Role, + input.IsPrimary); + + return new ContactDto + { + ContactId = contact.Id, + Name = contact.Name, + Title = contact.Title, + Email = contact.Email, + HomePhoneNumber = contact.HomePhoneNumber, + MobilePhoneNumber = contact.MobilePhoneNumber, + WorkPhoneNumber = contact.WorkPhoneNumber, + WorkPhoneExtension = contact.WorkPhoneExtension, + Role = link.Role, + IsPrimary = link.IsPrimary + }; + } + + /// + [Authorize(UnitySelector.Applicant.Contact.Update)] + public virtual async Task SetPrimaryAsync(Guid applicantId, Guid contactId) + { + await contactManager.SetPrimaryAsync(ApplicantEntityType, applicantId, contactId); + return true; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileAppService.cs deleted file mode 100644 index bcb856bb16..0000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileAppService.cs +++ /dev/null @@ -1,213 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Unity.GrantManager.Applicants; -using Unity.GrantManager.Applications; -using Unity.Notifications.Settings; -using Volo.Abp; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Repositories; -using Volo.Abp.MultiTenancy; -using Volo.Abp.Settings; -using Volo.Abp.TenantManagement; - -namespace Unity.GrantManager.ApplicantProfile -{ - [RemoteService(false)] - public class ApplicantProfileAppService( - ICurrentTenant currentTenant, - ITenantRepository tenantRepository, - IRepository applicantTenantMapRepository, - IRepository applicationFormSubmissionRepository, - IEnumerable dataProviders, - ISettingProvider settingProvider) - : ApplicationService, IApplicantProfileAppService - { - private readonly Dictionary _providersByKey - = dataProviders.ToDictionary(p => p.Key, StringComparer.OrdinalIgnoreCase); - - /// - /// Retrieves the applicant's profile information based on the specified request. - /// - /// An object containing the criteria used to identify the applicant profile to retrieve. Must not be null. - /// A task that represents the asynchronous operation. The task result contains an with the applicant's profile data. - public async Task GetApplicantProfileAsync(ApplicantProfileInfoRequest request) - { - var dto = new ApplicantProfileDto - { - ProfileId = request.ProfileId, - Subject = request.Subject, - TenantId = request.TenantId, - Key = request.Key - }; - - if (_providersByKey.TryGetValue(request.Key, out var provider)) - { - dto.Data = await provider.GetDataAsync(request); - } - else - { - Logger.LogWarning("Unknown applicant profile key provided"); - } - - return dto; - } - - /// - /// Retrieves a list of tenants associated with the specified applicant profile. - /// - /// The method extracts the username portion from the subject identifier in the request - /// to match tenant mappings. This operation is asynchronous and queries the host database for relevant tenant - /// associations. - /// An object containing applicant profile information, including the subject identifier used to locate tenant - /// mappings. - /// A list of objects representing the tenants linked to the applicant. The - /// list will be empty if no tenant associations are found. - public async Task> GetApplicantTenantsAsync(ApplicantProfileRequest request) - { - // Extract the username part from the OIDC sub (part before '@') - var subUsername = SubjectNormalizer.Normalize(request.Subject); - if (subUsername is null) return []; - List mappings = []; - - // Query the ApplicantTenantMaps table in the host database - using (currentTenant.Change(null)) - { - var queryable = await applicantTenantMapRepository.GetQueryableAsync(); - mappings = await queryable - .Where(m => m.OidcSubUsername == subUsername) - .Select(m => new ApplicantTenantDto - { - TenantId = m.TenantId, - TenantName = m.TenantName - }) - .ToListAsync(); - } - - // Apply tenant specific metadata - foreach (var map in mappings) - { - await AddTenantMetadataAsync(map); - } - - return mappings; - } - - /// - /// Add on any relevant tenant specific metadata - /// - /// The applicant tenant DTO to enrich with tenant-specific metadata. - private async Task AddTenantMetadataAsync(ApplicantTenantDto tenantMap) - { - using (currentTenant.Change(tenantMap.TenantId)) - { - var defaultEmailAddress = await settingProvider.GetOrNullAsync(NotificationsSettings.Mailing.DefaultFromAddress); - tenantMap.Metadata[ApplicantTenantMetadataKeys.DefaultFromAddress] = defaultEmailAddress ?? "NoReply@gov.bc.ca"; - } - } - - /// - /// Reconciles ApplicantTenantMaps by scanning all tenants for submissions - /// and ensuring mappings exist in the host database. - /// Phase 1: Collects all distinct OidcSub-to-tenant associations into memory. - /// Phase 2: Switches to host DB once and reconciles all mappings. - /// - /// Tuple of (created count, updated count) - public async Task<(int Created, int Updated)> ReconcileApplicantTenantMapsAsync() - { - Logger.LogInformation("Starting ApplicantTenantMap reconciliation..."); - - // Phase 1: Collect all OidcSub-to-tenant associations from each tenant DB - var desiredMappings = new List<(string SubUsername, Guid TenantId, string TenantName)>(); - var tenants = await tenantRepository.GetListAsync(); - - foreach (var tenant in tenants) - { - try - { - Logger.LogDebug("Collecting submissions from tenant: {TenantName}", tenant.Name); - - using (currentTenant.Change(tenant.Id)) - { - var submissionQueryable = await applicationFormSubmissionRepository.GetQueryableAsync(); - var distinctOidcSubs = await submissionQueryable - .Where(s => !string.IsNullOrWhiteSpace(s.OidcSub) && s.OidcSub != Guid.Empty.ToString()) - .Select(s => s.OidcSub) - .Distinct() - .ToListAsync(); - - foreach (var oidcSub in distinctOidcSubs) - { - var subUsername = oidcSub.Contains('@') - ? oidcSub[..oidcSub.IndexOf('@')].ToUpperInvariant() - : oidcSub.ToUpperInvariant(); - - desiredMappings.Add((subUsername, tenant.Id, tenant.Name)); - } - } - } - catch (Exception ex) - { - Logger.LogError(ex, "Error collecting submissions for tenant {TenantName}", tenant.Name); - } - } - - if (desiredMappings.Count == 0) - { - Logger.LogInformation("ApplicantTenantMap reconciliation completed. No submissions found across tenants."); - return (0, 0); - } - - // Phase 2: Switch to host DB once, load existing mappings, and reconcile - int totalMappingsCreated = 0; - int totalMappingsUpdated = 0; - - using (currentTenant.Change(null)) - { - var allSubUsernames = desiredMappings.Select(m => m.SubUsername).Distinct().ToList(); - - var mapQueryable = await applicantTenantMapRepository.GetQueryableAsync(); - var existingMappings = await mapQueryable - .Where(m => allSubUsernames.Contains(m.OidcSubUsername)) - .ToListAsync(); - - var existingByKey = existingMappings - .ToDictionary(m => (m.OidcSubUsername, m.TenantId)); - - foreach (var (subUsername, tenantId, tenantName) in desiredMappings) - { - if (existingByKey.TryGetValue((subUsername, tenantId), out var existing)) - { - existing.LastUpdated = DateTime.UtcNow; - await applicantTenantMapRepository.UpdateAsync(existing); - totalMappingsUpdated++; - } - else - { - var newMapping = new ApplicantTenantMap - { - OidcSubUsername = subUsername, - TenantId = tenantId, - TenantName = tenantName, - LastUpdated = DateTime.UtcNow - }; - await applicantTenantMapRepository.InsertAsync(newMapping); - existingByKey[(subUsername, tenantId)] = newMapping; - totalMappingsCreated++; - Logger.LogInformation("Created missing ApplicantTenantMap for {SubUsername} in tenant {TenantName}", - subUsername, tenantName); - } - } - } - - Logger.LogInformation("ApplicantTenantMap reconciliation completed. Created: {Created}, Updated: {Updated}", - totalMappingsCreated, totalMappingsUpdated); - - return (totalMappingsCreated, totalMappingsUpdated); - } - } -} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs deleted file mode 100644 index 06dd26366d..0000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs +++ /dev/null @@ -1,145 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Unity.GrantManager.ApplicantProfile.ProfileData; -using Unity.GrantManager.Applications; -using Unity.GrantManager.Contacts; -using Unity.GrantManager.GrantApplications; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; - -namespace Unity.GrantManager.ApplicantProfile; - -/// -/// Applicant-profile-specific contact service. Retrieves contacts linked to applicant profiles, -/// application-level contacts matched by OIDC subject, and applicant agent contacts derived from -/// the submission login token. Profile contacts are resolved by looking up form submissions that -/// match the OIDC subject to obtain applicant IDs, then querying -/// records against those IDs. When a single applicant ID is resolved the contacts are editable; -/// when multiple IDs are found the contacts are read-only. This service operates independently from the -/// generic and queries repositories directly. -/// -public class ApplicantProfileContactService( - IContactRepository contactRepository, - IContactLinkRepository contactLinkRepository, - IRepository applicationFormSubmissionRepository, - IRepository applicationContactRepository, - IRepository applicantAgentRepository, - IRepository applicationRepository) - : IApplicantProfileContactService, ITransientDependency -{ - private const string ApplicantEntityType = "Applicant"; - - /// - public async Task> GetApplicantContactsAsync(string subject) - { - var contactLinksQuery = await contactLinkRepository.GetQueryableAsync(); - var contactsQuery = await contactRepository.GetQueryableAsync(); - var submissionsQuery = await applicationFormSubmissionRepository.GetQueryableAsync(); - - var applicantIds = await submissionsQuery - .Where(s => s.OidcSub == subject) - .Select(s => s.ApplicantId) - .Distinct() - .ToListAsync(); - - var isEditable = applicantIds.Count <= 1; - - return await ( - from link in contactLinksQuery - join contact in contactsQuery on link.ContactId equals contact.Id - where link.RelatedEntityType == ApplicantEntityType - && applicantIds.Contains(link.RelatedEntityId) - && link.IsActive - select new ContactInfoItemDto - { - ContactId = contact.Id, - Name = contact.Name, - Title = contact.Title, - Email = contact.Email, - HomePhoneNumber = contact.HomePhoneNumber, - MobilePhoneNumber = contact.MobilePhoneNumber, - WorkPhoneNumber = contact.WorkPhoneNumber, - WorkPhoneExtension = contact.WorkPhoneExtension, - ContactType = link.RelatedEntityType, - Role = link.Role, - IsPrimary = link.IsPrimary, - IsEditable = isEditable, - ReferenceNo = null, - CreationTime = contact.CreationTime - }).ToListAsync(); - } - - /// - public async Task> GetApplicationContactsBySubjectAsync(string subject) - { - var submissionsQuery = await applicationFormSubmissionRepository.GetQueryableAsync(); - var applicationContactsQuery = await applicationContactRepository.GetQueryableAsync(); - var applicationsQuery = await applicationRepository.GetQueryableAsync(); - - var applicationContacts = await ( - from submission in submissionsQuery - join appContact in applicationContactsQuery on submission.ApplicationId equals appContact.ApplicationId - join application in applicationsQuery on submission.ApplicationId equals application.Id - where submission.OidcSub == subject - select new ContactInfoItemDto - { - ContactId = appContact.Id, - Name = appContact.ContactFullName, - Title = appContact.ContactTitle, - Email = appContact.ContactEmail, - MobilePhoneNumber = appContact.ContactMobilePhone, - WorkPhoneNumber = appContact.ContactWorkPhone, - Role = GetMatchingRole(appContact.ContactType), - ContactType = "Application", - IsPrimary = false, - IsEditable = false, - ApplicationId = appContact.ApplicationId, - ReferenceNo = application.ReferenceNo, - CreationTime = appContact.CreationTime - }).ToListAsync(); - - return applicationContacts; - } - - /// - public async Task> GetApplicantAgentContactsBySubjectAsync(string subject) - { - var submissionsQuery = await applicationFormSubmissionRepository.GetQueryableAsync(); - var agentsQuery = await applicantAgentRepository.GetQueryableAsync(); - var applicationsQuery = await applicationRepository.GetQueryableAsync(); - - var agentContacts = await ( - from submission in submissionsQuery - join agent in agentsQuery on submission.ApplicationId equals agent.ApplicationId - join application in applicationsQuery on submission.ApplicationId equals application.Id - where submission.OidcSub == subject - select new ContactInfoItemDto - { - ContactId = agent.Id, - Name = agent.Name, - Title = agent.Title, - Email = agent.Email, - WorkPhoneNumber = agent.Phone, - WorkPhoneExtension = agent.PhoneExtension, - MobilePhoneNumber = agent.Phone2, - Role = agent.RoleForApplicant, - ContactType = "ApplicantAgent", - IsPrimary = false, - IsEditable = false, - ApplicationId = agent.ApplicationId, - ReferenceNo = application.ReferenceNo, - CreationTime = agent.CreationTime - }).ToListAsync(); - - return agentContacts; - } - - private static string GetMatchingRole(string contactType) - { - return ApplicationContactOptionList.ContactTypeList.TryGetValue(contactType, out string? value) - ? value : contactType; - } -} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/AddressInfoDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/DataProviders/AddressInfoDataProvider.cs similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/AddressInfoDataProvider.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/DataProviders/AddressInfoDataProvider.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ContactInfoDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/DataProviders/ContactInfoDataProvider.cs similarity index 77% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ContactInfoDataProvider.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/DataProviders/ContactInfoDataProvider.cs index 8b7cbe547d..40253327a4 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ContactInfoDataProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/DataProviders/ContactInfoDataProvider.cs @@ -13,7 +13,7 @@ namespace Unity.GrantManager.ApplicantProfile [ExposeServices(typeof(IApplicantProfileDataProvider))] public class ContactInfoDataProvider( ICurrentTenant currentTenant, - IApplicantProfileContactService applicantProfileContactService) + IApplicantContactQueryService applicantContactQueryService) : IApplicantProfileDataProvider, ITransientDependency { /// @@ -34,13 +34,13 @@ public async Task GetDataAsync(ApplicantProfileInfoRequ using (currentTenant.Change(tenantId)) { - var applicantContacts = await applicantProfileContactService.GetApplicantContactsAsync(normalizedSubject); + var applicantContacts = await applicantContactQueryService.GetApplicantContactsAsync(normalizedSubject); dto.Contacts.AddRange(applicantContacts); - var applicationContacts = await applicantProfileContactService.GetApplicationContactsBySubjectAsync(normalizedSubject); + var applicationContacts = await applicantContactQueryService.GetApplicationContactsBySubjectAsync(normalizedSubject); dto.Contacts.AddRange(applicationContacts); - var agentContacts = await applicantProfileContactService.GetApplicantAgentContactsBySubjectAsync(normalizedSubject); + var agentContacts = await applicantContactQueryService.GetApplicantAgentContactsBySubjectAsync(normalizedSubject); dto.Contacts.AddRange(agentContacts); } @@ -50,6 +50,7 @@ public async Task GetDataAsync(ApplicantProfileInfoRequ .OrderByDescending(c => c.CreationTime) .First(); latest.IsPrimary = true; + latest.IsPrimaryInferred = true; } return dto; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/OrgInfoDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/DataProviders/OrgInfoDataProvider.cs similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/OrgInfoDataProvider.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/DataProviders/OrgInfoDataProvider.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/PaymentInfoDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/DataProviders/PaymentInfoDataProvider.cs similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/PaymentInfoDataProvider.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/DataProviders/PaymentInfoDataProvider.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/SubmissionInfoDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/DataProviders/SubmissionInfoDataProvider.cs similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/SubmissionInfoDataProvider.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/DataProviders/SubmissionInfoDataProvider.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/Queries/ApplicantContactQueryService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/Queries/ApplicantContactQueryService.cs new file mode 100644 index 0000000000..c06fedebe1 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/Queries/ApplicantContactQueryService.cs @@ -0,0 +1,324 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Unity.GrantManager.ApplicantProfile.ProfileData; +using Unity.GrantManager.Applications; +using Unity.GrantManager.Contacts; +using Unity.GrantManager.GrantApplications; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; + +namespace Unity.GrantManager.ApplicantProfile; + +/// +/// Applicant-profile-specific contact query service. Retrieves contacts linked to applicant profiles, +/// application-level contacts matched by OIDC subject, and applicant agent contacts derived from +/// the submission login token. Profile contacts are resolved by looking up form submissions that +/// match the OIDC subject to obtain applicant IDs, then querying +/// records against those IDs. When a single applicant ID is resolved the contacts are editable; +/// when multiple IDs are found the contacts are read-only. This service operates independently from the +/// generic and queries repositories directly. +/// +public class ApplicantContactQueryService( + IContactRepository contactRepository, + IContactLinkRepository contactLinkRepository, + IRepository applicationFormSubmissionRepository, + IRepository applicationContactRepository, + IRepository applicantAgentRepository, + IRepository applicationRepository) + : IApplicantContactQueryService, ITransientDependency +{ + private const string ApplicantEntityType = "Applicant"; + + /// + public async Task> GetApplicantContactsAsync(string subject) + { + var contactLinksQuery = await contactLinkRepository.GetQueryableAsync(); + var contactsQuery = await contactRepository.GetQueryableAsync(); + var submissionsQuery = await applicationFormSubmissionRepository.GetQueryableAsync(); + + var applicantIds = await submissionsQuery + .Where(s => s.OidcSub == subject) + .Select(s => s.ApplicantId) + .Distinct() + .ToListAsync(); + + var isEditable = applicantIds.Count <= 1; + + return await ( + from link in contactLinksQuery + join contact in contactsQuery on link.ContactId equals contact.Id + where link.RelatedEntityType == ApplicantEntityType + && applicantIds.Contains(link.RelatedEntityId) + && link.IsActive + select new ContactInfoItemDto + { + ContactId = contact.Id, + Name = contact.Name, + Title = contact.Title, + Email = contact.Email, + HomePhoneNumber = contact.HomePhoneNumber, + MobilePhoneNumber = contact.MobilePhoneNumber, + WorkPhoneNumber = contact.WorkPhoneNumber, + WorkPhoneExtension = contact.WorkPhoneExtension, + ContactType = link.RelatedEntityType, + Role = link.Role, + IsPrimary = link.IsPrimary, + IsEditable = isEditable, + ReferenceNo = null, + CreationTime = contact.CreationTime + }).ToListAsync(); + } + + /// + public async Task> GetApplicationContactsBySubjectAsync(string subject) + { + var submissionsQuery = await applicationFormSubmissionRepository.GetQueryableAsync(); + var applicationContactsQuery = await applicationContactRepository.GetQueryableAsync(); + var applicationsQuery = await applicationRepository.GetQueryableAsync(); + + var applicationContacts = await ( + from submission in submissionsQuery + join appContact in applicationContactsQuery on submission.ApplicationId equals appContact.ApplicationId + join application in applicationsQuery on submission.ApplicationId equals application.Id + where submission.OidcSub == subject + select new ContactInfoItemDto + { + ContactId = appContact.Id, + Name = appContact.ContactFullName, + Title = appContact.ContactTitle, + Email = appContact.ContactEmail, + MobilePhoneNumber = appContact.ContactMobilePhone, + WorkPhoneNumber = appContact.ContactWorkPhone, + Role = GetMatchingRole(appContact.ContactType), + ContactType = "Application", + IsPrimary = false, + IsEditable = false, + ApplicationId = appContact.ApplicationId, + ReferenceNo = application.ReferenceNo, + CreationTime = appContact.CreationTime + }).ToListAsync(); + + return applicationContacts; + } + + /// + public async Task> GetApplicantAgentContactsBySubjectAsync(string subject) + { + var submissionsQuery = await applicationFormSubmissionRepository.GetQueryableAsync(); + var agentsQuery = await applicantAgentRepository.GetQueryableAsync(); + var applicationsQuery = await applicationRepository.GetQueryableAsync(); + + var agentContacts = await ( + from submission in submissionsQuery + join agent in agentsQuery on submission.ApplicationId equals agent.ApplicationId + join application in applicationsQuery on submission.ApplicationId equals application.Id + where submission.OidcSub == subject + select new ContactInfoItemDto + { + ContactId = agent.Id, + Name = agent.Name, + Title = agent.Title, + Email = agent.Email, + WorkPhoneNumber = agent.Phone, + WorkPhoneExtension = agent.PhoneExtension, + MobilePhoneNumber = agent.Phone2, + Role = agent.RoleForApplicant, + ContactType = "ApplicantAgent", + IsPrimary = false, + IsEditable = false, + ApplicationId = agent.ApplicationId, + ReferenceNo = application.ReferenceNo, + CreationTime = agent.CreationTime + }).ToListAsync(); + + return agentContacts; + } + + /// + public async Task GetByApplicantIdAsync(Guid applicantId) + { + var dto = new ApplicantContactInfoDto { Contacts = [] }; + if (applicantId == Guid.Empty) + { + return dto; + } + + dto.Contacts.AddRange(await GetApplicantLinkedContactsAsync([applicantId], isEditable: true)); + dto.Contacts.AddRange(await GetApplicationContactsByApplicantIdAsync(applicantId)); + dto.Contacts.AddRange(await GetApplicantAgentContactsByApplicantIdAsync(applicantId)); + + ResolvePrimary(dto.Contacts); + + return dto; + } + + private async Task> GetApplicantLinkedContactsAsync( + IReadOnlyCollection applicantIds, + bool isEditable) + { + if (applicantIds.Count == 0) + { + return []; + } + + var contactLinksQuery = await contactLinkRepository.GetQueryableAsync(); + var contactsQuery = await contactRepository.GetQueryableAsync(); + + return await ( + from link in contactLinksQuery + join contact in contactsQuery on link.ContactId equals contact.Id + where link.RelatedEntityType == ApplicantEntityType + && applicantIds.Contains(link.RelatedEntityId) + && link.IsActive + select new ContactInfoItemDto + { + ContactId = contact.Id, + Name = contact.Name, + Title = contact.Title, + Email = contact.Email, + HomePhoneNumber = contact.HomePhoneNumber, + MobilePhoneNumber = contact.MobilePhoneNumber, + WorkPhoneNumber = contact.WorkPhoneNumber, + WorkPhoneExtension = contact.WorkPhoneExtension, + ContactType = ApplicantEntityType, + Role = link.Role, + IsPrimary = link.IsPrimary, + IsEditable = isEditable, + ReferenceNo = null, + CreationTime = contact.CreationTime + }).ToListAsync(); + } + + private async Task> GetApplicationContactsByApplicantIdAsync(Guid applicantId) + { + var applicationContactsQuery = await applicationContactRepository.GetQueryableAsync(); + var applicationsQuery = await applicationRepository.GetQueryableAsync(); + + // Resolve this applicant's applications up-front so we can map ReferenceNo by ApplicationId + // without relying on EF join translation (mirrors the ApplicantAddresses widget pattern). + var applicationRefMap = await applicationsQuery + .Where(a => a.ApplicantId == applicantId) + .Select(a => new { a.Id, a.ReferenceNo }) + .ToDictionaryAsync(a => a.Id, a => a.ReferenceNo); + + if (applicationRefMap.Count == 0) + { + return []; + } + + var applicationIds = applicationRefMap.Keys.ToList(); + + var contacts = await applicationContactsQuery + .Where(c => applicationIds.Contains(c.ApplicationId)) + .Select(appContact => new ContactInfoItemDto + { + ContactId = appContact.Id, + Name = appContact.ContactFullName, + Title = appContact.ContactTitle, + Email = appContact.ContactEmail, + MobilePhoneNumber = appContact.ContactMobilePhone, + WorkPhoneNumber = appContact.ContactWorkPhone, + Role = GetMatchingRole(appContact.ContactType), + ContactType = "Application", + IsPrimary = false, + IsEditable = false, + ApplicationId = appContact.ApplicationId, + CreationTime = appContact.CreationTime + }).ToListAsync(); + + foreach (var contact in contacts) + { + if (contact.ApplicationId.HasValue + && applicationRefMap.TryGetValue(contact.ApplicationId.Value, out var referenceNo)) + { + contact.ReferenceNo = referenceNo; + } + } + + return contacts; + } + + private async Task> GetApplicantAgentContactsByApplicantIdAsync(Guid applicantId) + { + var agentsQuery = await applicantAgentRepository.GetQueryableAsync(); + var applicationsQuery = await applicationRepository.GetQueryableAsync(); + + var agents = await agentsQuery + .Where(a => a.ApplicantId == applicantId) + .Select(agent => new ContactInfoItemDto + { + ContactId = agent.Id, + Name = agent.Name, + Title = agent.Title, + Email = agent.Email, + WorkPhoneNumber = agent.Phone, + WorkPhoneExtension = agent.PhoneExtension, + MobilePhoneNumber = agent.Phone2, + Role = agent.RoleForApplicant, + ContactType = "ApplicantAgent", + IsPrimary = false, + IsEditable = false, + ApplicationId = agent.ApplicationId, + CreationTime = agent.CreationTime + }).ToListAsync(); + + if (agents.Count == 0) + { + return agents; + } + + var applicationIds = agents + .Where(a => a.ApplicationId.HasValue) + .Select(a => a.ApplicationId!.Value) + .Distinct() + .ToList(); + + if (applicationIds.Count == 0) + { + return agents; + } + + var applicationRefMap = await applicationsQuery + .Where(a => applicationIds.Contains(a.Id)) + .Select(a => new { a.Id, a.ReferenceNo }) + .ToDictionaryAsync(a => a.Id, a => a.ReferenceNo); + + foreach (var agent in agents) + { + if (agent.ApplicationId.HasValue + && applicationRefMap.TryGetValue(agent.ApplicationId.Value, out var referenceNo)) + { + agent.ReferenceNo = referenceNo; + } + } + + return agents; + } + + /// + /// Ensures exactly one contact is flagged as primary. If none are explicitly flagged, + /// the most recently created contact is elected. + /// + private static void ResolvePrimary(List contacts) + { + if (contacts.Count == 0 || contacts.Any(c => c.IsPrimary)) + { + return; + } + + var latest = contacts + .OrderByDescending(c => c.CreationTime) + .First(); + latest.IsPrimary = true; + latest.IsPrimaryInferred = true; + } + + private static string GetMatchingRole(string contactType) + { + return ApplicationContactOptionList.ContactTypeList.TryGetValue(contactType, out string? value) + ? value : contactType; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/Queries/ApplicantProfileQueryService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/Queries/ApplicantProfileQueryService.cs new file mode 100644 index 0000000000..99bb33d239 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/Queries/ApplicantProfileQueryService.cs @@ -0,0 +1,114 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Unity.GrantManager.Applicants; +using Unity.Notifications.Settings; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Settings; + +namespace Unity.GrantManager.ApplicantProfile +{ + /// + /// Internal query service that backs ApplicantProfileController. Aggregates applicant profile + /// data from registered implementations and resolves + /// the applicant's tenant mappings. Not exposed as an ABP application service: the HTTP surface + /// lives in the controller, which applies its own routing and API-key authorization. + /// + public class ApplicantProfileQueryService( + ICurrentTenant currentTenant, + IRepository applicantTenantMapRepository, + IEnumerable dataProviders, + ISettingProvider settingProvider, + ILogger logger) + : IApplicantProfileQueryService, ITransientDependency + { + private readonly Dictionary _providersByKey + = dataProviders.ToDictionary(p => p.Key, StringComparer.OrdinalIgnoreCase); + + /// + /// Retrieves the applicant's profile information based on the specified request. + /// + /// An object containing the criteria used to identify the applicant profile to retrieve. Must not be null. + /// A task that represents the asynchronous operation. The task result contains an with the applicant's profile data. + public async Task GetApplicantProfileAsync(ApplicantProfileInfoRequest request) + { + var dto = new ApplicantProfileDto + { + ProfileId = request.ProfileId, + Subject = request.Subject, + TenantId = request.TenantId, + Key = request.Key + }; + + if (_providersByKey.TryGetValue(request.Key, out var provider)) + { + dto.Data = await provider.GetDataAsync(request); + } + else + { + logger.LogWarning("Unknown applicant profile key provided"); + } + + return dto; + } + + /// + /// Retrieves a list of tenants associated with the specified applicant profile. + /// + /// The method extracts the username portion from the subject identifier in the request + /// to match tenant mappings. This operation is asynchronous and queries the host database for relevant tenant + /// associations. + /// An object containing applicant profile information, including the subject identifier used to locate tenant + /// mappings. + /// A list of objects representing the tenants linked to the applicant. The + /// list will be empty if no tenant associations are found. + public async Task> GetApplicantTenantsAsync(ApplicantProfileRequest request) + { + // Extract the username part from the OIDC sub (part before '@') + var subUsername = SubjectNormalizer.Normalize(request.Subject); + if (subUsername is null) return []; + List mappings = []; + + // Query the ApplicantTenantMaps table in the host database + using (currentTenant.Change(null)) + { + var queryable = await applicantTenantMapRepository.GetQueryableAsync(); + mappings = await queryable + .Where(m => m.OidcSubUsername == subUsername) + .Select(m => new ApplicantTenantDto + { + TenantId = m.TenantId, + TenantName = m.TenantName + }) + .ToListAsync(); + } + + // Apply tenant specific metadata + foreach (var map in mappings) + { + await AddTenantMetadataAsync(map); + } + + return mappings; + } + + /// + /// Add on any relevant tenant specific metadata + /// + /// The applicant tenant DTO to enrich with tenant-specific metadata. + private async Task AddTenantMetadataAsync(ApplicantTenantDto tenantMap) + { + using (currentTenant.Change(tenantMap.TenantId)) + { + var defaultEmailAddress = await settingProvider.GetOrNullAsync(NotificationsSettings.Mailing.DefaultFromAddress); + tenantMap.Metadata[ApplicantTenantMetadataKeys.DefaultFromAddress] = defaultEmailAddress ?? "NoReply@gov.bc.ca"; + } + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/TenantMappings/ApplicantTenantMapReconciler.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/TenantMappings/ApplicantTenantMapReconciler.cs new file mode 100644 index 0000000000..0c43076a10 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/TenantMappings/ApplicantTenantMapReconciler.cs @@ -0,0 +1,123 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Unity.GrantManager.Applicants; +using Unity.GrantManager.Applications; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.MultiTenancy; +using Volo.Abp.TenantManagement; + +namespace Unity.GrantManager.ApplicantProfile; + +/// +/// Internal reconciler for records. Not exposed on any +/// application-service contract — background workers depend on this directly. +/// +public class ApplicantTenantMapReconciler( + ICurrentTenant currentTenant, + ITenantRepository tenantRepository, + IRepository applicantTenantMapRepository, + IRepository applicationFormSubmissionRepository, + ILogger logger) + : IApplicantTenantMapReconciler, ITransientDependency +{ + /// + public async Task<(int Created, int Updated)> ReconcileAsync() + { + logger.LogInformation("Starting ApplicantTenantMap reconciliation..."); + + // Phase 1: Collect all OidcSub-to-tenant associations from each tenant DB + var desiredMappings = new List<(string SubUsername, Guid TenantId, string TenantName)>(); + var tenants = await tenantRepository.GetListAsync(); + + foreach (var tenant in tenants) + { + try + { + logger.LogDebug("Collecting submissions from tenant: {TenantName}", tenant.Name); + + using (currentTenant.Change(tenant.Id)) + { + var submissionQueryable = await applicationFormSubmissionRepository.GetQueryableAsync(); + var distinctOidcSubs = await submissionQueryable + .Where(s => !string.IsNullOrWhiteSpace(s.OidcSub) && s.OidcSub != Guid.Empty.ToString()) + .Select(s => s.OidcSub) + .Distinct() + .ToListAsync(); + + foreach (var oidcSub in distinctOidcSubs) + { + var subUsername = SubjectNormalizer.Normalize(oidcSub); + if (subUsername is null) + { + continue; + } + + desiredMappings.Add((subUsername, tenant.Id, tenant.Name)); + } + } + } + catch (Exception ex) + { + logger.LogError(ex, "Error collecting submissions for tenant {TenantName}", tenant.Name); + } + } + + if (desiredMappings.Count == 0) + { + logger.LogInformation("ApplicantTenantMap reconciliation completed. No submissions found across tenants."); + return (0, 0); + } + + // Phase 2: Switch to host DB once, load existing mappings, and reconcile + int totalMappingsCreated = 0; + int totalMappingsUpdated = 0; + + using (currentTenant.Change(null)) + { + var allSubUsernames = desiredMappings.Select(m => m.SubUsername).Distinct().ToList(); + + var mapQueryable = await applicantTenantMapRepository.GetQueryableAsync(); + var existingMappings = await mapQueryable + .Where(m => allSubUsernames.Contains(m.OidcSubUsername)) + .ToListAsync(); + + var existingByKey = existingMappings + .ToDictionary(m => (m.OidcSubUsername, m.TenantId)); + + foreach (var (subUsername, tenantId, tenantName) in desiredMappings) + { + if (existingByKey.TryGetValue((subUsername, tenantId), out var existing)) + { + existing.LastUpdated = DateTime.UtcNow; + await applicantTenantMapRepository.UpdateAsync(existing); + totalMappingsUpdated++; + } + else + { + var newMapping = new ApplicantTenantMap + { + OidcSubUsername = subUsername, + TenantId = tenantId, + TenantName = tenantName, + LastUpdated = DateTime.UtcNow + }; + await applicantTenantMapRepository.InsertAsync(newMapping); + existingByKey[(subUsername, tenantId)] = newMapping; + totalMappingsCreated++; + logger.LogInformation("Created missing ApplicantTenantMap for {SubUsername} in tenant {TenantName}", + subUsername, tenantName); + } + } + } + + logger.LogInformation("ApplicantTenantMap reconciliation completed. Created: {Created}, Updated: {Updated}", + totalMappingsCreated, totalMappingsUpdated); + + return (totalMappingsCreated, totalMappingsUpdated); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/TenantMappings/IApplicantTenantMapReconciler.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/TenantMappings/IApplicantTenantMapReconciler.cs new file mode 100644 index 0000000000..f398d3de5a --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/TenantMappings/IApplicantTenantMapReconciler.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; + +namespace Unity.GrantManager.ApplicantProfile; + +/// +/// Internal helper that reconciles records by +/// scanning submissions across all tenants and syncing the host-database mapping table. +/// Intended for background-worker use only; not part of any public application-service contract. +/// +public interface IApplicantTenantMapReconciler +{ + /// + /// Phase 1: Collects all distinct OidcSub-to-tenant associations across all tenants. + /// Phase 2: Switches to the host DB once and reconciles all mappings. + /// + /// Tuple of (created count, updated count). + Task<(int Created, int Updated)> ReconcileAsync(); +} 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 0cdd0897f1..e5613a39ad 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantAppService.cs @@ -22,6 +22,7 @@ using Unity.Payments.Suppliers; using Volo.Abp.DependencyInjection; using Volo.Abp.Application.Dtos; +using Volo.Abp.Domain.Repositories; namespace Unity.GrantManager.Applicants; @@ -34,8 +35,12 @@ public class ApplicantAppService(IApplicantRepository applicantRepository, IApplicantAddressRepository addressRepository, IOrgBookService orgBookService, IApplicantAgentRepository applicantAgentRepository, - IApplicationRepository applicationRepository) : GrantManagerAppService, IApplicantAppService -{ + IApplicationRepository applicationRepository, + IRepository supplierRepository) : GrantManagerAppService, IApplicantAppService +{ + + private const string ApplicantIdDataKey = "ApplicantId"; + protected new ILogger Logger => LazyServiceProvider.LazyGetService(provider => LoggerFactory?.CreateLogger(GetType().FullName!) ?? NullLogger.Instance); [RemoteService(false)] @@ -272,11 +277,24 @@ private async Task UpdatePrimaryContactAsync(Guid applicantId, UpdatePrimaryCont throw new ArgumentException("Contact identifier is required.", nameof(input)); } + switch (input.Source) + { + case "Contact": + await UpdateLinkedContactAsync(applicantId, input); + break; + default: + await UpdateAgentContactAsync(applicantId, input); + break; + } + } + + private async Task UpdateAgentContactAsync(Guid applicantId, UpdatePrimaryContactDto input) + { var applicantAgent = await applicantAgentRepository.GetAsync(input.Id); if (applicantAgent.ApplicantId != applicantId) { throw new BusinessException("Unity:Applicant:ContactNotFound") - .WithData("ApplicantId", applicantId) + .WithData(ApplicantIdDataKey, applicantId) .WithData("ContactId", input.Id); } @@ -289,6 +307,36 @@ private async Task UpdatePrimaryContactAsync(Guid applicantId, UpdatePrimaryCont await applicantAgentRepository.UpdateAsync(applicantAgent); } + private async Task UpdateLinkedContactAsync(Guid applicantId, UpdatePrimaryContactDto input) + { + var contactRepository = LazyServiceProvider.LazyGetRequiredService(); + var contactLinkRepository = LazyServiceProvider.LazyGetRequiredService(); + + var linkQuery = await contactLinkRepository.GetQueryableAsync(); + var link = await linkQuery.FirstOrDefaultAsync(l => + l.ContactId == input.Id + && l.RelatedEntityType == "Applicant" + && l.RelatedEntityId == applicantId + && l.IsActive); + + if (link == null) + { + throw new BusinessException("Unity:Applicant:ContactNotFound") + .WithData(ApplicantIdDataKey, applicantId) + .WithData("ContactId", input.Id); + } + + var contact = await contactRepository.GetAsync(input.Id); + + contact.Name = input.FullName?.Trim() ?? string.Empty; + contact.Title = input.Title?.Trim(); + contact.Email = input.Email?.Trim(); + contact.WorkPhoneNumber = input.BusinessPhone?.Trim(); + contact.MobilePhoneNumber = input.CellPhone?.Trim(); + + await contactRepository.UpdateAsync(contact); + } + private async Task UpdatePrimaryAddressAsync(Guid applicantId, UpdatePrimaryApplicantAddressDto input, GrantApplications.AddressType expectedType) { if (input.Id == Guid.Empty) @@ -301,14 +349,14 @@ private async Task UpdatePrimaryAddressAsync(Guid applicantId, UpdatePrimaryAppl if (applicantAddress.ApplicantId != applicantId) { throw new BusinessException("Unity:Applicant:AddressNotFound") - .WithData("ApplicantId", applicantId) + .WithData(ApplicantIdDataKey, applicantId) .WithData("AddressId", input.Id); } if (applicantAddress.AddressType != expectedType) { throw new BusinessException("Unity:Applicant:AddressTypeMismatch") - .WithData("ApplicantId", applicantId) + .WithData(ApplicantIdDataKey, applicantId) .WithData("AddressId", input.Id) .WithData("ExpectedType", expectedType.ToString()); } @@ -403,7 +451,10 @@ public async Task UpdateApplicantOrgMatchAsync(Applicant applicant) return applicant; JsonElement orgData = results[0]; await UpdateApplicantOrgNumberAsync(applicant, orgData); - await UpdateApplicantNamesAsync(applicant, orgData.GetProperty("names").EnumerateArray()); + if (orgData.TryGetProperty("names", out JsonElement namesElement) && namesElement.ValueKind == JsonValueKind.Array) + { + await UpdateApplicantNamesAsync(applicant, namesElement.EnumerateArray()); + } } catch (Exception ex) { @@ -678,37 +729,58 @@ public async Task> GetListAsync(ApplicantListRe // Execute query var applicants = await query.ToListAsync(); + // Batch-load supplier data for all applicants in one query + var supplierIds = applicants + .Where(a => a.SupplierId.HasValue) + .Select(a => a.SupplierId!.Value) + .Distinct() + .ToList(); + + Dictionary supplierMap = new(); + if (supplierIds.Count > 0) + { + var suppliers = await supplierRepository.GetListAsync(s => supplierIds.Contains(s.Id)); + supplierMap = suppliers.ToDictionary(s => s.Id); + } + // Map to DTOs - var items = applicants.Select(applicant => new ApplicantListDto + var items = applicants.Select(applicant => { - Id = applicant.Id, - ApplicantName = applicant.ApplicantName, - UnityApplicantId = applicant.UnityApplicantId, - OrgName = applicant.OrgName, - OrgNumber = applicant.OrgNumber, - OrgStatus = applicant.OrgStatus, - OrganizationType = applicant.OrganizationType, - Status = applicant.Status, - RedStop = applicant.RedStop, - NonRegisteredBusinessName = applicant.NonRegisteredBusinessName, - NonRegOrgName = applicant.NonRegOrgName, - OrganizationSize = applicant.OrganizationSize, - Sector = applicant.Sector, - SubSector = applicant.SubSector, - ApproxNumberOfEmployees = applicant.ApproxNumberOfEmployees, - IndigenousOrgInd = applicant.IndigenousOrgInd, - SectorSubSectorIndustryDesc = applicant.SectorSubSectorIndustryDesc, - FiscalMonth = applicant.FiscalMonth, - BusinessNumber = applicant.BusinessNumber, - FiscalDay = applicant.FiscalDay, - StartedOperatingDate = applicant.StartedOperatingDate.HasValue - ? applicant.StartedOperatingDate.Value.ToDateTime(TimeOnly.MinValue) - : null, - SupplierId = applicant.SupplierId?.ToString(), - MatchPercentage = applicant.MatchPercentage, - IsDuplicated = applicant.IsDuplicated, - CreationTime = applicant.CreationTime, - LastModificationTime = applicant.LastModificationTime + supplierMap.TryGetValue(applicant.SupplierId ?? Guid.Empty, out var supplier); + return new ApplicantListDto + { + Id = applicant.Id, + ApplicantName = applicant.ApplicantName, + UnityApplicantId = applicant.UnityApplicantId, + OrgName = applicant.OrgName, + OrgNumber = applicant.OrgNumber, + OrgStatus = applicant.OrgStatus, + OrganizationType = applicant.OrganizationType, + Status = applicant.Status, + RedStop = applicant.RedStop, + NonRegisteredBusinessName = applicant.NonRegisteredBusinessName, + NonRegOrgName = applicant.NonRegOrgName, + OrganizationSize = applicant.OrganizationSize, + Sector = applicant.Sector, + SubSector = applicant.SubSector, + ApproxNumberOfEmployees = applicant.ApproxNumberOfEmployees, + IndigenousOrgInd = applicant.IndigenousOrgInd, + SectorSubSectorIndustryDesc = applicant.SectorSubSectorIndustryDesc, + FiscalMonth = applicant.FiscalMonth, + BusinessNumber = applicant.BusinessNumber, + FiscalDay = applicant.FiscalDay, + StartedOperatingDate = applicant.StartedOperatingDate.HasValue + ? applicant.StartedOperatingDate.Value.ToDateTime(TimeOnly.MinValue) + : null, + SupplierId = applicant.SupplierId?.ToString(), + SupplierNumber = supplier?.Number, + SupplierName = supplier?.Name, + SupplierStatus = supplier?.Status, + MatchPercentage = applicant.MatchPercentage, + IsDuplicated = applicant.IsDuplicated, + CreationTime = applicant.CreationTime, + LastModificationTime = applicant.LastModificationTime + }; }).ToList(); return new PagedResultDto(totalCount, items); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantSupplierAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantSupplierAppService.cs index 1a7ee7bb1c..b49bfd1371 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantSupplierAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantSupplierAppService.cs @@ -148,4 +148,57 @@ public async Task EnsureNoPendingPaymentsForApplicantAsync(Guid applicantId) if (!applicant.SupplierId.HasValue) return null; return await supplierAppService.GetAsync(applicant.SupplierId.Value); } + + [HttpGet("api/app/applicant-supplier/has-pending-payments-for-merge")] + public async Task HasPendingPaymentsForMergeAsync(Guid principalId, Guid nonPrincipalId) + { + if (!await FeatureChecker.IsEnabledAsync(PaymentConsts.UnityPaymentsFeature)) + { + return false; + } + + var principalAppIds = (await applicationRepository + .GetListAsync(a => a.ApplicantId == principalId)) + .Select(a => a.Id) + .ToList(); + + var nonPrincipalAppIds = (await applicationRepository + .GetListAsync(a => a.ApplicantId == nonPrincipalId)) + .Select(a => a.Id) + .ToList(); + + var allAppIds = principalAppIds.Concat(nonPrincipalAppIds).ToList(); + if (allAppIds.Count == 0) return false; + + var pendingPayments = await paymentRequestService + .GetPaymentPendingListByCorrelationIdsAsync(allAppIds); + + return pendingPayments != null && pendingPayments.Count > 0; + } + + [HttpPost("api/app/applicant-supplier/handle-supplier-after-merge")] + [Authorize(UnitySelector.Payment.Supplier.Update)] + public async Task HandleSupplierAfterMergeAsync(HandleSupplierAfterMergeDto dto) + { + await EnsureNoPendingPaymentsForApplicantAsync(dto.PrincipalId); + await EnsureNoPendingPaymentsForApplicantAsync(dto.NonPrincipalId); + + var principal = await applicantRepository.GetAsync(dto.PrincipalId); + principal.SupplierId = dto.SelectedSupplierId; + await applicantRepository.UpdateAsync(principal); + + var nonPrincipal = await applicantRepository.GetAsync(dto.NonPrincipalId); + nonPrincipal.SupplierId = dto.SelectedSupplierId; + await applicantRepository.UpdateAsync(nonPrincipal); + + // Null DefaultSiteId on all applications now belonging to the principal + // (both transferred and pre-existing). Staff re-set per application. + var applications = await applicationRepository + .GetListAsync(a => a.ApplicantId == dto.PrincipalId); + foreach (var application in applications) + { + application.DefaultSiteId = null; + await applicationRepository.UpdateAsync(application); + } + } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/BackgroundWorkers/ApplicantTenantMapReconciliationWorker.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/BackgroundWorkers/ApplicantTenantMapReconciliationWorker.cs index 61d07b1ff4..d5f13cb6c5 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/BackgroundWorkers/ApplicantTenantMapReconciliationWorker.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/BackgroundWorkers/ApplicantTenantMapReconciliationWorker.cs @@ -13,7 +13,7 @@ namespace Unity.GrantManager.Applicants.BackgroundWorkers [DisallowConcurrentExecution] public class ApplicantTenantMapReconciliationWorker : QuartzBackgroundWorkerBase { - private readonly IApplicantProfileAppService _applicantProfileAppService; + private readonly IApplicantTenantMapReconciler _reconciler; private readonly ILogger _logger; /// @@ -23,15 +23,15 @@ public class ApplicantTenantMapReconciliationWorker : QuartzBackgroundWorkerBase /// The scheduling behavior of the worker is determined by a cron expression retrieved /// from application settings. If the setting is unavailable or cannot be read, a default schedule is used. /// Logging is performed for any issues encountered during initialization. - /// The service used to access and manage applicant profile data. + /// The reconciler used to sync applicant-tenant maps across tenants. /// The setting manager used to retrieve configuration values, including the cron expression for scheduling. /// The logger used to record diagnostic and operational information for this worker. public ApplicantTenantMapReconciliationWorker( - IApplicantProfileAppService applicantProfileAppService, + IApplicantTenantMapReconciler reconciler, ISettingManager settingManager, ILogger logger) { - _applicantProfileAppService = applicantProfileAppService; + _reconciler = reconciler; _logger = logger; // 2 AM PST = 10 AM UTC @@ -93,7 +93,7 @@ public override async Task Execute(IJobExecutionContext context) try { - var (created, updated) = await _applicantProfileAppService.ReconcileApplicantTenantMapsAsync(); + var (created, updated) = await _reconciler.ReconcileAsync(); _logger.LogInformation("ApplicantTenantMapReconciliationWorker completed. Created: {Created}, Updated: {Updated}", created, updated); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/HandleSupplierAfterMergeDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/HandleSupplierAfterMergeDto.cs new file mode 100644 index 0000000000..58f2f6ba97 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/HandleSupplierAfterMergeDto.cs @@ -0,0 +1,10 @@ +using System; + +namespace Unity.GrantManager.Applicants; + +public class HandleSupplierAfterMergeDto +{ + public Guid PrincipalId { get; set; } + public Guid NonPrincipalId { get; set; } + public Guid? SelectedSupplierId { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/IApplicantSupplierAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/IApplicantSupplierAppService.cs index 5b144cdf88..992a6bae87 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/IApplicantSupplierAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/IApplicantSupplierAppService.cs @@ -16,4 +16,19 @@ public interface IApplicantSupplierAppService : IApplicationService Task UpdateApplicantSupplierNumberAsync(Guid applicantId, string supplierNumber, Guid? applicationId = null); Task UpdateAplicantSupplierByBn9Async(Guid applicantId, string bn9); Task EnsureNoPendingPaymentsForApplicantAsync(Guid applicantId); + + /// + /// Returns true if either the principal or non-principal applicant has any + /// in-progress payments (L1Pending / L2Pending / L3Pending). Used by the + /// Merge UI to show a warning before the merge is executed. + /// + Task HasPendingPaymentsForMergeAsync(Guid principalId, Guid nonPrincipalId); + + /// + /// Applies supplier and DefaultSiteId changes after a merge: + /// sets SupplierId = selectedSupplierId on both the principal and non-principal + /// (keeping the duplicate record in sync), and nulls DefaultSiteId on every + /// application that now belongs to the principal. + /// + Task HandleSupplierAfterMergeAsync(HandleSupplierAfterMergeDto dto); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Contacts/ContactAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Contacts/ContactAppService.cs index f8fcc31f74..52b3d0d9da 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Contacts/ContactAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Contacts/ContactAppService.cs @@ -20,7 +20,8 @@ namespace Unity.GrantManager.Contacts; [ExposeServices(typeof(ContactAppService), typeof(IContactAppService))] public class ContactAppService( IContactRepository contactRepository, - IContactLinkRepository contactLinkRepository) + IContactLinkRepository contactLinkRepository, + IContactManager contactManager) : GrantManagerAppService, IContactAppService { /// @@ -53,33 +54,60 @@ join contact in contactsQuery on link.ContactId equals contact.Id /// public async Task CreateContactAsync(CreateContactLinkDto input) { - var contact = await contactRepository.InsertAsync(new Contact - { - Name = input.Name, - Title = input.Title, - Email = input.Email, - HomePhoneNumber = input.HomePhoneNumber, - MobilePhoneNumber = input.MobilePhoneNumber, - WorkPhoneNumber = input.WorkPhoneNumber, - WorkPhoneExtension = input.WorkPhoneExtension - }, autoSave: true); + ArgumentNullException.ThrowIfNull(input); - if (input.IsPrimary) - { - await ClearPrimaryAsync(input.RelatedEntityType, input.RelatedEntityId); - } + var (contact, link) = await contactManager.CreateAsync( + input.RelatedEntityType, + input.RelatedEntityId, + ToContactInput(input), + input.Role, + input.IsPrimary); - await contactLinkRepository.InsertAsync(new ContactLink - { - ContactId = contact.Id, - RelatedEntityType = input.RelatedEntityType, - RelatedEntityId = input.RelatedEntityId, - Role = input.Role, - IsPrimary = input.IsPrimary, - IsActive = true - }, autoSave: true); + return MapToDto(contact, link); + } + + /// + public async Task UpdateContactAsync(string entityType, Guid entityId, Guid contactId, UpdateContactDto input) + { + ArgumentNullException.ThrowIfNull(input); + + var (contact, link) = await contactManager.UpdateAsync( + entityType, + entityId, + contactId, + ToContactInput(input), + input.Role, + input.IsPrimary); + + return MapToDto(contact, link); + } + + /// + public Task SetPrimaryContactAsync(string entityType, Guid entityId, Guid contactId) + { + return contactManager.SetPrimaryAsync(entityType, entityId, contactId); + } - return new ContactDto + private static ContactInput ToContactInput(CreateContactLinkDto input) => + new(input.Name, + input.Title, + input.Email, + input.HomePhoneNumber, + input.MobilePhoneNumber, + input.WorkPhoneNumber, + input.WorkPhoneExtension); + + private static ContactInput ToContactInput(UpdateContactDto input) => + new(input.Name, + input.Title, + input.Email, + input.HomePhoneNumber, + input.MobilePhoneNumber, + input.WorkPhoneNumber, + input.WorkPhoneExtension); + + private static ContactDto MapToDto(Contact contact, ContactLink link) => + new() { ContactId = contact.Id, Name = contact.Name, @@ -89,47 +117,7 @@ await contactLinkRepository.InsertAsync(new ContactLink MobilePhoneNumber = contact.MobilePhoneNumber, WorkPhoneNumber = contact.WorkPhoneNumber, WorkPhoneExtension = contact.WorkPhoneExtension, - Role = input.Role, - IsPrimary = input.IsPrimary + Role = link.Role, + IsPrimary = link.IsPrimary }; - } - - /// - public async Task SetPrimaryContactAsync(string entityType, Guid entityId, Guid contactId) - { - await ClearPrimaryAsync(entityType, entityId); - - var contactLinksQuery = await contactLinkRepository.GetQueryableAsync(); - var link = await contactLinksQuery - .Where(l => l.RelatedEntityType == entityType - && l.RelatedEntityId == entityId - && l.ContactId == contactId - && l.IsActive) - .FirstOrDefaultAsync() ?? throw new BusinessException("Contacts:ContactLinkNotFound") - .WithData("contactId", contactId) - .WithData("entityType", entityType) - .WithData("entityId", entityId); - link.IsPrimary = true; - await contactLinkRepository.UpdateAsync(link, autoSave: true); - } - - /// - /// Clears the primary flag on all active contact links for the specified entity. - /// - private async Task ClearPrimaryAsync(string entityType, Guid entityId) - { - var contactLinksQuery = await contactLinkRepository.GetQueryableAsync(); - var currentPrimaryLinks = await contactLinksQuery - .Where(l => l.RelatedEntityType == entityType - && l.RelatedEntityId == entityId - && l.IsPrimary - && l.IsActive) - .ToListAsync(); - - foreach (var existing in currentPrimaryLinks) - { - existing.IsPrimary = false; - await contactLinkRepository.UpdateAsync(existing, autoSave: true); - } - } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/AIGenerationStatusAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/AIGenerationStatusAppService.cs new file mode 100644 index 0000000000..e887a1fc8a --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/AIGenerationStatusAppService.cs @@ -0,0 +1,45 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Unity.GrantManager.GrantApplications; +using Volo.Abp.Application.Services; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.MultiTenancy; + +namespace Unity.GrantManager.GrantApplications; + +public class AIGenerationStatusAppService( + IRepository generationRequestRepository, + ICurrentTenant currentTenant) + : ApplicationService, IAIGenerationStatusAppService +{ + public virtual async Task GetLatestAsync(Guid applicationId, string operationType, Guid? tenantId = null) + { + var query = await generationRequestRepository.GetQueryableAsync(); + var resolvedTenantId = tenantId ?? currentTenant.Id; + + var item = query + .Where(x => + x.ApplicationId == applicationId && + x.OperationType == operationType && + x.TenantId == resolvedTenantId) + .OrderByDescending(x => x.CreationTime) + .ThenByDescending(x => x.Id) + .FirstOrDefault(); + + return item == null + ? null + : new AIGenerationRequestDto + { + Id = item.Id, + ApplicationId = item.ApplicationId, + OperationType = item.OperationType, + RequestKey = item.RequestKey, + Status = item.Status, + StartedAt = item.StartedAt, + CompletedAt = item.CompletedAt, + FailureReason = item.FailureReason, + IsActive = item.IsActive + }; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/ApplicationAIGenerationQueue.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/ApplicationAIGenerationQueue.cs index 25abe8819a..2d787933ab 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/ApplicationAIGenerationQueue.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/ApplicationAIGenerationQueue.cs @@ -1,22 +1,163 @@ using System; +using System.Linq; using System.Threading.Tasks; using Unity.AI.Automation; +using Unity.GrantManager.GrantApplications; using Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; +using Medallion.Threading; +using Microsoft.Extensions.Logging; +using Volo.Abp.Domain.Repositories; using Volo.Abp.BackgroundJobs; using Volo.Abp.DependencyInjection; namespace Unity.GrantManager.GrantApplications.Automation; -public class ApplicationAIGenerationQueue(IBackgroundJobManager backgroundJobManager) +public class ApplicationAIGenerationQueue( + IBackgroundJobManager backgroundJobManager, + IRepository generationRequestRepository, + IDistributedLockProvider distributedLockProvider, + ILogger logger) : IApplicationAIGenerationQueue, ITransientDependency { - public async Task QueueApplicationPipelineAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null) + public async Task QueueAttachmentSummaryAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null) { - await backgroundJobManager.EnqueueAsync(new RunApplicationAIPipelineJobArgs + var requestKey = AIGenerationRequestKeyHelper.BuildRequestKey(tenantId, applicationId, AIGenerationRequestKeyHelper.AttachmentSummaryOperationType); + await EnsureRequestAndEnqueueAsync( + requestKey, + tenantId, + AIGenerationRequestKeyHelper.AttachmentSummaryOperationType, + applicationId, + () => + { + return backgroundJobManager.EnqueueAsync(new GenerateAttachmentSummaryBackgroundJobArgs + { + ApplicationId = applicationId, + PromptVersion = promptVersion, + TenantId = tenantId, + RequestKey = requestKey + }); + }); + } + + public async Task QueueApplicationAnalysisAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null) + { + var requestKey = AIGenerationRequestKeyHelper.BuildRequestKey(tenantId, applicationId, AIGenerationRequestKeyHelper.ApplicationAnalysisOperationType); + await EnsureRequestAndEnqueueAsync( + requestKey, + tenantId, + AIGenerationRequestKeyHelper.ApplicationAnalysisOperationType, + applicationId, + () => + { + return backgroundJobManager.EnqueueAsync(new GenerateApplicationAnalysisBackgroundJobArgs + { + ApplicationId = applicationId, + PromptVersion = promptVersion, + TenantId = tenantId, + RequestKey = requestKey + }); + }); + } + + public async Task QueueApplicationScoringAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null) + { + var requestKey = AIGenerationRequestKeyHelper.BuildRequestKey(tenantId, applicationId, AIGenerationRequestKeyHelper.ApplicationScoringOperationType); + await EnsureRequestAndEnqueueAsync( + requestKey, + tenantId, + AIGenerationRequestKeyHelper.ApplicationScoringOperationType, + applicationId, + () => + { + return backgroundJobManager.EnqueueAsync(new GenerateApplicationScoringBackgroundJobArgs + { + ApplicationId = applicationId, + PromptVersion = promptVersion, + TenantId = tenantId, + RequestKey = requestKey + }); + }); + } + + public async Task QueueAllAIStagesAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null) + { + var requestKey = AIGenerationRequestKeyHelper.BuildRequestKey(tenantId, applicationId, AIGenerationRequestKeyHelper.PipelineOperationType); + await EnsureRequestAndEnqueueAsync( + requestKey, + tenantId, + AIGenerationRequestKeyHelper.PipelineOperationType, + applicationId, + () => + { + return backgroundJobManager.EnqueueAsync(new RunApplicationAIPipelineJobArgs + { + ApplicationId = applicationId, + PromptVersion = promptVersion, + TenantId = tenantId, + RequestKey = requestKey + }); + }); + } + + private async Task EnsureRequestAndEnqueueAsync( + string requestKey, + Guid? tenantId, + string operationType, + Guid? applicationId, + Func enqueue) + { + var requestLock = distributedLockProvider.CreateLock($"ai-generation:{requestKey}"); + + using (await requestLock.AcquireAsync()) + { + var query = await generationRequestRepository.GetQueryableAsync(); + var existingRequests = query.Where(x => + x.RequestKey == requestKey + && (x.Status == AIGenerationRequestStatus.Queued || x.Status == AIGenerationRequestStatus.Running)); + + var existing = existingRequests + .OrderByDescending(x => x.CreationTime) + .ThenByDescending(x => x.Id) + .FirstOrDefault(); + + if (existing != null) + { + return; + } + + var request = new AIGenerationRequest( + Guid.NewGuid(), + tenantId, + operationType, + applicationId, + requestKey); + + await generationRequestRepository.InsertAsync(request, autoSave: true); + + try + { + await enqueue(); + } + catch (Exception ex) + { + await MarkFailedBestEffortAsync(request, ex); + throw; + } + } + } + + private async Task MarkFailedBestEffortAsync(AIGenerationRequest request, Exception exception) + { + try + { + await AIGenerationRequestJobHelper.MarkFailedAsync(generationRequestRepository, request, exception.Message); + } + catch (Exception markException) { - ApplicationId = applicationId, - PromptVersion = promptVersion, - TenantId = tenantId - }); + logger.LogError( + markException, + "Failed to mark AI generation request {RequestId} as failed after enqueue failure.", + request.Id); + } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/AIGenerationRequestJobHelper.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/AIGenerationRequestJobHelper.cs new file mode 100644 index 0000000000..843b091203 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/AIGenerationRequestJobHelper.cs @@ -0,0 +1,63 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Unity.GrantManager.GrantApplications; +using Volo.Abp.Domain.Repositories; + +namespace Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; + +public static class AIGenerationRequestJobHelper +{ + public static async Task MarkRunningAsync( + IRepository generationRequestRepository, + AIGenerationRequest? request) + { + if (request == null) + { + return; + } + + request.MarkRunning(DateTime.UtcNow); + await generationRequestRepository.UpdateAsync(request, autoSave: true); + } + + public static async Task MarkCompletedAsync( + IRepository generationRequestRepository, + AIGenerationRequest? request) + { + if (request == null) + { + return; + } + + request.MarkCompleted(DateTime.UtcNow); + await generationRequestRepository.UpdateAsync(request, autoSave: true); + } + + public static async Task MarkFailedAsync( + IRepository generationRequestRepository, + AIGenerationRequest? request, + string? failureReason) + { + if (request == null) + { + return; + } + + request.MarkFailed(DateTime.UtcNow, failureReason); + await generationRequestRepository.UpdateAsync(request, autoSave: true); + } + + public static async Task GetLatestRequestAsync( + IRepository generationRequestRepository, + Expression> predicate) + { + var query = await generationRequestRepository.GetQueryableAsync(); + return query + .Where(predicate) + .OrderByDescending(x => x.CreationTime) + .ThenByDescending(x => x.Id) + .FirstOrDefault(); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisJob.cs new file mode 100644 index 0000000000..30ce556bf1 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisJob.cs @@ -0,0 +1,40 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Threading.Tasks; +using Unity.AI.Operations; +using Unity.GrantManager.GrantApplications; +using Volo.Abp.BackgroundJobs; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.MultiTenancy; + +namespace Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; + +public class GenerateApplicationAnalysisJob( + IApplicationAnalysisService applicationAnalysisService, + IRepository generationRequestRepository, + ICurrentTenant currentTenant, + ILogger logger) : AsyncBackgroundJob, ITransientDependency +{ + public override async Task ExecuteAsync(GenerateApplicationAnalysisBackgroundJobArgs args) + { + using (currentTenant.Change(args.TenantId)) + { + var request = await AIGenerationRequestJobHelper.GetLatestRequestAsync(generationRequestRepository, x => x.RequestKey == args.RequestKey); + await AIGenerationRequestJobHelper.MarkRunningAsync(generationRequestRepository, request); + try + { + logger.LogInformation("Executing AI application analysis job for application {ApplicationId}.", args.ApplicationId); + await applicationAnalysisService.RegenerateAndSaveAsync(args.ApplicationId, args.PromptVersion); + logger.LogInformation("Completed AI application analysis job for application {ApplicationId}.", args.ApplicationId); + + await AIGenerationRequestJobHelper.MarkCompletedAsync(generationRequestRepository, request); + } + catch (Exception ex) + { + await AIGenerationRequestJobHelper.MarkFailedAsync(generationRequestRepository, request, ex.Message); + throw; + } + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringJob.cs new file mode 100644 index 0000000000..87a0eed6ab --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringJob.cs @@ -0,0 +1,38 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Threading.Tasks; +using Unity.GrantManager.GrantApplications; +using Volo.Abp.BackgroundJobs; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.MultiTenancy; + +namespace Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; + +public class GenerateApplicationScoringJob( + IApplicationScoringAppService applicationScoringAppService, + IRepository generationRequestRepository, + ICurrentTenant currentTenant, + ILogger logger) : AsyncBackgroundJob, ITransientDependency +{ + public override async Task ExecuteAsync(GenerateApplicationScoringBackgroundJobArgs args) + { + using (currentTenant.Change(args.TenantId)) + { + var request = await AIGenerationRequestJobHelper.GetLatestRequestAsync(generationRequestRepository, x => x.RequestKey == args.RequestKey); + await AIGenerationRequestJobHelper.MarkRunningAsync(generationRequestRepository, request); + try + { + logger.LogInformation("Executing AI application scoring job for application {ApplicationId}.", args.ApplicationId); + await applicationScoringAppService.GenerateApplicationScoringAsync(args.ApplicationId, args.PromptVersion); + logger.LogInformation("Completed AI application scoring job for application {ApplicationId}.", args.ApplicationId); + await AIGenerationRequestJobHelper.MarkCompletedAsync(generationRequestRepository, request); + } + catch (Exception ex) + { + await AIGenerationRequestJobHelper.MarkFailedAsync(generationRequestRepository, request, ex.Message); + throw; + } + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryJob.cs new file mode 100644 index 0000000000..b6c1b186f6 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryJob.cs @@ -0,0 +1,40 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Threading.Tasks; +using Unity.AI.Operations; +using Unity.GrantManager.GrantApplications; +using Volo.Abp.BackgroundJobs; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.MultiTenancy; + +namespace Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; + +public class GenerateAttachmentSummaryJob( + IAttachmentSummaryService attachmentSummaryService, + IRepository generationRequestRepository, + ICurrentTenant currentTenant, + ILogger logger) : AsyncBackgroundJob, ITransientDependency +{ + public override async Task ExecuteAsync(GenerateAttachmentSummaryBackgroundJobArgs args) + { + using (currentTenant.Change(args.TenantId)) + { + var request = await AIGenerationRequestJobHelper.GetLatestRequestAsync(generationRequestRepository, x => x.RequestKey == args.RequestKey); + await AIGenerationRequestJobHelper.MarkRunningAsync(generationRequestRepository, request); + try + { + logger.LogInformation( + "Executing AI attachment summary job for application {ApplicationId}.", + args.ApplicationId); + await attachmentSummaryService.GenerateForApplicationAsync(args.ApplicationId, args.PromptVersion); + await AIGenerationRequestJobHelper.MarkCompletedAsync(generationRequestRepository, request); + } + catch (Exception ex) + { + await AIGenerationRequestJobHelper.MarkFailedAsync(generationRequestRepository, request, ex.Message); + throw; + } + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJob.cs index 3b55cf8093..c749f3ff88 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJob.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJob.cs @@ -9,6 +9,7 @@ using Unity.GrantManager.GrantApplications.Automation.Events; using Volo.Abp.BackgroundJobs; using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; using Volo.Abp.EventBus.Local; using Volo.Abp.Features; using Volo.Abp.MultiTenancy; @@ -22,82 +23,103 @@ public class RunApplicationAIPipelineJob( IApplicationScoringAppService applicationScoringAppService, IFeatureChecker featureChecker, ILocalEventBus localEventBus, + IRepository generationRequestRepository, ICurrentTenant currentTenant, ILogger logger) : AsyncBackgroundJob, ITransientDependency { public override async Task ExecuteAsync(RunApplicationAIPipelineJobArgs args) { + if (string.IsNullOrWhiteSpace(args.RequestKey)) + { + throw new ArgumentException("RequestKey is required.", nameof(args)); + } + using (currentTenant.Change(args.TenantId)) { - var attachmentSummariesEnabled = await featureChecker.IsEnabledAsync("Unity.AI.AttachmentSummaries"); - var applicationAnalysisEnabled = await featureChecker.IsEnabledAsync("Unity.AI.ApplicationAnalysis"); - var scoringEnabled = await featureChecker.IsEnabledAsync("Unity.AI.Scoring"); + var request = await AIGenerationRequestJobHelper.GetLatestRequestAsync( + generationRequestRepository, + x => x.RequestKey == args.RequestKey); + + await AIGenerationRequestJobHelper.MarkRunningAsync(generationRequestRepository, request); - if (!attachmentSummariesEnabled && !applicationAnalysisEnabled && !scoringEnabled) + try { - logger.LogDebug("All AI features are disabled, skipping queued AI generation for application {ApplicationId}.", args.ApplicationId); - return; - } + var attachmentSummariesEnabled = await featureChecker.IsEnabledAsync("Unity.AI.AttachmentSummaries"); + var applicationAnalysisEnabled = await featureChecker.IsEnabledAsync("Unity.AI.ApplicationAnalysis"); + var scoringEnabled = await featureChecker.IsEnabledAsync("Unity.AI.Scoring"); - logger.LogInformation("Executing queued AI content pipeline for application {ApplicationId}.", args.ApplicationId); + if (!attachmentSummariesEnabled && !applicationAnalysisEnabled && !scoringEnabled) + { + logger.LogDebug("All AI features are disabled, skipping queued AI generation for application {ApplicationId}.", args.ApplicationId); + await AIGenerationRequestJobHelper.MarkCompletedAsync(generationRequestRepository, request); + return; + } - if (attachmentSummariesEnabled) - { - var attachmentResults = await attachmentSummaryAppService.GenerateAttachmentSummariesForPipelineAsync( - await GetAttachmentIdsAsync(args.ApplicationId), - args.PromptVersion); + logger.LogInformation("Executing queued AI content pipeline for application {ApplicationId}.", args.ApplicationId); - logger.LogInformation("Completed AI attachment summaries for application {ApplicationId} with {AttachmentCount} result(s).", args.ApplicationId, attachmentResults.Count); - } + if (attachmentSummariesEnabled) + { + var attachmentIds = await GetAttachmentIdsAsync(args.ApplicationId); + var attachmentResults = await attachmentSummaryAppService.GenerateAttachmentSummariesForPipelineAsync(attachmentIds, args.PromptVersion); + logger.LogInformation("Completed AI attachment summaries for application {ApplicationId} with {AttachmentCount} result(s).", args.ApplicationId, attachmentResults.Count); + } - Exception? analysisException = null; - Exception? scoringException = null; + Exception? analysisException = null; + Exception? scoringException = null; - if (applicationAnalysisEnabled) - { - try + if (applicationAnalysisEnabled) { - var analysisResult = await applicationAnalysisAppService.GenerateApplicationAnalysisForPipelineAsync(args.ApplicationId, args.PromptVersion); - if (analysisResult.Completed) + try { - logger.LogInformation("Completed AI application analysis stage for application {ApplicationId}.", args.ApplicationId); + var analysisResult = await applicationAnalysisAppService.GenerateApplicationAnalysisForPipelineAsync(args.ApplicationId, args.PromptVersion); + if (analysisResult.Completed) + { + logger.LogInformation("Completed AI application analysis stage for application {ApplicationId}.", args.ApplicationId); + } + } + catch (Exception ex) + { + analysisException = ex; + logger.LogError(ex, "Error executing AI application analysis stage for application {ApplicationId}.", args.ApplicationId); } } - catch (Exception ex) - { - analysisException = ex; - logger.LogError(ex, "Error executing AI application analysis stage for application {ApplicationId}.", args.ApplicationId); - } - } - if (scoringEnabled) - { - try + if (scoringEnabled) { - var result = await applicationScoringAppService.GenerateApplicationScoringForPipelineAsync(args.ApplicationId, args.PromptVersion); - if (result.Completed) + try { - await localEventBus.PublishAsync(new ApplicationAIScoringGeneratedEvent + var result = await applicationScoringAppService.GenerateApplicationScoringForPipelineAsync(args.ApplicationId, args.PromptVersion); + if (result.Completed) { - ApplicationId = args.ApplicationId - }); + await localEventBus.PublishAsync(new ApplicationAIScoringGeneratedEvent + { + ApplicationId = args.ApplicationId + }); + } + } + catch (Exception ex) + { + scoringException = ex; + logger.LogError(ex, "Error executing AI application scoring stage for application {ApplicationId}.", args.ApplicationId); } } - catch (Exception ex) + + if (scoringException != null) { - scoringException = ex; - logger.LogError(ex, "Error executing AI application scoring stage for application {ApplicationId}.", args.ApplicationId); + throw scoringException; } - } - if (scoringException != null) - { - throw scoringException; - } + if (analysisException != null) + { + throw analysisException; + } - if (analysisException != null) + await AIGenerationRequestJobHelper.MarkCompletedAsync(generationRequestRepository, request); + } + catch (Exception ex) { - throw analysisException; + await AIGenerationRequestJobHelper.MarkFailedAsync(generationRequestRepository, request, ex.Message); + throw; } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Handlers/CreateAIAssessmentOnScoringGeneratedHandler.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Handlers/CreateAIAssessmentOnScoringGeneratedHandler.cs index c93eff4685..4f8da39ef8 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Handlers/CreateAIAssessmentOnScoringGeneratedHandler.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Handlers/CreateAIAssessmentOnScoringGeneratedHandler.cs @@ -1,21 +1,18 @@ using Microsoft.Extensions.Logging; using System; using System.Threading.Tasks; -using Unity.AI.Settings; using Unity.GrantManager.Applications; using Unity.GrantManager.Assessments; using Unity.GrantManager.GrantApplications.Automation.Events; using Volo.Abp.DependencyInjection; using Volo.Abp.EventBus; using Volo.Abp.Features; -using Volo.Abp.Settings; using Volo.Abp.Uow; namespace Unity.GrantManager.GrantApplications.Automation.Handlers; public class CreateAIAssessmentOnScoringGeneratedHandler( AssessmentManager assessmentManager, IApplicationRepository applicationRepository, IFeatureChecker featureChecker, - ISettingProvider settingProvider, IUnitOfWorkManager unitOfWorkManager, ILogger logger) : ILocalEventHandler, ITransientDependency { @@ -26,10 +23,6 @@ public async Task HandleEventAsync(ApplicationAIScoringGeneratedEvent eventData) logger.LogWarning("Event data or application ID is null in CreateAIAssessmentOnScoringGeneratedHandler."); return; } - if (!await settingProvider.GetAsync(AISettings.AutomaticGenerationEnabled, defaultValue: false)) - { - return; - } if (!await featureChecker.IsEnabledAsync("Unity.AI.Scoring")) { return; @@ -47,4 +40,4 @@ public async Task HandleEventAsync(ApplicationAIScoringGeneratedEvent eventData) logger.LogError(ex, "Error creating AI assessment for application {ApplicationId}.", eventData.ApplicationId); } } -} \ No newline at end of file +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Handlers/QueueApplicationAIPipelineOnProcessHandler.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Handlers/QueueApplicationAIPipelineOnProcessHandler.cs index 6bb6e37027..d80d0b4b88 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Handlers/QueueApplicationAIPipelineOnProcessHandler.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Handlers/QueueApplicationAIPipelineOnProcessHandler.cs @@ -1,21 +1,22 @@ using Microsoft.Extensions.Logging; using System; using System.Threading.Tasks; +using Unity.AI.Automation; using Unity.AI.Settings; using Unity.GrantManager.Applications; -using Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; using Unity.GrantManager.Intakes.Events; -using Volo.Abp.BackgroundJobs; using Volo.Abp.DependencyInjection; using Volo.Abp.EventBus; +using Volo.Abp.Features; using Volo.Abp.Settings; namespace Unity.GrantManager.GrantApplications.Automation.Handlers; public class QueueApplicationAIPipelineOnProcessHandler( - IBackgroundJobManager backgroundJobManager, + IApplicationAIGenerationQueue aiGenerationQueue, ISettingProvider settingProvider, IApplicationFormRepository applicationFormRepository, + IFeatureChecker featureChecker, ILogger logger) : ILocalEventHandler, ITransientDependency { public async Task HandleEventAsync(ApplicationProcessEvent eventData) @@ -40,13 +41,20 @@ public async Task HandleEventAsync(ApplicationProcessEvent eventData) return; } + var attachmentSummariesEnabled = await featureChecker.IsEnabledAsync("Unity.AI.AttachmentSummaries"); + var applicationAnalysisEnabled = await featureChecker.IsEnabledAsync("Unity.AI.ApplicationAnalysis"); + var scoringEnabled = await featureChecker.IsEnabledAsync("Unity.AI.Scoring"); + + if (!attachmentSummariesEnabled && !applicationAnalysisEnabled && !scoringEnabled) + { + logger.LogDebug("All AI features are disabled, skipping intake pipeline for application {ApplicationId}.", eventData.Application.Id); + return; + } + try { - await backgroundJobManager.EnqueueAsync(new RunApplicationAIPipelineJobArgs - { - ApplicationId = eventData.Application.Id, - TenantId = eventData.Application.TenantId - }); + await aiGenerationQueue.QueueAllAIStagesAsync(eventData.Application.Id, eventData.Application.TenantId); + logger.LogInformation("Queued AI pipeline for application {ApplicationId}.", eventData.Application.Id); } catch (Exception ex) 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 c5c6bac45a..a746d2e2bb 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs @@ -26,15 +26,20 @@ using Unity.Modules.Shared; using Unity.Modules.Shared.Correlation; using Unity.Payments.PaymentRequests; +using Unity.AI.Automation; +using Unity.AI.Permissions; using Volo.Abp; using Volo.Abp.Application.Dtos; using Volo.Abp.Authorization; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Repositories; +using Volo.Abp.Features; namespace Unity.GrantManager.GrantApplications; +// Suppress S107: Methods should not have too many parameters +#pragma warning disable S107 // Methods should not have too many parameters [Authorize] [Dependency(ReplaceServices = true)] [ExposeServices(typeof(GrantApplicationAppService), typeof(IGrantApplicationAppService))] @@ -48,8 +53,12 @@ public class GrantApplicationAppService( IApplicantAgentRepository applicantAgentRepository, IApplicantAddressRepository applicantAddressRepository, IApplicantSupplierAppService applicantSupplierService, - IPaymentRequestAppService paymentRequestService) + IPaymentRequestAppService paymentRequestService, + IApplicationAIGenerationQueue aiGenerationQueue, + IAIGenerationStatusAppService aiGenerationStatusAppService, + IFeatureChecker featureChecker) : GrantManagerAppService, IGrantApplicationAppService +#pragma warning restore S107 // Methods should not have too many parameters { private static readonly JsonSerializerOptions AiAnalysisReadOptions = new() { @@ -1026,6 +1035,125 @@ await LocalEventBus.PublishAsync( return ObjectMapper.Map(application); } + + public async Task QueueAIGenerationAsync(Guid applicationId, string? promptVersion = null) + { + await EnsureAIAnalysisEnabledAsync(); + return await QueueApplicationAnalysisAsync(applicationId, promptVersion); + } + + [Authorize(AIPermissions.Analysis.GenerateApplicationAnalysis)] + public async Task QueueApplicationAnalysisAsync(Guid applicationId, string? promptVersion = null) + { + await EnsureAIAnalysisEnabledAsync(); + await aiGenerationQueue.QueueApplicationAnalysisAsync(applicationId, CurrentTenant.Id, promptVersion); + + var request = await aiGenerationStatusAppService.GetLatestAsync( + applicationId, + AIGenerationRequestKeyHelper.ApplicationAnalysisOperationType, + CurrentTenant.Id); + + return request ?? throw new UserFriendlyException("Unable to queue AI analysis request."); + } + + [Authorize(AIPermissions.Analysis.GenerateAttachmentSummaries)] + public async Task QueueAttachmentSummaryAsync(Guid applicationId, string? promptVersion = null) + { + await EnsureAttachmentSummariesEnabledAsync(); + await aiGenerationQueue.QueueAttachmentSummaryAsync(applicationId, CurrentTenant.Id, promptVersion); + + var request = await aiGenerationStatusAppService.GetLatestAsync( + applicationId, + AIGenerationRequestKeyHelper.AttachmentSummaryOperationType, + CurrentTenant.Id); + + return request ?? throw new UserFriendlyException("Unable to queue AI attachment summary request."); + } + + [Authorize(AIPermissions.Analysis.GenerateScoring)] + public async Task QueueApplicationScoringAsync(Guid applicationId, string? promptVersion = null) + { + await EnsureScoringEnabledAsync(); + await aiGenerationQueue.QueueApplicationScoringAsync(applicationId, CurrentTenant.Id, promptVersion); + + var request = await aiGenerationStatusAppService.GetLatestAsync( + applicationId, + AIGenerationRequestKeyHelper.ApplicationScoringOperationType, + CurrentTenant.Id); + + return request ?? throw new UserFriendlyException("Unable to queue AI scoring request."); + } + + public async Task GetAIGenerationStatusAsync(Guid applicationId, string operationType, string? promptVersion = null) + { + await EnsureAIGenerationStatusAccessAsync(operationType); + return await aiGenerationStatusAppService.GetLatestAsync(applicationId, operationType, CurrentTenant.Id); + } + + [Authorize(AIPermissions.Analysis.GenerateApplicationAnalysis)] + [Authorize(AIPermissions.Analysis.GenerateAttachmentSummaries)] + [Authorize(AIPermissions.Analysis.GenerateScoring)] + public async Task QueueAllAIStagesAsync(Guid applicationId, string? promptVersion = null) + { + await EnsureAttachmentSummariesEnabledAsync(); + await EnsureAIAnalysisEnabledAsync(); + await EnsureScoringEnabledAsync(); + await aiGenerationQueue.QueueAllAIStagesAsync(applicationId, CurrentTenant.Id, promptVersion); + + var request = await aiGenerationStatusAppService.GetLatestAsync( + applicationId, + AIGenerationRequestKeyHelper.PipelineOperationType, + CurrentTenant.Id); + + return request ?? throw new UserFriendlyException("Unable to queue AI generation request."); + } + + private async Task EnsureAIGenerationStatusAccessAsync(string operationType) + { + switch (operationType) + { + case AIGenerationRequestKeyHelper.ApplicationAnalysisOperationType: + await AuthorizationService.CheckAsync(AIPermissions.Analysis.ViewApplicationAnalysis); + return; + case AIGenerationRequestKeyHelper.AttachmentSummaryOperationType: + await AuthorizationService.CheckAsync(AIPermissions.Analysis.ViewAttachmentSummary); + return; + case AIGenerationRequestKeyHelper.ApplicationScoringOperationType: + await AuthorizationService.CheckAsync(AIPermissions.Analysis.ViewScoringResult); + return; + case AIGenerationRequestKeyHelper.PipelineOperationType: + await AuthorizationService.CheckAsync(AIPermissions.Analysis.ViewApplicationAnalysis); + await AuthorizationService.CheckAsync(AIPermissions.Analysis.ViewAttachmentSummary); + await AuthorizationService.CheckAsync(AIPermissions.Analysis.ViewScoringResult); + return; + default: + throw new UserFriendlyException("Unknown AI generation operation type."); + } + } + + private async Task EnsureAttachmentSummariesEnabledAsync() + { + if (!await featureChecker.IsEnabledAsync("Unity.AI.AttachmentSummaries")) + { + throw new UserFriendlyException("AI attachment summaries are not enabled."); + } + } + + private async Task EnsureAIAnalysisEnabledAsync() + { + if (!await featureChecker.IsEnabledAsync("Unity.AI.ApplicationAnalysis")) + { + throw new UserFriendlyException("AI application analysis is not enabled."); + } + } + + private async Task EnsureScoringEnabledAsync() + { + if (!await featureChecker.IsEnabledAsync("Unity.AI.Scoring")) + { + throw new UserFriendlyException("AI scoring is not enabled."); + } + } #endregion APPLICATION WORKFLOW public async Task> GetAllApplicationsAsync() diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationAutoMapperProfile.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationAutoMapperProfile.cs index 168a2dc905..ab80deee8b 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationAutoMapperProfile.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationAutoMapperProfile.cs @@ -33,7 +33,7 @@ public GrantManagerApplicationAutoMapperProfile() CreateMap(); CreateMap(); - CreateMap(); + CreateMap(); CreateMap(); CreateMap() .ForMember(d => d.AssigneeId, opt => opt.MapFrom(src => src.Id)) @@ -58,9 +58,9 @@ public GrantManagerApplicationAutoMapperProfile() opts => opts.MapFrom(src => src.CreationTime)); CreateMap() .ForMember(d => d.SubTotal, opt => opt.Ignore()); - CreateMap() - .ForMember(dest => dest.CreatedTime, opt => opt.MapFrom(src => src.CreationTime)) - .ForMember(dest => dest.UpdatedTime, opt => opt.MapFrom(src => src.LastModificationTime)); + CreateMap() + .ForMember(dest => dest.CreatedTime, opt => opt.MapFrom(src => src.CreationTime)) + .ForMember(dest => dest.UpdatedTime, opt => opt.MapFrom(src => src.LastModificationTime)); CreateMap(); CreateMap(); CreateMap(); @@ -95,11 +95,12 @@ public GrantManagerApplicationAutoMapperProfile() CreateMap(); CreateMap(); CreateMap(); - CreateMap(); - CreateMap() - .ForMember(dest => dest.Tag, opt => opt.MapFrom(src => src.Tag)); - - //-- APPLICANT HISTORY + CreateMap(); + CreateMap() + .ForMember(dest => dest.Tag, opt => opt.MapFrom(src => src.Tag)); + CreateMap(); + + //-- APPLICANT HISTORY 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 dae27b6bc8..b76bd5707e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs @@ -46,6 +46,7 @@ using Unity.GrantManager.GrantsPortal.Configuration; using Unity.GrantManager.GrantsPortal.Handlers; using Unity.GrantManager.Messaging; +using Unity.GrantManager.Analytics; namespace Unity.GrantManager; @@ -134,7 +135,8 @@ public override void ConfigureServices(ServiceConfigurationContext context) context.Services.AddTransient(); context.Services.AddTransient(); - context.Services.AddScoped(); + context.Services.AddScoped(); + context.Services.AddScoped(); context.Services.AddScoped(); context.Services.Configure(configuration.GetSection("Payments")); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Reporting/Configuration/FormMetadataService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Reporting/Configuration/FormMetadataService.cs index ef8b73697a..8621683fe6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Reporting/Configuration/FormMetadataService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Reporting/Configuration/FormMetadataService.cs @@ -224,19 +224,9 @@ private static string CreateDataPath(FormComponentMetaDataItemDto component) var pathSegments = workingPath.Split(separator, StringSplitOptions.RemoveEmptyEntries); var keySegments = component.TypePath.Split(separator, StringSplitOptions.RemoveEmptyEntries); - var dataPathSegments = new List(); - var index = 0; - - // Process each segment to determine if it should be included in the data path - foreach (var segment in pathSegments) - { - // Always include segments that don't look like container segments - if (!IsContainerSegment(keySegments[index])) - { - dataPathSegments.Add(segment); - } - index++; - } + var dataPathSegments = pathSegments + .Where((segment, index) => !IsContainerSegment(keySegments[index])) + .ToList(); // Reconstruct the data path var dataPath = string.Join("->", dataPathSegments); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Reporting/DataGenerators/ReportingDataGenerator.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Reporting/DataGenerators/ReportingDataGenerator.cs index 916432a916..587507635c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Reporting/DataGenerators/ReportingDataGenerator.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Reporting/DataGenerators/ReportingDataGenerator.cs @@ -44,7 +44,7 @@ public class ReportingDataGenerator : ApplicationService, IReportingDataGenerato // Clean up the JSON strings foreach (var key in reportResult.Keys.ToList()) { - reportResult[key] = reportResult[key].Select(CleanJsonString).ToList(); + reportResult[key] = reportResult[key].Where(x => x != null).Select(CleanJsonString).ToList(); } // Sort the dictionary by keys alphabetically diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Unity.GrantManager.Application.csproj b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Unity.GrantManager.Application.csproj index dcd0c31780..d917f3c323 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Unity.GrantManager.Application.csproj +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Unity.GrantManager.Application.csproj @@ -5,49 +5,57 @@ net9.0 enable - Unity.GrantManager + Unity.GrantManager + + + + + - + - - + + + + - - - - - + + + + + + + - - - + + + - - + + + + - - - + - - + \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/GrantApplications/AIGenerationRequestKeyHelper.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/GrantApplications/AIGenerationRequestKeyHelper.cs new file mode 100644 index 0000000000..0492a5d6ad --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/GrantApplications/AIGenerationRequestKeyHelper.cs @@ -0,0 +1,22 @@ +using System; + +namespace Unity.GrantManager.GrantApplications; + +public static class AIGenerationRequestKeyHelper +{ + public const string AttachmentSummaryOperationType = "attachment-summary"; + public const string ApplicationAnalysisOperationType = "application-analysis"; + public const string ApplicationScoringOperationType = "application-scoring"; + public const string PipelineOperationType = "pipeline"; + + public static string BuildRequestKey(Guid? tenantId, Guid applicationId, string operationType) + { + var normalizedTenantId = tenantId?.ToString("D") ?? "host"; + + return string.Join( + ':', + normalizedTenantId, + applicationId.ToString("D"), + operationType.Trim().ToLowerInvariant()); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/GrantApplications/AIGenerationRequestStatus.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/GrantApplications/AIGenerationRequestStatus.cs new file mode 100644 index 0000000000..52cc71b874 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/GrantApplications/AIGenerationRequestStatus.cs @@ -0,0 +1,9 @@ +namespace Unity.GrantManager.GrantApplications; + +public enum AIGenerationRequestStatus +{ + Queued = 0, + Running = 1, + Completed = 2, + Failed = 3 +} 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 f6f2147a98..64379739bc 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 @@ -14,4 +14,5 @@ public static class DynamicUrlKeyNames 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"; + public const string ANALYTICS_MATOMO_BASE = "ANALYTICS_MATOMO_BASE"; } \ 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 560172e365..ce7fb443f2 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 @@ -220,6 +220,7 @@ "Unity.GrantManager.ApplicationManagement.Applicant.Summary": "Applicant Info", "Unity.GrantManager.ApplicationManagement.Applicant.Summary.Update": "Edit Applicant Info", "Unity.GrantManager.ApplicationManagement.Applicant.Contact": "Contact Info", + "Unity.GrantManager.ApplicationManagement.Applicant.Contact.Create": "Create Contact Info", "Unity.GrantManager.ApplicationManagement.Applicant.Contact.Update": "Edit Contact Info", "Unity.GrantManager.ApplicationManagement.Applicant.Authority": "Signing Authority", "Unity.GrantManager.ApplicationManagement.Applicant.Authority.Update": "Edit Signing Authority", @@ -474,34 +475,24 @@ "ApplicationContact:MobilePhone": "Mobile Phone Number", "ApplicationContact:WorkPhone": "Work Phone Number", "ApplicationContact:Delete": "Delete This Contact", - - "ApplicationBatchApprovalRequest:Title": "Approve Applications", - "ApplicationBatchApprovalRequest:SubmitButtonText": "Approve", - "ApplicationBatchApprovalRequest:CancelButtonText": "Cancel", - "ApplicationBatchApprovalRequest:DecisionDateDefaulted": "Decision Date has been defaulted", - "ApplicationBatchApprovalRequest:ApprovedAmountDefaulted": "Approved Amount has been defaulted", - "ApplicationBatchApprovalRequest:InvalidStatus": "The assessment for the selected item is not in the Assessment Completed state", - "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", - - "ApplicationLinks:Category": "Category", - "ApplicationLinks:ID": "ID", - "ApplicationLinks:Status": "Status", - "ApplicationLinks:LinkType": "Link Type", - - "ApplicantPortalSettings:Title": "Applicant Portal Configuration", - "ApplicantPortalSettings:ManageStatuses": "Manage Statuses", - "ApplicantPortalSettings:PortalStatusHeading": "Applicant Portal Status", - "ApplicantPortalSettings:InternalStatus": "Internal Status", - "ApplicantPortalSettings:PortalStatusLabel": "Portal Status Label", - "ApplicantPortalSettings:SaveChanges": "Save Changes", - "ApplicantPortalSettings:ResetChanges": "Reset", - "ApplicantPortalSettings:SaveSuccess": "Portal status labels updated successfully.", - "ApplicantPortalSettings:SaveError": "An error occurred while saving portal status labels.", - "ApplicantPortalSettings:ValidationRequired": "Portal status label cannot be empty.", - "ApplicantPortalSettings:NoChanges": "No changes to save.", - "ApplicantPortalSettings:ChangesReset": "Changes have been reset to original values." + "ApplicantContact:SetAsPrimary": "Set as Primary", + "ApplicantContact:PrimaryHint": "This contact is currently primary because it was auto-selected by most recent timestamp. Saving with Set as Primary checked will explicitly set it as primary.", + "ApplicantContacts:ApplicantNotFound": "Applicant information not found.", + "ApplicantContacts:PrimaryContact": "Primary Contact", + "ApplicantContacts:NoPrimaryContact": "No primary contact on record.", + "ApplicantContacts:ContactsTitle": "Contacts", + "ApplicantContacts:PrimaryExplicitTooltip": "Primary contact", + "ApplicantContacts:PrimaryInferredTooltip": "Primary contact (auto-selected by most recent timestamp; not explicitly set).", + "ApplicantContacts:SourceInfoApplication": "Sourced from the Application submission. Managed on the Application Details form and cannot be edited here.", + "ApplicantContacts:SourceInfoApplicantAgent": "Sourced from the Applicant Agent on the CHEFS submission. Captured at intake and cannot be edited here.", + "ApplicantContacts:SourceInfoGeneric": "Sourced from {0} and cannot be edited here.", + "ApplicantContacts:View": "View", + "ApplicantContacts:ContactSaved": "Contact saved.", + "ApplicantContacts:ContactSetPrimary": "Contact set as primary.", + "ApplicantContacts:ServiceUnavailable": "Applicant contact service is not available.", + "ApplicantContacts:SetPrimaryFailed": "Failed to set contact as primary.", + "ApplicantContacts:ColumnName": "Name", + "ApplicantContacts:ColumnPhone": "Phone (Work)", + "ApplicantContacts:ColumnSubmission": "Submission #" } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Contacts/ContactInput.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Contacts/ContactInput.cs new file mode 100644 index 0000000000..df4bf78334 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Contacts/ContactInput.cs @@ -0,0 +1,14 @@ +namespace Unity.GrantManager.Contacts; + +/// +/// Domain-layer input for creating or updating a . +/// Kept free of application-layer DTO dependencies so Domain remains self-contained. +/// +public record ContactInput( + string Name, + string? Title, + string? Email, + string? HomePhoneNumber, + string? MobilePhoneNumber, + string? WorkPhoneNumber, + string? WorkPhoneExtension); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Contacts/ContactManager.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Contacts/ContactManager.cs new file mode 100644 index 0000000000..8fb041ecfa --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Contacts/ContactManager.cs @@ -0,0 +1,162 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Domain.Services; + +namespace Unity.GrantManager.Contacts; + +/// +/// Domain service that owns / write invariants +/// (create, update, set-primary). Application services delegate here so they do not need to +/// inject each other. +/// +public class ContactManager( + IContactRepository contactRepository, + IContactLinkRepository contactLinkRepository) : DomainService, IContactManager +{ + /// + public virtual async Task<(Contact Contact, ContactLink Link)> CreateAsync( + string entityType, + Guid entityId, + ContactInput input, + string? role, + bool isPrimary) + { + ArgumentException.ThrowIfNullOrWhiteSpace(entityType); + ArgumentNullException.ThrowIfNull(input); + + var contact = await contactRepository.InsertAsync(new Contact + { + Name = input.Name, + Title = input.Title, + Email = input.Email, + HomePhoneNumber = input.HomePhoneNumber, + MobilePhoneNumber = input.MobilePhoneNumber, + WorkPhoneNumber = input.WorkPhoneNumber, + WorkPhoneExtension = input.WorkPhoneExtension + }, autoSave: true); + + if (isPrimary) + { + await ClearPrimaryAsync(entityType, entityId); + } + + var link = await contactLinkRepository.InsertAsync(new ContactLink + { + ContactId = contact.Id, + RelatedEntityType = entityType, + RelatedEntityId = entityId, + Role = role, + IsPrimary = isPrimary, + IsActive = true + }, autoSave: true); + + return (contact, link); + } + + /// + public virtual async Task<(Contact Contact, ContactLink Link)> UpdateAsync( + string entityType, + Guid entityId, + Guid contactId, + ContactInput input, + string? role, + bool isPrimary) + { + ArgumentException.ThrowIfNullOrWhiteSpace(entityType); + ArgumentNullException.ThrowIfNull(input); + + var contactLinksQuery = await contactLinkRepository.GetQueryableAsync(); + var links = await AsyncExecuter.ToListAsync(contactLinksQuery + .Where(l => l.RelatedEntityType == entityType + && l.RelatedEntityId == entityId + && l.IsActive)); + + var targetLink = links.FirstOrDefault(l => l.ContactId == contactId) + ?? throw new BusinessException("Contacts:ContactLinkNotFound") + .WithData("contactId", contactId) + .WithData("entityType", entityType) + .WithData("entityId", entityId); + + var contact = await contactRepository.GetAsync(contactId); + + contact.Name = input.Name; + contact.Title = input.Title; + contact.Email = input.Email; + contact.HomePhoneNumber = input.HomePhoneNumber; + contact.MobilePhoneNumber = input.MobilePhoneNumber; + contact.WorkPhoneNumber = input.WorkPhoneNumber; + contact.WorkPhoneExtension = input.WorkPhoneExtension; + + await contactRepository.UpdateAsync(contact); + + if (isPrimary) + { + foreach (var stale in links.Where(l => l.IsPrimary && l.ContactId != contactId)) + { + stale.IsPrimary = false; + await contactLinkRepository.UpdateAsync(stale); + } + } + + var linkChanged = false; + if (targetLink.IsPrimary != isPrimary) + { + targetLink.IsPrimary = isPrimary; + linkChanged = true; + } + if (role is not null && targetLink.Role != role) + { + targetLink.Role = role; + linkChanged = true; + } + if (linkChanged) + { + await contactLinkRepository.UpdateAsync(targetLink); + } + + return (contact, targetLink); + } + + /// + public virtual async Task SetPrimaryAsync(string entityType, Guid entityId, Guid contactId) + { + ArgumentException.ThrowIfNullOrWhiteSpace(entityType); + + await ClearPrimaryAsync(entityType, entityId); + + var contactLinksQuery = await contactLinkRepository.GetQueryableAsync(); + var link = await AsyncExecuter.FirstOrDefaultAsync(contactLinksQuery + .Where(l => l.RelatedEntityType == entityType + && l.RelatedEntityId == entityId + && l.ContactId == contactId + && l.IsActive)) + ?? throw new BusinessException("Contacts:ContactLinkNotFound") + .WithData("contactId", contactId) + .WithData("entityType", entityType) + .WithData("entityId", entityId); + + link.IsPrimary = true; + await contactLinkRepository.UpdateAsync(link, autoSave: true); + } + + /// + /// Clears the primary flag on all active contact links for the specified entity. + /// + private async Task ClearPrimaryAsync(string entityType, Guid entityId) + { + var contactLinksQuery = await contactLinkRepository.GetQueryableAsync(); + var currentPrimaryLinks = await AsyncExecuter.ToListAsync(contactLinksQuery + .Where(l => l.RelatedEntityType == entityType + && l.RelatedEntityId == entityId + && l.IsPrimary + && l.IsActive)); + + foreach (var existing in currentPrimaryLinks) + { + existing.IsPrimary = false; + await contactLinkRepository.UpdateAsync(existing, autoSave: true); + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Contacts/IContactManager.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Contacts/IContactManager.cs new file mode 100644 index 0000000000..cf68d3360d --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Contacts/IContactManager.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading.Tasks; + +namespace Unity.GrantManager.Contacts; + +/// +/// Domain service for managing and writes. +/// Centralises primary-contact invariants so application services can stay thin and do not +/// need to call each other across module boundaries. +/// +public interface IContactManager +{ + /// + /// Creates a new contact and links it to the specified related entity. + /// When is true, any existing primary link on the entity is cleared first. + /// + Task<(Contact Contact, ContactLink Link)> CreateAsync( + string entityType, + Guid entityId, + ContactInput input, + string? role, + bool isPrimary); + + /// + /// Updates an existing contact's fields and its link to the specified related entity. + /// When is true, any other primary link on the entity is cleared first. + /// is only applied when non-null, matching legacy behaviour. + /// + Task<(Contact Contact, ContactLink Link)> UpdateAsync( + string entityType, + Guid entityId, + Guid contactId, + ContactInput input, + string? role, + bool isPrimary); + + /// + /// Marks the specified contact as the primary contact for the given related entity, + /// clearing the primary flag on any other active links. + /// + Task SetPrimaryAsync(string entityType, Guid entityId, Guid contactId); +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/GrantApplications/AIGenerationRequest.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/GrantApplications/AIGenerationRequest.cs new file mode 100644 index 0000000000..7ab2d9a8e5 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/GrantApplications/AIGenerationRequest.cs @@ -0,0 +1,58 @@ +using System; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; + +namespace Unity.GrantManager.GrantApplications; + +public class AIGenerationRequest : FullAuditedAggregateRoot, IMultiTenant +{ + public Guid? TenantId { get; set; } + public Guid? ApplicationId { get; set; } + public string OperationType { get; set; } = string.Empty; + public string RequestKey { get; set; } = string.Empty; + public AIGenerationRequestStatus Status { get; set; } + public DateTime? StartedAt { get; set; } + public DateTime? CompletedAt { get; set; } + public string? FailureReason { get; set; } + public bool IsActive => Status is AIGenerationRequestStatus.Queued or AIGenerationRequestStatus.Running; + + protected AIGenerationRequest() + { + } + + public AIGenerationRequest( + Guid id, + Guid? tenantId, + string operationType, + Guid? applicationId, + string requestKey) + : base(id) + { + TenantId = tenantId; + OperationType = operationType; + ApplicationId = applicationId; + RequestKey = requestKey; + Status = AIGenerationRequestStatus.Queued; + } + + public void MarkRunning(DateTime startedAt) + { + Status = AIGenerationRequestStatus.Running; + StartedAt = startedAt; + FailureReason = null; + } + + public void MarkCompleted(DateTime completedAt) + { + Status = AIGenerationRequestStatus.Completed; + CompletedAt = completedAt; + FailureReason = null; + } + + public void MarkFailed(DateTime completedAt, string? failureReason) + { + Status = AIGenerationRequestStatus.Failed; + CompletedAt = completedAt; + FailureReason = failureReason; + } +} 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 ea707ca86d..f14f85f12a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlDataSeeder.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlDataSeeder.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading.Tasks; using Unity.GrantManager.Applications; using Volo.Abp.Data; @@ -31,6 +32,20 @@ public static class DynamicUrls 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"; public const string REPORTING_AI = $"{PROTOCOL}//reporting.grants.gov.bc.ca"; + public const string MATOMO_DEV_URL = $"{PROTOCOL}//dev-analytics-matomo.apps.silver.devops.gov.bc.ca"; + public const string MATOMO_TEST_URL = $"{PROTOCOL}//test-analytics-matomo.apps.silver.devops.gov.bc.ca"; + public const string MATOMO_PROD_URL = $"{PROTOCOL}//prod-analytics-matomo.apps.silver.devops.gov.bc.ca"; + } + + private static string GetMatomoUrl() + { + var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? string.Empty; + if (string.IsNullOrEmpty(env) || env.StartsWith("dev", StringComparison.OrdinalIgnoreCase)) + return DynamicUrls.MATOMO_DEV_URL; + if (env.StartsWith("test", StringComparison.OrdinalIgnoreCase) || + env.Equals("uat", StringComparison.OrdinalIgnoreCase)) + return DynamicUrls.MATOMO_TEST_URL; + return DynamicUrls.MATOMO_PROD_URL; } private async Task SeedDynamicUrlAsync() @@ -51,6 +66,7 @@ private async Task SeedDynamicUrlAsync() new() { KeyName = DynamicUrlKeyNames.NOTIFICATION_API_BASE, Url = DynamicUrls.CHES_PROD_URL, Description = "Common Hosted Email Service API" }, new() { KeyName = DynamicUrlKeyNames.REPORTING_AI, Url = DynamicUrls.REPORTING_AI, Description = "Reporting AI iFrame Source" }, new() { KeyName = DynamicUrlKeyNames.NOTIFICATION_AUTH, Url = DynamicUrls.CHES_PROD_AUTH, Description = "Common Hosted Email Service OAUTH" }, + new() { KeyName = DynamicUrlKeyNames.ANALYTICS_MATOMO_BASE, Url = GetMatomoUrl(), Description = "Matomo Analytics" }, 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}" }, @@ -66,6 +82,12 @@ private async Task SeedDynamicUrlAsync() { await DynamicUrlRepository.InsertAsync(dynamicUrl); } + else if (existing.KeyName == DynamicUrlKeyNames.ANALYTICS_MATOMO_BASE && + existing.Url != dynamicUrl.Url) + { + existing.Url = dynamicUrl.Url; + await DynamicUrlRepository.UpdateAsync(existing); + } } } } 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 205592bfbd..8e81c33850 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantManagerDbContext.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantManagerDbContext.cs @@ -2,8 +2,9 @@ using System.Linq; using Unity.AI.Domain; using Unity.AI.EntityFrameworkCore; -using Unity.GrantManager.Applicants; -using Unity.GrantManager.Locality; +using Unity.GrantManager.Applicants; +using Unity.GrantManager.GrantApplications; +using Unity.GrantManager.Locality; using Unity.GrantManager.Tokens; using Volo.Abp.AuditLogging.EntityFrameworkCore; using Volo.Abp.BackgroundJobs.EntityFrameworkCore; @@ -46,8 +47,9 @@ public class GrantManagerDbContext : public DbSet RegionalDistricts { get; set; } public DbSet TenantTokens { get; set; } public DbSet Communities { get; set; } - public DbSet InboxMessages { get; set; } - public DbSet OutboxMessages { get; set; } + public DbSet InboxMessages { get; set; } + public DbSet OutboxMessages { get; set; } + public DbSet AIGenerationRequests { get; set; } // Unity.AI entities public DbSet AIPrompts { get; set; } @@ -214,8 +216,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.HasIndex(x => new { x.Source, x.Status }); }); - modelBuilder.Entity(b => - { + modelBuilder.Entity(b => + { b.ToTable(GrantManagerConsts.DbTablePrefix + "OutboxMessages", GrantManagerConsts.DbSchema); @@ -233,8 +235,20 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .IsRequired() .HasConversion(new EnumToStringConverter()); - b.HasIndex(x => new { x.Source, x.Status }); - }); + b.HasIndex(x => new { x.Source, x.Status }); + }); + + modelBuilder.Entity(b => + { + b.ToTable(GrantManagerConsts.DbTablePrefix + "AIRequests", AIDbProperties.DbSchema); + b.ConfigureByConvention(); + b.Property(x => x.OperationType).IsRequired().HasMaxLength(64); + b.Property(x => x.RequestKey).IsRequired().HasMaxLength(256); + b.Property(x => x.FailureReason).HasMaxLength(2000); + b.Property(x => x.Status).IsRequired(); + b.HasIndex(x => x.RequestKey); + b.HasIndex(x => new { x.TenantId, x.ApplicationId, x.OperationType, x.Status }); + }); var allEntityTypes = modelBuilder.Model.GetEntityTypes(); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260415121500_AddAIRequestTable.Designer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260415121500_AddAIRequestTable.Designer.cs new file mode 100644 index 0000000000..cd817fc6b7 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260415121500_AddAIRequestTable.Designer.cs @@ -0,0 +1,619 @@ +// +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("20260415121500_AddAIRequestTable")] + partial class AddAIRequestTable + { + /// + 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("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.AI.Domain.AIPrompt", 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("Description") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp1") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.HasKey("Id"); + + b.ToTable("AIPrompts", "AI"); + }); + + modelBuilder.Entity("Unity.AI.Domain.AIPromptVersion", 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("IsDeprecated") + .HasColumnType("boolean"); + + b.Property("IsPublished") + .HasColumnType("boolean"); + + b.Property("MetadataJson") + .HasColumnType("jsonb"); + + b.Property("MaxTokens") + .HasColumnType("integer"); + + b.Property("Temperature") + .HasColumnType("double precision"); + + b.Property("PromptId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TargetModel") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TargetProvider") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("VersionNumber") + .HasColumnType("integer"); + + b.Property("SystemPrompt") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserPromptTemplate") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeveloperNotes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("PromptId", "VersionNumber") + .IsUnique(); + + b.ToTable("AIPromptVersions", "AI"); + }); + + modelBuilder.Entity("Unity.AI.Domain.AIPromptVersion", b => + { + b.HasOne("Unity.AI.Domain.AIPrompt", "Prompt") + .WithMany("Versions") + .HasForeignKey("PromptId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Prompt"); + }); + + modelBuilder.Entity("Unity.AI.Domain.AIPrompt", b => + { + b.Navigation("Versions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260415121500_AddAIRequestTable.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260415121500_AddAIRequestTable.cs new file mode 100644 index 0000000000..f2f4954571 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260415121500_AddAIRequestTable.cs @@ -0,0 +1,60 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Unity.GrantManager.Migrations.HostMigrations; + +public partial class AddAIRequestTable : Migration +{ + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AIRequests", + schema: "AI", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + TenantId = table.Column(type: "uuid", nullable: true), + ApplicationId = table.Column(type: "uuid", nullable: true), + OperationType = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + RequestKey = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Status = table.Column(type: "integer", nullable: false), + StartedAt = table.Column(type: "timestamp without time zone", nullable: true), + CompletedAt = table.Column(type: "timestamp without time zone", nullable: true), + FailureReason = table.Column(type: "character varying(2000)", maxLength: 2000, 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), + 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_AIRequests", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_AIRequests_TenantId_ApplicationId_OperationType_Status", + schema: "AI", + table: "AIRequests", + columns: new[] { "TenantId", "ApplicationId", "OperationType", "Status" }); + + migrationBuilder.CreateIndex( + name: "IX_AIRequests_RequestKey", + schema: "AI", + table: "AIRequests", + column: "RequestKey"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AIRequests", + schema: "AI"); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260428215651_InstallPgStatStatements.Designer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260428215651_InstallPgStatStatements.Designer.cs new file mode 100644 index 0000000000..cc02836724 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260428215651_InstallPgStatStatements.Designer.cs @@ -0,0 +1,3030 @@ +// +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("20260428215651_InstallPgStatStatements")] + partial class InstallPgStatStatements + { + /// + 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("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.AI.Domain.AIPrompt", 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("Description") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("AIPrompts", "AI"); + }); + + modelBuilder.Entity("Unity.AI.Domain.AIPromptVersion", 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("DeveloperNotes") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeprecated") + .HasColumnType("boolean"); + + b.Property("IsPublished") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MaxTokens") + .HasColumnType("integer"); + + b.Property("MetadataJson") + .HasColumnType("jsonb"); + + b.Property("PromptId") + .HasColumnType("uuid"); + + b.Property("SystemPrompt") + .IsRequired() + .HasColumnType("text"); + + b.Property("TargetModel") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TargetProvider") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Temperature") + .HasColumnType("double precision"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UserPromptTemplate") + .IsRequired() + .HasColumnType("text"); + + b.Property("VersionNumber") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PromptId", "VersionNumber") + .IsUnique(); + + b.ToTable("AIPromptVersions", "AI"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applicants.ApplicantTenantMap", 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("LastUpdated") + .HasColumnType("timestamp without time zone"); + + b.Property("OidcSubUsername") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TenantName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OidcSubUsername"); + + b.HasIndex("OidcSubUsername", "TenantId") + .IsUnique(); + + b.ToTable("ApplicantTenantMaps", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Integrations.CasClientCode", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClientCode") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)"); + + b.Property("ClientId") + .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("FinancialMinistry") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + 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("LastUpdatedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("MinistryPrefix") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)"); + + b.HasKey("Id"); + + b.ToTable("CasClientCodes", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Integrations.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("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("KeyName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + 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("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("DynamicUrls", (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.Messaging.InboxMessage", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DataType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Details") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + 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("MessageId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Payload") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ReceivedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("RetryCount") + .HasColumnType("integer"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("MessageId") + .IsUnique(); + + b.HasIndex("Source", "Status"); + + b.ToTable("InboxMessages", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Messaging.OutboxMessage", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AckStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DataType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Details") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + 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("MessageId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("OriginalMessageId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PublishedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("RetryCount") + .HasColumnType("integer"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Source", "Status"); + + b.ToTable("OutboxMessages", (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("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + 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("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + 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("ExtraProperties") + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IpAddresses") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + 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.AI.Domain.AIPromptVersion", b => + { + b.HasOne("Unity.AI.Domain.AIPrompt", "Prompt") + .WithMany("Versions") + .HasForeignKey("PromptId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Prompt"); + }); + + 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.AI.Domain.AIPrompt", b => + { + b.Navigation("Versions"); + }); + + 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/20260428215651_InstallPgStatStatements.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260428215651_InstallPgStatStatements.cs new file mode 100644 index 0000000000..0eac1eae2f --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260428215651_InstallPgStatStatements.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Unity.GrantManager.Migrations.HostMigrations +{ + /// + public partial class InstallPgStatStatements : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql("CREATE EXTENSION IF NOT EXISTS pg_stat_statements;"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + } + } +} 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 2faf68a4e6..8cb9d112f7 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 @@ -658,6 +658,92 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("ApplicantTenantMaps", (string)null); }); + modelBuilder.Entity("Unity.GrantManager.GrantApplications.AIGenerationRequest", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("CompletedAt") + .HasColumnType("timestamp without time zone"); + + 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("FailureReason") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + 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("OperationType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("RequestKey") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("StartedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("RequestKey"); + + b.HasIndex("TenantId", "ApplicationId", "OperationType", "Status"); + + b.ToTable("AIRequests", "AI"); + }); + modelBuilder.Entity("Unity.GrantManager.Integrations.CasClientCode", b => { b.Property("Id") diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260428225018_InstallPgStatStatements.Designer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260428225018_InstallPgStatStatements.Designer.cs new file mode 100644 index 0000000000..75af13f311 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260428225018_InstallPgStatStatements.Designer.cs @@ -0,0 +1,4946 @@ +// +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("20260428225018_InstallPgStatStatements")] + partial class InstallPgStatStatements + { + /// + 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("IsArchived") + .HasColumnType("boolean"); + + 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("Definition") + .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("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("AuditComments") + .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("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FiscalDay") + .HasColumnType("integer"); + + b.Property("FiscalMonth") + .HasColumnType("text"); + + b.Property("FundingHistoryComments") + .HasColumnType("text"); + + b.Property("IndigenousOrgInd") + .HasColumnType("text"); + + b.Property("IsDuplicated") + .HasColumnType("boolean"); + + b.Property("IssueTrackingComments") + .HasColumnType("text"); + + 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("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.ToTable("ApplicantAgents", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .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("ApplicantId"); + + b.ToTable("ApplicantAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AIAnalysis") + .HasColumnType("text"); + + b.Property("AIScoresheetAnswers") + .HasColumnType("jsonb"); + + b.Property("Acquisition") + .HasColumnType("text"); + + b.Property("ApplicantElectoralDistrict") + .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("DefaultSiteId") + .HasColumnType("uuid"); + + 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.Property("UnityApplicationId") + .HasColumnType("text"); + + 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("AISummary") + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ChefsFileId") + .HasColumnType("text"); + + b.Property("ChefsSubmissionId") + .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("AutomaticallyGenerateAIAnalysis") + .HasColumnType("boolean"); + + 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("DefaultPaymentGroup") + .HasColumnType("integer"); + + 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("FormHierarchy") + .HasColumnType("integer"); + + 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("ManuallyInitiateAIAnalysis") + .HasColumnType("boolean"); + + b.Property("ParentFormId") + .HasColumnType("uuid"); + + b.Property("Payable") + .HasColumnType("boolean"); + + b.Property("PaymentApprovalThreshold") + .HasColumnType("numeric"); + + b.Property("Prefix") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PreventPayment") + .HasColumnType("boolean"); + + b.Property("RenderFormIoToHtml") + .HasColumnType("boolean"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("SuffixType") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IntakeId"); + + b.HasIndex("ParentFormId"); + + 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.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.Applications.AuditHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("AuditDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AuditNote") + .HasColumnType("text"); + + b.Property("AuditTrackingNumber") + .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") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("AuditHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.FundingHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApprovedAmount") + .HasColumnType("numeric"); + + 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("FundingNotes") + .HasColumnType("text"); + + b.Property("FundingYear") + .HasColumnType("text"); + + b.Property("GrantCategory") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OneTimeConsideration") + .HasColumnType("numeric"); + + b.Property("ReconsiderationAmount") + .HasColumnType("numeric"); + + b.Property("RenewedFunding") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TotalGrantAmount") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("FundingHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.IssueTracking", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .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("IssueDescription") + .HasColumnType("text"); + + b.Property("IssueHeading") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ResolutionNote") + .HasColumnType("text"); + + b.Property("Resolved") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Year") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("IssueTrackings", (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("IsAiAssessment") + .HasColumnType("boolean"); + + 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.ApplicantComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .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("PinDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("CommenterId"); + + b.ToTable("ApplicantComments", (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("PinDateTime") + .HasColumnType("timestamp without time zone"); + + 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("PinDateTime") + .HasColumnType("timestamp without time zone"); + + 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.Contacts.Contact", 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("Email") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("HomePhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MobilePhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("WorkPhoneExtension") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("WorkPhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.ToTable("Contacts", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Contacts.ContactLink", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + 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("IsActive") + .HasColumnType("boolean"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("RelatedEntityId") + .HasColumnType("uuid"); + + b.Property("RelatedEntityType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Role") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("RelatedEntityType", "RelatedEntityId"); + + b.HasIndex("ContactId", "RelatedEntityType", "RelatedEntityId"); + + b.ToTable("ContactLinks", (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("ChesHttpStatusCode") + .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("PaymentRequestIds") + .IsRequired() + .HasColumnType("text"); + + 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.Emails.EmailLogAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + 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("EmailLogId") + .HasColumnType("uuid"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + 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("EmailLogId"); + + b.HasIndex("S3ObjectKey"); + + b.ToTable("EmailLogAttachments", "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("Description") + .HasMaxLength(35) + .HasColumnType("character varying(35)"); + + 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("FsbApNotified") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("FsbNotificationEmailLogId") + .HasColumnType("uuid"); + + b.Property("FsbNotificationSentDate") + .HasColumnType("timestamp without time zone"); + + 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("FsbNotificationEmailLogId"); + + 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.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("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.Reporting.Domain.Configuration.ReportColumnsMap", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + 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("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Mapping") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RoleStatus") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("ViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ViewStatus") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ReportColumnsMaps", "Reporting"); + }); + + 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.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAttachment", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) + .WithMany() + .HasForeignKey("ParentFormId") + .OnDelete(DeleteBehavior.NoAction); + }); + + 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("ApplicationLinks") + .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.Applications.AuditHistory", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.FundingHistory", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.IssueTracking", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + 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.ApplicantComment", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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.GrantManager.Contacts.ContactLink", b => + { + b.HasOne("Unity.GrantManager.Contacts.Contact", null) + .WithMany() + .HasForeignKey("ContactId") + .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.Emails.EmailLogAttachment", b => + { + b.HasOne("Unity.Notifications.Emails.EmailLog", null) + .WithMany() + .HasForeignKey("EmailLogId") + .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("ApplicationLinks"); + + 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/20260428225018_InstallPgStatStatements.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260428225018_InstallPgStatStatements.cs new file mode 100644 index 0000000000..0cd4206e9d --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260428225018_InstallPgStatStatements.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + /// + public partial class InstallPgStatStatements : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql("CREATE EXTENSION IF NOT EXISTS pg_stat_statements;"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicantRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicantRepository.cs index 886f68310a..7f16fec56a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicantRepository.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicantRepository.cs @@ -6,6 +6,7 @@ using Microsoft.EntityFrameworkCore; using Unity.GrantManager.Applications; using Unity.GrantManager.EntityFrameworkCore; +using Unity.Payments.Domain.Suppliers; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore; @@ -73,12 +74,36 @@ public async Task GetApplicantAutocompleteQueryAsync(string? appli .AsNoTracking() .ToListAsync(); - var filteredApplicants = applicants + var filtered = applicants .Where(a => (!string.IsNullOrEmpty(a.ApplicantName) && a.ApplicantName.Contains(searchQuery, StringComparison.OrdinalIgnoreCase)) || (!string.IsNullOrEmpty(a.UnityApplicantId) && a.UnityApplicantId.Contains(searchQuery, StringComparison.OrdinalIgnoreCase)) ) - .Select(a => new + .Take(10) + .ToList(); + + // Batch-fetch supplier data for the matched applicants + var supplierIds = filtered + .Where(a => a.SupplierId.HasValue) + .Select(a => a.SupplierId!.Value) + .Distinct() + .ToList(); + + Dictionary supplierMap = new(); + if (supplierIds.Count > 0) + { + var suppliers = await dbContext.Set() + .AsNoTracking() + .Where(s => supplierIds.Contains(s.Id) && !s.IsDeleted) + .Select(s => new { s.Id, s.Number, s.Name, s.Status }) + .ToListAsync(); + supplierMap = suppliers.ToDictionary(s => s.Id, s => (s.Number, s.Name, s.Status)); + } + + var filteredApplicants = filtered.Select(a => + { + supplierMap.TryGetValue(a.SupplierId ?? Guid.Empty, out var sup); + return new { a.Id, a.ApplicantName, @@ -97,10 +122,13 @@ public async Task GetApplicantAutocompleteQueryAsync(string? appli a.FiscalDay, a.FiscalMonth, a.UnityApplicantId, - a.IsDuplicated - }) - .Take(10) - .ToList(); + a.IsDuplicated, + SupplierId = a.SupplierId?.ToString(), + SupplierNumber = sup.Number, + SupplierName = sup.Name, + SupplierStatus = sup.Status + }; + }); var json = JsonSerializer.Serialize(filteredApplicants); return JsonDocument.Parse(json); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/ApplicantProfileController.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/ApplicantProfileController.cs index 127a2861ba..6fdf2c1c9f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/ApplicantProfileController.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/ApplicantProfileController.cs @@ -10,7 +10,7 @@ namespace Unity.GrantManager.Controllers [ApiController] [Route("api/app/applicant-profiles")] [ServiceFilter(typeof(ApiKeyAuthorizationFilter))] - public class ApplicantProfileController(IApplicantProfileAppService applicantProfileAppService) : AbpControllerBase + public class ApplicantProfileController(IApplicantProfileQueryService applicantProfileService) : AbpControllerBase { /// @@ -29,7 +29,7 @@ public class ApplicantProfileController(IApplicantProfileAppService applicantPro [ProducesResponseType(typeof(ApplicantProfileDto), StatusCodes.Status200OK)] public async Task GetApplicantProfileAsync([FromQuery] ApplicantProfileInfoRequest applicantProfileRequest) { - var profile = await applicantProfileAppService.GetApplicantProfileAsync(applicantProfileRequest); + var profile = await applicantProfileService.GetApplicantProfileAsync(applicantProfileRequest); return Ok(profile); } @@ -37,7 +37,7 @@ public async Task GetApplicantProfileAsync([FromQuery] ApplicantP [Route("tenants")] public async Task GetApplicantProfileTenantsAsync([FromQuery] ApplicantProfileRequest applicantProfileRequest) { - var tenants = await applicantProfileAppService.GetApplicantTenantsAsync(applicantProfileRequest); + var tenants = await applicantProfileService.GetApplicantTenantsAsync(applicantProfileRequest); return Ok(tenants); } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Analytics/MatomoUrlProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Analytics/MatomoUrlProvider.cs new file mode 100644 index 0000000000..ad42bb1083 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Analytics/MatomoUrlProvider.cs @@ -0,0 +1,38 @@ +using System.Threading.Tasks; +using Unity.GrantManager.Analytics; +using Unity.GrantManager.Integrations; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Features; +using Volo.Abp.MultiTenancy; + +namespace Unity.GrantManager.Web.Analytics; + +/// +/// Supplies the Matomo base URL from the DynamicUrl store, but only when the +/// Unity.Analytics feature flag is enabled for the current tenant. +/// In host context (no tenant, e.g. local dev) the feature gate is skipped +/// and the URL is returned directly. +/// +[Dependency(ReplaceServices = true)] +public class MatomoUrlProvider( + IEndpointManagementAppService endpointManagementAppService, + IFeatureChecker featureChecker, + ICurrentTenant currentTenant) + : IAnalyticsUrlProvider, ITransientDependency +{ + public async Task GetMatomoUrlAsync() + { + try + { + // Feature flag is a per-tenant toggle; skip check in host context (local dev) + if (currentTenant.Id != null && !await featureChecker.IsEnabledAsync("Unity.Analytics")) + return string.Empty; + + return await endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.ANALYTICS_MATOMO_BASE); + } + catch + { + return string.Empty; + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebAutoMapperProfile.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebAutoMapperProfile.cs new file mode 100644 index 0000000000..e38ed515e5 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebAutoMapperProfile.cs @@ -0,0 +1,17 @@ +using AutoMapper; +using Unity.GrantManager.ApplicantProfile; +using Unity.GrantManager.ApplicantProfile.ProfileData; +using Unity.GrantManager.Web.Pages.ApplicantContact; + +namespace Unity.GrantManager.Web; + +public class GrantManagerWebAutoMapperProfile : Profile +{ + public GrantManagerWebAutoMapperProfile() + { + CreateMap() + .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.ContactId)); + + CreateMap(); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebModule.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebModule.cs index ce8c47bbf6..4c73bc85e3 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebModule.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebModule.cs @@ -302,7 +302,7 @@ private static void ConfigureAuthentication(ServiceConfigurationContext context, options.ClientId = configuration["AuthServer:ClientId"]; options.ClientSecret = configuration["AuthServer:ClientSecret"]; - options.SaveTokens = true; + options.SaveTokens = false; options.GetClaimsFromUserInfoEndpoint = true; options.MaxAge = TimeSpan.FromHours(8); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/IdentityProfileLoginHandler.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/IdentityProfileLoginHandler.cs index 28762372d1..bf5cf48de0 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/IdentityProfileLoginHandler.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/IdentityProfileLoginHandler.cs @@ -61,12 +61,9 @@ await securityLogManager.SaveAsync(securityLog => private static void AddTenantClaims(ClaimsPrincipal claimsPrincipal, IList userTenantAccounts) { - foreach (var tenantAcc in userTenantAccounts) + if (userTenantAccounts.Count > 1) { - if (tenantAcc != null && tenantAcc.TenantId != null) - { - claimsPrincipal.AddClaim(UnityClaimsTypes.Tenant, tenantAcc.TenantId.ToString() ?? Guid.Empty.ToString()); - } + claimsPrincipal.AddClaim(UnityClaimsTypes.HasMultipleTenants, "true"); } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/UnityClaimsTypes.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/UnityClaimsTypes.cs index 804c618861..560f187f2a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/UnityClaimsTypes.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/UnityClaimsTypes.cs @@ -17,4 +17,5 @@ public static class UnityClaimsTypes public const string Permission = "Permission"; public const string IdpProvider = "identity_provider"; public const string Tenant = "tenant"; + public const string HasMultipleTenants = "has_multiple_tenants"; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantContact/ApplicantContactModalViewModel.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantContact/ApplicantContactModalViewModel.cs new file mode 100644 index 0000000000..4d9c66fada --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantContact/ApplicantContactModalViewModel.cs @@ -0,0 +1,72 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Unity.GrantManager.ApplicantProfile; + +namespace Unity.GrantManager.Web.Pages.ApplicantContact; + +public class ApplicantContactModalViewModel +{ + [HiddenInput] + public Guid ApplicantId { get; set; } + + [HiddenInput] + public Guid Id { get; set; } + + [HiddenInput] + public bool IsPrimaryInferred { get; set; } + + [DisplayName("ApplicationContact:Type")] + [Required] + [StringLength(100)] + public string Role { get; set; } = string.Empty; + + public List RoleOptions { get; set; } = CreateRoleOptions(); + + [DisplayName("ApplicationContact:FullName")] + [Required] + [MinLength(2)] + [StringLength(250)] + public string Name { get; set; } = string.Empty; + + [DisplayName("ApplicationContact:Title")] + [StringLength(200)] + public string? Title { get; set; } + + [DisplayName("ApplicationContact:Email")] + [EmailAddress] + [StringLength(200)] + public string? Email { get; set; } + + [DisplayName("ApplicationContact:MobilePhone")] + [StringLength(50)] + [RegularExpression(@"^[\+]?[0-9\-\.\(\)\s]*$", ErrorMessage = "Enter a valid phone number")] + public string? MobilePhoneNumber { get; set; } + + [DisplayName("ApplicationContact:WorkPhone")] + [StringLength(50)] + [RegularExpression(@"^[\+]?[0-9\-\.\(\)\s]*$", ErrorMessage = "Enter a valid phone number")] + public string? WorkPhoneNumber { get; set; } + + [DisplayName("ApplicantContact:SetAsPrimary")] + public bool IsPrimary { get; set; } + + public void EnsureRoleOptions() + { + if (RoleOptions is null || RoleOptions.Count == 0) + { + RoleOptions = CreateRoleOptions(); + } + } + + public static List CreateRoleOptions() + { + return ApplicantContactRoleOptions.Options + .Select(option => new SelectListItem { Value = option.Value, Text = option.Label }) + .ToList(); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantContact/EditModal.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantContact/EditModal.cshtml new file mode 100644 index 0000000000..feb60c2bde --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantContact/EditModal.cshtml @@ -0,0 +1,45 @@ +@page +@using Microsoft.Extensions.Localization +@using Unity.GrantManager.Localization +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal + +@model Unity.GrantManager.Web.Pages.ApplicantContact.EditModal + +@inject IStringLocalizer L +@{ + Layout = null; +} + +
+ + + +
+ + + + + + + + + + +
+
+ +
+
+ + @if (Model.ContactForm!.IsPrimary && Model.ContactForm.IsPrimaryInferred) + { +
@L["ApplicantContact:PrimaryHint"].Value
+ } +
+
+ + + + +
+
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantContact/EditModal.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantContact/EditModal.cshtml.cs new file mode 100644 index 0000000000..a9622bf3b9 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantContact/EditModal.cshtml.cs @@ -0,0 +1,59 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Linq; +using System.Threading.Tasks; +using Unity.GrantManager.ApplicantProfile; +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; + +namespace Unity.GrantManager.Web.Pages.ApplicantContact; + +public class EditModal : AbpPageModel +{ + private readonly IApplicantContactAppService _applicantContactAppService; + + [BindProperty] + public ApplicantContactModalViewModel? ContactForm { get; set; } + + public EditModal(IApplicantContactAppService applicantContactAppService) + { + _applicantContactAppService = applicantContactAppService; + } + + public async Task OnGetAsync(Guid id, Guid applicantId) + { + var contactInfo = await _applicantContactAppService.GetByApplicantIdAsync(applicantId); + var contact = contactInfo.Contacts.FirstOrDefault(c => c.ContactId == id); + + if (contact is null || !contact.IsEditable) + { + return NotFound(); + } + + ContactForm = ObjectMapper.Map(contact); + ContactForm.ApplicantId = applicantId; + ContactForm.Id = contact.ContactId; + ContactForm.EnsureRoleOptions(); + + return Page(); + } + + public async Task OnPostAsync() + { + if (ContactForm is null) + { + return BadRequest(); + } + + ContactForm.EnsureRoleOptions(); + + if (!ModelState.IsValid) + { + return Page(); + } + + var updateDto = ObjectMapper.Map(ContactForm); + await _applicantContactAppService.UpdateAsync(ContactForm.ApplicantId, ContactForm.Id, updateDto); + + return NoContent(); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.js index 44624dc76a..f17597dc1a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.js @@ -1,6 +1,7 @@ $(function () { const menuItems = $('#ConfigurationManagementSideMenu .nav-item'); const configSections = $('.config-section'); + const ACTIVE_MENU_KEY = 'ConfigurationManagement_ActiveMenu'; init(); @@ -12,11 +13,13 @@ adjustDataTables(); }); - // Auto-select the first visible menu item - const firstMenuItem = menuItems.first(); - if (firstMenuItem.length) { - firstMenuItem.addClass('active'); - const targetId = firstMenuItem.data('target'); + // Restore the last active menu item from localStorage, fallback to first + const savedMenuId = localStorage.getItem(ACTIVE_MENU_KEY); + const savedMenuItem = savedMenuId ? menuItems.filter('#' + savedMenuId) : $(); + const activeMenuItem = (savedMenuItem.length ? savedMenuItem : menuItems.first()); + if (activeMenuItem.length) { + activeMenuItem.addClass('active'); + const targetId = activeMenuItem.data('target'); $('#' + targetId).removeClass('hide'); } @@ -39,6 +42,9 @@ const clickedItem = $(e.currentTarget); const targetId = clickedItem.data('target'); + // Persist selection + localStorage.setItem(ACTIVE_MENU_KEY, clickedItem.attr('id')); + // Update active menu item menuItems.removeClass('active'); clickedItem.addClass('active'); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml index 320ea89124..294997e161 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml @@ -49,18 +49,31 @@ && tenantManualEnabled && formManualEnabled && await PermissionChecker.IsGrantedAsync(AIPermissions.Analysis.GenerateApplicationAnalysis); + + var aiAttachmentSummariesEnabled = await FeatureChecker.IsEnabledAsync("Unity.AI.AttachmentSummaries") + && tenantManualEnabled + && formManualEnabled + && await PermissionChecker.IsGrantedAsync(AIPermissions.Analysis.GenerateAttachmentSummaries); + + var aiScoringEnabled = await FeatureChecker.IsEnabledAsync("Unity.AI.Scoring") + && tenantManualEnabled + && formManualEnabled + && await PermissionChecker.IsGrantedAsync(AIPermissions.Analysis.GenerateScoring); } -@section styles -{ - - -} -@section scripts -{ - - - -} +@section styles +{ + + +} +@section scripts +{ + + + + + + +}
@await Component.InvokeAsync("ApplicationBreadcrumbWidget", new { applicationId = @Model.ApplicationId }) @@ -428,7 +441,7 @@
Application Analysis
@if (aiApplicationAnalysisGenerateEnabled) { - + @if (aiAttachmentSummariesEnabled && aiApplicationAnalysisEnabled && aiScoringEnabled) + { + + }
@@ -544,7 +560,7 @@
Attachment Summary
- + +
`; + } + + function setAsPrimary(contact) { + const service = unity?.grantManager?.applicantProfile?.applicantContact; + if (!service) { + abp.notify.error(t('serviceUnavailable', 'Applicant contact service is not available.')); + return; + } + service.setPrimary(applicantId, contact.contactId) + .done(function () { + abp.notify.success(t('contactSetPrimary', 'Contact set as primary.')); + refreshWidget(); + }) + .fail(function () { + abp.notify.error(t('setPrimaryFailed', 'Failed to set contact as primary.')); + }); + } + + function initializeContactsTable(order) { + if (!$.fn.DataTable || !$('#ApplicantContactsTable').length) { return null; } - return $(selector).DataTable( + if ($.fn.DataTable.isDataTable('#ApplicantContactsTable')) { + $('#ApplicantContactsTable').DataTable().destroy(); + } + + return $('#ApplicantContactsTable').DataTable( abp.libs.datatables.normalizeConfiguration({ - data: data, + data: contactsData, serverSide: false, - order: [[0, 'asc']], + order: order || [[0, 'asc']], searching: true, paging: true, pageLength: 10, select: false, info: true, + processing: true, scrollX: true, + stateSave: true, + stateDuration: 0, + stateSaveCallback: function (settings, data) { + try { + localStorage.setItem( + 'DataTables_ApplicantContactsTable_' + (applicantId || 'none'), + JSON.stringify(data)); + } catch (e) { console.error('Failed to save DataTables state to localStorage.', e); } + }, + stateLoadCallback: function () { + try { + const raw = localStorage.getItem( + 'DataTables_ApplicantContactsTable_' + (applicantId || 'none')); + return raw ? JSON.parse(raw) : null; + } catch (e) { console.error('Failed to load DataTables state from localStorage.', e); return null; } + }, drawCallback: function () { this.api().columns.adjust(); + $('#ApplicantContactsTable [data-bs-toggle="tooltip"]').each(function () { + const existing = bootstrap.Tooltip.getInstance(this); + if (existing) { existing.dispose(); } + bootstrap.Tooltip.getOrCreateInstance(this); + }); }, - ...extraConfig, - columnDefs: columnDefs + lengthMenu: [[10, 25, 50], [10, 25, 50]], + columnDefs: [ + { + title: t('columnName', 'Name'), + data: 'name', + width: '18%', + render: (data, type, row) => { + const name = data || t('nullPlaceholder', '—'); + if (type !== 'display') { + return name; + } + return renderPrimaryBadge(row) + escapeHtml(name); + }, + targets: 0 + }, + { + title: t('columnType', 'Type'), + data: 'role', + width: '13%', + render: (data, type) => renderEscapedText(roleLabelMap[data] || data || t('nullPlaceholder', '—'), type), + targets: 1 + }, + { + title: t('columnEmail', 'Email'), + data: 'email', + width: '22%', + render: (data, type) => renderEscapedText(data || t('nullPlaceholder', '—'), type), + targets: 2 + }, + { + title: t('columnPhone', 'Phone'), + data: null, + width: '13%', + render: (data, type, row) => { + const phone = row.workPhoneNumber || row.mobilePhoneNumber || t('nullPlaceholder', '—'); + return renderEscapedText(phone, type); + }, + targets: 3 + }, + { + title: t('columnTitle', 'Title'), + data: 'title', + width: '18%', + render: (data, type) => renderEscapedText(data || t('nullPlaceholder', '—'), type), + targets: 4 + }, + { + title: t('columnSubmission', 'Submission #'), + data: 'referenceNo', + width: '10%', + render: renderReferenceLink, + targets: 5 + }, + { + title: t('columnActions', ''), + data: null, + orderable: false, + searchable: false, + width: '48px', + className: 'text-center', + render: renderActions, + targets: 6 + } + ] }) ); } @@ -46,114 +288,56 @@ $(function () { }); } - contactsTable = initializeContactsTable( - '#ApplicantContactsTable', - contactsData, - [ - { - title: 'Name', - data: 'name', - width: '18%', - render: (data) => data || nullPlaceholder - }, - { - title: 'Email', - data: 'email', - width: '22%', - render: (data) => data || nullPlaceholder - }, - { - title: 'Phone', - data: 'phone', - width: '13%', - render: (data) => data || nullPlaceholder - }, - { - title: 'Title', - data: 'title', - width: '17%', - render: (data) => data || nullPlaceholder - }, - { - title: 'Type', - data: 'type', - width: '10%', - render: (data) => data || nullPlaceholder - }, - { - title: 'Submission #', - data: 'referenceNo', - width: '15%', - render: (data, type, row) => renderTableLink(data, row) - } - ], - { - lengthMenu: [[10, 25, 50], [10, 25, 50]] + function refreshWidget() { + if (contactsTable) { + try { + savedOrder = contactsTable.order(); + contactsTable.processing(true); + } catch (e) { console.error('Failed to enable DataTables processing indicator.', e); } } - ); - - scheduleLayoutNotifications(); - - const form = $('#ApplicantContactsForm'); - const saveButton = $('#saveApplicantContactsBtn'); - if (form.length && saveButton.length && typeof UnityZoneForm === 'function') { - zoneForm = new UnityZoneForm(form, { - saveButtonSelector: '#saveApplicantContactsBtn' - }); - - zoneForm.init(); - - saveButton.on('click', function (event) { - event.preventDefault(); - - if (!zoneForm || zoneForm.modifiedFields.size === 0) { - return; - } - - const applicantId = $('#ApplicantContacts_ApplicantId').val(); - if (!applicantId) { - abp.notify.warn('Applicant identifier is missing.'); - return; + $.ajax({ + url: abp.appPath + 'Widget/ApplicantContacts/Refresh', + type: 'GET', + dataType: 'html', + data: { applicantId: applicantId }, + success: function (html) { + const container = widgetRoot.parent(); + container.html(html); + bindWidget(savedOrder); + savedOrder = null; + abp.event.trigger('applicant-contacts-refreshed'); + }, + error: function () { + if (contactsTable) { + try { contactsTable.processing(false); } catch (e) { console.error('Failed to disable DataTables processing indicator.', e); } + } } + }); + } - const modifiedFields = Array.from(zoneForm.modifiedFields ?? []); - const contactDirty = modifiedFields.some((field) => field.startsWith('PrimaryContact.')); + function bindWidget(order) { + readWidgetState(); + contactsTable = initializeContactsTable(order); + scheduleLayoutNotifications(); + } - if (!contactDirty) { - return; - } + $(document).on('click', '.applicant-contact-edit-btn', function () { + const id = $(this).data('contact-id'); + ensureEditContactModal().open({ + id: id, + applicantId: applicantId + }); + }); - const contactId = $('#ApplicantContacts_PrimaryContactId').val(); - if (isGuidEmpty(contactId)) { - return; - } + $(document).on('click', '.applicant-contact-set-primary-btn', function () { + const id = $(this).data('contact-id'); + const contact = contactsData.find((c) => c.contactId === id); + if (!contact) return; + setAsPrimary(contact); + }); - const payload = { - primaryContact: { - id: contactId, - fullName: form.find('[name="PrimaryContact.FullName"]').val(), - title: form.find('[name="PrimaryContact.Title"]').val(), - email: form.find('[name="PrimaryContact.Email"]').val(), - businessPhone: form.find('[name="PrimaryContact.BusinessPhone"]').val(), - cellPhone: form.find('[name="PrimaryContact.CellPhone"]').val() - } - }; - - zoneForm.setSaving(true); - unity.grantManager.applicants.applicant - .updateApplicantContactAddresses(applicantId, payload) - .done(function () { - abp.notify.success('Contact updated.'); - zoneForm.resetTracking(); - updateContactTableAfterSave(payload.primaryContact, contactsTable); - }) - .fail(function () { - abp.notify.error('Failed to update contact.'); - zoneForm.setSaving(false); - }); - }); - } + bindWidget(); }); function safeParse(value) { @@ -165,29 +349,20 @@ function safeParse(value) { } } -function notifyApplicantContactsLayoutChange() { - globalThis.dispatchEvent(new CustomEvent('applicant-contacts-layout-changed')); -} - -function isGuidEmpty(value) { - return !value || value === '00000000-0000-0000-0000-000000000000'; -} - -function updateContactTableAfterSave(contactPayload, contactsDt) { - if (!contactsDt || !contactPayload) { - return; +function toCamelCase(obj) { + if (obj === null || typeof obj !== 'object') { + return obj; } - - contactsDt.rows().every(function () { - const rowData = this.data(); - if (rowData.id === contactPayload.id) { - rowData.name = contactPayload.fullName || ''; - rowData.email = contactPayload.email || ''; - rowData.phone = contactPayload.businessPhone || contactPayload.cellPhone || ''; - rowData.title = contactPayload.title || ''; - this.data(rowData); - } + const result = {}; + Object.keys(obj).forEach(function (key) { + const camelKey = key.length > 0 + ? key.charAt(0).toLowerCase() + key.slice(1) + : key; + result[camelKey] = obj[key]; }); + return result; +} - contactsDt.rows().invalidate().draw(false); +function notifyApplicantContactsLayoutChange() { + globalThis.dispatchEvent(new CustomEvent('applicant-contacts-layout-changed')); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.cshtml index 960f806cc0..5b0dba2458 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.cshtml @@ -629,6 +629,21 @@ + + Supplier + + + + + + +
@@ -646,6 +661,8 @@ The other record will not be deleted; instead, it will be flagged as a duplicate and can be removed from the Applicant list in a separate process.

Note: The address and contact information for any affected applications will be preserved and remain untouched.

+

Note: All default sites associated with submissions linked to the merged applicant will be removed and must be re‑set after the merge is completed.

+
Warning: One or more affected submissions have in‑progress payments. These payments will be impacted by the merge. Please complete final approval of all payments before proceeding.

Are you sure?