diff --git a/.gitignore b/.gitignore index cf56f6bd31..8cae5022ff 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ bld/ # Visual Studio cache/options directory .vs/ .vscode/ +*.lscache # Uncomment if you have tasks that create the project's static files in wwwroot **/wwwroot/lib/ diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/IAIGenerationPrerequisiteValidator.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/IAIGenerationPrerequisiteValidator.cs new file mode 100644 index 0000000000..67df324548 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/IAIGenerationPrerequisiteValidator.cs @@ -0,0 +1,13 @@ +using System; +using System.Threading.Tasks; + +namespace Unity.AI.Operations; + +public interface IAIGenerationPrerequisiteValidator +{ + Task EnsureAttachmentSummaryAvailableAsync(Guid applicationId); + + Task EnsureApplicationAnalysisAvailableAsync(Guid applicationId); + + Task EnsureApplicationScoringAvailableAsync(Guid applicationId); +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AIGenerationPrerequisiteValidator.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AIGenerationPrerequisiteValidator.cs new file mode 100644 index 0000000000..50b4217195 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AIGenerationPrerequisiteValidator.cs @@ -0,0 +1,57 @@ +using Microsoft.Extensions.Localization; +using System; +using System.Linq; +using System.Threading.Tasks; +using Unity.Flex.Domain.Scoresheets; +using Unity.AI.Localization; +using Unity.GrantManager.Applications; +using Volo.Abp; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Linq; + +namespace Unity.AI.Operations; + +public class AIGenerationPrerequisiteValidator( + IApplicationRepository applicationRepository, + IApplicationFormRepository applicationFormRepository, + IApplicationFormSubmissionRepository applicationFormSubmissionRepository, + IApplicationChefsFileAttachmentRepository applicationChefsFileAttachmentRepository, + IScoresheetRepository scoresheetRepository, + IAsyncQueryableExecuter asyncExecuter, + IStringLocalizer localizer) : IAIGenerationPrerequisiteValidator, ITransientDependency +{ + public async Task EnsureAttachmentSummaryAvailableAsync(Guid applicationId) + { + var attachmentQuery = await applicationChefsFileAttachmentRepository.GetQueryableAsync(); + var hasAttachments = await asyncExecuter.AnyAsync(attachmentQuery.Where(a => a.ApplicationId == applicationId)); + if (!hasAttachments) + { + throw new UserFriendlyException(localizer[AILocalizationKeys.NoAttachmentsAvailable]); + } + } + + public async Task EnsureApplicationAnalysisAvailableAsync(Guid applicationId) + { + var submission = await applicationFormSubmissionRepository.GetByApplicationAsync(applicationId); + if (submission == null || string.IsNullOrWhiteSpace(submission.Submission)) + { + throw new UserFriendlyException(localizer[AILocalizationKeys.ApplicationAnalysisRequiresSubmission]); + } + } + + public async Task EnsureApplicationScoringAvailableAsync(Guid applicationId) + { + var application = await applicationRepository.GetAsync(applicationId); + var applicationForm = await applicationFormRepository.GetAsync(application.ApplicationFormId); + if (applicationForm.ScoresheetId == null) + { + throw new UserFriendlyException(localizer[AILocalizationKeys.ScoringRequiresScoresheet]); + } + + var scoresheet = await scoresheetRepository.GetWithChildrenAsync(applicationForm.ScoresheetId.Value); + if (scoresheet == null || !scoresheet.Sections.Any() || !scoresheet.Sections.SelectMany(s => s.Fields).Any()) + { + throw new UserFriendlyException(localizer[AILocalizationKeys.ScoringRequiresScoresheetFields]); + } + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationAnalysisService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationAnalysisService.cs index 12b9b24342..1d56edc349 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationAnalysisService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationAnalysisService.cs @@ -1,12 +1,8 @@ using Microsoft.Extensions.Logging; -using Newtonsoft.Json.Linq; using System; -using System.Collections.Generic; -using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Unity.AI.Models; using Unity.AI.Prompts; using Unity.AI.Requests; using Unity.AI.Runtime; @@ -21,35 +17,23 @@ public class ApplicationAnalysisService( IApplicationFormVersionRepository applicationFormVersionRepository, IApplicationChefsFileAttachmentRepository applicationChefsFileAttachmentRepository, IAIService aiService, + IAIGenerationPrerequisiteValidator aiGenerationPrerequisiteValidator, ILogger logger) : IApplicationAnalysisService, ITransientDependency { - private const string ComponentsKey = "components"; - private static readonly HashSet ExcludedSchemaKeys = new(StringComparer.OrdinalIgnoreCase) - { - "applicantAgent" - }; - public async Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null, CancellationToken cancellationToken = default) { + await aiGenerationPrerequisiteValidator.EnsureApplicationAnalysisAvailableAsync(applicationId); + var application = await applicationRepository.GetAsync(applicationId); var formSubmission = await applicationFormSubmissionRepository.GetByApplicationAsync(applicationId); var attachments = await applicationChefsFileAttachmentRepository.GetListAsync(a => a.ApplicationId == applicationId); var formSchema = await GetFormSchemaAsync(formSubmission?.ApplicationFormVersionId); - var attachmentSummaries = attachments - .Where(a => !string.IsNullOrWhiteSpace(a.AISummary)) - .Select(a => new AIAttachmentItem - { - Name = string.IsNullOrWhiteSpace(a.FileName) ? "attachment" : a.FileName.Trim(), - Summary = a.AISummary!.Trim() - }) - .ToList(); - - object formFieldConfiguration = new { message = "Form configuration not available." }; - if (formSubmission?.ApplicationFormVersionId != null) - { - formFieldConfiguration = await ExtractFormFieldConfigurationAsync(formSubmission.ApplicationFormVersionId.Value); - } + var attachmentSummaries = PromptDataPayloadBuilder.BuildAttachmentSummaries(attachments); + var formFieldConfiguration = await PromptDataPayloadBuilder.BuildFormFieldConfigurationAsync( + applicationFormVersionRepository, + formSubmission?.ApplicationFormVersionId, + logger); var analysis = await aiService.GenerateApplicationAnalysisAsync(new ApplicationAnalysisRequest { @@ -84,115 +68,5 @@ public async Task RegenerateAndSaveAsync(Guid applicationId, string? pro } } - private async Task ExtractFormFieldConfigurationAsync(Guid formVersionId) - { - try - { - var formVersion = await applicationFormVersionRepository.GetAsync(formVersionId); - if (formVersion == null || string.IsNullOrEmpty(formVersion.FormSchema)) - { - return new { message = "Form configuration not available." }; - } - - var schema = JObject.Parse(formVersion.FormSchema); - var components = schema[ComponentsKey] as JArray; - if (components == null || components.Count == 0) - { - return new { message = "No form fields configured." }; - } - - var requiredFields = new List(); - var optionalFields = new List(); - ExtractFieldRequirements(components, requiredFields, optionalFields, string.Empty); - - return new - { - required_fields = requiredFields, - optional_fields = optionalFields - }; - } - catch (Exception ex) - { - logger.LogError(ex, "Error extracting form field configuration for form version {FormVersionId}", formVersionId); - return new { message = "Form configuration could not be extracted." }; - } - } - - private static void ExtractFieldRequirements(JArray components, List requiredFields, List optionalFields, string currentPath) - { - foreach (var component in components.OfType()) - { - var key = component["key"]?.ToString(); - var label = component["label"]?.ToString(); - var type = component["type"]?.ToString(); - var skipTypes = new HashSet { "button", "simplebuttonadvanced", "html", "htmlelement", "content", "simpleseparator" }; - - if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(type) || skipTypes.Contains(type) || ExcludedSchemaKeys.Contains(key)) - { - ProcessNestedFieldRequirements(component, type, requiredFields, optionalFields, currentPath); - continue; - } - - var displayName = !string.IsNullOrEmpty(label) ? $"{label} ({key})" : key; - var fullPath = string.IsNullOrEmpty(currentPath) ? displayName : $"{currentPath} > {displayName}"; - var validate = component["validate"] as JObject; - var isRequired = validate?["required"]?.Value() ?? false; - - if (component["input"]?.Value() == true) - { - if (isRequired) requiredFields.Add(fullPath); - else optionalFields.Add(fullPath); - } - - ProcessNestedFieldRequirements(component, type, requiredFields, optionalFields, fullPath); - } - } - - private static void ProcessNestedFieldRequirements(JObject component, string? type, List requiredFields, List optionalFields, string currentPath) - { - switch (type) - { - case "panel": - case "simplepanel": - case "fieldset": - case "well": - case "container": - case "datagrid": - case "table": - if (component[ComponentsKey] is JArray nestedComponents) - { - ExtractFieldRequirements(nestedComponents, requiredFields, optionalFields, currentPath); - } - break; - case "columns": - case "simplecols2": - case "simplecols3": - case "simplecols4": - if (component["columns"] is JArray columns) - { - foreach (var column in columns.OfType()) - { - if (column[ComponentsKey] is JArray columnComponents) - { - ExtractFieldRequirements(columnComponents, requiredFields, optionalFields, currentPath); - } - } - } - break; - case "tabs": - case "simpletabs": - if (component[ComponentsKey] is JArray tabs) - { - foreach (var tab in tabs.OfType()) - { - if (tab[ComponentsKey] is JArray tabComponents) - { - ExtractFieldRequirements(tabComponents, requiredFields, optionalFields, currentPath); - } - } - } - break; - } - } } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationScoringService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationScoringService.cs index f6188f689b..3a4ee6f7d2 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationScoringService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationScoringService.cs @@ -10,8 +10,11 @@ using Unity.AI.Prompts; using Unity.AI.Requests; using Unity.AI.Runtime; +using Unity.AI.Localization; using Unity.GrantManager.Applications; +using Volo.Abp; using Volo.Abp.DependencyInjection; +using Microsoft.Extensions.Localization; namespace Unity.AI.Operations { @@ -24,7 +27,8 @@ public class ApplicationScoringService( IScoresheetRepository scoresheetRepository, IAIService aiService, AIExecutionModeResolver executionModeResolver, - ILogger logger) : IApplicationScoringService, ITransientDependency + ILogger logger, + IStringLocalizer localizer) : IApplicationScoringService, ITransientDependency { public async Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null, CancellationToken cancellationToken = default) { @@ -32,24 +36,19 @@ public async Task RegenerateAndSaveAsync(Guid applicationId, string? pro var applicationForm = await applicationFormRepository.GetAsync(application.ApplicationFormId); if (applicationForm.ScoresheetId == null) { - return "{}"; + throw new UserFriendlyException(localizer[AILocalizationKeys.ScoringRequiresScoresheet]); } var scoresheet = await scoresheetRepository.GetWithChildrenAsync(applicationForm.ScoresheetId.Value); - if (scoresheet == null) + if (scoresheet == null || !scoresheet.Sections.Any() || !scoresheet.Sections.SelectMany(s => s.Fields).Any()) { - return "{}"; + throw new UserFriendlyException(localizer[AILocalizationKeys.ScoringRequiresScoresheetFields]); } var attachments = await applicationChefsFileAttachmentRepository.GetListAsync(a => a.ApplicationId == applicationId); - var attachmentSummaries = attachments - .Where(a => !string.IsNullOrEmpty(a.AISummary)) - .Select(a => new AIAttachmentItem - { - Name = string.IsNullOrWhiteSpace(a.FileName) ? "attachment" : a.FileName.Trim(), - Summary = a.AISummary!.Trim() - }) - .ToList(); + var attachmentSummaries = PromptDataPayloadBuilder.BuildAttachmentSummaries( + attachments, + excludeWhitespaceOnlySummaries: false); var formSubmission = await applicationFormSubmissionRepository.GetByApplicationAsync(applicationId); var formSchema = await GetFormSchemaAsync(formSubmission?.ApplicationFormVersionId); diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryService.cs index 78762cea48..bef40bc051 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryService.cs @@ -1,13 +1,16 @@ using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Localization; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Unity.AI.Extraction; +using Unity.AI.Localization; using Unity.AI.Requests; using Unity.GrantManager.Applications; using Unity.GrantManager.Intakes; +using Volo.Abp; using Volo.Abp.DependencyInjection; namespace Unity.AI.Operations; @@ -17,8 +20,10 @@ public class AttachmentSummaryService( IChefsFileAttachmentStreamProvider chefsFileAttachmentStreamProvider, ITextExtractionService textExtractionService, IAIService aiService, + IAIGenerationPrerequisiteValidator aiGenerationPrerequisiteValidator, AIExecutionModeResolver executionModeResolver, - ILogger logger) : IAttachmentSummaryService, ITransientDependency + ILogger logger, + IStringLocalizer localizer) : IAttachmentSummaryService, ITransientDependency { private const string SummaryGenerationFailedMessage = "AI summary generation failed."; @@ -47,6 +52,11 @@ public async Task GenerateAndSaveAsync(Guid attachmentId, string? prompt public async Task> GenerateAndSaveAsync(IEnumerable attachmentIds, string? promptVersion = null, CancellationToken cancellationToken = default) { var ids = attachmentIds as IReadOnlyCollection ?? attachmentIds.ToList(); + if (ids.Count == 0) + { + throw new UserFriendlyException(localizer[AILocalizationKeys.SelectAttachmentForSummaries]); + } + var mode = executionModeResolver.ResolveMode(AIExecutionModeResolver.AttachmentSummaryOperation); if (mode != AIExecutionMode.Sequential) { @@ -103,6 +113,8 @@ public async Task> GenerateForApplicationAsync( IReadOnlyCollection? attachmentIds = null, CancellationToken cancellationToken = default) { + await aiGenerationPrerequisiteValidator.EnsureAttachmentSummaryAvailableAsync(applicationId); + var applicationAttachmentIds = (await applicationChefsFileAttachmentRepository.GetListAsync(a => a.ApplicationId == applicationId)) .Select(a => a.Id) .ToList(); diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/PromptDataPayloadBuilder.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/PromptDataPayloadBuilder.cs index 315a26fc92..66e5cf93e6 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/PromptDataPayloadBuilder.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/PromptDataPayloadBuilder.cs @@ -4,6 +4,8 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json; +using System.Threading.Tasks; +using Unity.AI.Models; using Unity.GrantManager.Applications; namespace Unity.AI.Prompts @@ -33,6 +35,23 @@ internal static class PromptDataPayloadBuilder "simpleseparator" }; + private static readonly HashSet NonFieldRequirementComponentTypes = new(StringComparer.OrdinalIgnoreCase) + { + "button", + "simplebuttonadvanced", + "html", + "htmlelement", + "content", + "simpleseparator" + }; + + private const string ComponentsKey = "components"; + + private static readonly HashSet ExcludedSchemaKeys = new(StringComparer.OrdinalIgnoreCase) + { + "applicantAgent" + }; + public static JsonElement BuildPromptDataPayload( Application application, ApplicationFormSubmission? formSubmission, @@ -56,6 +75,64 @@ public static JsonElement BuildPromptDataPayload( return JsonSerializer.SerializeToElement(fallbackPayload); } + public static List BuildAttachmentSummaries( + IEnumerable attachments, + bool excludeWhitespaceOnlySummaries = true) + { + return attachments + .Where(a => excludeWhitespaceOnlySummaries + ? !string.IsNullOrWhiteSpace(a.AISummary) + : !string.IsNullOrEmpty(a.AISummary)) + .Select(a => new AIAttachmentItem + { + Name = string.IsNullOrWhiteSpace(a.FileName) ? "attachment" : a.FileName.Trim(), + Summary = a.AISummary!.Trim() + }) + .ToList(); + } + + public static async Task BuildFormFieldConfigurationAsync( + IApplicationFormVersionRepository applicationFormVersionRepository, + Guid? formVersionId, + ILogger logger) + { + if (formVersionId == null) + { + return new { message = "Form configuration not available." }; + } + + try + { + var formVersion = await applicationFormVersionRepository.GetAsync(formVersionId.Value); + if (formVersion == null || string.IsNullOrWhiteSpace(formVersion.FormSchema)) + { + return new { message = "Form configuration not available." }; + } + + var schema = JObject.Parse(formVersion.FormSchema); + var components = schema[ComponentsKey] as JArray; + if (components == null || components.Count == 0) + { + return new { message = "No form fields configured." }; + } + + var requiredFields = new List(); + var optionalFields = new List(); + ExtractFieldRequirements(components, requiredFields, optionalFields, string.Empty); + + return new + { + required_fields = requiredFields, + optional_fields = optionalFields + }; + } + catch (Exception ex) + { + logger.LogError(ex, "Error extracting form field configuration for form version {FormVersionId}", formVersionId); + return new { message = "Form configuration could not be extracted." }; + } + } + private static object BuildFallbackPromptDataPayload(Application application) { var notSpecified = "Not specified"; @@ -164,7 +241,7 @@ private static HashSet ExtractAllowedSchemaKeys(string? formSchema) try { var schema = JObject.Parse(formSchema); - if (schema["components"] is not JArray components) + if (schema[ComponentsKey] is not JArray components) { return new HashSet(StringComparer.OrdinalIgnoreCase); } @@ -217,5 +294,81 @@ private static void ProcessNestedSchemaComponents(JObject component, HashSet requiredFields, List optionalFields, string currentPath) + { + foreach (var component in components.OfType()) + { + var key = component["key"]?.ToString(); + var label = component["label"]?.ToString(); + var type = component["type"]?.ToString(); + + if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(type) || NonFieldRequirementComponentTypes.Contains(type) || ExcludedSchemaKeys.Contains(key)) + { + ProcessNestedFieldRequirements(component, type, requiredFields, optionalFields, currentPath); + continue; + } + + var displayName = !string.IsNullOrEmpty(label) ? $"{label} ({key})" : key; + var fullPath = string.IsNullOrEmpty(currentPath) ? displayName : $"{currentPath} > {displayName}"; + var validate = component["validate"] as JObject; + var isRequired = validate?["required"]?.Value() ?? false; + + if (component["input"]?.Value() == true) + { + if (isRequired) requiredFields.Add(fullPath); + else optionalFields.Add(fullPath); + } + + ProcessNestedFieldRequirements(component, type, requiredFields, optionalFields, fullPath); + } + } + + private static void ProcessNestedFieldRequirements(JObject component, string? type, List requiredFields, List optionalFields, string currentPath) + { + switch (type) + { + case "panel": + case "simplepanel": + case "fieldset": + case "well": + case "container": + case "datagrid": + case "table": + if (component[ComponentsKey] is JArray nestedComponents) + { + ExtractFieldRequirements(nestedComponents, requiredFields, optionalFields, currentPath); + } + break; + case "columns": + case "simplecols2": + case "simplecols3": + case "simplecols4": + if (component["columns"] is JArray columns) + { + foreach (var column in columns.OfType()) + { + if (column[ComponentsKey] is JArray columnComponents) + { + ExtractFieldRequirements(columnComponents, requiredFields, optionalFields, currentPath); + } + } + } + break; + case "tabs": + case "simpletabs": + if (component[ComponentsKey] is JArray tabs) + { + foreach (var tab in tabs.OfType()) + { + if (tab[ComponentsKey] is JArray tabComponents) + { + ExtractFieldRequirements(tabComponents, requiredFields, optionalFields, currentPath); + } + } + } + break; + } + } } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIPromptRenderer.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIPromptRenderer.cs index c90ed92916..5bd7070207 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIPromptRenderer.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIPromptRenderer.cs @@ -261,8 +261,9 @@ private static string RenderPromptTemplateInternal( var template = GetRequiredPromptTemplate(version, templateName); var replacements = new Dictionary(runtimeReplacements, StringComparer.Ordinal); var baseTemplateName = GetTemplateBaseName(templateName); + var placeholders = GetTemplatePlaceholders(template, version, templateName); - foreach (var placeholder in GetTemplatePlaceholders(template)) + foreach (var placeholder in placeholders) { if (replacements.ContainsKey(placeholder)) { @@ -280,19 +281,21 @@ private static string RenderPromptTemplateInternal( } } - var rendered = template; - foreach (var replacement in replacements) - { - rendered = rendered.Replace($"{{{{{replacement.Key}}}}}", replacement.Value ?? string.Empty, StringComparison.Ordinal); - } - - var unresolved = GetTemplatePlaceholders(rendered); + var unresolved = placeholders + .Where(placeholder => !replacements.ContainsKey(placeholder)) + .ToList(); if (unresolved.Count > 0) { throw new InvalidOperationException( $"Unresolved prompt placeholders in '{templateName}.txt' for prompt version '{version}': {string.Join(", ", unresolved.OrderBy(item => item))}"); } + var rendered = template; + foreach (var placeholder in placeholders) + { + rendered = rendered.Replace($"{{{{{placeholder}}}}}", replacements[placeholder] ?? string.Empty, StringComparison.Ordinal); + } + resolutionStack.Remove(templateName); return rendered; } @@ -349,9 +352,10 @@ private static string GetTemplateBaseName(string templateName) return templateName.Substring(0, separatorIndex); } - private static HashSet GetTemplatePlaceholders(string template) + private static HashSet GetTemplatePlaceholders(string template, string version, string templateName) { var placeholders = new HashSet(StringComparer.Ordinal); + var invalidPlaceholders = new List(); var searchIndex = 0; while (searchIndex < template.Length) @@ -369,14 +373,30 @@ private static HashSet GetTemplatePlaceholders(string template) } var placeholder = template.Substring(start + 2, end - start - 2).Trim(); - if (!string.IsNullOrWhiteSpace(placeholder)) + if (IsPromptPlaceholder(placeholder)) { placeholders.Add(placeholder); } + else + { + invalidPlaceholders.Add(placeholder); + } searchIndex = end + 2; } + if (invalidPlaceholders.Count > 0) + { + throw new InvalidOperationException( + $"Invalid prompt placeholders in '{templateName}.txt' for prompt version '{version}': {string.Join(", ", invalidPlaceholders.OrderBy(item => item))}"); + } + return placeholders; } + + private static bool IsPromptPlaceholder(string placeholder) + { + return !string.IsNullOrWhiteSpace(placeholder) && + placeholder.All(character => char.IsUpper(character) || char.IsDigit(character) || character == '_'); + } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIRuntimeService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIRuntimeService.cs index c3c0f9fb5e..dd3543b66d 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIRuntimeService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIRuntimeService.cs @@ -99,13 +99,13 @@ public async Task GenerateApplicationAnalysisAsync( AIProviderPayloadValidator.IsValidApplicationAnalysisJson, "application analysis", cancellationToken); - await LogPromptOutputAsync(ApplicationAnalysisPromptType, promptVersion, result.CaptureOutput, cancellationToken); if (result.Outcome != AIOperationOutcome.Success) { return new ApplicationAnalysisResponse(); } + await LogPromptOutputAsync(ApplicationAnalysisPromptType, promptVersion, result.CaptureOutput, cancellationToken); return OpenAIResponseParser.ParseApplicationAnalysisResponse(result.Content); } @@ -154,7 +154,6 @@ public async Task GenerateAttachmentSummaryAsync(Atta AIProviderPayloadValidator.IsValidAttachmentSummaryText, "attachment summary", cancellationToken); - await LogPromptOutputAsync(AttachmentSummaryPromptType, promptVersion, result.CaptureOutput, cancellationToken); if (result.Outcome != AIOperationOutcome.Success) { @@ -164,6 +163,7 @@ public async Task GenerateAttachmentSummaryAsync(Atta }; } + await LogPromptOutputAsync(AttachmentSummaryPromptType, promptVersion, result.CaptureOutput, cancellationToken); return new AttachmentSummaryResponse { Summary = ExtractSummaryFromJson(result.Content) @@ -230,13 +230,13 @@ public async Task GenerateApplicationScoringAsync(Ap content => AIProviderPayloadValidator.IsValidApplicationScoringJson(content, section), $"application scoring section {request.SectionName}", cancellationToken); - await LogPromptOutputAsync(ApplicationScoringPromptType, promptVersion, result.CaptureOutput, cancellationToken); if (result.Outcome != AIOperationOutcome.Success) { return new ApplicationScoringResponse(); } + await LogPromptOutputAsync(ApplicationScoringPromptType, promptVersion, result.CaptureOutput, cancellationToken); return OpenAIResponseParser.ParseApplicationScoringResponse(result.Content, questionIdAliasMap); } catch (OperationCanceledException) @@ -317,12 +317,22 @@ private async Task GenerateWithRetryAsync( private async Task LogPromptInputAsync(string promptType, string promptVersion, string? systemPrompt, string userPrompt, CancellationToken cancellationToken = default) { + if (!CanWritePromptFileLog()) + { + return; + } + var formattedInput = FormatPromptInputForLog(systemPrompt, userPrompt); await WritePromptLogFileAsync(promptType, promptVersion, "INPUT", formattedInput, cancellationToken); } private async Task LogPromptOutputAsync(string promptType, string promptVersion, string output, CancellationToken cancellationToken = default) { + if (!CanWritePromptFileLog()) + { + return; + } + var formattedOutput = FormatPromptOutputForLog(output); await WritePromptLogFileAsync(promptType, promptVersion, "OUTPUT", formattedOutput, cancellationToken); } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAITransportService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAITransportService.cs index b3cafcae36..1428408375 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAITransportService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAITransportService.cs @@ -93,7 +93,10 @@ public async Task GenerateSummaryAsync( if (!response.IsSuccessStatusCode) { - _logger.LogError("OpenAI API request failed: {StatusCode} - {Content}", response.StatusCode, responseContent); + _logger.LogError( + "OpenAI API request failed with status {StatusCode}. Response body length: {ResponseLength}.", + response.StatusCode, + responseContent.Length); return MapFailureOutcome(response.StatusCode, providerResponse); } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/RateLimit/AIRateLimiter.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/RateLimit/AIRateLimiter.cs index fe5a68b569..2f0f06f309 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/RateLimit/AIRateLimiter.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/RateLimit/AIRateLimiter.cs @@ -86,15 +86,11 @@ public virtual async Task GetStateAsync() return new AIRateLimitStateDto { RetryAfterSeconds = 0, IsGenerating = false }; } - var userLock = distributedLockProvider.CreateLock(CooldownLockPrefix + userId); - using (await userLock.AcquireAsync()) + return new AIRateLimitStateDto { - return new AIRateLimitStateDto - { - RetryAfterSeconds = await GetRemainingSecondsAsync(userId), - IsGenerating = await HasActiveGenerationAsync() - }; - } + RetryAfterSeconds = await GetRemainingSecondsAsync(userId), + IsGenerating = await HasActiveGenerationAsync() + }; } private async Task HasActiveGenerationAsync() diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AI/en.json b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AI/en.json index c9b41c00d7..95dbbba4b4 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AI/en.json +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AI/en.json @@ -23,6 +23,11 @@ "AI:ApplicationAnalysisDisabled": "AI application analysis is not enabled.", "AI:ScoringDisabled": "AI scoring is not enabled.", "AI:GenerateAllDisabled": "AI generation is not enabled.", + "AI:NoAttachmentsAvailable": "No attachments are available to summarize.", + "AI:ApplicationAnalysisRequiresSubmission": "AI application analysis requires application submission data.", + "AI:ScoringRequiresScoresheet": "AI scoring requires a configured scoresheet.", + "AI:ScoringRequiresScoresheetFields": "AI scoring requires a scoresheet with scoring fields.", + "AI:SelectAttachmentForSummaries": "Select at least one attachment to generate summaries.", "AIPrompts": "AI Prompts", "AIPrompt": "AI Prompt", diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AILocalizationKeys.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AILocalizationKeys.cs index 1b099e5fe8..2fdcdbd2e1 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AILocalizationKeys.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AILocalizationKeys.cs @@ -6,4 +6,9 @@ public static class AILocalizationKeys public const string ApplicationAnalysisDisabled = "AI:ApplicationAnalysisDisabled"; public const string ScoringDisabled = "AI:ScoringDisabled"; public const string GenerateAllDisabled = "AI:GenerateAllDisabled"; + public const string NoAttachmentsAvailable = "AI:NoAttachmentsAvailable"; + public const string ApplicationAnalysisRequiresSubmission = "AI:ApplicationAnalysisRequiresSubmission"; + public const string ScoringRequiresScoresheet = "AI:ScoringRequiresScoresheet"; + public const string ScoringRequiresScoresheetFields = "AI:ScoringRequiresScoresheetFields"; + public const string SelectAttachmentForSummaries = "AI:SelectAttachmentForSummaries"; } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Scoresheets/IScoresheetAppService.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Scoresheets/IScoresheetAppService.cs index 5faabc4abe..1d7c9b6e20 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Scoresheets/IScoresheetAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Scoresheets/IScoresheetAppService.cs @@ -22,6 +22,7 @@ public interface IScoresheetAppService : IApplicationService Task> GetSelectListQuestionsAsync(List questionIdsToCheck); Task ValidateChangeableScoresheet(Guid scoresheetId); Task PublishScoresheetAsync(Guid id); + Task ArchiveAsync(Guid id, bool archive); Task ExportScoresheet(Guid scoresheetId); Task ImportScoresheetAsync(ScoresheetImportDto scoresheetImportDto); Task SaveScoresheetOrder(List scoresheetIds); diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Scoresheets/ScoresheetDto.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Scoresheets/ScoresheetDto.cs index 15f36c75db..ff455aa482 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Scoresheets/ScoresheetDto.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Scoresheets/ScoresheetDto.cs @@ -10,6 +10,7 @@ public class ScoresheetDto : ExtensibleFullAuditedEntityDto public string Name { get; set; } = string.Empty; public uint Version { get; set; } public bool Published { get; set; } + public bool IsArchived { get; set; } public Collection Sections { get; private set; } = []; } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Scoresheets/Scoresheet.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Scoresheets/Scoresheet.cs index 73c693b271..f271ad0aef 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Scoresheets/Scoresheet.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Scoresheets/Scoresheet.cs @@ -17,6 +17,7 @@ public class Scoresheet : FullAuditedAggregateRoot, IMultiTenant, IReporta public virtual uint Version { get; set; } = 1; public virtual uint Order { get; set; } = 0; public virtual bool Published { get; set; } = false; + public virtual bool IsArchived { get; private set; } = false; public Guid? TenantId { get; set; } // For reporting purposes @@ -88,6 +89,12 @@ public Scoresheet IncreaseVersion() return this; } + public Scoresheet SetArchived(bool archived) + { + IsArchived = archived; + return this; + } + public Scoresheet SetName(string name) { this.Name = name; diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Scoresheets/ScoresheetAppService.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Scoresheets/ScoresheetAppService.cs index da85afbcf9..2f166258d7 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Scoresheets/ScoresheetAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Scoresheets/ScoresheetAppService.cs @@ -195,6 +195,18 @@ public async Task ValidateChangeableScoresheet(Guid scoresheetId) { throw new UserFriendlyException("Cannot change scoresheet. Scoresheet is already published."); } + if (scoresheet.IsArchived) + { + throw new UserFriendlyException("Cannot change scoresheet. Scoresheet is archived."); + } + } + + public virtual async Task ArchiveAsync(Guid id, bool archive) + { + var scoresheet = await scoresheetRepository.GetAsync(id); + scoresheet.SetArchived(archive); + await scoresheetRepository.UpdateAsync(scoresheet); + return true; } public async Task PublishScoresheetAsync(Guid id) diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/Localization/Flex/en.json b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/Localization/Flex/en.json index eb026f2630..abec19b96e 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/Localization/Flex/en.json +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/Localization/Flex/en.json @@ -16,6 +16,16 @@ "Scoresheet:Configuration:CloneScoresheetButtonText": "CLONE", "Scoresheet:Configuration:PublishScoresheetButtonText": "PUBLISH", "Scoresheet:Configuration:ExportScoresheetButtonText": "EXPORT", + "Scoresheet:Configuration:ArchiveScoresheetButtonText": "ARCHIVE", + "Scoresheet:Configuration:UnarchiveScoresheetButtonText": "UNARCHIVE", + "Scoresheet:Configuration:ArchiveModal.Message": "Archived scoresheets are hidden from the default view. You can find them using the Archived filter.", + "Scoresheet:Configuration:UnarchiveModal.Message": "Unarchived scoresheets are shown in the default view. You can find them using the All filter.", + "Scoresheet:Configuration:ArchivedSuccess": "Scoresheet has been archived.", + "Scoresheet:Configuration:UnarchivedSuccess": "Scoresheet has been unarchived.", + "Scoresheet:Configuration:ArchiveFailed": "Failed to archive the scoresheet.", + "Scoresheet:Configuration:UnarchiveFailed": "Failed to unarchive the scoresheet.", + "Scoresheet:Configuration:ArchiveModal.Confirm": "Confirm", + "Scoresheet:Configuration:ArchiveModal.Cancel": "Cancel", "Scoresheet:Configuration:SaveChangesButtonText": "SAVE CHANGES", "Scoresheet:Configuration:DiscardChangesButtonText": "DISCARD CHANGES", "Worksheet:Configuration:SaveChangesButtonText": "SAVE CHANGES", diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/ScoresheetConfiguration/Index.js b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/ScoresheetConfiguration/Index.js index b78866304a..681b5597f9 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/ScoresheetConfiguration/Index.js +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/ScoresheetConfiguration/Index.js @@ -44,7 +44,7 @@ $(function () { }); } - // --- Scoresheet name filter & published toggle --- + // --- Scoresheet name filter, published toggle, and archived toggle --- $(document).on('input', '#scoresheet-name-filter', applyScoresheetFilters); $(document).on('click', '#scoresheet-published-toggle button', function () { @@ -53,6 +53,12 @@ $(function () { applyScoresheetFilters(); }); + $(document).on('click', '.archive-scoresheet-btn', function () { + const scoresheetId = $(this).data('scoresheet-id'); + const isArchived = $(this).data('is-archived') === true || $(this).data('is-archived') === 'true'; + handleArchiveScoresheet(scoresheetId, isArchived); + }); + applyScoresheetFilters(); }); @@ -66,12 +72,17 @@ function applyScoresheetFilters() { const title = $item.find('.scoresheet-title').text().toLowerCase(); const name = $item.find('.scoresheet-name').text().toLowerCase(); const isPublished = !$item.find('.scoresheet-published-icon').hasClass('hidden'); + const isArchived = $item.data('is-archived') === true || $item.data('is-archived') === 'true'; const matchesText = !searchText || title.includes(searchText) || name.includes(searchText); const matchesFilter = - publishedFilter === 'all' || - (publishedFilter === 'published' && isPublished) || - (publishedFilter === 'unpublished' && !isPublished); + publishedFilter === 'archived' + ? isArchived + : !isArchived && ( + publishedFilter === 'all' || + (publishedFilter === 'published' && isPublished) || + (publishedFilter === 'unpublished' && !isPublished) + ); const visible = matchesText && matchesFilter; $item.toggle(visible); @@ -81,6 +92,40 @@ function applyScoresheetFilters() { $('#scoresheet-no-results').toggle(visibleCount === 0); } +function handleArchiveScoresheet(scoresheetId, currentlyArchived) { + const l = abp.localization.getResource('Flex'); + const label = currentlyArchived ? l('Scoresheet:Configuration:UnarchiveScoresheetButtonText') : l('Scoresheet:Configuration:ArchiveScoresheetButtonText'); + const message = currentlyArchived + ? l('Scoresheet:Configuration:UnarchiveModal.Message') + : l('Scoresheet:Configuration:ArchiveModal.Message'); + + Swal.fire({ + title: label + '?', + text: message, + showCancelButton: true, + confirmButtonText: l('Scoresheet:Configuration:ArchiveModal.Confirm'), + cancelButtonText: l('Scoresheet:Configuration:ArchiveModal.Cancel'), + customClass: { + confirmButton: 'btn btn-primary', + cancelButton: 'btn btn-secondary' + } + }).then((result) => { + if (result.isConfirmed) { + unity.flex.scoresheets.scoresheet.archive(scoresheetId, !currentlyArchived) + .done(function () { + PubSub.publish('refresh_scoresheet_list', { scoresheetId: null }); + abp.notify.success( + currentlyArchived ? l('Scoresheet:Configuration:UnarchivedSuccess') : l('Scoresheet:Configuration:ArchivedSuccess'), + label + ); + }) + .fail(function () { + abp.notify.error(currentlyArchived ? l('Scoresheet:Configuration:UnarchiveFailed') : l('Scoresheet:Configuration:ArchiveFailed')); + }); + } + }); +} + function importScoresheetFile(inputId) { importFlexFile(inputId, "/api/app/scoresheet/import", "Scoresheet", 'refresh_scoresheet_list'); } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/CheckboxGroupDefinitionWidget/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/CheckboxGroupDefinitionWidget/Default.cshtml index 3b7a384844..40e498b845 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/CheckboxGroupDefinitionWidget/Default.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/CheckboxGroupDefinitionWidget/Default.cshtml @@ -22,8 +22,8 @@ @foreach (var option in Model.CheckboxOptions) { - - + + - - + + - - ` - } - function bindCancelOption() { let cancel = $('#cancel-checkbox-option-btn'); if (cancel) { @@ -101,3 +97,11 @@ $(function () { ); }); +function getCheckboxGroupRowTemplate(key, label) { + return ` + + + + `; +} + diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/RadioDefinitionWidget/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/RadioDefinitionWidget/Default.cshtml index 1f5295287b..d70ea84c66 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/RadioDefinitionWidget/Default.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/RadioDefinitionWidget/Default.cshtml @@ -57,7 +57,7 @@ newOption.id = optionId; newOption.innerHTML = ` - + `; document.getElementById("radioOptions").appendChild(newOption); }); diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/Scoresheet/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/Scoresheet/Default.cshtml index 6a8f2148b4..4b270ad47c 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/Scoresheet/Default.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/Scoresheet/Default.cshtml @@ -17,7 +17,7 @@ { var totalQuestions = scoresheet.Sections.SelectMany(sec => sec.Fields).Count(); var totalSections = scoresheet.Sections.Count; -
+
- - @if (!@scoresheet.Published) + @if (scoresheet.IsArchived) { - + } + else + { + + + @if (!scoresheet.Published) + { + + } + + } -
@foreach (var sec in scoresheet.Sections) @@ -66,7 +78,7 @@
Section: @sec.Name - @if (!@scoresheet.Published) + @if (!scoresheet.Published && !scoresheet.IsArchived) {
- @if (!@scoresheet.Published) + @if (!scoresheet.Published && !scoresheet.IsArchived) {
+
diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/PaymentRequestDto.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/PaymentRequestDto.cs index b6ef9d46d7..bdab6921b9 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/PaymentRequestDto.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/PaymentRequestDto.cs @@ -47,6 +47,7 @@ public class PaymentRequestDto : AuditedEntityDto, IMultiTenant public DateTime? FsbNotificationSentDate { get; set; } public string? FsbApNotified { get; set; } + public Guid? ApplicantId { get; set; } public Guid? TenantId { get; set; } public static explicit operator PaymentRequestDto(CreatePaymentRequestDto v) diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/PaymentRequestQueryManager.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/PaymentRequestQueryManager.cs index c9b7f6e813..d0f0ca4d25 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/PaymentRequestQueryManager.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/PaymentRequestQueryManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Unity.GrantManager.Applications; using Unity.Payments.Domain.PaymentRequests; using Unity.Payments.Domain.Suppliers; using Unity.Payments.Enums; @@ -19,7 +20,8 @@ public class PaymentRequestQueryManager( ISiteRepository siteRepository, IExternalUserLookupServiceProvider externalUserLookupServiceProvider, CasPaymentRequestCoordinator casPaymentRequestCoordinator, - IObjectMapper objectMapper) : DomainService, IPaymentRequestQueryManager + IObjectMapper objectMapper, + IApplicationRepository applicationRepository) : DomainService, IPaymentRequestQueryManager { public Task GetPaymentRequestCountBySiteIdAsync(Guid siteId) { @@ -128,6 +130,23 @@ public async Task> MapToDtoAndLoadDetailsAsync(List, List>(paymentsList); + // Batch-fetch applicant IDs for all unique correlation IDs (application IDs) + var applicationIds = paymentDtos + .Select(p => p.CorrelationId) + .Distinct() + .ToArray(); + + var applications = await applicationRepository.GetListByIdsAsync(applicationIds) ?? []; + var applicantIdByApplicationId = applications.ToDictionary(a => a.Id, a => a.ApplicantId); + + foreach (var paymentDto in paymentDtos) + { + if (applicantIdByApplicationId.TryGetValue(paymentDto.CorrelationId, out var applicantId)) + { + paymentDto.ApplicantId = applicantId; + } + } + // Flatten all DecisionUserIds from ExpenseApprovals across all PaymentRequestDtos List paymentRequesterIds = [.. paymentDtos .Select(payment => payment.CreatorId) diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/Index.js b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/Index.js index f2deb13b7c..7b858aa3cc 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/Index.js +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/Index.js @@ -389,7 +389,7 @@ $(function () { const safeApplicantName = $.fn.dataTable.render.text().display(applicantName); if (type === 'display' && abp.auth.isGranted('GrantApplicationManagement.Applicants.ViewList')) { - const applicantId = row?.id; + const applicantId = row?.applicantId; const isGuid = applicantId && guidPattern.test(applicantId); if (isGuid) { @@ -421,11 +421,11 @@ $(function () { const safeCode = $.fn.dataTable.render.text().display(code); if (type === 'display' && abp.auth.isGranted('GrantApplicationManagement.Applicants.ViewList')) { - const applicantId = row?.id; - const isGuid = applicantId && guidPattern.test(applicantId); + const applicationId = row?.correlationId; + const isGuid = applicationId && guidPattern.test(applicationId); if (isGuid) { - return `${safeCode}`; + return `${safeCode}`; } return safeCode; diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequestQueryManager_PaymentRollup_Tests.cs b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequestQueryManager_PaymentRollup_Tests.cs index 49c2c4bd1d..f4e2af138f 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequestQueryManager_PaymentRollup_Tests.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequestQueryManager_PaymentRollup_Tests.cs @@ -404,7 +404,8 @@ private static PaymentRequestQueryManager CreateManager(IPaymentRequestRepositor Substitute.For(), Substitute.For(), null!, // CasPaymentRequestCoordinator - not used by Rollup methods - null! // IObjectMapper - not used by Rollup methods + null!, // IObjectMapper - not used by Rollup methods + null! // IApplicationRepository - not used by Rollup methods ); } diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/ConsolidatedFormVersionFieldsProvider.cs b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/ConsolidatedFormVersionFieldsProvider.cs index a81086a22f..72bad667f8 100644 --- a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/ConsolidatedFormVersionFieldsProvider.cs +++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/ConsolidatedFormVersionFieldsProvider.cs @@ -29,9 +29,9 @@ public class ConsolidatedFormVersionFieldsProvider( /// Fields with the same (Label, Path) but different Type produce per-version conflict entries. /// Fields unique to one version are included with a VersionLabel marker. /// - public async Task GetFieldsMetadataAsync(Guid formId) + public async Task GetFieldsMetadataAsync(Guid correlationId) { - var versions = await applicationFormAppService.GetVersionsAsync(formId); + var versions = await applicationFormAppService.GetVersionsAsync(correlationId); var versionsWithFields = new List<(Guid VersionId, string VersionLabel, FieldPathTypeDto[] Fields)>(); var metadataInfo = new Dictionary(); @@ -138,7 +138,7 @@ private static List MergeFields( var result = new List(); var processedExactKeys = new HashSet(StringComparer.OrdinalIgnoreCase); - foreach (var (_, versionLabel, fields) in versionsWithFields) + foreach (var (_, _, fields) in versionsWithFields) { foreach (var field in fields) { @@ -167,8 +167,8 @@ private static List MergeFields( Id = field.Id, Key = field.Key, Label = field.Label, - Path = field.Path, - Type = field.Type, + Path = field.Path ?? string.Empty, + Type = field.Type ?? string.Empty, TypePath = field.TypePath, DataPath = field.DataPath, VersionLabel = string.Join(", ", exactGroup.Select(e => e.VersionLabel)) @@ -182,8 +182,8 @@ private static List MergeFields( Id = field.Id, Key = field.Key, Label = field.Label, - Path = field.Path, - Type = field.Type, + Path = field.Path ?? string.Empty, + Type = field.Type ?? string.Empty, TypePath = field.TypePath, DataPath = field.DataPath, VersionLabel = null @@ -197,8 +197,8 @@ private static List MergeFields( Id = field.Id, Key = field.Key, Label = field.Label, - Path = field.Path, - Type = field.Type, + Path = field.Path ?? string.Empty, + Type = field.Type ?? string.Empty, TypePath = field.TypePath, DataPath = field.DataPath, VersionLabel = string.Join(", ", exactGroup.Select(e => e.VersionLabel)) diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/ConsolidatedWorksheetFieldsProvider.cs b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/ConsolidatedWorksheetFieldsProvider.cs index 1ad032d941..bb8d055188 100644 --- a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/ConsolidatedWorksheetFieldsProvider.cs +++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/ConsolidatedWorksheetFieldsProvider.cs @@ -31,9 +31,9 @@ public class ConsolidatedWorksheetFieldsProvider( /// Fields with the same (Label, Path) but different Type produce per-version conflict entries. /// Fields unique to one version are included with a VersionLabel marker. /// - public async Task GetFieldsMetadataAsync(Guid formId) + public async Task GetFieldsMetadataAsync(Guid correlationId) { - var versions = await applicationFormAppService.GetVersionsAsync(formId); + var versions = await applicationFormAppService.GetVersionsAsync(correlationId); var versionsWithFields = new List<(Guid VersionId, string VersionLabel, FieldPathTypeDto[] Fields)>(); var metadataInfo = new Dictionary(); @@ -164,7 +164,7 @@ private static List MergeFields( var result = new List(); var processedExactKeys = new HashSet(StringComparer.OrdinalIgnoreCase); - foreach (var (_, versionLabel, fields) in versionsWithFields) + foreach (var (_, _, fields) in versionsWithFields) { foreach (var field in fields) { @@ -193,8 +193,8 @@ private static List MergeFields( Id = field.Id, Key = field.Key, Label = field.Label, - Path = field.Path, - Type = field.Type, + Path = field.Path ?? string.Empty, + Type = field.Type ?? string.Empty, TypePath = field.TypePath, DataPath = field.DataPath, VersionLabel = string.Join(", ", exactGroup.Select(e => e.VersionLabel)) @@ -208,8 +208,8 @@ private static List MergeFields( Id = field.Id, Key = field.Key, Label = field.Label, - Path = field.Path, - Type = field.Type, + Path = field.Path ?? string.Empty, + Type = field.Type ?? string.Empty, TypePath = field.TypePath, DataPath = field.DataPath, VersionLabel = null @@ -223,8 +223,8 @@ private static List MergeFields( Id = field.Id, Key = field.Key, Label = field.Label, - Path = field.Path, - Type = field.Type, + Path = field.Path ?? string.Empty, + Type = field.Type ?? string.Empty, TypePath = field.TypePath, DataPath = field.DataPath, VersionLabel = string.Join(", ", exactGroup.Select(e => e.VersionLabel)) diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfiguration/Default.js b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfiguration/Default.js index 7985f2ed8d..4d9505829f 100644 --- a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfiguration/Default.js +++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfiguration/Default.js @@ -1,6 +1,7 @@ $(function () { let hasUnsavedChanges = false; let dataTable = null; + let _suppressColvisSave = false; // Initialize provider from the hidden input let currentProvider = $('#reportingProvider').val() || 'formversion'; @@ -247,12 +248,45 @@ $(function () { } } - function refreshConfigurationState(correlationId) { - checkConfigurationExists(correlationId, function (exists) { - updateGenerateViewButtonVisibility(exists); - updateDeleteButtonVisibility(exists); - }); - refreshViewStatusWidget(correlationId, getCorrelationProvider()); + // Persist column visibility state per provider in localStorage + function saveColvisState(provider) { + if (!dataTable) return; + try { + const state = {}; + dataTable.columns().every(function (colIdx) { + const colName = dataTable.settings()[0].aoColumns[colIdx].name; + if (colName) state[colName] = this.visible(); + }); + localStorage.setItem('reportingColvis_' + provider, JSON.stringify(state)); + } catch (e) { + /* storage unavailable */ + console.error(e); + } + } + + function restoreColvisState(provider) { + if (!dataTable) return; + try { + const stored = localStorage.getItem('reportingColvis_' + provider); + if (!stored) return; + const state = JSON.parse(stored); + dataTable.columns().every(function (colIdx) { + const colName = dataTable.settings()[0].aoColumns[colIdx].name; + if (colName && colName in state) this.visible(state[colName]); + }); + } catch (e) { + /* ignore invalid stored state */ + console.error(e); + } + } + + // Destroy and reinitialize the DataTable for the current provider + function reinitTable() { + if (dataTable) { + dataTable.destroy(); + dataTable = null; + } + initializeReportConfigTable(); } // Function to handle provider change @@ -269,48 +303,7 @@ $(function () { updateVersionSelectorVisibility(); resetChangesState(); $('#reportingViewStatus').val(''); - updateDataTableColumnHeaders(); - - const correlationId = getCurrentCorrelationId(); - if (correlationId) { - refreshConfigurationState(correlationId); - } else { - updateGenerateViewButtonVisibility(false); - updateDeleteButtonVisibility(false); - } - - if (dataTable) { - dataTable.ajax.reload(); - } - } - - // Function to update DataTable column headers - function updateDataTableColumnHeaders() { - if (!dataTable) return; - - const providerConfig = getCurrentProviderConfig(); - const columns = providerConfig.columns; - - // Update column headers - const columnIndices = [ - { index: 0, title: columns.label }, - { index: 1, title: columns.key }, - { index: 2, title: columns.type }, - { index: 3, title: columns.path }, - { index: 4, title: columns.columnName }, - { index: 5, title: columns.typePath } - ]; - - columnIndices.forEach(col => { - const header = $(dataTable.column(col.index).header()); - header.text(col.title); - }); - - // Show the Version(s) column only for consolidated providers - dataTable.column('versionLabel:name').visible(currentProvider === 'worksheet_consolidated' || currentProvider === 'formversion_consolidated'); - - // Force redraw to update headers - dataTable.draw(false); + reinitTable(); } // Handle provider toggle change event @@ -818,6 +811,16 @@ $(function () { fixedHeaders: true }); + // Persist column visibility per provider + dataTable.on('column-visibility.dt', function () { + if (!_suppressColvisSave) saveColvisState(currentProvider); + }); + + // Restore saved visibility for the initial provider + _suppressColvisSave = true; + restoreColvisState(currentProvider); + _suppressColvisSave = false; + // Add event handler for when DataTable completes drawing dataTable.on('draw.dt', function () { // Ensure inputs are always enabled (edit mode is always on) @@ -832,31 +835,6 @@ $(function () { updateDynamicColumnsWarning(hasDynamicColumns); }); - // Track changes on column name inputs with validation - $('#ReportConfigurationTable').on('input', '.column-name-input', function () { - const $input = $(this); - const value = $input.val(); - const path = $input.data('path'); - - // Validate and provide feedback - validateColumnNameInput($input, value, path); - markAsChanged(); - }); - - // Handle blur event to sanitize input - $('#ReportConfigurationTable').on('blur', '.column-name-input', function () { - const $input = $(this); - const value = $input.val().trim(); - - if (value) { - const sanitized = sanitizeColumnName(value); - if (sanitized !== value) { - $input.val(sanitized); - validateColumnNameInput($input, sanitized, $input.data('path')); - } - } - }); - // Initial check for button visibility on page load const initialCorrelationId = getCurrentCorrelationId(); if (initialCorrelationId) { @@ -2154,6 +2132,25 @@ $(function () { // Initialize on page load initializeReportConfigTable(); + // Delegated handlers registered once — survive DataTable destroy/reinit + $('#ReportConfigurationTable').on('input', '.column-name-input', function () { + const $input = $(this); + validateColumnNameInput($input, $input.val(), $input.data('path')); + markAsChanged(); + }); + + $('#ReportConfigurationTable').on('blur', '.column-name-input', function () { + const $input = $(this); + const value = $input.val().trim(); + if (value) { + const sanitized = sanitizeColumnName(value); + if (sanitized !== value) { + $input.val(sanitized); + validateColumnNameInput($input, sanitized, $input.data('path')); + } + } + }); + // Set initial undo button state updateUndoButtonVisibility(); 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 index 5c1ac1ee8c..0dfccaa949 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/AIGenerationRequestDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/AIGenerationRequestDto.cs @@ -13,6 +13,4 @@ public class AIGenerationRequestDto : EntityDto public DateTime? CompletedAt { get; set; } public string? FailureReason { get; set; } public bool IsActive { get; set; } - public bool IsGenerating { get; set; } - public int RetryAfterSeconds { get; set; } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/AIGenerationStatusDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/AIGenerationStatusDto.cs new file mode 100644 index 0000000000..55814b6beb --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/AIGenerationStatusDto.cs @@ -0,0 +1,8 @@ +namespace Unity.GrantManager.GrantApplications; + +public class AIGenerationStatusDto +{ + public AIGenerationRequestDto? GenerationRequest { get; set; } + public bool IsGenerating { get; set; } + public int RetryAfterSeconds { get; set; } +} 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 1c876c1425..ce55d62db4 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 @@ -18,12 +18,12 @@ public interface IGrantApplicationAppService 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(QueueAttachmentSummaryRequestDto input, 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 QueueAIGenerationAsync(Guid applicationId, string? promptVersion = null); + Task QueueApplicationAnalysisAsync(Guid applicationId, string? promptVersion = null); + Task QueueAttachmentSummaryAsync(QueueAttachmentSummaryRequestDto input, 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); 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 6eec9bc4aa..c2aa5f6a2c 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 @@ -3,14 +3,20 @@ using System.Linq; using System.Threading.Tasks; using Unity.AI.Automation; +using Unity.AI.Features; +using Unity.AI.Localization; +using Unity.AI.Operations; using Unity.AI.RateLimit; using Unity.GrantManager.GrantApplications; using Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; using Medallion.Threading; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Localization; +using Volo.Abp; using Volo.Abp.Domain.Repositories; using Volo.Abp.BackgroundJobs; using Volo.Abp.DependencyInjection; +using Volo.Abp.Features; using Volo.Abp.Users; namespace Unity.GrantManager.GrantApplications.Automation; @@ -19,9 +25,12 @@ public class ApplicationAIGenerationQueue( IBackgroundJobManager backgroundJobManager, IRepository generationRequestRepository, IDistributedLockProvider distributedLockProvider, + IAIGenerationPrerequisiteValidator aiGenerationPrerequisiteValidator, + IFeatureChecker featureChecker, IAIRateLimiter aiRateLimiter, ICurrentUser currentUser, - ILogger logger) + ILogger logger, + IStringLocalizer localizer) : IApplicationAIGenerationQueue, ITransientDependency { public async Task QueueAttachmentSummaryAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null, List? attachmentIds = null) @@ -32,6 +41,7 @@ await EnsureRequestAndEnqueueAsync( tenantId, AIGenerationRequestKeyHelper.AttachmentSummaryOperationType, applicationId, + () => aiGenerationPrerequisiteValidator.EnsureAttachmentSummaryAvailableAsync(applicationId), () => { return backgroundJobManager.EnqueueAsync(new GenerateAttachmentSummaryBackgroundJobArgs @@ -54,6 +64,7 @@ await EnsureRequestAndEnqueueAsync( tenantId, AIGenerationRequestKeyHelper.ApplicationAnalysisOperationType, applicationId, + () => aiGenerationPrerequisiteValidator.EnsureApplicationAnalysisAvailableAsync(applicationId), () => { return backgroundJobManager.EnqueueAsync(new GenerateApplicationAnalysisBackgroundJobArgs @@ -75,6 +86,7 @@ await EnsureRequestAndEnqueueAsync( tenantId, AIGenerationRequestKeyHelper.ApplicationScoringOperationType, applicationId, + () => aiGenerationPrerequisiteValidator.EnsureApplicationScoringAvailableAsync(applicationId), () => { return backgroundJobManager.EnqueueAsync(new GenerateApplicationScoringBackgroundJobArgs @@ -96,6 +108,7 @@ await EnsureRequestAndEnqueueAsync( tenantId, AIGenerationRequestKeyHelper.PipelineOperationType, applicationId, + () => EnsureAnyPipelineStageAvailableAsync(applicationId), () => { return backgroundJobManager.EnqueueAsync(new RunApplicationAIPipelineJobArgs @@ -114,6 +127,7 @@ private async Task EnsureRequestAndEnqueueAsync( Guid? tenantId, string operationType, Guid? applicationId, + Func validateInput, Func enqueue) { var requestLock = distributedLockProvider.CreateLock($"ai-generation:{requestKey}"); @@ -135,6 +149,8 @@ private async Task EnsureRequestAndEnqueueAsync( return; } + await validateInput(); + // Single chokepoint for all AI generate flows (manual + auto). // The limiter is a no-op for system/background callers without an authenticated user. await aiRateLimiter.EnsureAsync(); @@ -160,6 +176,58 @@ private async Task EnsureRequestAndEnqueueAsync( } } + private async Task EnsureAnyPipelineStageAvailableAsync(Guid applicationId) + { + var hasEnabledStage = false; + var hasAvailableStage = false; + UserFriendlyException? lastPrerequisiteException = null; + + async Task CheckStageAsync(bool isEnabled, Func ensurePrerequisite) + { + if (!isEnabled) + { + return; + } + + hasEnabledStage = true; + + try + { + await ensurePrerequisite(); + hasAvailableStage = true; + } + catch (UserFriendlyException ex) + { + lastPrerequisiteException = ex; + } + } + + await CheckStageAsync( + await featureChecker.IsEnabledAsync(AIFeatures.AttachmentSummaries), + () => aiGenerationPrerequisiteValidator.EnsureAttachmentSummaryAvailableAsync(applicationId)); + await CheckStageAsync( + await featureChecker.IsEnabledAsync(AIFeatures.ApplicationAnalysis), + () => aiGenerationPrerequisiteValidator.EnsureApplicationAnalysisAvailableAsync(applicationId)); + await CheckStageAsync( + await featureChecker.IsEnabledAsync(AIFeatures.Scoring), + () => aiGenerationPrerequisiteValidator.EnsureApplicationScoringAvailableAsync(applicationId)); + + if (hasAvailableStage) + { + return; + } + + if (lastPrerequisiteException != null) + { + throw lastPrerequisiteException; + } + + if (!hasEnabledStage) + { + throw new UserFriendlyException(localizer[AILocalizationKeys.GenerateAllDisabled]); + } + } + private async Task MarkFailedBestEffortAsync(AIGenerationRequest request, Exception exception) { try 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 269d08aae6..a9182d3ab6 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 @@ -3,11 +3,13 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Unity.AI.Operations; using Unity.AI.RateLimit; using Unity.GrantManager.Applications; using Unity.GrantManager.Attachments; using Unity.GrantManager.GrantApplications; using Unity.GrantManager.GrantApplications.Automation.Events; +using Volo.Abp; using Volo.Abp.BackgroundJobs; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories; @@ -23,6 +25,7 @@ public class RunApplicationAIPipelineJob( IAttachmentSummaryAppService attachmentSummaryAppService, IApplicationAnalysisAppService applicationAnalysisAppService, IApplicationScoringAppService applicationScoringAppService, + IAIGenerationPrerequisiteValidator aiGenerationPrerequisiteValidator, IFeatureChecker featureChecker, ILocalEventBus localEventBus, IRepository generationRequestRepository, @@ -58,7 +61,10 @@ public override async Task ExecuteAsync(RunApplicationAIPipelineJobArgs args) logger.LogInformation("Executing queued AI content pipeline for application {ApplicationId}.", args.ApplicationId); - if (attachmentSummariesEnabled) + if (attachmentSummariesEnabled && await HasStageInputAsync( + args.ApplicationId, + "attachment summaries", + () => aiGenerationPrerequisiteValidator.EnsureAttachmentSummaryAvailableAsync(args.ApplicationId))) { var attachmentIds = await GetAttachmentIdsAsync(args.ApplicationId); var attachmentResults = await attachmentSummaryAppService.GenerateAttachmentSummariesForPipelineAsync(attachmentIds, args.PromptVersion); @@ -68,7 +74,10 @@ public override async Task ExecuteAsync(RunApplicationAIPipelineJobArgs args) Exception? analysisException = null; Exception? scoringException = null; - if (applicationAnalysisEnabled) + if (applicationAnalysisEnabled && await HasStageInputAsync( + args.ApplicationId, + "application analysis", + () => aiGenerationPrerequisiteValidator.EnsureApplicationAnalysisAvailableAsync(args.ApplicationId))) { try { @@ -85,7 +94,10 @@ public override async Task ExecuteAsync(RunApplicationAIPipelineJobArgs args) } } - if (scoringEnabled) + if (scoringEnabled && await HasStageInputAsync( + args.ApplicationId, + "application scoring", + () => aiGenerationPrerequisiteValidator.EnsureApplicationScoringAvailableAsync(args.ApplicationId))) { try { @@ -126,6 +138,24 @@ await localEventBus.PublishAsync(new ApplicationAIScoringGeneratedEvent } } + private async Task HasStageInputAsync(Guid applicationId, string stageName, Func ensurePrerequisite) + { + try + { + await ensurePrerequisite(); + return true; + } + catch (UserFriendlyException ex) + { + logger.LogInformation( + ex, + "Skipping AI {StageName} stage for application {ApplicationId} because required input is unavailable.", + stageName, + applicationId); + return false; + } + } + private async Task> GetAttachmentIdsAsync(Guid applicationId) { var attachments = await applicationChefsFileAttachmentRepository.GetListAsync(a => a.ApplicationId == applicationId); 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 be44fb379e..01b0be6f94 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs @@ -1165,14 +1165,14 @@ await LocalEventBus.PublishAsync( return ObjectMapper.Map(application); } - public async Task QueueAIGenerationAsync(Guid applicationId, string? promptVersion = null) + 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) + public async Task QueueApplicationAnalysisAsync(Guid applicationId, string? promptVersion = null) { await EnsureAIAnalysisEnabledAsync(); await aiGenerationQueue.QueueApplicationAnalysisAsync(applicationId, CurrentTenant.Id, promptVersion); @@ -1182,12 +1182,12 @@ public async Task QueueApplicationAnalysisAsync(Guid app AIGenerationRequestKeyHelper.ApplicationAnalysisOperationType, CurrentTenant.Id); - return await ApplyRateLimitStateAsync( + return await CreateGenerationStatusAsync( request ?? throw new UserFriendlyException("Unable to queue AI analysis request.")); } [Authorize(AIPermissions.Analysis.GenerateAttachmentSummaries)] - public async Task QueueAttachmentSummaryAsync(QueueAttachmentSummaryRequestDto input, string? promptVersion = null) + public async Task QueueAttachmentSummaryAsync(QueueAttachmentSummaryRequestDto input, string? promptVersion = null) { await EnsureAttachmentSummariesEnabledAsync(); var attachmentIds = await ResolveAttachmentSummaryIdsAsync(input); @@ -1198,12 +1198,12 @@ public async Task QueueAttachmentSummaryAsync(QueueAttac AIGenerationRequestKeyHelper.AttachmentSummaryOperationType, CurrentTenant.Id); - return await ApplyRateLimitStateAsync( + return await CreateGenerationStatusAsync( request ?? throw new UserFriendlyException("Unable to queue AI attachment summary request.")); } [Authorize(AIPermissions.Analysis.GenerateScoring)] - public async Task QueueApplicationScoringAsync(Guid applicationId, string? promptVersion = null) + public async Task QueueApplicationScoringAsync(Guid applicationId, string? promptVersion = null) { await EnsureScoringEnabledAsync(); await aiGenerationQueue.QueueApplicationScoringAsync(applicationId, CurrentTenant.Id, promptVersion); @@ -1213,21 +1213,21 @@ public async Task QueueApplicationScoringAsync(Guid appl AIGenerationRequestKeyHelper.ApplicationScoringOperationType, CurrentTenant.Id); - return await ApplyRateLimitStateAsync( + return await CreateGenerationStatusAsync( request ?? throw new UserFriendlyException("Unable to queue AI scoring request.")); } - public async Task GetAIGenerationStatusAsync(Guid applicationId, string operationType, string? promptVersion = null) + public async Task GetAIGenerationStatusAsync(Guid applicationId, string operationType, string? promptVersion = null) { await EnsureAIGenerationStatusAccessAsync(operationType); - return await ApplyRateLimitStateOrDefaultAsync( + return await CreateGenerationStatusAsync( 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) + public async Task QueueAllAIStagesAsync(Guid applicationId, string? promptVersion = null) { await EnsureAttachmentSummariesEnabledAsync(); await EnsureAIAnalysisEnabledAsync(); @@ -1239,26 +1239,19 @@ public async Task QueueAllAIStagesAsync(Guid application AIGenerationRequestKeyHelper.PipelineOperationType, CurrentTenant.Id); - return await ApplyRateLimitStateAsync( + return await CreateGenerationStatusAsync( request ?? throw new UserFriendlyException("Unable to queue AI generation request.")); } - private async Task ApplyRateLimitStateOrDefaultAsync(AIGenerationRequestDto? request) - { - if (request is null) - { - return null; - } - - return await ApplyRateLimitStateAsync(request); - } - - private async Task ApplyRateLimitStateAsync(AIGenerationRequestDto request) + private async Task CreateGenerationStatusAsync(AIGenerationRequestDto? request) { var state = await aiRateLimiter.GetStateAsync(); - request.IsGenerating = state.IsGenerating; - request.RetryAfterSeconds = state.RetryAfterSeconds; - return request; + return new AIGenerationStatusDto + { + GenerationRequest = request, + IsGenerating = state.IsGenerating, + RetryAfterSeconds = state.RetryAfterSeconds + }; } private async Task EnsureAIGenerationStatusAccessAsync(string operationType) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationMapperlyProfile.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationMapperlyProfile.cs index 6a8b608d1f..c2eca7860d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationMapperlyProfile.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationMapperlyProfile.cs @@ -536,17 +536,7 @@ public partial class ApplicationActionResultItemToDtoMapper : MapperBase { public override partial CommunityDto Map(Community source); public override partial void Map(Community source, CommunityDto destination); } [Mapper] public partial class RegionalDistrictToDtoMapper : MapperBase { public override partial RegionalDistrictDto Map(RegionalDistrict source); public override partial void Map(RegionalDistrict source, RegionalDistrictDto destination); } [Mapper] public partial class ApplicationTagsToDtoMapper : MapperBase { public override partial ApplicationTagsDto Map(ApplicationTags source); public override partial void Map(ApplicationTags source, ApplicationTagsDto destination); } -[Mapper] -public partial class AIGenerationRequestToDtoMapper : MapperBase -{ - [MapperIgnoreTarget(nameof(AIGenerationRequestDto.IsGenerating))] - [MapperIgnoreTarget(nameof(AIGenerationRequestDto.RetryAfterSeconds))] - public override partial AIGenerationRequestDto Map(AIGenerationRequest source); - - [MapperIgnoreTarget(nameof(AIGenerationRequestDto.IsGenerating))] - [MapperIgnoreTarget(nameof(AIGenerationRequestDto.RetryAfterSeconds))] - public override partial void Map(AIGenerationRequest source, AIGenerationRequestDto destination); -} +[Mapper] public partial class AIGenerationRequestToDtoMapper : MapperBase { public override partial AIGenerationRequestDto Map(AIGenerationRequest source); public override partial void Map(AIGenerationRequest source, AIGenerationRequestDto destination); } [Mapper(AllowNullPropertyAssignment = true)] public partial class ApplicantToGrantApplicationApplicantDtoMapper : MapperBase { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260604200305_AddIsArchivedToScoresheets.Designer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260604200305_AddIsArchivedToScoresheets.Designer.cs new file mode 100644 index 0000000000..64d647d7e9 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260604200305_AddIsArchivedToScoresheets.Designer.cs @@ -0,0 +1,5040 @@ +// +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("20260604200305_AddIsArchivedToScoresheets")] + partial class AddIsArchivedToScoresheets + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.PostgreSql) + .HasAnnotation("ProductVersion", "10.0.3") + .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("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("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("ReportsComments") + .HasColumnType("text"); + + 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.HasIndex("TenantId"); + + b.ToTable("Applicants", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAddress", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AddressType") + .HasColumnType("integer"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Postal") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("Street") + .HasColumnType("text"); + + b.Property("Street2") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Unit") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicantAddresses", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAgent", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("BceidBusinessGuid") + .HasColumnType("uuid"); + + b.Property("BceidBusinessName") + .HasColumnType("text"); + + b.Property("BceidUserGuid") + .HasColumnType("uuid"); + + b.Property("BceidUserName") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactOrder") + .HasColumnType("integer"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IdentityEmail") + .HasColumnType("text"); + + b.Property("IdentityName") + .HasColumnType("text"); + + b.Property("IdentityProvider") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsConfirmed") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OidcSubUser") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("Phone2") + .HasColumnType("text"); + + b.Property("Phone2Extension") + .HasColumnType("text"); + + b.Property("PhoneExtension") + .HasColumnType("text"); + + b.Property("RoleForApplicant") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationId") + .IsUnique(); + + b.HasIndex("TenantId", "ApplicationId"); + + 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.HasIndex("ReferenceNo"); + + b.HasIndex("TenantId", "SubmissionDate") + .HasFilter("\"IsDeleted\" = false"); + + 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.HasIndex("TenantId", "ApplicationId"); + + 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("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.HasIndex("TenantId", "IsDeleted") + .HasFilter("\"IsDeleted\" = false"); + + 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("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.HasIndex("TenantId", "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.HasIndex("TenantId", "ApplicationId"); + + 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("AuditStatus") + .HasColumnType("text"); + + b.Property("AuditTrackingNumber") + .HasColumnType("text"); + + b.Property("AuditorName") + .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.Applications.ReportsHistory", 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("FiscalYear") + .HasColumnType("text"); + + b.Property("IncompleteReport") + .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("Outstanding") + .HasColumnType("boolean"); + + b.Property("ReportDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("ReportsHistories", (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.HasIndex("TenantId"); + + 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.Applications.ReportsHistory", 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); + + 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/20260604200305_AddIsArchivedToScoresheets.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260604200305_AddIsArchivedToScoresheets.cs new file mode 100644 index 0000000000..0704b9e28c --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260604200305_AddIsArchivedToScoresheets.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + /// + public partial class AddIsArchivedToScoresheets : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsArchived", + schema: "Flex", + table: "Scoresheets", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsArchived", + schema: "Flex", + table: "Scoresheets"); + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/GrantTenantDbContextModelSnapshot.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/GrantTenantDbContextModelSnapshot.cs index 62fbafaadd..aeb709113a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/GrantTenantDbContextModelSnapshot.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/GrantTenantDbContextModelSnapshot.cs @@ -270,6 +270,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text") .HasColumnName("ExtraProperties"); + b.Property("IsArchived") + .HasColumnType("boolean"); + b.Property("IsDeleted") .ValueGeneratedOnAdd() .HasColumnType("boolean") diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js index a29267f48d..424a8d9666 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js @@ -451,12 +451,13 @@ globalThis.queueApplicationAnalysis = function(triggerButton = null) { unity.grantManager.grantApplications.grantApplication .queueApplicationAnalysis(applicationId) - .done(function(request) { + .done(function(generationStatus) { + const request = generationStatus?.generationRequest; const status = globalThis.AIGenerationButtonState?.resolveStatus(request?.status) ?? ''; if (status === 'Completed') { globalThis.AIGenerationButtonState?.restoreForCooldownCheck($button, existingHtml); - globalThis.AIGenerationButtonState?.applyStatusState(request); + globalThis.AIGenerationButtonState?.applyStatusState(generationStatus); loadAIAnalysis(); return; } @@ -540,7 +541,8 @@ $(function() { unity.grantManager.grantApplications.grantApplication .getAIGenerationStatus(applicationId, 'application-analysis') - .done(function(request) { + .done(function(generationStatus) { + const request = generationStatus?.generationRequest; if (request?.isActive !== true) { return; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js index 02aea6030d..1ffd85a1c6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js @@ -13,11 +13,12 @@ .prop('disabled', true); } - function applyRateLimitState(request, options = {}) { + function applyRateLimitState(generationStatus, options = {}) { + const request = generationStatus?.generationRequest; global.applyAIRateLimitState?.( { - isGenerating: request?.isGenerating === true || request?.isActive === true, - retryAfterSeconds: Number(request?.retryAfterSeconds) || 0 + isGenerating: generationStatus?.isGenerating === true || request?.isActive === true, + retryAfterSeconds: Number(generationStatus?.retryAfterSeconds) || 0 }, { pollWhenGenerating: options.pollWhenGenerating === true } ); @@ -47,8 +48,8 @@ restoreForCooldownCheck($button, html) { restoreButtonForCooldownCheck($button, html); }, - applyStatusState(request) { - applyRateLimitState(request, { pollWhenGenerating: true }); + applyStatusState(generationStatus) { + applyRateLimitState(generationStatus, { pollWhenGenerating: true }); }, monitor(options) { const intervalMs = options.intervalMs || 15000; @@ -77,14 +78,15 @@ const poll = () => { options.getStatus() - .done((request) => { + .done((generationStatus) => { failures = 0; + const request = generationStatus?.generationRequest; const status = this.resolveStatus(request?.status); if (status === 'Failed') { stop(); restoreButton(options.$button, options.originalHtml); - applyRateLimitState(request, { pollWhenGenerating: true }); + applyRateLimitState(generationStatus, { pollWhenGenerating: true }); options.onFailed?.(request); return; } @@ -100,12 +102,12 @@ if (request.isActive === false || status === 'Completed') { stop(); restoreButtonForCooldownCheck(options.$button, options.originalHtml); - applyRateLimitState(request, { pollWhenGenerating: true }); + applyRateLimitState(generationStatus, { pollWhenGenerating: true }); options.onComplete?.(request); return; } - applyRateLimitState(request); + applyRateLimitState(generationStatus); scheduleNextPoll(); }) .fail((error) => { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js index 4f17bbbb06..b627970d32 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js @@ -694,12 +694,13 @@ function queueApplicationScoring(triggerButton = null) { unity.grantManager.grantApplications.grantApplication .queueApplicationScoring(applicationId) - .done(function (request) { + .done(function (generationStatus) { + const request = generationStatus?.generationRequest; const status = globalThis.AIGenerationButtonState?.resolveStatus(request?.status) ?? ''; if (status === 'Completed') { globalThis.AIGenerationButtonState?.restoreForCooldownCheck($button, existingHtml); - globalThis.AIGenerationButtonState?.applyStatusState(request); + globalThis.AIGenerationButtonState?.applyStatusState(generationStatus); PubSub.publish('refresh_assessment_scores', null); return; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js index c0c85d3641..fedcbeed87 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js @@ -210,7 +210,8 @@ $(function () { applicationId: getApplicationId(), attachmentIds: summaryAttachmentIds }) - .done(function (request) { + .done(function (generationStatus) { + const request = generationStatus?.generationRequest; const status = globalThis.AIGenerationButtonState?.resolveStatus(request?.status) ?? ''; if (status === 'Completed') { @@ -218,7 +219,7 @@ $(function () { chefsDataTable.ajax.reload(); abp.notify.success('AI summaries generated successfully.'); globalThis.AIGenerationButtonState?.restoreForCooldownCheck($activeButton, existingHTML); - globalThis.AIGenerationButtonState?.applyStatusState(request); + globalThis.AIGenerationButtonState?.applyStatusState(generationStatus); return; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js index 53c123bb51..67687361df 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js @@ -472,12 +472,13 @@ function generateAiButtonAction(e, dt, button, config) { } unity.grantManager.grantApplications.grantApplication.queueApplicationScoring(pageApplicationId) - .done(function (request) { + .done(function (generationStatus) { + const request = generationStatus?.generationRequest; const status = globalThis.AIGenerationButtonState?.resolveStatus(request?.status) ?? ''; if (status === 'Completed') { restoreReviewListAiButtonForCooldownCheck($button); - globalThis.AIGenerationButtonState?.applyStatusState(request); + globalThis.AIGenerationButtonState?.applyStatusState(generationStatus); refreshReviewListAfterAiScoring(); return; } @@ -518,7 +519,8 @@ function resumeActiveReviewListAiButton(reviewListTable) { const $button = $(button.node()); unity.grantManager.grantApplications.grantApplication .getAIGenerationStatus(pageApplicationId, 'application-scoring') - .done(function(request) { + .done(function(generationStatus) { + const request = generationStatus?.generationRequest; if (request?.isActive !== true) { return; } diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/ApplicationAnalysisServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/ApplicationAnalysisServiceTests.cs new file mode 100644 index 0000000000..42c27d35e6 --- /dev/null +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/ApplicationAnalysisServiceTests.cs @@ -0,0 +1,189 @@ +using Microsoft.Extensions.Logging.Abstractions; +using NSubstitute; +using Shouldly; +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Text.Json; +using System.Threading.Tasks; +using Unity.AI; +using Unity.AI.Operations; +using Unity.AI.Requests; +using Unity.AI.Responses; +using Unity.GrantManager.Applications; +using Volo.Abp.Domain.Entities; +using Xunit; +using Xunit.Abstractions; + +namespace Unity.GrantManager.AI.Operations; + +public class ApplicationAnalysisServiceTests : GrantManagerApplicationTestBase +{ + public ApplicationAnalysisServiceTests(ITestOutputHelper outputHelper) : base(outputHelper) + { + } + + [Fact] + public async Task RegenerateAndSaveAsync_Builds_Filtered_Prompt_Payload_From_Form_Submission() + { + var applicationId = Guid.NewGuid(); + var formVersionId = Guid.NewGuid(); + ApplicationAnalysisRequest? capturedRequest = null; + var application = WithId(new Application + { + ApplicationFormId = Guid.NewGuid(), + ProjectName = "Fallback project", + ReferenceNo = "REF-001", + RequestedAmount = 100, + TotalProjectBudget = 200, + SubmissionDate = new DateTime(2026, 1, 2) + }, applicationId); + var submission = new ApplicationFormSubmission + { + ApplicationId = applicationId, + ApplicationFormVersionId = formVersionId, + Submission = """ + { + "data": { + "projectName": "Submitted project", + "requestedAmount": 12345, + "metadata": { "timezone": "utc" }, + "files": [{ "name": "large.pdf" }], + "ignoredField": "not in schema" + } + } + """ + }; + var formVersion = new ApplicationFormVersion + { + FormSchema = """ + { + "components": [ + { "key": "projectName", "label": "Project Name", "type": "textfield", "input": true, "validate": { "required": true } }, + { "key": "requestedAmount", "label": "Requested Amount", "type": "number", "input": true }, + { "key": "applicantAgent", "label": "Applicant Agent", "type": "textfield", "input": true }, + { "key": "ignoredField", "label": "Ignored Field", "type": "html", "input": false } + ] + } + """ + }; + + var applicationRepository = Substitute.For(); + applicationRepository.GetAsync(applicationId).Returns(application); + + var submissionRepository = Substitute.For(); + submissionRepository.GetByApplicationAsync(applicationId).Returns(submission); + + var formVersionRepository = Substitute.For(); + formVersionRepository.GetAsync(formVersionId).Returns(formVersion); + + var attachmentRepository = Substitute.For(); + attachmentRepository + .GetListAsync(Arg.Any>>()) + .Returns([ + new ApplicationChefsFileAttachment + { + FileName = " summary.pdf ", + AISummary = " Summary text " + }, + new ApplicationChefsFileAttachment + { + FileName = "empty.txt", + AISummary = " " + } + ]); + + var aiService = Substitute.For(); + aiService.GenerateApplicationAnalysisAsync(Arg.Do(request => capturedRequest = request)) + .Returns(new ApplicationAnalysisResponse { Decision = "ok" }); + var prerequisiteValidator = Substitute.For(); + + var service = new ApplicationAnalysisService( + applicationRepository, + submissionRepository, + formVersionRepository, + attachmentRepository, + aiService, + prerequisiteValidator, + NullLogger.Instance); + + var result = await service.RegenerateAndSaveAsync(applicationId, "v1"); + + result.ShouldContain("\"Decision\": \"ok\""); + capturedRequest.ShouldNotBeNull(); + capturedRequest.PromptVersion.ShouldBe("v1"); + capturedRequest.Attachments.Count.ShouldBe(1); + capturedRequest.Attachments[0].Name.ShouldBe("summary.pdf"); + capturedRequest.Attachments[0].Summary.ShouldBe("Summary text"); + + capturedRequest.Data.GetProperty("projectName").GetString().ShouldBe("Submitted project"); + capturedRequest.Data.GetProperty("requestedAmount").GetInt32().ShouldBe(12345); + capturedRequest.Data.TryGetProperty("metadata", out _).ShouldBeFalse(); + capturedRequest.Data.TryGetProperty("files", out _).ShouldBeFalse(); + capturedRequest.Data.TryGetProperty("ignoredField", out _).ShouldBeFalse(); + + var schemaJson = JsonSerializer.Serialize(capturedRequest.Schema); + schemaJson.ShouldContain("Project Name (projectName)"); + schemaJson.ShouldContain("Requested Amount (requestedAmount)"); + schemaJson.ShouldNotContain("Applicant Agent (applicantAgent)"); + await applicationRepository.Received(1).UpdateAsync(application); + } + + [Fact] + public async Task RegenerateAndSaveAsync_Falls_Back_To_Application_Data_When_Submission_Is_Missing() + { + var applicationId = Guid.NewGuid(); + var application = WithId(new Application + { + ProjectName = "Fallback project", + ReferenceNo = "REF-002", + RequestedAmount = 1500, + TotalProjectBudget = 3000, + ProjectSummary = "Fallback summary", + SubmissionDate = new DateTime(2026, 2, 3) + }, applicationId); + ApplicationAnalysisRequest? capturedRequest = null; + + var applicationRepository = Substitute.For(); + applicationRepository.GetAsync(applicationId).Returns(application); + + var submissionRepository = Substitute.For(); + submissionRepository.GetByApplicationAsync(applicationId).Returns((ApplicationFormSubmission?)null); + + var formVersionRepository = Substitute.For(); + var attachmentRepository = Substitute.For(); + attachmentRepository + .GetListAsync(Arg.Any>>()) + .Returns([]); + + var aiService = Substitute.For(); + aiService.GenerateApplicationAnalysisAsync(Arg.Do(request => capturedRequest = request)) + .Returns(new ApplicationAnalysisResponse()); + var prerequisiteValidator = Substitute.For(); + + var service = new ApplicationAnalysisService( + applicationRepository, + submissionRepository, + formVersionRepository, + attachmentRepository, + aiService, + prerequisiteValidator, + NullLogger.Instance); + + await service.RegenerateAndSaveAsync(applicationId); + + capturedRequest.ShouldNotBeNull(); + capturedRequest.Data.GetProperty("project_name").GetString().ShouldBe("Fallback project"); + capturedRequest.Data.GetProperty("reference_number").GetString().ShouldBe("REF-002"); + capturedRequest.Data.GetProperty("requested_amount").GetDecimal().ShouldBe(1500); + capturedRequest.Attachments.ShouldBeEmpty(); + } + + private static T WithId(T entity, Guid id) where T : Entity + { + typeof(Entity) + .GetProperty(nameof(Entity.Id))! + .SetValue(entity, id); + return entity; + } +} diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/AttachmentSummaryServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/AttachmentSummaryServiceTests.cs index 0a5c618e2a..81574bb842 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/AttachmentSummaryServiceTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/AttachmentSummaryServiceTests.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; using Shouldly; @@ -8,11 +9,13 @@ using System.Threading.Tasks; using Unity.AI; using Unity.AI.Extraction; +using Unity.AI.Localization; using Unity.AI.Operations; using Unity.AI.Requests; using Unity.AI.Responses; using Unity.GrantManager.Applications; using Unity.GrantManager.Intakes; +using Volo.Abp; using Xunit; namespace Unity.GrantManager.AI.Operations; @@ -51,13 +54,17 @@ public async Task GenerateAndSaveAsync_Uses_Streamed_Attachment_Text() aiService.GenerateAttachmentSummaryAsync(Arg.Do(request => capturedRequest = request)) .Returns(new AttachmentSummaryResponse { Summary = "summary text" }); + var prerequisiteValidator = Substitute.For(); + var service = new AttachmentSummaryService( attachmentRepository, streamProvider, textExtractionService, aiService, + prerequisiteValidator, new AIExecutionModeResolver(new ConfigurationBuilder().Build()), - NullLogger.Instance); + NullLogger.Instance, + Substitute.For>()); var result = await service.GenerateAndSaveAsync(attachmentId, "v1"); @@ -103,10 +110,36 @@ public async Task GenerateAndSaveAsync_Should_Propagate_Cancellation() streamProvider, Substitute.For(), Substitute.For(), + Substitute.For(), new AIExecutionModeResolver(new ConfigurationBuilder().Build()), - NullLogger.Instance); + NullLogger.Instance, + Substitute.For>()); await Should.ThrowAsync(() => service.GenerateAndSaveAsync(attachmentId, "v1", cancellationTokenSource.Token)); } + + [Fact] + public async Task GenerateAndSaveAsync_Should_Reject_Empty_Attachment_List() + { + var attachmentRepository = Substitute.For(); + var streamProvider = Substitute.For(); + var textExtractionService = Substitute.For(); + var aiService = Substitute.For(); + var prerequisiteValidator = Substitute.For(); + + var service = new AttachmentSummaryService( + attachmentRepository, + streamProvider, + textExtractionService, + aiService, + prerequisiteValidator, + new AIExecutionModeResolver(new ConfigurationBuilder().Build()), + NullLogger.Instance, + Substitute.For>()); + + await Should.ThrowAsync(() => service.GenerateAndSaveAsync([], "v1")); + + await aiService.DidNotReceive().GenerateAttachmentSummaryAsync(Arg.Any()); + } } diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/RateLimit/AIRateLimiterTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/RateLimit/AIRateLimiterTests.cs index 85fae0d005..ab35d12f62 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/RateLimit/AIRateLimiterTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/RateLimit/AIRateLimiterTests.cs @@ -57,6 +57,17 @@ public async Task GetStateAsync_Returns_Generating_When_ActivityProvider_HasActi state.IsGenerating.ShouldBeTrue(); } + [Fact] + public async Task GetStateAsync_Does_Not_Acquire_Cooldown_Lock() + { + var lockProvider = new CountingDistributedLockProvider(); + var limiter = new AIRateLimiter(_cache, _currentUser, _configuration, lockProvider, []); + + await limiter.GetStateAsync(); + + lockProvider.CreatedLockCount.ShouldBe(0); + } + [Fact] public async Task EnsureAsync_AllowsThrough_When_NoCooldown() { @@ -167,6 +178,17 @@ private sealed class TestDistributedLockProvider : IDistributedLockProvider public IDistributedLock CreateLock(string name) => new TestDistributedLock(name); } + private sealed class CountingDistributedLockProvider : IDistributedLockProvider + { + public int CreatedLockCount { get; private set; } + + public IDistributedLock CreateLock(string name) + { + CreatedLockCount++; + return new TestDistributedLock(name); + } + } + private sealed class TestDistributedLock(string name) : IDistributedLock { private static readonly SemaphoreSlim Gate = new(1, 1); diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/OpenAIPromptRendererTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/OpenAIPromptRendererTests.cs index cc2c48823d..527d232c46 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/OpenAIPromptRendererTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/OpenAIPromptRendererTests.cs @@ -1,5 +1,8 @@ using Shouldly; using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Reflection; using Unity.AI.Runtime; using Xunit; @@ -34,12 +37,74 @@ public void BuildApplicationScoringUserPrompt_Should_Render_Structured_Empty_Att "v1", "{}", "[]", - "{\"name\":\"Test\",\"questions\":[]}", + "{\"name\":\"Test\",\"questions\":[{\"id\":\"q1\",\"type\":\"YesNo\"}]}", "{}"); - prompt.ShouldContain("ATTACHMENTS"); - prompt.ShouldContain("[]"); - prompt.ShouldContain("If ATTACHMENTS is empty, use DATA only"); - prompt.ShouldNotContain("No attachments provided."); + var normalized = prompt.Replace("\r\n", "\n"); + + normalized.ShouldContain("ATTACHMENTS\n[]\n\nSECTION"); + normalized.ShouldContain("If ATTACHMENTS is empty, use DATA only"); + normalized.ShouldNotContain("No attachments provided."); + } + + [Fact] + public void BuildApplicationAnalysisUserPrompt_Should_Not_Treat_FormIo_Mustache_Data_As_Prompt_Placeholders() + { + const string data = """ + { + "template": "{{ item.label }}", + "fileNameTemplate": "{{fileName}}", + "sector": "{{ item.SectorName }}", + "topicSourceId": "{{ item.topic_source_id }}", + "uppercaseTemplateValue": "{{FORMIO_VALUE}}" + } + """; + + var result = OpenAIPromptRenderer.BuildApplicationAnalysisUserPrompt( + "v1", + "{}", + data, + "[]"); + + result.ShouldContain("{{ item.label }}"); + result.ShouldContain("{{fileName}}"); + result.ShouldContain("{{ item.SectorName }}"); + result.ShouldContain("{{ item.topic_source_id }}"); + result.ShouldContain("{{FORMIO_VALUE}}"); + } + + [Fact] + public void RenderPromptTemplate_Should_Reject_Non_Prompt_Placeholders_In_Template_Text() + { + var templateCache = GetPromptTemplateCache(); + templateCache["v1/unit-test.invalid"] = "Bad template token: {{ item.label }}"; + + var exception = Should.Throw(() => + RenderPromptTemplate("v1", "unit-test.invalid", new Dictionary())); + + exception.InnerException.ShouldBeOfType(); + exception.InnerException!.Message.ShouldContain("Invalid prompt placeholders"); + exception.InnerException.Message.ShouldContain("item.label"); + } + + private static string RenderPromptTemplate( + string version, + string templateName, + IReadOnlyDictionary runtimeReplacements) + { + var method = typeof(OpenAIPromptRenderer).GetMethod( + "RenderPromptTemplate", + BindingFlags.Static | BindingFlags.NonPublic); + method.ShouldNotBeNull(); + return (string)method.Invoke(null, [version, templateName, runtimeReplacements])!; + } + + private static ConcurrentDictionary GetPromptTemplateCache() + { + var field = typeof(OpenAIPromptRenderer).GetField( + "PromptTemplateCache", + BindingFlags.Static | BindingFlags.NonPublic); + field.ShouldNotBeNull(); + return (ConcurrentDictionary)field.GetValue(null)!; } } diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationQueueTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationQueueTests.cs index f499604bc3..99e3a4c22f 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationQueueTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationQueueTests.cs @@ -1,4 +1,5 @@ using Medallion.Threading; +using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using NSubstitute; using Shouldly; @@ -7,13 +8,18 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Unity.AI.Features; +using Unity.AI.Localization; using Unity.AI.RateLimit; +using Unity.AI.Operations; using Unity.GrantManager.GrantApplications; using Unity.GrantManager.GrantApplications.Automation; using Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; using Volo.Abp.BackgroundJobs; using Volo.Abp.Domain.Repositories; using Volo.Abp.DistributedLocking; +using Volo.Abp; +using Volo.Abp.Features; using Volo.Abp.Users; using Xunit; using Xunit.Abstractions; @@ -63,6 +69,101 @@ await repository.Received(1).InsertAsync(Arg.Is(r => r.Status == AIGenerationRequestStatus.Queued), Arg.Any(), Arg.Any()); } + [Fact] + public async Task QueueAllAIStagesAsync_Should_Enqueue_When_Any_Enabled_Stage_Has_Required_Input() + { + var applicationId = Guid.NewGuid(); + var tenantId = Guid.NewGuid(); + var repository = Substitute.For>(); + repository.GetQueryableAsync().Returns(Task.FromResult>(Array.Empty().AsQueryable())); + repository.InsertAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(callInfo => Task.FromResult(callInfo.Arg())); + + var backgroundJobManager = Substitute.For(); + var prerequisiteValidator = Substitute.For(); + prerequisiteValidator.EnsureAttachmentSummaryAvailableAsync(applicationId) + .Returns(_ => throw new UserFriendlyException("No attachments are available to summarize.")); + prerequisiteValidator.EnsureApplicationAnalysisAvailableAsync(applicationId).Returns(Task.CompletedTask); + prerequisiteValidator.EnsureApplicationScoringAvailableAsync(applicationId) + .Returns(_ => throw new UserFriendlyException("AI scoring requires a configured scoresheet.")); + + var queue = CreateQueue( + backgroundJobManager, + repository, + prerequisiteValidator: prerequisiteValidator); + + await queue.QueueAllAIStagesAsync(applicationId, tenantId); + + await repository.Received(1).InsertAsync(Arg.Any(), Arg.Any(), Arg.Any()); + await backgroundJobManager.Received(1).EnqueueAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task QueueAllAIStagesAsync_Should_Not_Insert_Or_Enqueue_When_No_Enabled_Stage_Has_Required_Input() + { + var applicationId = Guid.NewGuid(); + var tenantId = Guid.NewGuid(); + var repository = Substitute.For>(); + repository.GetQueryableAsync().Returns(Task.FromResult>(Array.Empty().AsQueryable())); + + var backgroundJobManager = Substitute.For(); + var rateLimiter = Substitute.For(); + rateLimiter.EnsureAsync().Returns(Task.CompletedTask); + var prerequisiteValidator = Substitute.For(); + prerequisiteValidator.EnsureAttachmentSummaryAvailableAsync(applicationId) + .Returns(_ => throw new UserFriendlyException("No attachments are available to summarize.")); + prerequisiteValidator.EnsureApplicationAnalysisAvailableAsync(applicationId) + .Returns(_ => throw new UserFriendlyException("AI application analysis requires application submission data.")); + prerequisiteValidator.EnsureApplicationScoringAvailableAsync(applicationId) + .Returns(_ => throw new UserFriendlyException("AI scoring requires a configured scoresheet.")); + + var queue = CreateQueue( + backgroundJobManager, + repository, + rateLimiter, + prerequisiteValidator); + + await Should.ThrowAsync(() => queue.QueueAllAIStagesAsync(applicationId, tenantId)); + + await rateLimiter.DidNotReceive().EnsureAsync(); + await repository.DidNotReceive().InsertAsync(Arg.Any(), Arg.Any(), Arg.Any()); + await backgroundJobManager.DidNotReceive().EnqueueAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task QueueAllAIStagesAsync_Should_Not_Insert_Or_Enqueue_When_No_AI_Stages_Are_Enabled() + { + var applicationId = Guid.NewGuid(); + var tenantId = Guid.NewGuid(); + var repository = Substitute.For>(); + repository.GetQueryableAsync().Returns(Task.FromResult>(Array.Empty().AsQueryable())); + + var backgroundJobManager = Substitute.For(); + var rateLimiter = Substitute.For(); + rateLimiter.EnsureAsync().Returns(Task.CompletedTask); + var prerequisiteValidator = Substitute.For(); + var featureChecker = Substitute.For(); + featureChecker.IsEnabledAsync(AIFeatures.AttachmentSummaries).Returns(Task.FromResult(false)); + featureChecker.IsEnabledAsync(AIFeatures.ApplicationAnalysis).Returns(Task.FromResult(false)); + featureChecker.IsEnabledAsync(AIFeatures.Scoring).Returns(Task.FromResult(false)); + + var queue = CreateQueue( + backgroundJobManager, + repository, + rateLimiter, + prerequisiteValidator, + featureChecker); + + await Should.ThrowAsync(() => queue.QueueAllAIStagesAsync(applicationId, tenantId)); + + await prerequisiteValidator.DidNotReceive().EnsureAttachmentSummaryAvailableAsync(Arg.Any()); + await prerequisiteValidator.DidNotReceive().EnsureApplicationAnalysisAvailableAsync(Arg.Any()); + await prerequisiteValidator.DidNotReceive().EnsureApplicationScoringAvailableAsync(Arg.Any()); + await rateLimiter.DidNotReceive().EnsureAsync(); + await repository.DidNotReceive().InsertAsync(Arg.Any(), Arg.Any(), Arg.Any()); + await backgroundJobManager.DidNotReceive().EnqueueAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + [Fact] public async Task QueueApplicationAnalysisAsync_Should_Not_Enqueue_When_An_Active_Request_Already_Exists() { @@ -175,6 +276,29 @@ public async Task QueueApplicationAnalysisAsync_Should_Not_Insert_Or_Enqueue_Whe await backgroundJobManager.DidNotReceive().EnqueueAsync(Arg.Any(), Arg.Any(), Arg.Any()); } + [Fact] + public async Task QueueAttachmentSummaryAsync_Should_Not_Insert_Or_Enqueue_When_No_Attachments_Are_Available() + { + var applicationId = Guid.NewGuid(); + var tenantId = Guid.NewGuid(); + var repository = Substitute.For>(); + repository.GetQueryableAsync().Returns(Task.FromResult>(Array.Empty().AsQueryable())); + + var backgroundJobManager = Substitute.For(); + var rateLimiter = Substitute.For(); + rateLimiter.EnsureAsync().Returns(Task.CompletedTask); + var prerequisiteValidator = Substitute.For(); + prerequisiteValidator.EnsureAttachmentSummaryAvailableAsync(applicationId) + .Returns(_ => throw new UserFriendlyException("No attachments are available to summarize.")); + var queue = CreateQueue(backgroundJobManager, repository, rateLimiter, prerequisiteValidator); + + await Should.ThrowAsync(() => queue.QueueAttachmentSummaryAsync(applicationId, tenantId)); + + await rateLimiter.DidNotReceive().EnsureAsync(); + await repository.DidNotReceive().InsertAsync(Arg.Any(), Arg.Any(), Arg.Any()); + await backgroundJobManager.DidNotReceive().EnqueueAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + [Fact] public async Task QueueAttachmentSummaryAsync_Should_Not_Enqueue_When_An_Active_Request_Already_Exists() { @@ -349,7 +473,9 @@ public ValueTask DisposeAsync() private static ApplicationAIGenerationQueue CreateQueue( IBackgroundJobManager backgroundJobManager, IRepository repository, - Unity.AI.RateLimit.IAIRateLimiter? rateLimiter = null) + Unity.AI.RateLimit.IAIRateLimiter? rateLimiter = null, + IAIGenerationPrerequisiteValidator? prerequisiteValidator = null, + IFeatureChecker? featureChecker = null) { if (rateLimiter == null) { @@ -357,13 +483,32 @@ private static ApplicationAIGenerationQueue CreateQueue( rateLimiter.EnsureAsync().Returns(Task.CompletedTask); } + if (prerequisiteValidator == null) + { + prerequisiteValidator = Substitute.For(); + prerequisiteValidator.EnsureAttachmentSummaryAvailableAsync(Arg.Any()).Returns(Task.CompletedTask); + prerequisiteValidator.EnsureApplicationAnalysisAvailableAsync(Arg.Any()).Returns(Task.CompletedTask); + prerequisiteValidator.EnsureApplicationScoringAvailableAsync(Arg.Any()).Returns(Task.CompletedTask); + } + + if (featureChecker == null) + { + featureChecker = Substitute.For(); + featureChecker.IsEnabledAsync(AIFeatures.AttachmentSummaries).Returns(Task.FromResult(true)); + featureChecker.IsEnabledAsync(AIFeatures.ApplicationAnalysis).Returns(Task.FromResult(true)); + featureChecker.IsEnabledAsync(AIFeatures.Scoring).Returns(Task.FromResult(true)); + } + return new ApplicationAIGenerationQueue( backgroundJobManager, repository, new TestDistributedLockProvider(), + prerequisiteValidator, + featureChecker, rateLimiter, CreateCurrentUser(), - Substitute.For>()); + Substitute.For>(), + Substitute.For>()); } private static readonly Guid CreateQueueCurrentUserId = Guid.NewGuid(); diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/RunApplicationAIPipelineJobTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/RunApplicationAIPipelineJobTests.cs index bc24754ba3..a8eee00532 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/RunApplicationAIPipelineJobTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/RunApplicationAIPipelineJobTests.cs @@ -6,10 +6,12 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Unity.AI.Operations; using Unity.GrantManager.Applications; using Unity.GrantManager.Attachments; using Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; using Unity.AI.RateLimit; +using Volo.Abp; using Volo.Abp.Domain.Repositories; using Volo.Abp.EventBus.Local; using Volo.Abp.Features; @@ -87,6 +89,49 @@ await localEventBus.Received(1).PublishAsync( Arg.Is(x => x.ApplicationId == request.ApplicationId)); } + [Fact] + public async Task ExecuteAsync_Should_Skip_Unavailable_Stage_And_Run_Available_Stage() + { + var featureChecker = Substitute.For(); + featureChecker.IsEnabledAsync("Unity.AI.AttachmentSummaries").Returns(true); + featureChecker.IsEnabledAsync("Unity.AI.ApplicationAnalysis").Returns(true); + featureChecker.IsEnabledAsync("Unity.AI.Scoring").Returns(false); + + var repository = BuildRequestRepository(out var requests); + var request = CreateRequest(); + requests.Add(request); + + var prerequisiteValidator = Substitute.For(); + prerequisiteValidator.EnsureAttachmentSummaryAvailableAsync(request.ApplicationId!.Value) + .Returns(_ => throw new UserFriendlyException("No attachments are available to summarize.")); + prerequisiteValidator.EnsureApplicationAnalysisAvailableAsync(request.ApplicationId.Value) + .Returns(Task.CompletedTask); + + var attachmentSummaryAppService = Substitute.For(); + var applicationAnalysisAppService = Substitute.For(); + applicationAnalysisAppService.GenerateApplicationAnalysisForPipelineAsync(Arg.Any(), Arg.Any()) + .Returns(new ApplicationAnalysisResultDto { Completed = true }); + + var job = BuildJob( + featureChecker, + repository, + attachmentSummaryAppService: attachmentSummaryAppService, + applicationAnalysisAppService: applicationAnalysisAppService, + prerequisiteValidator: prerequisiteValidator); + + await job.ExecuteAsync(new RunApplicationAIPipelineJobArgs + { + ApplicationId = request.ApplicationId.Value, + RequestKey = request.RequestKey, + RequestedByUserId = Guid.NewGuid(), + TenantId = request.TenantId + }); + + request.Status.ShouldBe(AIGenerationRequestStatus.Completed); + await attachmentSummaryAppService.DidNotReceive().GenerateAttachmentSummariesForPipelineAsync(Arg.Any>(), Arg.Any()); + await applicationAnalysisAppService.Received(1).GenerateApplicationAnalysisForPipelineAsync(request.ApplicationId.Value, Arg.Any()); + } + private RunApplicationAIPipelineJob BuildJob( IFeatureChecker featureChecker, IRepository generationRequestRepository, @@ -94,13 +139,23 @@ private RunApplicationAIPipelineJob BuildJob( IAttachmentSummaryAppService? attachmentSummaryAppService = null, IApplicationAnalysisAppService? applicationAnalysisAppService = null, IApplicationScoringAppService? applicationScoringAppService = null, + IAIGenerationPrerequisiteValidator? prerequisiteValidator = null, IAIRateLimiter? rateLimiter = null) { + if (prerequisiteValidator == null) + { + prerequisiteValidator = Substitute.For(); + prerequisiteValidator.EnsureAttachmentSummaryAvailableAsync(Arg.Any()).Returns(Task.CompletedTask); + prerequisiteValidator.EnsureApplicationAnalysisAvailableAsync(Arg.Any()).Returns(Task.CompletedTask); + prerequisiteValidator.EnsureApplicationScoringAvailableAsync(Arg.Any()).Returns(Task.CompletedTask); + } + return new RunApplicationAIPipelineJob( Substitute.For(), attachmentSummaryAppService ?? Substitute.For(), applicationAnalysisAppService ?? Substitute.For(), applicationScoringAppService ?? Substitute.For(), + prerequisiteValidator, featureChecker, localEventBus ?? Substitute.For(), generationRequestRepository, diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/GrantApplicationAIGenerationStatusTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/GrantApplicationAIGenerationStatusTests.cs new file mode 100644 index 0000000000..a8323f6cc8 --- /dev/null +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/GrantApplicationAIGenerationStatusTests.cs @@ -0,0 +1,79 @@ +using NSubstitute; +using Shouldly; +using System; +using System.Threading.Tasks; +using Unity.AI.Automation; +using Unity.AI.RateLimit; +using Unity.GrantManager.Applicants; +using Unity.GrantManager.ApplicationForms; +using Unity.GrantManager.Applications; +using Unity.GrantManager.Flex; +using Unity.GrantManager.GlobalTag; +using Unity.GrantManager.Identity; +using Unity.GrantManager.Payments; +using Unity.Payments.PaymentRequests; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Features; +using Xunit; +using Xunit.Abstractions; + +namespace Unity.GrantManager.GrantApplications; + +public class GrantApplicationAIGenerationStatusTests(ITestOutputHelper outputHelper) : GrantManagerApplicationTestBase(outputHelper) +{ + [Fact] + public async Task GetAIGenerationStatusAsync_Should_Return_RateLimit_State_When_No_Generation_Request_Exists() + { + var applicationId = Guid.NewGuid(); + var aiGenerationStatusAppService = Substitute.For(); + aiGenerationStatusAppService + .GetLatestAsync(applicationId, AIGenerationRequestKeyHelper.ApplicationAnalysisOperationType, null) + .Returns((AIGenerationRequestDto?)null); + + var aiRateLimiter = Substitute.For(); + aiRateLimiter.GetStateAsync().Returns(new AIRateLimitStateDto + { + IsGenerating = true, + RetryAfterSeconds = 17 + }); + + var appService = CreateAppService(aiGenerationStatusAppService, aiRateLimiter); + + var result = await appService.GetAIGenerationStatusAsync( + applicationId, + AIGenerationRequestKeyHelper.ApplicationAnalysisOperationType); + + result.ShouldNotBeNull(); + result.GenerationRequest.ShouldBeNull(); + result.IsGenerating.ShouldBeTrue(); + result.RetryAfterSeconds.ShouldBe(17); + await aiGenerationStatusAppService.Received(1) + .GetLatestAsync(applicationId, AIGenerationRequestKeyHelper.ApplicationAnalysisOperationType, null); + await aiRateLimiter.Received(1).GetStateAsync(); + } + + private GrantApplicationAppService CreateAppService( + IAIGenerationStatusAppService aiGenerationStatusAppService, + IAIRateLimiter aiRateLimiter) + { + var appService = new GrantApplicationAppService( + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + aiGenerationStatusAppService, + aiRateLimiter, + Substitute.For()); + + appService.LazyServiceProvider = GetRequiredService(); + return appService; + } +} diff --git a/documentation/reporting/reporting-configuration.md b/documentation/reporting/reporting-configuration.md index 7deb364e05..9d607119b1 100644 --- a/documentation/reporting/reporting-configuration.md +++ b/documentation/reporting/reporting-configuration.md @@ -162,6 +162,7 @@ The **Path** column shows the data-centric navigation path used by the PostgreSQ | Internal `path` (not displayed) | Internal `typePath` | Displayed "Path" (DataPath) | Why | | -------------------------------- | ------------------- | --------------------------- | --- | | `panel1->firstName` | `panel->textfield` | `firstName` | `panel` is a container — stripped | +| `tab1->firstName` | `tab->textfield` | `firstName` | `tab` is a container — stripped | | `datagrid1->itemName` | `datagrid->textfield` | `datagrid1->itemName` | `datagrid` is kept — it IS in the JSON | | `checkboxField->optionA` | `simplecheckboxes->option` | `checkboxField->optionA` | Both segments present in data |