diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Extraction/ITextExtractionService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Extraction/ITextExtractionService.cs index 5ad933602..5db96abd7 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Extraction/ITextExtractionService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Extraction/ITextExtractionService.cs @@ -1,10 +1,11 @@ using System.IO; +using System.Threading; using System.Threading.Tasks; namespace Unity.AI.Extraction { public interface ITextExtractionService { - Task ExtractTextAsync(string fileName, Stream fileContent, string contentType); + Task ExtractTextAsync(string fileName, Stream fileContent, string contentType, CancellationToken cancellationToken = default); } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/IAIService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/IAIService.cs index 84c1e1c08..0606899c8 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/IAIService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/IAIService.cs @@ -1,3 +1,4 @@ +using System.Threading; using System.Threading.Tasks; using Unity.AI.Requests; using Unity.AI.Responses; @@ -8,9 +9,8 @@ public interface IAIService { Task IsAvailableAsync(); - Task GenerateCompletionAsync(AICompletionRequest request); - Task GenerateAttachmentSummaryAsync(AttachmentSummaryRequest request); - Task GenerateApplicationAnalysisAsync(ApplicationAnalysisRequest request); - Task GenerateApplicationScoringAsync(ApplicationScoringRequest request); + Task GenerateAttachmentSummaryAsync(AttachmentSummaryRequest request, CancellationToken cancellationToken = default); + Task GenerateApplicationAnalysisAsync(ApplicationAnalysisRequest request, CancellationToken cancellationToken = default); + Task GenerateApplicationScoringAsync(ApplicationScoringRequest request, CancellationToken cancellationToken = default); } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Requests/AICompletionRequest.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Requests/AICompletionRequest.cs deleted file mode 100644 index cc134ef98..000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Requests/AICompletionRequest.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Unity.AI.Requests -{ - public class AICompletionRequest - { - [JsonPropertyName("userPrompt")] - public string UserPrompt { get; set; } = string.Empty; - - [JsonPropertyName("maxTokens")] - public int MaxTokens { get; set; } = 150; - - [JsonPropertyName("temperature")] - public double? Temperature { get; set; } - } -} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/AICompletionResponse.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/AICompletionResponse.cs deleted file mode 100644 index 0f0c1c8ef..000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/AICompletionResponse.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Unity.AI.Responses -{ - public class AICompletionResponse - { - [JsonPropertyName("content")] - public string Content { get; set; } = string.Empty; - } -} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/RateLimit/AIRateLimitStateDto.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/RateLimit/AIRateLimitStateDto.cs new file mode 100644 index 000000000..9f53b07bb --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/RateLimit/AIRateLimitStateDto.cs @@ -0,0 +1,6 @@ +namespace Unity.AI.RateLimit; + +public class AIRateLimitStateDto +{ + public int RetryAfterSeconds { get; set; } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/RateLimit/IAIRateLimitAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/RateLimit/IAIRateLimitAppService.cs new file mode 100644 index 000000000..cb117a877 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/RateLimit/IAIRateLimitAppService.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace Unity.AI.RateLimit; + +public interface IAIRateLimitAppService : IApplicationService +{ + Task GetStateAsync(); +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/RateLimit/IAIRateLimiter.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/RateLimit/IAIRateLimiter.cs new file mode 100644 index 000000000..cf2317e2e --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/RateLimit/IAIRateLimiter.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using System; + +namespace Unity.AI.RateLimit; + +public interface IAIRateLimiter +{ + /// + /// Throws if the current user is still + /// inside their AI generate cooldown window. + /// + Task EnsureAsync(); + + /// + /// Starts a fresh cooldown for the current user. No-op for callers without a user. + /// + Task StampAsync(); + + /// + /// Starts a fresh cooldown for the supplied user. No-op when userId is null. + /// + Task StampAsync(Guid? userId); + + /// + /// Returns the remaining cooldown for the current user. RetryAfterSeconds is 0 when + /// the user can generate immediately. + /// + Task GetStateAsync(); +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Extraction/TextExtractionService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Extraction/TextExtractionService.cs index e7490cfac..1da60206a 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Extraction/TextExtractionService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Extraction/TextExtractionService.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; using UglyToad.PdfPig; @@ -32,7 +33,7 @@ public TextExtractionService(ILogger logger) _logger = logger; } - public Task ExtractTextAsync(string fileName, Stream fileContent, string contentType) + public Task ExtractTextAsync(string fileName, Stream fileContent, string contentType, CancellationToken cancellationToken = default) { if (fileContent == null) { @@ -42,6 +43,7 @@ public Task ExtractTextAsync(string fileName, Stream fileContent, string try { + cancellationToken.ThrowIfCancellationRequested(); var normalizedContentType = contentType?.ToLowerInvariant() ?? string.Empty; var extension = Path.GetExtension(fileName)?.ToLowerInvariant() ?? string.Empty; @@ -53,12 +55,12 @@ public Task ExtractTextAsync(string fileName, Stream fileContent, string var rawText = extension switch { - ".txt" or ".csv" or ".json" or ".xml" => ExtractTextFromTextFile(fileContent), - ".pdf" => ExtractTextFromPdfFile(fileName, fileContent), - ".docx" => ExtractTextFromWordDocx(fileName, fileContent), - ".xls" or ".xlsx" => ExtractTextFromExcelFile(fileName, fileContent), - ".pptx" => ExtractTextFromPowerPointFile(fileName, fileContent), - _ => ExtractByContentType(fileName, fileContent, normalizedContentType) + ".txt" or ".csv" or ".json" or ".xml" => ExtractTextFromTextFile(fileContent, cancellationToken), + ".pdf" => ExtractTextFromPdfFile(fileName, fileContent, cancellationToken), + ".docx" => ExtractTextFromWordDocx(fileName, fileContent, cancellationToken), + ".xls" or ".xlsx" => ExtractTextFromExcelFile(fileName, fileContent, cancellationToken), + ".pptx" => ExtractTextFromPowerPointFile(fileName, fileContent, cancellationToken), + _ => ExtractByContentType(fileName, fileContent, normalizedContentType, cancellationToken) }; if (string.IsNullOrEmpty(rawText)) @@ -69,6 +71,10 @@ public Task ExtractTextAsync(string fileName, Stream fileContent, string return Task.FromResult(NormalizeAndLimitText(rawText, fileName)); } + catch (OperationCanceledException) + { + throw; + } catch (Exception ex) { _logger.LogError(ex, "Error extracting text from {FileName}", fileName); @@ -76,29 +82,33 @@ public Task ExtractTextAsync(string fileName, Stream fileContent, string } } - private string ExtractByContentType(string fileName, Stream fileContent, string normalizedContentType) + private string ExtractByContentType( + string fileName, + Stream fileContent, + string normalizedContentType, + CancellationToken cancellationToken) { if (normalizedContentType.Contains("text/")) { - return ExtractTextFromTextFile(fileContent); + return ExtractTextFromTextFile(fileContent, cancellationToken); } if (normalizedContentType.Contains("pdf")) { - return ExtractTextFromPdfFile(fileName, fileContent); + return ExtractTextFromPdfFile(fileName, fileContent, cancellationToken); } if (normalizedContentType.Contains("word") || normalizedContentType.Contains("msword") || normalizedContentType.Contains("officedocument.wordprocessingml")) { - return ExtractTextFromWordDocx(fileName, fileContent); + return ExtractTextFromWordDocx(fileName, fileContent, cancellationToken); } if (normalizedContentType.Contains("excel") || normalizedContentType.Contains("spreadsheet")) { - return ExtractTextFromExcelFile(fileName, fileContent); + return ExtractTextFromExcelFile(fileName, fileContent, cancellationToken); } if (normalizedContentType.Contains("presentation") || normalizedContentType.Contains("powerpoint")) { - return ExtractTextFromPowerPointFile(fileName, fileContent); + return ExtractTextFromPowerPointFile(fileName, fileContent, cancellationToken); } return string.Empty; } @@ -111,7 +121,7 @@ private static void RewindIfPossible(Stream stream) } } - private string ExtractTextFromTextFile(Stream fileContent) + private string ExtractTextFromTextFile(Stream fileContent, CancellationToken cancellationToken) { try { @@ -122,6 +132,7 @@ private string ExtractTextFromTextFile(Stream fileContent) int read; while ((read = reader.Read(buffer, 0, buffer.Length)) > 0) { + cancellationToken.ThrowIfCancellationRequested(); var remaining = MaxExtractedTextLength - builder.Length; if (remaining <= 0) break; builder.Append(buffer, 0, Math.Min(read, remaining)); @@ -142,7 +153,7 @@ private string ExtractTextFromTextFile(Stream fileContent) } } - private string ExtractTextFromPdfFile(string fileName, Stream fileContent) + private string ExtractTextFromPdfFile(string fileName, Stream fileContent, CancellationToken cancellationToken) { try { @@ -156,6 +167,7 @@ private string ExtractTextFromPdfFile(string fileName, Stream fileContent) foreach (var pageText in pageTexts) { + cancellationToken.ThrowIfCancellationRequested(); if (builder.Length >= MaxExtractedTextLength) { break; @@ -178,15 +190,15 @@ private string ExtractTextFromPdfFile(string fileName, Stream fileContent) } } - private string ExtractTextFromWordDocx(string fileName, Stream fileContent) + private string ExtractTextFromWordDocx(string fileName, Stream fileContent, CancellationToken cancellationToken) { try { RewindIfPossible(fileContent); using var document = new XWPFDocument(fileContent); var builder = new StringBuilder(); - var processedParagraphCount = AppendDocxParagraphText(document, builder); - var processedTableRowCount = AppendDocxTableText(document, builder); + var processedParagraphCount = AppendDocxParagraphText(document, builder, cancellationToken); + var processedTableRowCount = AppendDocxTableText(document, builder, cancellationToken); _logger.LogDebug( "Extracted Word text from {ProcessedParagraphCount} paragraphs and {ProcessedTableRowCount} table rows for {FileName}", @@ -202,7 +214,10 @@ private string ExtractTextFromWordDocx(string fileName, Stream fileContent) } } - private static int AppendDocxParagraphText(XWPFDocument document, StringBuilder builder) + private static int AppendDocxParagraphText( + XWPFDocument document, + StringBuilder builder, + CancellationToken cancellationToken) { var processedParagraphCount = 0; var paragraphTexts = document.Paragraphs @@ -212,6 +227,7 @@ private static int AppendDocxParagraphText(XWPFDocument document, StringBuilder foreach (var paragraphText in paragraphTexts) { + cancellationToken.ThrowIfCancellationRequested(); if (builder.Length >= MaxExtractedTextLength) { break; @@ -227,7 +243,10 @@ private static int AppendDocxParagraphText(XWPFDocument document, StringBuilder return processedParagraphCount; } - private static int AppendDocxTableText(XWPFDocument document, StringBuilder builder) + private static int AppendDocxTableText( + XWPFDocument document, + StringBuilder builder, + CancellationToken cancellationToken) { if (builder.Length >= MaxExtractedTextLength) { @@ -237,8 +256,10 @@ private static int AppendDocxTableText(XWPFDocument document, StringBuilder buil var processedTableRowCount = 0; foreach (var table in document.Tables) { + cancellationToken.ThrowIfCancellationRequested(); foreach (var row in table.Rows.Take(MaxDocxTableRows)) { + cancellationToken.ThrowIfCancellationRequested(); if (builder.Length >= MaxExtractedTextLength) { return processedTableRowCount; @@ -252,6 +273,7 @@ private static int AppendDocxTableText(XWPFDocument document, StringBuilder buil var rowHadValue = false; foreach (var cellText in cellTexts) { + cancellationToken.ThrowIfCancellationRequested(); rowHadValue = true; if (TryAppendWithTrailingNewline(builder, cellText)) { @@ -269,7 +291,7 @@ private static int AppendDocxTableText(XWPFDocument document, StringBuilder buil return processedTableRowCount; } - private string ExtractTextFromExcelFile(string fileName, Stream fileContent) + private string ExtractTextFromExcelFile(string fileName, Stream fileContent, CancellationToken cancellationToken) { try { @@ -282,13 +304,14 @@ private string ExtractTextFromExcelFile(string fileName, Stream fileContent) for (var sheetIndex = 0; sheetIndex < sheetCount; sheetIndex++) { + cancellationToken.ThrowIfCancellationRequested(); if (builder.Length >= MaxExtractedTextLength) { break; } var sheet = workbook.GetSheetAt(sheetIndex); - var (rowsProcessed, limitReached) = TryAppendExcelSheet(sheet, builder); + var (rowsProcessed, limitReached) = TryAppendExcelSheet(sheet, builder, cancellationToken); if (rowsProcessed > 0) { processedSheetCount++; @@ -315,7 +338,7 @@ private string ExtractTextFromExcelFile(string fileName, Stream fileContent) } } - private string ExtractTextFromPowerPointFile(string fileName, Stream fileContent) + private string ExtractTextFromPowerPointFile(string fileName, Stream fileContent, CancellationToken cancellationToken) { try { @@ -328,6 +351,7 @@ private string ExtractTextFromPowerPointFile(string fileName, Stream fileContent foreach (var slideEntry in slideEntries) { + cancellationToken.ThrowIfCancellationRequested(); if (builder.Length >= MaxExtractedTextLength) { break; @@ -398,7 +422,10 @@ private IEnumerable GetOrderedPowerPointSlideEntries(ZipArchive return orderedEntries; } - private static (int RowsProcessed, bool LimitReached) TryAppendExcelSheet(ISheet? sheet, StringBuilder builder) + private static (int RowsProcessed, bool LimitReached) TryAppendExcelSheet( + ISheet? sheet, + StringBuilder builder, + CancellationToken cancellationToken) { if (sheet == null) { @@ -408,12 +435,13 @@ private static (int RowsProcessed, bool LimitReached) TryAppendExcelSheet(ISheet var processedRows = 0; foreach (IRow row in sheet) { + cancellationToken.ThrowIfCancellationRequested(); if (processedRows >= MaxExcelRowsPerSheet || builder.Length >= MaxExtractedTextLength) { break; } - var (rowHadValue, limitReached) = TryAppendExcelRow(row, builder); + var (rowHadValue, limitReached) = TryAppendExcelRow(row, builder, cancellationToken); if (rowHadValue) { processedRows++; @@ -428,11 +456,15 @@ private static (int RowsProcessed, bool LimitReached) TryAppendExcelSheet(ISheet return (processedRows, builder.Length >= MaxExtractedTextLength); } - private static (bool RowHadValue, bool LimitReached) TryAppendExcelRow(IRow row, StringBuilder builder) + private static (bool RowHadValue, bool LimitReached) TryAppendExcelRow( + IRow row, + StringBuilder builder, + CancellationToken cancellationToken) { var rowHasValue = false; foreach (var cell in row.Cells.Take(MaxExcelCellsPerRow)) { + cancellationToken.ThrowIfCancellationRequested(); var value = GetCellText(cell); if (string.IsNullOrWhiteSpace(value)) { 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 473a50d5b..12b9b2434 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 @@ -4,10 +4,12 @@ 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; using Unity.GrantManager.Applications; using Volo.Abp.DependencyInjection; @@ -21,18 +23,13 @@ public class ApplicationAnalysisService( IAIService aiService, ILogger logger) : IApplicationAnalysisService, ITransientDependency { - private readonly JsonSerializerOptions _jsonOptionsIndented = new() - { - WriteIndented = true - }; - private const string ComponentsKey = "components"; private static readonly HashSet ExcludedSchemaKeys = new(StringComparer.OrdinalIgnoreCase) { "applicantAgent" }; - public async Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null) + public async Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null, CancellationToken cancellationToken = default) { var application = await applicationRepository.GetAsync(applicationId); var formSubmission = await applicationFormSubmissionRepository.GetByApplicationAsync(applicationId); @@ -60,9 +57,9 @@ public async Task RegenerateAndSaveAsync(Guid applicationId, string? pro Data = PromptDataPayloadBuilder.BuildPromptDataPayload(application, formSubmission, formSchema, logger), Attachments = attachmentSummaries, PromptVersion = promptVersion, - }); + }, cancellationToken); - var analysisJson = JsonSerializer.Serialize(analysis, _jsonOptionsIndented); + var analysisJson = JsonSerializer.Serialize(analysis, AIJsonDefaults.Indented); application.AIAnalysis = analysisJson; await applicationRepository.UpdateAsync(application); return analysisJson; 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 b030aad9c..f6188f689 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 @@ -3,11 +3,13 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using Unity.Flex.Domain.Scoresheets; using Unity.AI.Models; using Unity.AI.Prompts; using Unity.AI.Requests; +using Unity.AI.Runtime; using Unity.GrantManager.Applications; using Volo.Abp.DependencyInjection; @@ -24,18 +26,7 @@ public class ApplicationScoringService( AIExecutionModeResolver executionModeResolver, ILogger logger) : IApplicationScoringService, ITransientDependency { - private readonly JsonSerializerOptions _jsonOptions = new() - { - WriteIndented = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; - - private readonly JsonSerializerOptions _jsonOptionsIndented = new() - { - WriteIndented = true - }; - - public async Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null) + public async Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null, CancellationToken cancellationToken = default) { var application = await applicationRepository.GetAsync(applicationId); var applicationForm = await applicationFormRepository.GetAsync(application.ApplicationFormId); @@ -70,8 +61,8 @@ public async Task RegenerateAndSaveAsync(Guid applicationId, string? pro var perSectionResults = await AIExecutionStrategy.RunAsync( sections, mode, - section => ProcessSectionAsync(applicationId, section, promptData, attachmentSummaries, promptVersion), - batch => ProcessSectionsAsync(applicationId, batch, promptData, attachmentSummaries, promptVersion)); + section => ProcessSectionAsync(applicationId, section, promptData, attachmentSummaries, promptVersion, cancellationToken), + batch => ProcessSectionsAsync(applicationId, batch, promptData, attachmentSummaries, promptVersion, cancellationToken)); var allSectionResults = new Dictionary(); foreach (var sectionResult in perSectionResults) @@ -82,7 +73,7 @@ public async Task RegenerateAndSaveAsync(Guid applicationId, string? pro } } - var combinedResults = JsonSerializer.Serialize(allSectionResults, _jsonOptionsIndented); + var combinedResults = JsonSerializer.Serialize(allSectionResults, AIJsonDefaults.Indented); var validatedJson = ValidateApplicationScoringJson(combinedResults); application.AIScoresheetAnswers = validatedJson; await applicationRepository.UpdateAsync(application); @@ -94,7 +85,8 @@ private async Task> ProcessSectionAsync( ScoresheetSection section, JsonElement promptData, List attachmentSummaries, - string? promptVersion) + string? promptVersion, + CancellationToken cancellationToken) { var sectionResults = new Dictionary(); try @@ -104,16 +96,20 @@ private async Task> ProcessSectionAsync( Data = promptData, Attachments = attachmentSummaries, SectionName = section.Name, - SectionSchema = JsonSerializer.SerializeToElement(BuildSectionQuestionsData(section), _jsonOptions), + SectionSchema = JsonSerializer.SerializeToElement(BuildSectionQuestionsData(section), AIJsonDefaults.IndentedCamelCase), PromptVersion = promptVersion, }; - var applicationScoringResponse = await aiService.GenerateApplicationScoringAsync(applicationScoringRequest); + var applicationScoringResponse = await aiService.GenerateApplicationScoringAsync(applicationScoringRequest, cancellationToken); if (applicationScoringResponse.Answers.Count > 0) { CopyAnswers(applicationScoringResponse.Answers, sectionResults); } } + catch (OperationCanceledException) + { + throw; + } catch (Exception ex) { logger.LogError(ex, "Error processing AI application scoring section {SectionName} for application {ApplicationId}", section.Name, applicationId); @@ -126,7 +122,8 @@ private async Task>> ProcessSectionsAsync( IReadOnlyCollection sections, JsonElement promptData, List attachmentSummaries, - string? promptVersion) + string? promptVersion, + CancellationToken cancellationToken) { var sectionResults = new Dictionary(); try @@ -141,16 +138,20 @@ private async Task>> ProcessSectionsAsync( Data = promptData, Attachments = attachmentSummaries, SectionName = "All Sections", - SectionSchema = JsonSerializer.SerializeToElement(questions, _jsonOptions), + SectionSchema = JsonSerializer.SerializeToElement(questions, AIJsonDefaults.IndentedCamelCase), PromptVersion = promptVersion, }; - var applicationScoringResponse = await aiService.GenerateApplicationScoringAsync(applicationScoringRequest); + var applicationScoringResponse = await aiService.GenerateApplicationScoringAsync(applicationScoringRequest, cancellationToken); if (applicationScoringResponse.Answers.Count > 0) { CopyAnswers(applicationScoringResponse.Answers, sectionResults); } } + catch (OperationCanceledException) + { + throw; + } catch (Exception ex) { logger.LogError(ex, "Error processing AI application scoring batch for application {ApplicationId}", applicationId); @@ -182,7 +183,7 @@ private static List BuildSectionQuestionsData(ScoresheetSection section) private void CopyAnswers(Dictionary answers, Dictionary results) { - var answersJson = JsonSerializer.Serialize(answers, _jsonOptions); + var answersJson = JsonSerializer.Serialize(answers, AIJsonDefaults.IndentedCamelCase); using var answersDoc = JsonDocument.Parse(answersJson); foreach (var property in answersDoc.RootElement.EnumerateObject()) { 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 073b66d86..156ccfda9 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 @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Unity.AI.Extraction; using Unity.AI.Requests; @@ -21,13 +22,13 @@ public class AttachmentSummaryService( { private const string SummaryGenerationFailedMessage = "AI summary generation failed."; - public async Task GenerateAndSaveAsync(Guid attachmentId, string? promptVersion = null) + public async Task GenerateAndSaveAsync(Guid attachmentId, string? promptVersion = null, CancellationToken cancellationToken = default) { var attachment = await applicationChefsFileAttachmentRepository.GetAsync(attachmentId); var fileName = string.IsNullOrWhiteSpace(attachment.FileName) ? "unknown" : attachment.FileName; - await using var attachmentStream = await OpenAttachmentStreamAsync(attachment, fileName); - var extractedText = await textExtractionService.ExtractTextAsync(fileName, attachmentStream.Content, attachmentStream.ContentType); + await using var attachmentStream = await OpenAttachmentStreamAsync(attachment, fileName, cancellationToken); + var extractedText = await textExtractionService.ExtractTextAsync(fileName, attachmentStream.Content, attachmentStream.ContentType, cancellationToken); var summaryResponse = await aiService.GenerateAttachmentSummaryAsync(new AttachmentSummaryRequest { @@ -35,7 +36,7 @@ public async Task GenerateAndSaveAsync(Guid attachmentId, string? prompt ContentType = attachmentStream.ContentType, ExtractedText = extractedText, PromptVersion = promptVersion, - }); + }, cancellationToken); attachment.AISummary = summaryResponse.Summary; await applicationChefsFileAttachmentRepository.UpdateAsync(attachment); @@ -43,7 +44,7 @@ public async Task GenerateAndSaveAsync(Guid attachmentId, string? prompt return summaryResponse.Summary; } - public async Task> GenerateAndSaveAsync(IEnumerable attachmentIds, string? promptVersion = null) + public async Task> GenerateAndSaveAsync(IEnumerable attachmentIds, string? promptVersion = null, CancellationToken cancellationToken = default) { var ids = attachmentIds as IReadOnlyCollection ?? attachmentIds.ToList(); var mode = executionModeResolver.ResolveMode(AIExecutionModeResolver.AttachmentSummaryOperation); @@ -58,26 +59,36 @@ public async Task> GenerateAndSaveAsync(IEnumerable attachmen return await AIExecutionStrategy.RunAsync( ids, mode, - id => GenerateOrFallbackAsync(id, promptVersion), - batch => GenerateSequentiallyAsync(batch, promptVersion)); + id => GenerateOrFallbackAsync(id, promptVersion, cancellationToken), + batch => GenerateSequentiallyAsync(batch, promptVersion, cancellationToken)); } - private async Task> GenerateSequentiallyAsync(IReadOnlyCollection attachmentIds, string? promptVersion) + private async Task> GenerateSequentiallyAsync( + IReadOnlyCollection attachmentIds, + string? promptVersion, + CancellationToken cancellationToken) { var summaries = new List(attachmentIds.Count); foreach (var attachmentId in attachmentIds) { - summaries.Add(await GenerateOrFallbackAsync(attachmentId, promptVersion)); + summaries.Add(await GenerateOrFallbackAsync(attachmentId, promptVersion, cancellationToken)); } return summaries; } - private async Task GenerateOrFallbackAsync(Guid attachmentId, string? promptVersion) + private async Task GenerateOrFallbackAsync( + Guid attachmentId, + string? promptVersion, + CancellationToken cancellationToken) { try { - return await GenerateAndSaveAsync(attachmentId, promptVersion); + return await GenerateAndSaveAsync(attachmentId, promptVersion, cancellationToken); + } + catch (OperationCanceledException) + { + throw; } catch (Exception ex) { @@ -86,16 +97,19 @@ private async Task GenerateOrFallbackAsync(Guid attachmentId, string? pr } } - public async Task> GenerateForApplicationAsync(Guid applicationId, string? promptVersion = null) + public async Task> GenerateForApplicationAsync(Guid applicationId, string? promptVersion = null, CancellationToken cancellationToken = default) { var attachmentIds = (await applicationChefsFileAttachmentRepository.GetListAsync(a => a.ApplicationId == applicationId)) .Select(a => a.Id) .ToList(); - return await GenerateAndSaveAsync(attachmentIds, promptVersion); + return await GenerateAndSaveAsync(attachmentIds, promptVersion, cancellationToken); } - private async Task OpenAttachmentStreamAsync(ApplicationChefsFileAttachment attachment, string fileName) + private async Task OpenAttachmentStreamAsync( + ApplicationChefsFileAttachment attachment, + string fileName, + CancellationToken cancellationToken) { if (!Guid.TryParse(attachment.ChefsSubmissionId, out var submissionId) || !Guid.TryParse(attachment.ChefsFileId, out var fileId)) @@ -108,9 +122,14 @@ private async Task OpenAttachmentStreamAsync(Applicat try { + cancellationToken.ThrowIfCancellationRequested(); var stream = await chefsFileAttachmentStreamProvider.OpenAsync(submissionId, fileId, fileName); return stream ?? ChefsFileAttachmentStream.Empty; } + catch (OperationCanceledException) + { + throw; + } catch (Exception ex) { logger.LogWarning( diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationAnalysisService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationAnalysisService.cs index 991a9fe3c..ac0284276 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationAnalysisService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationAnalysisService.cs @@ -1,10 +1,11 @@ using System; +using System.Threading; using System.Threading.Tasks; namespace Unity.AI.Operations { public interface IApplicationAnalysisService { - Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null); + Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null, CancellationToken cancellationToken = default); } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationScoringService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationScoringService.cs index 96fd5db6d..2b2bc1796 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationScoringService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationScoringService.cs @@ -1,10 +1,11 @@ using System; +using System.Threading; using System.Threading.Tasks; namespace Unity.AI.Operations { public interface IApplicationScoringService { - Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null); + Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null, CancellationToken cancellationToken = default); } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IAttachmentSummaryService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IAttachmentSummaryService.cs index c14ea0e01..feed62805 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IAttachmentSummaryService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IAttachmentSummaryService.cs @@ -1,12 +1,13 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace Unity.AI.Operations; public interface IAttachmentSummaryService { - Task GenerateAndSaveAsync(Guid attachmentId, string? promptVersion = null); - Task> GenerateAndSaveAsync(IEnumerable attachmentIds, string? promptVersion = null); - Task> GenerateForApplicationAsync(Guid applicationId, string? promptVersion = null); + Task GenerateAndSaveAsync(Guid attachmentId, string? promptVersion = null, CancellationToken cancellationToken = default); + Task> GenerateAndSaveAsync(IEnumerable attachmentIds, string? promptVersion = null, CancellationToken cancellationToken = default); + Task> GenerateForApplicationAsync(Guid applicationId, string? promptVersion = null, CancellationToken cancellationToken = default); } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIJsonDefaults.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIJsonDefaults.cs new file mode 100644 index 000000000..29e2e0ee3 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIJsonDefaults.cs @@ -0,0 +1,20 @@ +using System.Text.Json; + +namespace Unity.AI.Runtime; + +internal static class AIJsonDefaults +{ + internal static readonly JsonSerializerOptions Indented = new() { WriteIndented = true }; + + internal static readonly JsonSerializerOptions IndentedCamelCase = new() + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + static AIJsonDefaults() + { + Indented.MakeReadOnly(); + IndentedCamelCase.MakeReadOnly(); + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIProviderPayloadValidator.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIProviderPayloadValidator.cs index f34cb9509..06016a970 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIProviderPayloadValidator.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIProviderPayloadValidator.cs @@ -19,16 +19,11 @@ public static bool IsValidApplicationAnalysisJson(string response) return false; } - return root.TryGetProperty(AIJsonKeys.Decision, out var decision) - && decision.ValueKind == JsonValueKind.String - && root.TryGetProperty(AIJsonKeys.Errors, out var errors) - && errors.ValueKind == JsonValueKind.Array - && root.TryGetProperty(AIJsonKeys.Warnings, out var warnings) - && warnings.ValueKind == JsonValueKind.Array - && root.TryGetProperty(AIJsonKeys.Summaries, out var summaries) - && summaries.ValueKind == JsonValueKind.Array - && root.TryGetProperty(AIJsonKeys.Recommendations, out var recommendations) - && recommendations.ValueKind == JsonValueKind.Array; + return HasStringProperty(root, AIJsonKeys.Decision) && + HasArrayProperty(root, AIJsonKeys.Errors) && + HasArrayProperty(root, AIJsonKeys.Warnings) && + HasArrayProperty(root, AIJsonKeys.Summaries) && + HasArrayProperty(root, AIJsonKeys.Recommendations); } public static bool IsValidApplicationScoringJson(string response, string sectionJson) @@ -46,24 +41,17 @@ public static bool IsValidApplicationScoringJson(string response, string section foreach (var questionId in expectedQuestionIds) { - if (!root.TryGetProperty(questionId, out var answerObject) || answerObject.ValueKind != JsonValueKind.Object) + if (!TryGetRequiredObject(root, questionId, out var answerObject)) { return false; } - if (!answerObject.TryGetProperty(AIJsonKeys.Answer, out var answerValue) - || answerValue.ValueKind == JsonValueKind.Null - || answerValue.ValueKind == JsonValueKind.Object - || answerValue.ValueKind == JsonValueKind.Array) + if (!HasPrimitiveProperty(answerObject, AIJsonKeys.Answer)) { return false; } - if (!answerObject.TryGetProperty(AIJsonKeys.Confidence, out var confidenceValue) - || confidenceValue.ValueKind != JsonValueKind.Number - || !confidenceValue.TryGetInt32(out var confidence) - || confidence < 0 - || confidence > 100) + if (!IsValidConfidenceProperty(answerObject, AIJsonKeys.Confidence)) { return false; } @@ -72,6 +60,41 @@ public static bool IsValidApplicationScoringJson(string response, string section return true; } + private static bool HasStringProperty(JsonElement element, string name) + { + return element.TryGetProperty(name, out var property) && + property.ValueKind == JsonValueKind.String; + } + + private static bool HasArrayProperty(JsonElement element, string name) + { + return element.TryGetProperty(name, out var property) && + property.ValueKind == JsonValueKind.Array; + } + + private static bool TryGetRequiredObject(JsonElement element, string name, out JsonElement value) + { + return element.TryGetProperty(name, out value) && + value.ValueKind == JsonValueKind.Object; + } + + private static bool HasPrimitiveProperty(JsonElement element, string name) + { + return element.TryGetProperty(name, out var property) && + property.ValueKind != JsonValueKind.Null && + property.ValueKind != JsonValueKind.Object && + property.ValueKind != JsonValueKind.Array; + } + + private static bool IsValidConfidenceProperty(JsonElement element, string name) + { + return element.TryGetProperty(name, out var property) && + property.ValueKind == JsonValueKind.Number && + property.TryGetInt32(out var confidence) && + confidence >= 0 && + confidence <= 100; + } + private static HashSet ExtractQuestionIds(string sectionJson) { var ids = new HashSet(StringComparer.OrdinalIgnoreCase); @@ -146,6 +169,5 @@ private static bool TryParseRootObject(string response, out JsonElement root) return false; } } - } } 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 a56d56f6a..e011df881 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 @@ -26,7 +26,7 @@ public class OpenAIPromptRenderer : ITransientDependency [PromptVersionV1] = PromptVersionV1 }; private static readonly ConcurrentDictionary PromptTemplateCache = new(StringComparer.OrdinalIgnoreCase); - private static readonly JsonSerializerOptions JsonLogOptions = new() { WriteIndented = true }; + public static string BuildApplicationAnalysisSystemPrompt(string version) { @@ -111,7 +111,7 @@ public static string BuildApplicationScoringResponseTemplate(string sectionPaylo return "{}"; } - return JsonSerializer.Serialize(template, JsonLogOptions); + return JsonSerializer.Serialize(template, AIJsonDefaults.Indented); } catch (JsonException) { @@ -125,7 +125,7 @@ public static string BuildAliasedApplicationScoringSection(string? sectionName, if (string.IsNullOrWhiteSpace(sectionJson)) { - return JsonSerializer.Serialize(new { name = sectionName, questions = sectionJson }, JsonLogOptions); + return JsonSerializer.Serialize(new { name = sectionName, questions = sectionJson }, AIJsonDefaults.Indented); } try @@ -133,7 +133,7 @@ public static string BuildAliasedApplicationScoringSection(string? sectionName, using var sectionDoc = JsonDocument.Parse(sectionJson); if (sectionDoc.RootElement.ValueKind != JsonValueKind.Array) { - return JsonSerializer.Serialize(new { name = sectionName, questions = sectionDoc.RootElement.Clone() }, JsonLogOptions); + return JsonSerializer.Serialize(new { name = sectionName, questions = sectionDoc.RootElement.Clone() }, AIJsonDefaults.Indented); } var aliasedQuestions = new List>(); @@ -174,11 +174,11 @@ public static string BuildAliasedApplicationScoringSection(string? sectionName, } questionIdAliasMap = aliasMap; - return JsonSerializer.Serialize(new { name = sectionName, questions = aliasedQuestions }, JsonLogOptions); + return JsonSerializer.Serialize(new { name = sectionName, questions = aliasedQuestions }, AIJsonDefaults.Indented); } catch (JsonException) { - return JsonSerializer.Serialize(new { name = sectionName, questions = sectionJson }, JsonLogOptions); + return JsonSerializer.Serialize(new { name = sectionName, questions = sectionJson }, AIJsonDefaults.Indented); } } 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 7d7ad49e3..ded93f28c 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 @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using Unity.AI.Models; using Unity.AI.Prompts; @@ -24,11 +25,7 @@ public class OpenAIRuntimeService : IAIService, ITransientDependency private const string ApplicationAnalysisPromptType = AIPromptTypes.ApplicationAnalysis; private const string AttachmentSummaryPromptType = AIPromptTypes.AttachmentSummary; private const string ApplicationScoringPromptType = AIPromptTypes.ApplicationScoring; - private const string AIServiceNotConfiguredMessage = "AI service not available - service not configured."; - private const string AIServiceTemporarilyUnavailableMessage = "AI request failed - service temporarily unavailable."; - private const string AIRequestFailedRetryMessage = "AI request failed - please try again later."; private const int MaxAiAttempts = 3; - private const int DefaultCompletionTokens = 2000; private const int DefaultAttachmentSummaryCompletionTokens = 2000; private const int DefaultApplicationAnalysisCompletionTokens = 4000; private const int DefaultApplicationScoringCompletionTokens = 8000; @@ -44,7 +41,7 @@ public class OpenAIRuntimeService : IAIService, ITransientDependency private const string PromptLogDirectoryName = "logs"; private static readonly string PromptLogFileName = $"ai-prompts-{DateTime.UtcNow:yyyyMMdd-HHmmss}-{Environment.ProcessId}.log"; - private static readonly JsonSerializerOptions JsonLogOptions = new() { WriteIndented = true }; + public OpenAIRuntimeService( IConfiguration configuration, @@ -69,25 +66,12 @@ public Task IsAvailableAsync() return Task.FromResult(true); } - public async Task GenerateCompletionAsync(AICompletionRequest request) - { - var result = await GenerateWithRetryAsync( - () => _openAITransportService.GenerateSummaryAsync( - request?.UserPrompt ?? string.Empty, - null, - request?.MaxTokens ?? DefaultCompletionTokens, - request?.Temperature), - AIProviderPayloadValidator.IsValidAttachmentSummaryText, - "completion"); - return new AICompletionResponse { Content = ResolveNarrativeContent(result) }; - } - - public async Task GenerateApplicationAnalysisAsync(ApplicationAnalysisRequest request) + public async Task GenerateApplicationAnalysisAsync(ApplicationAnalysisRequest request, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); var promptVersion = OpenAIPromptRenderer.ResolvePromptVersion(request.PromptVersion ?? ResolvePromptVersionSetting(ApplicationAnalysisPromptType)); - var data = JsonSerializer.Serialize(request.Data, JsonLogOptions); - var schema = JsonSerializer.Serialize(request.Schema, JsonLogOptions); + var data = JsonSerializer.Serialize(request.Data, AIJsonDefaults.Indented); + var schema = JsonSerializer.Serialize(request.Schema, AIJsonDefaults.Indented); var attachmentsPayload = request.Attachments .Select(a => new @@ -97,24 +81,26 @@ public async Task GenerateApplicationAnalysisAsync( }) .Cast(); - var attachments = JsonSerializer.Serialize(attachmentsPayload, JsonLogOptions); + var attachments = JsonSerializer.Serialize(attachmentsPayload, AIJsonDefaults.Indented); var systemPrompt = OpenAIPromptRenderer.BuildApplicationAnalysisSystemPrompt(promptVersion); var applicationAnalysisContent = OpenAIPromptRenderer.BuildApplicationAnalysisUserPrompt( promptVersion, schema, data, attachments); - await LogPromptInputAsync(ApplicationAnalysisPromptType, promptVersion, systemPrompt, applicationAnalysisContent); + await LogPromptInputAsync(ApplicationAnalysisPromptType, promptVersion, systemPrompt, applicationAnalysisContent, cancellationToken); var result = await GenerateWithRetryAsync( () => _openAITransportService.GenerateSummaryAsync( applicationAnalysisContent, systemPrompt, ApplicationAnalysisCompletionTokens, operationName: ApplicationAnalysisPromptType, - promptVersion: promptVersion), + promptVersion: promptVersion, + cancellationToken: cancellationToken), AIProviderPayloadValidator.IsValidApplicationAnalysisJson, - "application analysis"); - await LogPromptOutputAsync(ApplicationAnalysisPromptType, promptVersion, result.CaptureOutput); + "application analysis", + cancellationToken); + await LogPromptOutputAsync(ApplicationAnalysisPromptType, promptVersion, result.CaptureOutput, cancellationToken); if (result.Outcome != AIOperationOutcome.Success) { @@ -124,7 +110,7 @@ public async Task GenerateApplicationAnalysisAsync( return OpenAIResponseParser.ParseApplicationAnalysisResponse(result.Content); } - public async Task GenerateAttachmentSummaryAsync(AttachmentSummaryRequest request) + public async Task GenerateAttachmentSummaryAsync(AttachmentSummaryRequest request, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); var fileName = request.FileName ?? string.Empty; @@ -152,10 +138,10 @@ public async Task GenerateAttachmentSummaryAsync(Atta contentType, text = attachmentText }; - var attachment = JsonSerializer.Serialize(attachmentPayload, JsonLogOptions); + var attachment = JsonSerializer.Serialize(attachmentPayload, AIJsonDefaults.Indented); var contentToAnalyze = OpenAIPromptRenderer.BuildAttachmentSummaryUserPrompt(promptVersion, attachment); - await LogPromptInputAsync(AttachmentSummaryPromptType, promptVersion, prompt, contentToAnalyze); + await LogPromptInputAsync(AttachmentSummaryPromptType, promptVersion, prompt, contentToAnalyze, cancellationToken); var result = await GenerateWithRetryAsync( () => _openAITransportService.GenerateSummaryAsync( contentToAnalyze, @@ -163,10 +149,12 @@ public async Task GenerateAttachmentSummaryAsync(Atta AttachmentSummaryCompletionTokens, operationName: AttachmentSummaryPromptType, promptVersion: promptVersion, - fileName: fileName), + fileName: fileName, + cancellationToken: cancellationToken), AIProviderPayloadValidator.IsValidAttachmentSummaryText, - "attachment summary"); - await LogPromptOutputAsync(AttachmentSummaryPromptType, promptVersion, result.CaptureOutput); + "attachment summary", + cancellationToken); + await LogPromptOutputAsync(AttachmentSummaryPromptType, promptVersion, result.CaptureOutput, cancellationToken); if (result.Outcome != AIOperationOutcome.Success) { @@ -181,6 +169,10 @@ public async Task GenerateAttachmentSummaryAsync(Atta Summary = ExtractSummaryFromJson(result.Content) }; } + catch (OperationCanceledException) + { + throw; + } catch (Exception ex) { _logger.LogError(ex, "Error generating attachment summary for {FileName}", fileName); @@ -191,12 +183,12 @@ public async Task GenerateAttachmentSummaryAsync(Atta } } - public async Task GenerateApplicationScoringAsync(ApplicationScoringRequest request) + public async Task GenerateApplicationScoringAsync(ApplicationScoringRequest request, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); var promptVersion = OpenAIPromptRenderer.ResolvePromptVersion(request.PromptVersion ?? ResolvePromptVersionSetting(ApplicationScoringPromptType)); - var dataJson = JsonSerializer.Serialize(request.Data, JsonLogOptions); - var sectionJson = JsonSerializer.Serialize(request.SectionSchema, JsonLogOptions); + var dataJson = JsonSerializer.Serialize(request.Data, AIJsonDefaults.Indented); + var sectionJson = JsonSerializer.Serialize(request.SectionSchema, AIJsonDefaults.Indented); var attachmentSummaries = request.Attachments .Select(a => $"{a.Name}: {a.Summary}") @@ -231,17 +223,19 @@ public async Task GenerateApplicationScoringAsync(Ap response); var systemPrompt = OpenAIPromptRenderer.BuildApplicationScoringSystemPrompt(promptVersion); - await LogPromptInputAsync(ApplicationScoringPromptType, promptVersion, systemPrompt, applicationScoringContent); + await LogPromptInputAsync(ApplicationScoringPromptType, promptVersion, systemPrompt, applicationScoringContent, cancellationToken); var result = await GenerateWithRetryAsync( () => _openAITransportService.GenerateSummaryAsync( applicationScoringContent, systemPrompt, ApplicationScoringCompletionTokens, operationName: ApplicationScoringPromptType, - promptVersion: promptVersion), + promptVersion: promptVersion, + cancellationToken: cancellationToken), content => AIProviderPayloadValidator.IsValidApplicationScoringJson(content, section), - $"application scoring section {request.SectionName}"); - await LogPromptOutputAsync(ApplicationScoringPromptType, promptVersion, result.CaptureOutput); + $"application scoring section {request.SectionName}", + cancellationToken); + await LogPromptOutputAsync(ApplicationScoringPromptType, promptVersion, result.CaptureOutput, cancellationToken); if (result.Outcome != AIOperationOutcome.Success) { @@ -250,6 +244,10 @@ public async Task GenerateApplicationScoringAsync(Ap return OpenAIResponseParser.ParseApplicationScoringResponse(result.Content, questionIdAliasMap); } + catch (OperationCanceledException) + { + throw; + } catch (Exception ex) { _logger.LogError(ex, "Error generating application scoring answers for section {SectionName}", request.SectionName); @@ -260,12 +258,14 @@ public async Task GenerateApplicationScoringAsync(Ap private async Task GenerateWithRetryAsync( Func> operation, Func validator, - string operationName) + string operationName, + CancellationToken cancellationToken = default) { var lastResult = AIOperationResult.InvalidOutput(); for (var attempt = 1; attempt <= MaxAiAttempts; attempt++) { + cancellationToken.ThrowIfCancellationRequested(); lastResult = await operation(); if (lastResult.Outcome == AIOperationOutcome.Success && validator(lastResult.Content)) @@ -311,17 +311,6 @@ private async Task GenerateWithRetryAsync( return lastResult; } - private static string ResolveNarrativeContent(AIOperationResult result) - { - return result.Outcome switch - { - AIOperationOutcome.Success => result.Content, - AIOperationOutcome.PermanentFailure => AIServiceNotConfiguredMessage, - AIOperationOutcome.TransientFailure => AIServiceTemporarilyUnavailableMessage, - _ => AIRequestFailedRetryMessage - }; - } - private static int? TryGetInt32(JsonElement element, string propertyName) { return element.TryGetProperty(propertyName, out var property) @@ -348,21 +337,21 @@ private static string ResolveNarrativeContent(AIOperationResult result) return _configuration["Azure:OpenAI:PromptVersion"]; } - private async Task LogPromptInputAsync(string promptType, string promptVersion, string? systemPrompt, string userPrompt) + private async Task LogPromptInputAsync(string promptType, string promptVersion, string? systemPrompt, string userPrompt, CancellationToken cancellationToken = default) { var formattedInput = FormatPromptInputForLog(systemPrompt, userPrompt); _logger.LogInformation("AI {PromptType} ({PromptVersion}) input payload: {PromptInput}", promptType, promptVersion, formattedInput); - await WritePromptLogFileAsync(promptType, promptVersion, "INPUT", formattedInput); + await WritePromptLogFileAsync(promptType, promptVersion, "INPUT", formattedInput, cancellationToken); } - private async Task LogPromptOutputAsync(string promptType, string promptVersion, string output) + private async Task LogPromptOutputAsync(string promptType, string promptVersion, string output, CancellationToken cancellationToken = default) { var formattedOutput = FormatPromptOutputForLog(output); _logger.LogInformation("AI {PromptType} ({PromptVersion}) model output payload: {ModelOutput}", promptType, promptVersion, formattedOutput); - await WritePromptLogFileAsync(promptType, promptVersion, "OUTPUT", formattedOutput); + await WritePromptLogFileAsync(promptType, promptVersion, "OUTPUT", formattedOutput, cancellationToken); } - private async Task WritePromptLogFileAsync(string promptType, string promptVersion, string payloadType, string payload) + private async Task WritePromptLogFileAsync(string promptType, string promptVersion, string payloadType, string payload, CancellationToken cancellationToken = default) { if (!CanWritePromptFileLog()) { @@ -377,7 +366,11 @@ private async Task WritePromptLogFileAsync(string promptType, string promptVersi var logPath = Path.Combine(logDirectory, PromptLogFileName); var entry = $"{now} [{promptType}] [{promptVersion}] {payloadType}\n{payload}\n\n"; - await File.AppendAllTextAsync(logPath, entry); + await File.AppendAllTextAsync(logPath, entry, cancellationToken); + } + catch (OperationCanceledException) + { + throw; } catch (Exception ex) { @@ -410,7 +403,7 @@ private static string FormatPromptOutputForLog(string output) if (TryParseJsonObjectFromResponse(output, out var jsonObject)) { - return JsonSerializer.Serialize(jsonObject, JsonLogOptions); + return JsonSerializer.Serialize(jsonObject, AIJsonDefaults.Indented); } return output.Trim(); @@ -500,7 +493,7 @@ private static string FormatPromptOutputContent(string content) { if (TryParseJsonObjectFromResponse(content, out var contentObject)) { - return JsonSerializer.Serialize(contentObject, JsonLogOptions); + return JsonSerializer.Serialize(contentObject, AIJsonDefaults.Indented); } return content.Trim(); 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 374d0755e..f5bc0e219 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 @@ -5,6 +5,7 @@ using System.Net.Http; using System.Text; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using Volo.Abp.DependencyInjection; @@ -26,7 +27,8 @@ public async Task GenerateSummaryAsync( double? temperature = null, string? operationName = null, string? promptVersion = null, - string? fileName = null) + string? fileName = null, + CancellationToken cancellationToken = default) { var providerName = _configurationResolver.ResolveProviderName(operationName); if (!string.Equals(providerName, "OpenAI", StringComparison.Ordinal)) @@ -73,8 +75,8 @@ public async Task GenerateSummaryAsync( }; request.Headers.TryAddWithoutValidation("Authorization", apiKey); - var response = await _httpClient.SendAsync(request); - var responseContent = await response.Content.ReadAsStringAsync(); + using var response = await _httpClient.SendAsync(request, cancellationToken); + var responseContent = await response.Content.ReadAsStringAsync(cancellationToken); var metadata = TryExtractProviderMetadata(responseContent); var providerResponse = BuildProviderResponseFromMetadata( string.Empty, @@ -118,6 +120,10 @@ public async Task GenerateSummaryAsync( return AIOperationResult.InvalidOutput(providerResponse); } } + catch (OperationCanceledException) + { + throw; + } catch (Exception ex) { _logger.LogError(ex, "Error generating AI summary"); diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Attachments/AttachmentSummaryAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Attachments/AttachmentSummaryAppService.cs index 9a97ee921..6e1cedc43 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Attachments/AttachmentSummaryAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Attachments/AttachmentSummaryAppService.cs @@ -2,11 +2,13 @@ using System.Collections.Generic; using System.Threading.Tasks; using Unity.AI; +using Unity.AI.Features; +using Unity.AI.Localization; using Unity.AI.Operations; using Unity.AI.Permissions; +using Unity.AI.Settings; using Volo.Abp; using Volo.Abp.DependencyInjection; -using Volo.Abp.Features; namespace Unity.GrantManager.Attachments; @@ -14,25 +16,23 @@ namespace Unity.GrantManager.Attachments; [ExposeServices(typeof(AttachmentSummaryAppService), typeof(IAttachmentSummaryAppService))] public class AttachmentSummaryAppService( IAttachmentSummaryService attachmentSummaryService, - IFeatureChecker featureChecker) : AIAppService, IAttachmentSummaryAppService + AIFeatureGuard featureGuard) : AIAppService, IAttachmentSummaryAppService { - public async Task GenerateAttachmentSummaryAsync(System.Guid attachmentId, string? promptVersion = null) + public virtual async Task GenerateAttachmentSummaryAsync(System.Guid attachmentId, string? promptVersion = null) { - if (!await featureChecker.IsEnabledAsync("Unity.AI.AttachmentSummaries")) - { - throw new UserFriendlyException("AI attachment summaries are not enabled."); - } + await featureGuard.EnsureEnabledAsync( + AIFeatures.AttachmentSummaries, + AILocalizationKeys.AttachmentSummariesDisabled); await attachmentSummaryService.GenerateAndSaveAsync(attachmentId, promptVersion); return new AttachmentSummaryResultDto { Completed = true }; } - public async Task> GenerateAttachmentSummariesAsync(List attachmentIds, string? promptVersion = null) + public virtual async Task> GenerateAttachmentSummariesAsync(List attachmentIds, string? promptVersion = null) { - if (!await featureChecker.IsEnabledAsync("Unity.AI.AttachmentSummaries")) - { - throw new UserFriendlyException("AI attachment summaries are not enabled."); - } + await featureGuard.EnsureEnabledAsync( + AIFeatures.AttachmentSummaries, + AILocalizationKeys.AttachmentSummariesDisabled); if (attachmentIds.Count == 0) { @@ -54,7 +54,10 @@ public async Task> GenerateAttachmentSummariesA [RemoteService(IsEnabled = false)] public virtual async Task> GenerateAttachmentSummariesForPipelineAsync(List attachmentIds, string? promptVersion = null) { - if (attachmentIds.Count == 0) return []; + if (attachmentIds.Count == 0) + { + return []; + } var results = new List(); foreach (var attachmentId in attachmentIds) @@ -62,6 +65,7 @@ public virtual async Task> GenerateAttachmentSu await attachmentSummaryService.GenerateAndSaveAsync(attachmentId, promptVersion); results.Add(new AttachmentSummaryResultDto { Completed = true }); } + return results; } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationAnalysisAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationAnalysisAppService.cs index 9c1f173d7..843d4a89f 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationAnalysisAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationAnalysisAppService.cs @@ -3,9 +3,11 @@ using System.Threading.Tasks; using Unity.AI; using Unity.AI.Automation; +using Unity.AI.Features; +using Unity.AI.Localization; using Unity.AI.Permissions; +using Unity.AI.Settings; using Volo.Abp; -using Volo.Abp.Features; using Volo.Abp.MultiTenancy; namespace Unity.GrantManager.GrantApplications; @@ -14,16 +16,15 @@ namespace Unity.GrantManager.GrantApplications; public class ApplicationAnalysisAppService( Unity.AI.Operations.IApplicationAnalysisService applicationAnalysisService, IApplicationAIGenerationQueue aiGenerationQueue, - IFeatureChecker featureChecker, + AIFeatureGuard featureGuard, ICurrentTenant currentTenant) : AIAppService, IApplicationAnalysisAppService { public virtual async Task GenerateApplicationAnalysisAsync(Guid applicationId, string? promptVersion = null) { - if (!await featureChecker.IsEnabledAsync("Unity.AI.ApplicationAnalysis")) - { - throw new UserFriendlyException("AI application analysis is not enabled."); - } + await featureGuard.EnsureEnabledAsync( + AIFeatures.ApplicationAnalysis, + AILocalizationKeys.ApplicationAnalysisDisabled); await aiGenerationQueue.QueueApplicationAnalysisAsync(applicationId, currentTenant.Id, promptVersion); return new ApplicationAnalysisResultDto { Completed = false }; diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationContentAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationContentAppService.cs index d0fbe4d7d..72c01b8c1 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationContentAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationContentAppService.cs @@ -3,9 +3,10 @@ using System.Threading.Tasks; using Unity.AI; using Unity.AI.Automation; +using Unity.AI.Features; +using Unity.AI.Localization; using Unity.AI.Permissions; -using Volo.Abp; -using Volo.Abp.Features; +using Unity.AI.Settings; using Volo.Abp.MultiTenancy; namespace Unity.GrantManager.GrantApplications; @@ -15,20 +16,15 @@ namespace Unity.GrantManager.GrantApplications; [Authorize(AIPermissions.Analysis.ViewScoringResult)] public class ApplicationContentAppService( IApplicationAIGenerationQueue aiGenerationQueue, - IFeatureChecker featureChecker, + AIFeatureGuard featureGuard, ICurrentTenant currentTenant) : AIAppService, IApplicationContentAppService { - public async Task GenerateContentAsync(Guid applicationId, string? promptVersion = null) + public virtual async Task GenerateContentAsync(Guid applicationId, string? promptVersion = null) { - var attachmentSummariesEnabled = await featureChecker.IsEnabledAsync("Unity.AI.AttachmentSummaries"); - var applicationAnalysisEnabled = await featureChecker.IsEnabledAsync("Unity.AI.ApplicationAnalysis"); - var scoringEnabled = await featureChecker.IsEnabledAsync("Unity.AI.Scoring"); - - if (!attachmentSummariesEnabled || !applicationAnalysisEnabled || !scoringEnabled) - { - throw new UserFriendlyException("AI generate all is not enabled."); - } + await featureGuard.EnsureEnabledAsync(AIFeatures.AttachmentSummaries, AILocalizationKeys.GenerateAllDisabled); + await featureGuard.EnsureEnabledAsync(AIFeatures.ApplicationAnalysis, AILocalizationKeys.GenerateAllDisabled); + await featureGuard.EnsureEnabledAsync(AIFeatures.Scoring, AILocalizationKeys.GenerateAllDisabled); await aiGenerationQueue.QueueAllAIStagesAsync(applicationId, currentTenant.Id, promptVersion); diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationScoringAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationScoringAppService.cs index ff79dfef6..9c65c1d1f 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationScoringAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationScoringAppService.cs @@ -2,28 +2,29 @@ using System; using System.Threading.Tasks; using Unity.AI; +using Unity.AI.Features; +using Unity.AI.Localization; using Unity.AI.Operations; using Unity.AI.Permissions; +using Unity.AI.Settings; using Unity.GrantManager.GrantApplications.Automation.Events; using Volo.Abp; using Volo.Abp.EventBus.Local; -using Volo.Abp.Features; namespace Unity.GrantManager.GrantApplications; [Authorize(AIPermissions.Analysis.GenerateScoring)] public class ApplicationScoringAppService( IApplicationScoringService applicationScoringService, - IFeatureChecker featureChecker, + AIFeatureGuard featureGuard, ILocalEventBus localEventBus) : AIAppService, IApplicationScoringAppService { public virtual async Task GenerateApplicationScoringAsync(Guid applicationId, string? promptVersion = null) { - if (!await featureChecker.IsEnabledAsync("Unity.AI.Scoring")) - { - throw new UserFriendlyException("AI scoring is not enabled."); - } + await featureGuard.EnsureEnabledAsync( + AIFeatures.Scoring, + AILocalizationKeys.ScoringDisabled); await applicationScoringService.RegenerateAndSaveAsync(applicationId, promptVersion); diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Properties/AssemblyInfo.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..46526950c --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Unity.GrantManager.Application.Tests")] diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/RateLimit/AIRateLimitAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/RateLimit/AIRateLimitAppService.cs new file mode 100644 index 000000000..a8d018a36 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/RateLimit/AIRateLimitAppService.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; + +namespace Unity.AI.RateLimit; + +[Authorize] +public class AIRateLimitAppService(IAIRateLimiter rateLimiter) + : AIAppService, IAIRateLimitAppService +{ + public virtual Task GetStateAsync() => rateLimiter.GetStateAsync(); +} 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 new file mode 100644 index 000000000..ab3f05ad2 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/RateLimit/AIRateLimiter.cs @@ -0,0 +1,117 @@ +using System; +using System.Globalization; +using System.Threading.Tasks; +using Medallion.Threading; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Configuration; +using Volo.Abp; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Users; + +namespace Unity.AI.RateLimit; + +/// +/// Per-user cooldown for AI generate calls. KISS: a single cache entry per user +/// holds the cooldown end ticks; the cache TTL matches the cooldown so a missing +/// entry means the user can generate again. Anonymous/system callers are not +/// rate-limited (background event handlers also flow through the AI queue). +/// +public class AIRateLimiter( + IDistributedCache cache, + ICurrentUser currentUser, + IConfiguration configuration, + IDistributedLockProvider distributedLockProvider) : IAIRateLimiter, ITransientDependency +{ + private const string KeyPrefix = "AI:Cooldown:"; + private const string LockPrefix = "AI:CooldownLock:"; + private const int DefaultCooldownSeconds = 60; + + private int CooldownSeconds + { + get + { + var configured = configuration.GetValue("AI:RateLimit:CooldownSeconds"); + return configured > 0 ? configured.Value : DefaultCooldownSeconds; + } + } + + public virtual async Task EnsureAsync() + { + if (currentUser.Id is not Guid userId) + { + // No user (background/system flow). User-level rate limit does not apply. + return; + } + + var userLock = distributedLockProvider.CreateLock(LockPrefix + userId); + using (await userLock.AcquireAsync()) + { + var remaining = await GetRemainingSecondsAsync(userId); + if (remaining > 0) + { + throw new UserFriendlyException( + $"AI generation is rate limited. Try again in {remaining} second{(remaining == 1 ? "" : "s")}."); + } + } + } + + public virtual async Task StampAsync() + { + await StampAsync(currentUser.Id); + } + + public virtual async Task StampAsync(Guid? userId) + { + if (userId is Guid resolvedUserId) + { + var userLock = distributedLockProvider.CreateLock(LockPrefix + resolvedUserId); + using (await userLock.AcquireAsync()) + { + await StampAsync(resolvedUserId, CooldownSeconds); + } + } + } + + public virtual async Task GetStateAsync() + { + if (currentUser.Id is not Guid userId) + { + return new AIRateLimitStateDto { RetryAfterSeconds = 0 }; + } + + var userLock = distributedLockProvider.CreateLock(LockPrefix + userId); + using (await userLock.AcquireAsync()) + { + return new AIRateLimitStateDto + { + RetryAfterSeconds = await GetRemainingSecondsAsync(userId) + }; + } + } + + private async Task GetRemainingSecondsAsync(Guid userId) + { + var raw = await cache.GetStringAsync(KeyFor(userId)); + if (string.IsNullOrEmpty(raw) || + !long.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var untilTicks) || + untilTicks < DateTime.MinValue.Ticks || + untilTicks > DateTime.MaxValue.Ticks) + { + return 0; + } + + var seconds = (int)Math.Ceiling((new DateTime(untilTicks, DateTimeKind.Utc) - DateTime.UtcNow).TotalSeconds); + return seconds > 0 ? seconds : 0; + } + + private async Task StampAsync(Guid userId, int seconds) + { + var until = DateTime.UtcNow.AddSeconds(seconds); + await cache.SetStringAsync( + KeyFor(userId), + until.Ticks.ToString(CultureInfo.InvariantCulture), + new DistributedCacheEntryOptions { AbsoluteExpiration = until }); + } + + private static string KeyFor(Guid userId) => KeyPrefix + userId; +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Settings/AIFeatureGuard.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Settings/AIFeatureGuard.cs new file mode 100644 index 000000000..2a71584b3 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Settings/AIFeatureGuard.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.Localization; +using System.Threading.Tasks; +using Unity.AI.Localization; +using Volo.Abp; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Features; + +namespace Unity.AI.Settings; + +public class AIFeatureGuard( + IFeatureChecker featureChecker, + IStringLocalizer localizer) : ITransientDependency +{ + public async Task EnsureEnabledAsync(string featureName, string disabledMessageKey) + { + if (!await featureChecker.IsEnabledAsync(featureName)) + { + throw new UserFriendlyException(localizer[disabledMessageKey]); + } + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Unity.AI.Application.csproj b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Unity.AI.Application.csproj index 4f5521639..78d3c1414 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Unity.AI.Application.csproj +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Unity.AI.Application.csproj @@ -14,6 +14,7 @@ + diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Features/AIFeatures.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Features/AIFeatures.cs new file mode 100644 index 000000000..e658c3635 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Features/AIFeatures.cs @@ -0,0 +1,9 @@ +namespace Unity.AI.Features; + +public static class AIFeatures +{ + public const string Reporting = "Unity.AIReporting"; + public const string AttachmentSummaries = "Unity.AI.AttachmentSummaries"; + public const string ApplicationAnalysis = "Unity.AI.ApplicationAnalysis"; + public const string Scoring = "Unity.AI.Scoring"; +} 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 f11515ca6..c9b41c00d 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 @@ -14,10 +14,16 @@ "Permission:AI.Prompts.Create": "Create Prompts", "Permission:AI.Prompts.Update": "Edit Prompts", "Permission:AI.Prompts.Delete": "Delete Prompts", + "Menu:AIReporting": "AI Reporting", "Setting:AI.AutomaticGenerationEnabled": "Automatically Generate AI Analysis", "Setting:AI.ManualGenerationEnabled": "Manually Initiate AI Analysis", + "AI:AttachmentSummariesDisabled": "AI attachment summaries are not enabled.", + "AI:ApplicationAnalysisDisabled": "AI application analysis is not enabled.", + "AI:ScoringDisabled": "AI scoring is not enabled.", + "AI:GenerateAllDisabled": "AI generation is not enabled.", + "AIPrompts": "AI Prompts", "AIPrompt": "AI Prompt", "AIPromptVersion": "Prompt Version", 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 new file mode 100644 index 000000000..1b099e5fe --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AILocalizationKeys.cs @@ -0,0 +1,9 @@ +namespace Unity.AI.Localization; + +public static class AILocalizationKeys +{ + public const string AttachmentSummariesDisabled = "AI:AttachmentSummariesDisabled"; + public const string ApplicationAnalysisDisabled = "AI:ApplicationAnalysisDisabled"; + public const string ScoringDisabled = "AI:ScoringDisabled"; + public const string GenerateAllDisabled = "AI:GenerateAllDisabled"; +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Menus/AIMenuContributor.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Menus/AIMenuContributor.cs index 453ad3549..bf7f06d89 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Menus/AIMenuContributor.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Menus/AIMenuContributor.cs @@ -1,5 +1,9 @@ using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Unity.AI.Localization; +using Unity.AI.Permissions; using Unity.Modules.Shared.Permissions; +using Volo.Abp.Features; using Volo.Abp.UI.Navigation; namespace Unity.AI.Web.Menus; @@ -14,8 +18,11 @@ public async Task ConfigureMenuAsync(MenuConfigurationContext context) } } - private static Task ConfigureMainMenuAsync(MenuConfigurationContext context) + private static async Task ConfigureMainMenuAsync(MenuConfigurationContext context) { + var l = context.GetLocalizer(); + var featureChecker = context.ServiceProvider.GetRequiredService(); + context.Menu.AddItem(new ApplicationMenuItem( name: AIMenus.Prompts, displayName: "AI Prompts", @@ -25,6 +32,16 @@ private static Task ConfigureMainMenuAsync(MenuConfigurationContext context) requiredPermissionName: IdentityConsts.ITOperationsPermissionName )); - return Task.CompletedTask; + if (await featureChecker.IsEnabledAsync("Unity.AIReporting")) + { + context.Menu.AddItem(new ApplicationMenuItem( + name: AIMenus.Reporting, + displayName: l["Menu:AIReporting"], + url: "~/AIReporting", + icon: "fl fl-view-dashboard", + order: 9, + requiredPermissionName: AIPermissions.Reporting.ReportingDefault + )); + } } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Menus/AIMenus.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Menus/AIMenus.cs index f93517efe..c455ea168 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Menus/AIMenus.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Menus/AIMenus.cs @@ -5,4 +5,5 @@ public static class AIMenus private const string Prefix = "AI"; public const string Prompts = Prefix + ".Prompts"; + public const string Reporting = Prefix + ".Reporting"; } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/AIReporting/Index.cshtml b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/AIReporting/Index.cshtml new file mode 100644 index 000000000..a7ffb565f --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/AIReporting/Index.cshtml @@ -0,0 +1,36 @@ +@page +@model Unity.AI.Web.Pages.AIReporting.IndexModel + +@section styles { + @if (Model.CanViewAiReporting) + { + + } +} + +@section scripts { + @if (Model.CanViewAiReporting) + { + + + + + } +} + +@if (Model.CanViewAiReporting) +{ + + +
+} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/AIReporting/Index.cshtml.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/AIReporting/Index.cshtml.cs new file mode 100644 index 000000000..6d8259c9f --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/AIReporting/Index.cshtml.cs @@ -0,0 +1,42 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using Unity.GrantManager.Integrations; +using Unity.Modules.Shared.Permissions; +using Volo.Abp; +using Volo.Abp.Features; + +namespace Unity.AI.Web.Pages.AIReporting +{ + public class IndexModel( + IEndpointManagementAppService endpointManagementAppService, + IFeatureChecker featureChecker, + IAuthorizationService authorizationService, + ILogger logger) : PageModel + { + public bool CanViewAiReporting { get; private set; } + public string ReportingAiUrl { get; private set; } = string.Empty; + + public async Task OnGetAsync() + { + CanViewAiReporting = await featureChecker.IsEnabledAsync("Unity.AIReporting") + || (await authorizationService.AuthorizeAsync(User, IdentityConsts.ITAdminPolicyName)).Succeeded; + + if (!CanViewAiReporting) + { + return; + } + + try + { + ReportingAiUrl = await endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.REPORTING_AI); + } + catch (UserFriendlyException ex) + { + logger.LogWarning(ex, "AI Reporting endpoint is not configured."); + ReportingAiUrl = string.Empty; + } + } + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/AIReporting/Index.js b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/AIReporting/Index.js new file mode 100644 index 000000000..a8b364e85 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/AIReporting/Index.js @@ -0,0 +1,72 @@ +const showInitializationError = (container, message, error) => { + console.error(message, error); + container.textContent = message; +}; +const reportingAiUrl = globalThis.reportingAiUrl; +const container = document.getElementById('container'); + +const initializeAIReporting = async () => { + if (!container) { + return; + } + + if (!reportingAiUrl) { + showInitializationError(container, 'AI Reporting is not configured.'); + return; + } + + let reportingUrl; + try { + reportingUrl = new URL(reportingAiUrl); + } catch (error) { + showInitializationError(container, 'AI Reporting is not configured correctly.', error); + return; + } + + let token; + try { + token = await unity.grantManager.identity.jwtToken.generateJWTToken(); + } catch (error) { + showInitializationError(container, 'Failed to initialize AI Reporting. Please refresh the page and try again.', error); + return; + } + + const iframe = document.createElement('iframe'); + + iframe.style.width = '100%'; + iframe.style.height = '100%'; + iframe.style.border = 'none'; + + const targetOrigin = reportingUrl.origin; + + const messageHandler = (event) => { + if (event.origin !== targetOrigin) { + return; + } + + if (event.data?.type === 'READY') { + try { + iframe.contentWindow.postMessage( + { type: 'AUTH_TOKEN', token }, + targetOrigin + ); + } catch (error) { + console.error('Failed to send authentication token to AI Reporting iframe:', error); + } + + globalThis.removeEventListener('message', messageHandler); + } + }; + + globalThis.addEventListener('message', messageHandler); + + iframe.onerror = () => { + console.error('Failed to load AI Reporting iframe'); + globalThis.removeEventListener('message', messageHandler); + }; + + iframe.src = reportingUrl.href; + container.appendChild(iframe); +}; + +initializeAIReporting(); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.cshtml.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/AIReporting/Index.native.cshtml.cs.disabled similarity index 95% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.cshtml.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/AIReporting/Index.native.cshtml.cs.disabled index 9b37015de..a5bb9d4e4 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.cshtml.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/AIReporting/Index.native.cshtml.cs.disabled @@ -5,7 +5,7 @@ using Unity.Modules.Shared.Permissions; using Volo.Abp.Features; -namespace Unity.GrantManager.Web.Pages.AIReporting +namespace Unity.AI.Web.Pages.AIReporting { public class IndexModel( IEndpointManagementAppService endpointManagementAppService, diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.cshtml b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/AIReporting/Index.native.cshtml.disabled similarity index 99% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.cshtml rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/AIReporting/Index.native.cshtml.disabled index 6579b5967..3d3de4406 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/AIReporting/Index.native.cshtml.disabled @@ -1,5 +1,5 @@ @page -@model Unity.GrantManager.Web.Pages.AIReporting.IndexModel +@model Unity.AI.Web.Pages.AIReporting.IndexModel @section styles { @if (Model.CanViewAiReporting) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.css b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/AIReporting/Index.native.css.disabled similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.css rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/AIReporting/Index.native.css.disabled diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.js b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/AIReporting/Index.native.js.disabled similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.js rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/AIReporting/Index.native.js.disabled diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/AIReporting/README.md b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/AIReporting/README.md new file mode 100644 index 000000000..2082a91fd --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/AIReporting/README.md @@ -0,0 +1,18 @@ +# AI Reporting Page + +`Index.*` currently uses the iframe-hosted AI Reporting app. + +The converted native Razor implementation is parked beside it as: + +- `Index.native.cshtml.disabled` +- `Index.native.cshtml.cs.disabled` +- `Index.native.css.disabled` +- `Index.native.js.disabled` + +To switch back to the native implementation: + +1. Replace `Index.cshtml` with `Index.native.cshtml.disabled`. +2. Replace `Index.cshtml.cs` with `Index.native.cshtml.cs.disabled`. +3. Replace `Index.js` with `Index.native.js.disabled`. +4. Rename `Index.native.css.disabled` to `Index.css`. +5. Build `Unity.AI.Web` and `Unity.GrantManager.Web`. diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js index 8bc5385e7..0b00bdc5e 100644 --- a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js +++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js @@ -229,7 +229,8 @@ function initializeDataTable(options) { onStateLoadParams, onStateLoaded, fixedHeaders = false, - lengthMenu = [25, 50, 75, 100, -1] + lengthMenu = [25, 50, 75, 100, -1], + deferRender = false } = options; // Process columns and visibility @@ -255,7 +256,7 @@ function initializeDataTable(options) { scrollX: true, scrollCollapse: true, autoWidth: true, - deferRender: false, + deferRender: deferRender, deferLoading: serverSideEnabled ? 0 : null, ajax: abp.libs.datatables.createAjax( dataEndpoint, diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisBackgroundJobArgs.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisBackgroundJobArgs.cs index 5c0f79d2d..5f8979af4 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisBackgroundJobArgs.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisBackgroundJobArgs.cs @@ -4,6 +4,7 @@ public class GenerateApplicationAnalysisBackgroundJobArgs { public Guid ApplicationId { get; set; } public Guid? TenantId { get; set; } + public Guid? RequestedByUserId { get; set; } public string? PromptVersion { get; set; } public string RequestKey { get; set; } = string.Empty; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringBackgroundJobArgs.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringBackgroundJobArgs.cs index 3a1bdd1d5..8ac67c4d4 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringBackgroundJobArgs.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringBackgroundJobArgs.cs @@ -4,6 +4,7 @@ public class GenerateApplicationScoringBackgroundJobArgs { public Guid ApplicationId { get; set; } public Guid? TenantId { get; set; } + public Guid? RequestedByUserId { get; set; } public string? PromptVersion { get; set; } public string RequestKey { get; set; } = string.Empty; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryBackgroundJobArgs.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryBackgroundJobArgs.cs index b75c5d3e0..af4a185c6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryBackgroundJobArgs.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryBackgroundJobArgs.cs @@ -4,6 +4,7 @@ public class GenerateAttachmentSummaryBackgroundJobArgs { public Guid ApplicationId { get; set; } public Guid? TenantId { get; set; } + public Guid? RequestedByUserId { get; set; } public string? PromptVersion { get; set; } public string RequestKey { get; set; } = string.Empty; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJobArgs.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJobArgs.cs index 3f5fee559..55b88431b 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJobArgs.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJobArgs.cs @@ -6,6 +6,7 @@ public class RunApplicationAIPipelineJobArgs { public Guid ApplicationId { get; set; } public Guid? TenantId { get; set; } + public Guid? RequestedByUserId { get; set; } public string? PromptVersion { get; set; } public string RequestKey { get; set; } = string.Empty; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/GrantApplicationListInputDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/GrantApplicationListInputDto.cs index 7de297c31..88c0e3799 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/GrantApplicationListInputDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/GrantApplicationListInputDto.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Volo.Abp.Application.Dtos; namespace Unity.GrantManager.GrantApplications @@ -7,5 +8,11 @@ public class GrantApplicationListInputDto : PagedAndSortedResultRequestDto { public DateTime? SubmittedFromDate { get; set; } public DateTime? SubmittedToDate { get; set; } + /// + /// Column names that are currently visible in the UI. When provided, only + /// the database JOINs required to populate those columns are executed. + /// Pass null or an empty list to load everything (backward-compatible). + /// + public List? RequestedFields { get; set; } } } 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 2d787933a..3c42325d0 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 @@ -2,6 +2,7 @@ using System.Linq; using System.Threading.Tasks; using Unity.AI.Automation; +using Unity.AI.RateLimit; using Unity.GrantManager.GrantApplications; using Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; using Medallion.Threading; @@ -9,6 +10,7 @@ using Volo.Abp.Domain.Repositories; using Volo.Abp.BackgroundJobs; using Volo.Abp.DependencyInjection; +using Volo.Abp.Users; namespace Unity.GrantManager.GrantApplications.Automation; @@ -16,6 +18,8 @@ public class ApplicationAIGenerationQueue( IBackgroundJobManager backgroundJobManager, IRepository generationRequestRepository, IDistributedLockProvider distributedLockProvider, + IAIRateLimiter aiRateLimiter, + ICurrentUser currentUser, ILogger logger) : IApplicationAIGenerationQueue, ITransientDependency { @@ -33,6 +37,7 @@ await EnsureRequestAndEnqueueAsync( { ApplicationId = applicationId, PromptVersion = promptVersion, + RequestedByUserId = currentUser.Id, TenantId = tenantId, RequestKey = requestKey }); @@ -53,6 +58,7 @@ await EnsureRequestAndEnqueueAsync( { ApplicationId = applicationId, PromptVersion = promptVersion, + RequestedByUserId = currentUser.Id, TenantId = tenantId, RequestKey = requestKey }); @@ -73,6 +79,7 @@ await EnsureRequestAndEnqueueAsync( { ApplicationId = applicationId, PromptVersion = promptVersion, + RequestedByUserId = currentUser.Id, TenantId = tenantId, RequestKey = requestKey }); @@ -93,6 +100,7 @@ await EnsureRequestAndEnqueueAsync( { ApplicationId = applicationId, PromptVersion = promptVersion, + RequestedByUserId = currentUser.Id, TenantId = tenantId, RequestKey = requestKey }); @@ -125,6 +133,10 @@ private async Task EnsureRequestAndEnqueueAsync( return; } + // 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(); + var request = new AIGenerationRequest( Guid.NewGuid(), tenantId, diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/AIGenerationRequestJobHelper.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/AIGenerationRequestJobHelper.cs index 978b3d587..59a1fb80f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/AIGenerationRequestJobHelper.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/AIGenerationRequestJobHelper.cs @@ -2,6 +2,8 @@ using System.Linq; using System.Linq.Expressions; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Unity.AI.RateLimit; using Unity.GrantManager.GrantApplications; using Volo.Abp.Domain.Repositories; using Volo.Abp.Uow; @@ -84,6 +86,27 @@ public static async Task MarkFailedInNewUowAsync( await uow.CompleteAsync(); } + public static async Task StampRateLimitBestEffortAsync( + IAIRateLimiter aiRateLimiter, + ILogger logger, + Guid? requestedByUserId, + Guid applicationId, + string requestKey) + { + try + { + await aiRateLimiter.StampAsync(requestedByUserId); + } + catch (Exception ex) + { + logger.LogWarning( + ex, + "AI rate-limit cooldown stamp failed after completed AI generation request for application {ApplicationId} and request {RequestKey}.", + applicationId, + requestKey); + } + } + public static async Task GetLatestRequestAsync( IRepository generationRequestRepository, Expression> predicate) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisJob.cs index b7a78850a..35d3f554c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisJob.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisJob.cs @@ -2,6 +2,7 @@ using System; using System.Threading.Tasks; using Unity.AI.Operations; +using Unity.AI.RateLimit; using Unity.GrantManager.GrantApplications; using Volo.Abp.BackgroundJobs; using Volo.Abp.DependencyInjection; @@ -16,6 +17,7 @@ public class GenerateApplicationAnalysisJob( IRepository generationRequestRepository, ICurrentTenant currentTenant, IUnitOfWorkManager unitOfWorkManager, + IAIRateLimiter aiRateLimiter, ILogger logger) : AsyncBackgroundJob, ITransientDependency { public override async Task ExecuteAsync(GenerateApplicationAnalysisBackgroundJobArgs args) @@ -30,6 +32,7 @@ public override async Task ExecuteAsync(GenerateApplicationAnalysisBackgroundJob logger.LogInformation("Completed AI application analysis job for application {ApplicationId}.", args.ApplicationId); await AIGenerationRequestJobHelper.MarkCompletedInNewUowAsync(unitOfWorkManager, generationRequestRepository, args.RequestKey); + await AIGenerationRequestJobHelper.StampRateLimitBestEffortAsync(aiRateLimiter, logger, args.RequestedByUserId, args.ApplicationId, args.RequestKey); } catch (Exception ex) { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringJob.cs index aa6bcb749..2292324a5 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringJob.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringJob.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Logging; using System; using System.Threading.Tasks; +using Unity.AI.RateLimit; using Unity.GrantManager.GrantApplications; using Unity.GrantManager.GrantApplications.Automation.Events; using Volo.Abp.BackgroundJobs; @@ -18,6 +19,7 @@ public class GenerateApplicationScoringJob( ICurrentTenant currentTenant, IUnitOfWorkManager unitOfWorkManager, ILocalEventBus localEventBus, + IAIRateLimiter aiRateLimiter, ILogger logger) : AsyncBackgroundJob, ITransientDependency { public override async Task ExecuteAsync(GenerateApplicationScoringBackgroundJobArgs args) @@ -37,7 +39,9 @@ await localEventBus.PublishAsync(new ApplicationAIScoringGeneratedEvent }); } logger.LogInformation("Completed AI application scoring job for application {ApplicationId}.", args.ApplicationId); + await AIGenerationRequestJobHelper.MarkCompletedInNewUowAsync(unitOfWorkManager, generationRequestRepository, args.RequestKey); + await AIGenerationRequestJobHelper.StampRateLimitBestEffortAsync(aiRateLimiter, logger, args.RequestedByUserId, args.ApplicationId, args.RequestKey); } catch (Exception ex) { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryJob.cs index b9a710478..63507cd58 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryJob.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryJob.cs @@ -2,6 +2,7 @@ using System; using System.Threading.Tasks; using Unity.AI.Operations; +using Unity.AI.RateLimit; using Unity.GrantManager.GrantApplications; using Volo.Abp.BackgroundJobs; using Volo.Abp.DependencyInjection; @@ -16,6 +17,7 @@ public class GenerateAttachmentSummaryJob( IRepository generationRequestRepository, ICurrentTenant currentTenant, IUnitOfWorkManager unitOfWorkManager, + IAIRateLimiter aiRateLimiter, ILogger logger) : AsyncBackgroundJob, ITransientDependency { public override async Task ExecuteAsync(GenerateAttachmentSummaryBackgroundJobArgs args) @@ -29,7 +31,9 @@ public override async Task ExecuteAsync(GenerateAttachmentSummaryBackgroundJobAr "Executing AI attachment summary job for application {ApplicationId}.", args.ApplicationId); await attachmentSummaryService.GenerateForApplicationAsync(args.ApplicationId, args.PromptVersion); + await AIGenerationRequestJobHelper.MarkCompletedInNewUowAsync(unitOfWorkManager, generationRequestRepository, args.RequestKey); + await AIGenerationRequestJobHelper.StampRateLimitBestEffortAsync(aiRateLimiter, logger, args.RequestedByUserId, args.ApplicationId, args.RequestKey); } catch (Exception ex) { 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 9ecd21a82..2a46fc353 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,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Unity.AI.RateLimit; using Unity.GrantManager.Applications; using Unity.GrantManager.Attachments; using Unity.GrantManager.GrantApplications; @@ -27,6 +28,7 @@ public class RunApplicationAIPipelineJob( IRepository generationRequestRepository, ICurrentTenant currentTenant, IUnitOfWorkManager unitOfWorkManager, + IAIRateLimiter aiRateLimiter, ILogger logger) : AsyncBackgroundJob, ITransientDependency { public override async Task ExecuteAsync(RunApplicationAIPipelineJobArgs args) @@ -50,6 +52,7 @@ public override async Task ExecuteAsync(RunApplicationAIPipelineJobArgs args) { logger.LogDebug("All AI features are disabled, skipping queued AI generation for application {ApplicationId}.", args.ApplicationId); await AIGenerationRequestJobHelper.MarkCompletedInNewUowAsync(unitOfWorkManager, generationRequestRepository, args.RequestKey); + await AIGenerationRequestJobHelper.StampRateLimitBestEffortAsync(aiRateLimiter, logger, args.RequestedByUserId, args.ApplicationId, args.RequestKey); return; } @@ -113,6 +116,7 @@ await localEventBus.PublishAsync(new ApplicationAIScoringGeneratedEvent } await AIGenerationRequestJobHelper.MarkCompletedInNewUowAsync(unitOfWorkManager, generationRequestRepository, args.RequestKey); + await AIGenerationRequestJobHelper.StampRateLimitBestEffortAsync(aiRateLimiter, logger, args.RequestedByUserId, args.ApplicationId, args.RequestKey); } catch (Exception ex) { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs index 389934299..462bb0d17 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs @@ -22,6 +22,7 @@ using Unity.GrantManager.Events; using Unity.GrantManager.Flex; using Unity.GrantManager.Identity; +using Unity.GrantManager.GlobalTag; using Unity.GrantManager.Payments; using Unity.Modules.Shared; using Unity.Modules.Shared.Correlation; @@ -72,56 +73,177 @@ public class GrantApplicationAppService( public async Task> GetListAsync(GrantApplicationListInputDto input) { - // 1. Fetch applications with filters + paging in DB - var applications = await applicationRepository.WithFullDetailsAsync( + var listRecords = await applicationRepository.GetApplicationListRecordsAsync( input.SkipCount, input.MaxResultCount, input.Sorting, input.SubmittedFromDate, - input.SubmittedToDate + input.SubmittedToDate, + requestedFields: input.RequestedFields ); - var applicationIds = applications.Select(a => a.Id).ToList(); + var applicationIds = listRecords.Select(r => r.Id).ToList(); - // 2. Fetch payment rollup batch if feature enabled - bool paymentsFeatureEnabled = await FeatureChecker.IsEnabledAsync(PaymentConsts.UnityPaymentsFeature); + + // Fetch payment rollup batch only when the feature is enabled AND at least one + // payment column is visible. The two payment column sNames are "totalPaidAmount" + // and "paymentInfo"; null RequestedFields means all columns are requested. + bool paymentColumnsRequested = input.RequestedFields == null || input.RequestedFields.Any(f => + f.Equals("totalPaidAmount", StringComparison.OrdinalIgnoreCase) + || f.Equals("paymentInfo", StringComparison.OrdinalIgnoreCase)); + + + bool paymentsFeatureEnabled = false; + if (paymentColumnsRequested) // Only even check if a column is requested. + { + paymentsFeatureEnabled = await FeatureChecker.IsEnabledAsync(PaymentConsts.UnityPaymentsFeature); + } Dictionary paymentRollupBatch = []; - if (paymentsFeatureEnabled && applicationIds.Count > 0) + if (paymentColumnsRequested && paymentsFeatureEnabled && applicationIds.Count > 0) { paymentRollupBatch = await paymentRequestService.GetApplicationPaymentRollupBatchAsync(applicationIds); } - // 3. Map applications to DTOs - var appDtos = applications.Select(app => + var appDtos = listRecords.Select(rec => { - var appDto = ObjectMapper.Map(app); - - appDto.Status = app.ApplicationStatus.InternalStatus; - appDto.Applicant = ObjectMapper.Map(app.Applicant); - appDto.Category = app.ApplicationForm.Category ?? string.Empty; - appDto.Owner = BuildApplicationOwner(app.Owner); - appDto.OrganizationName = app.Applicant?.OrgName ?? string.Empty; - appDto.ApplicationTag = ObjectMapper.Map, List>(app.ApplicationTags?.ToList() ?? []); - appDto.NonRegOrgName = app.Applicant?.NonRegOrgName ?? string.Empty; - appDto.OrganizationType = app.Applicant?.OrganizationType ?? string.Empty; - appDto.Assignees = BuildApplicationAssignees(app.ApplicationAssignments); - appDto.SubStatusDisplayValue = MapSubstatusDisplayValue(appDto.SubStatus ?? string.Empty); - appDto.DeclineRational = MapDeclineRationalDisplayValue(appDto.DeclineRational ?? string.Empty); - appDto.ContactFullName = app.ApplicantAgent?.Name; - appDto.ContactEmail = app.ApplicantAgent?.Email; - appDto.ContactTitle = app.ApplicantAgent?.Title; - appDto.ContactBusinessPhone = app.ApplicantAgent?.Phone; - appDto.ContactCellPhone = app.ApplicantAgent?.Phone2; - appDto.ApplicationLinks = ObjectMapper.Map, List>(app.ApplicationLinks?.ToList() ?? []); + var appDto = new GrantApplicationDto + { + Id = rec.Id, + ProjectName = rec.ProjectName, + ReferenceNo = rec.ReferenceNo, + RequestedAmount = rec.RequestedAmount, + TotalProjectBudget = rec.TotalProjectBudget, + EconomicRegion = rec.EconomicRegion ?? string.Empty, + City = rec.City ?? string.Empty, + ProposalDate = rec.ProposalDate, + SubmissionDate = rec.SubmissionDate, + FinalDecisionDate = rec.FinalDecisionDate, + DueDate = rec.DueDate, + NotificationDate = rec.NotificationDate, + ProjectSummary = rec.ProjectSummary ?? string.Empty, + TotalScore = rec.TotalScore ?? 0, + RecommendedAmount = rec.RecommendedAmount, + ApprovedAmount = rec.ApprovedAmount, + LikelihoodOfFunding = rec.LikelihoodOfFunding ?? string.Empty, + DueDiligenceStatus = rec.DueDiligenceStatus ?? string.Empty, + SubStatus = rec.SubStatus ?? string.Empty, + SubStatusDisplayValue = MapSubstatusDisplayValue(rec.SubStatus ?? string.Empty), + DeclineRational = MapDeclineRationalDisplayValue(rec.DeclineRational ?? string.Empty), + Notes = rec.Notes ?? string.Empty, + AssessmentResultStatus = rec.AssessmentResultStatus ?? string.Empty, + AssessmentResultDate = rec.AssessmentResultDate, + ProjectStartDate = rec.ProjectStartDate, + ProjectEndDate = rec.ProjectEndDate, + PercentageTotalProjectBudget = rec.PercentageTotalProjectBudget, + ProjectFundingTotal = rec.ProjectFundingTotal, + Community = rec.Community, + CommunityPopulation = rec.CommunityPopulation, + Acquisition = rec.Acquisition, + Forestry = rec.Forestry, + ForestryFocus = rec.ForestryFocus, + ElectoralDistrict = rec.ElectoralDistrict, + ApplicantElectoralDistrict = rec.ApplicantElectoralDistrict, + Place = rec.Place, + RegionalDistrict = rec.RegionalDistrict, + OwnerId = rec.OwnerId, + DefaultSiteId = rec.DefaultSiteId, + SigningAuthorityFullName = rec.SigningAuthorityFullName, + SigningAuthorityTitle = rec.SigningAuthorityTitle, + SigningAuthorityEmail = rec.SigningAuthorityEmail, + SigningAuthorityBusinessPhone = rec.SigningAuthorityBusinessPhone, + SigningAuthorityCellPhone = rec.SigningAuthorityCellPhone, + ContractNumber = rec.ContractNumber, + ContractExecutionDate = rec.ContractExecutionDate, + RiskRanking = rec.RiskRanking, + UnityApplicationId = rec.UnityApplicationId, + + // From ApplicationStatus + Status = rec.Status, + + // From ApplicationForm + Category = rec.Category, + + // From Applicant - both the nested DTO and the flattened top-level properties + Applicant = new GrantApplicationApplicantDto + { + Id = rec.ApplicantId, + ApplicantName = rec.ApplicantName ?? string.Empty, + SupplierId = rec.ApplicantSupplierId ?? Guid.Empty, + Sector = rec.ApplicantSector ?? string.Empty, + SubSector = rec.ApplicantSubSector ?? string.Empty, + OrgNumber = rec.ApplicantOrgNumber ?? string.Empty, + OrgName = rec.ApplicantOrgName ?? string.Empty, + OrgStatus = rec.ApplicantOrgStatus ?? string.Empty, + BusinessNumber = rec.ApplicantBusinessNumber ?? string.Empty, + OrganizationType = rec.ApplicantOrganizationType ?? string.Empty, + OrganizationSize = rec.ApplicantOrganizationSize ?? string.Empty, + SectorSubSectorIndustryDesc = rec.ApplicantSectorSubSectorIndustryDesc ?? string.Empty, + RedStop = rec.ApplicantRedStop ?? false, + IndigenousOrgInd = rec.ApplicantIndigenousOrgInd ?? string.Empty, + UnityApplicantId = rec.ApplicantUnityApplicantId ?? string.Empty, + FiscalDay = rec.ApplicantFiscalDay?.ToString() ?? string.Empty, + FiscalMonth = rec.ApplicantFiscalMonth ?? string.Empty + }, + OrganizationName = rec.ApplicantOrgName ?? string.Empty, + NonRegOrgName = rec.ApplicantNonRegOrgName ?? string.Empty, + OrganizationType = rec.ApplicantOrganizationType ?? string.Empty, + OrgStatus = rec.ApplicantOrgStatus, + BusinessNumber = rec.ApplicantBusinessNumber, + OrgNumber = rec.ApplicantOrgNumber, + OrganizationSize = rec.ApplicantOrganizationSize, + SectorSubSectorIndustryDesc = rec.ApplicantSectorSubSectorIndustryDesc, + Sector = rec.ApplicantSector, + SubSector = rec.ApplicantSubSector, + + // From ApplicantAgent + ContactFullName = rec.ContactFullName, + ContactTitle = rec.ContactTitle, + ContactEmail = rec.ContactEmail, + ContactBusinessPhone = rec.ContactBusinessPhone, + ContactCellPhone = rec.ContactCellPhone, + + // From Owner + Owner = rec.OwnerPersonId.HasValue + ? new GrantApplicationAssigneeDto { Id = rec.OwnerPersonId.Value, FullName = rec.OwnerFullName ?? string.Empty } + : new GrantApplicationAssigneeDto(), + + // Collections + ApplicationTag = rec.Tags + .Select(t => new ApplicationTagsDto + { + Id = t.Id, + ApplicationId = t.ApplicationId, + Tag = t.TagName != null ? new TagDto { Name = t.TagName } : null + }).ToList(), + + Assignees = rec.Assignments + .Select(a => new GrantApplicationAssigneeDto + { + Id = a.Id, + ApplicationId = a.ApplicationId, + AssigneeId = a.AssigneeId, + FullName = a.AssigneeName, + Duty = a.Duty + }).ToList(), + + ApplicationLinks = rec.Links + .Select(l => new ApplicationLinksDto + { + Id = l.Id, + ApplicationId = l.ApplicationId, + LinkedApplicationId = l.LinkedApplicationId, + LinkType = l.LinkType + }).ToList() + }; if (paymentsFeatureEnabled && paymentRollupBatch.Count > 0) { - paymentRollupBatch.TryGetValue(app.Id, out var rollup); + paymentRollupBatch.TryGetValue(rec.Id, out var rollup); appDto.PaymentInfo = new PaymentInfoDto { - ApprovedAmount = app.ApprovedAmount, + ApprovedAmount = rec.ApprovedAmount, TotalPaid = rollup?.TotalPaid ?? 0 }; } @@ -129,11 +251,15 @@ public async Task> GetListAsync(GrantApplica }).ToList(); - // 4. Get total count using same filters - var totalCount = await applicationRepository.GetCountAsync( - input.SubmittedFromDate, - input.SubmittedToDate - ); +#pragma warning disable S125 + //Code is temporarily commented out as this will be the way to get the accurate count + //once the core GrantApplications data table is moved server side from client side. + //Until then, since it is client side and always requests all records at once to be + //loaded, an extra round-trip to the database for a query is unnecessary. + + //var totalCount = await applicationRepository.GetCountAsync(input.SubmittedFromDate,input.SubmittedToDate); +#pragma warning restore S125 + var totalCount = appDtos.Count; return new PagedResultDto(totalCount, appDtos); } @@ -961,7 +1087,7 @@ public async Task GetApplicationStatusAsync(Guid id) return form.AccountCodingId; } - + public async Task IsApplicantRedStopAsync(Guid applicationId) { @@ -988,7 +1114,7 @@ public async Task> GetActions(Guid applicati // NOTE: Authorization is applied on the AppService layer and is false by default // AUTHORIZATION HANDLING - bool isRedStop = application.Applicant != null && application.Applicant.RedStop == true; + bool isRedStop = application.Applicant != null && application.Applicant.RedStop == true; foreach (var item in actionDtos) { item.IsPermitted = !isRedStop && item.IsPermitted && (await AuthorizationService.IsGrantedAsync(application, GetActionAuthorizationRequirement(item.ApplicationAction))); @@ -1298,4 +1424,4 @@ private static void UpdateFindingDismissedState(IEnumerable { public override partial ReportsHistoryDto Map(ReportsHistory source); public override partial void Map(ReportsHistory source, ReportsHistoryDto destination); } +[Mapper] +public partial class CreateUpdateReportsHistoryDtoToEntityMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(ReportsHistory.Id))] + [MapperIgnoreTarget(nameof(ReportsHistory.TenantId))] + [MapperIgnoreTarget(nameof(ReportsHistory.ConcurrencyStamp))] + [MapperIgnoreTarget(nameof(ReportsHistory.CreationTime))] + [MapperIgnoreTarget(nameof(ReportsHistory.CreatorId))] + [MapperIgnoreTarget(nameof(ReportsHistory.LastModificationTime))] + [MapperIgnoreTarget(nameof(ReportsHistory.LastModifierId))] + public override partial ReportsHistory Map(CreateUpdateReportsHistoryDto source); + + [MapperIgnoreTarget(nameof(ReportsHistory.Id))] + [MapperIgnoreTarget(nameof(ReportsHistory.TenantId))] + [MapperIgnoreTarget(nameof(ReportsHistory.ConcurrencyStamp))] + [MapperIgnoreTarget(nameof(ReportsHistory.CreationTime))] + [MapperIgnoreTarget(nameof(ReportsHistory.CreatorId))] + [MapperIgnoreTarget(nameof(ReportsHistory.LastModificationTime))] + [MapperIgnoreTarget(nameof(ReportsHistory.LastModifierId))] + public override partial void Map(CreateUpdateReportsHistoryDto source, ReportsHistory destination); +} + +[Mapper] +public partial class ReportsHistoryDtoToEntityMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(ReportsHistory.TenantId))] + [MapperIgnoreTarget(nameof(ReportsHistory.ConcurrencyStamp))] + public override partial ReportsHistory Map(ReportsHistoryDto source); + + [MapperIgnoreTarget(nameof(ReportsHistory.TenantId))] + [MapperIgnoreTarget(nameof(ReportsHistory.ConcurrencyStamp))] + public override partial void Map(ReportsHistoryDto source, ReportsHistory destination); +} + [Mapper] public partial class ApplicationToApplicantInfoDtoMapper : MapperBase { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json index 486e25d52..1b593184c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json @@ -3,7 +3,6 @@ "texts": { "Menu:Home": "Home", "Menu:Dashboard": "Dashboard", - "Menu:AIReporting": "AI Reporting", "Menu:GrantPrograms": "Grant Programs", "Menu:GrantTracker": "Grant Tracker", "Menu:Applications": "Applications", diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationListRecord.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationListRecord.cs new file mode 100644 index 000000000..f086d1299 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationListRecord.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; + +namespace Unity.GrantManager.Applications; + +/// +/// Flattened projection returned by +/// . +/// Only the columns required for the application list view are selected +/// +public class ApplicationListRecord +{ + public Guid Id { get; init; } + public string ProjectName { get; init; } = string.Empty; + public string ReferenceNo { get; init; } = string.Empty; + public decimal RequestedAmount { get; init; } + public decimal TotalProjectBudget { get; init; } + public string? EconomicRegion { get; init; } + public string? City { get; init; } + public DateTime? ProposalDate { get; init; } + public DateTime SubmissionDate { get; init; } + public DateTime? FinalDecisionDate { get; init; } + public DateTime? DueDate { get; init; } + public DateTime? NotificationDate { get; init; } + public string? ProjectSummary { get; init; } + public int? TotalScore { get; init; } + public decimal RecommendedAmount { get; init; } + public decimal ApprovedAmount { get; init; } + public string? LikelihoodOfFunding { get; init; } + public string? DueDiligenceStatus { get; init; } + public string? SubStatus { get; init; } + public string? DeclineRational { get; init; } + public string? Notes { get; init; } + public string? AssessmentResultStatus { get; init; } + public DateTime? AssessmentResultDate { get; init; } + public DateTime? ProjectStartDate { get; init; } + public DateTime? ProjectEndDate { get; init; } + public double? PercentageTotalProjectBudget { get; init; } + public decimal? ProjectFundingTotal { get; init; } + public string? Community { get; init; } + public int? CommunityPopulation { get; init; } + public string? Acquisition { get; init; } + public string? Forestry { get; init; } + public string? ForestryFocus { get; init; } + public string? ElectoralDistrict { get; init; } + public string? ApplicantElectoralDistrict { get; init; } + public string? Place { get; init; } + public string? RegionalDistrict { get; init; } + public Guid? OwnerId { get; init; } + public Guid? DefaultSiteId { get; init; } + public string? SigningAuthorityFullName { get; init; } + public string? SigningAuthorityTitle { get; init; } + public string? SigningAuthorityEmail { get; init; } + public string? SigningAuthorityBusinessPhone { get; init; } + public string? SigningAuthorityCellPhone { get; init; } + public string? ContractNumber { get; init; } + public DateTime? ContractExecutionDate { get; init; } + public string? RiskRanking { get; init; } + public string? UnityApplicationId { get; init; } + + // ApplicationStatus (always joined) + public string Status { get; init; } = string.Empty; + + // ApplicationForm (always joined) + public string Category { get; init; } = string.Empty; + + // Applicant (always joined) + public Guid ApplicantId { get; init; } + public string? ApplicantName { get; init; } + public Guid? ApplicantSupplierId { get; init; } + public string? ApplicantSector { get; init; } + public string? ApplicantSubSector { get; init; } + public string? ApplicantOrgName { get; init; } + public string? ApplicantNonRegOrgName { get; init; } + public string? ApplicantOrganizationType { get; init; } + public string? ApplicantOrgNumber { get; init; } + public string? ApplicantOrgStatus { get; init; } + public string? ApplicantBusinessNumber { get; init; } + public string? ApplicantOrganizationSize { get; init; } + public string? ApplicantSectorSubSectorIndustryDesc { get; init; } + public bool? ApplicantRedStop { get; init; } + public string? ApplicantIndigenousOrgInd { get; init; } + public int? ApplicantFiscalDay { get; init; } + public string? ApplicantFiscalMonth { get; init; } + public string? ApplicantUnityApplicantId { get; init; } + + // ApplicantAgent (left-joined when present) + public string? ContactFullName { get; init; } + public string? ContactTitle { get; init; } + public string? ContactEmail { get; init; } + public string? ContactBusinessPhone { get; init; } + public string? ContactCellPhone { get; init; } + + // Owner / Person (left-joined when present) + public Guid? OwnerPersonId { get; init; } + public string? OwnerFullName { get; init; } + + // Collections (correlated subqueries) + public List Tags { get; init; } = []; + public List Assignments { get; init; } = []; + public List Links { get; init; } = []; +} + +public class ApplicationTagListItem +{ + public Guid Id { get; init; } + public Guid ApplicationId { get; init; } + public string? TagName { get; init; } +} + +public class ApplicationAssignmentListItem +{ + public Guid Id { get; init; } + public Guid ApplicationId { get; init; } + public Guid AssigneeId { get; init; } + public string AssigneeName { get; init; } = string.Empty; + public string? Duty { get; init; } +} + +public class ApplicationLinkListItem +{ + public Guid Id { get; init; } + public Guid ApplicationId { get; init; } + public Guid LinkedApplicationId { get; init; } + public ApplicationLinkType LinkType { get; init; } +} \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IApplicationRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IApplicationRepository.cs index e19610e96..62a57ce6d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IApplicationRepository.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IApplicationRepository.cs @@ -27,6 +27,17 @@ Task> WithFullDetailsAsync( string? searchTerm = null // optional search filter ); + // Optimized projected list for the application list table. + Task> GetApplicationListRecordsAsync( + int skipCount, + int maxResultCount, + string? sorting = null, + DateTime? submittedFromDate = null, + DateTime? submittedToDate = null, + string? searchTerm = null, + IReadOnlyList? requestedFields = null + ); + // Get applications by applicant ID Task> GetByApplicantIdAsync(Guid applicantId); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/DbWarmupOptions.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/DbWarmupOptions.cs new file mode 100644 index 000000000..04aa7a77b --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/DbWarmupOptions.cs @@ -0,0 +1,40 @@ +namespace Unity.GrantManager.EntityFrameworkCore; + +/// +/// Configuration options for . +/// Bind from appsettings.json under the "DbWarmup" section. +/// +/// Example: +/// +/// "DbWarmup": { +/// "IsPhase2Enabled": true, +/// "MaxTenants": 5, +/// "Phase2TimeoutSeconds": 30 +/// } +/// +/// +public class DbWarmupOptions +{ + public const string SectionName = "DbWarmup"; + + /// + /// When false, Phase 2 (per-tenant DB round-trips) is skipped entirely. + /// Phase 1 (EF Core model compilation) always runs regardless of this setting. + /// Default: true. + /// + public bool IsPhase2Enabled { get; set; } = true; + + /// + /// Maximum number of tenants to warm in Phase 2. + /// 0 means no limit. Default: 0. + /// Useful in constrained environments or when tenant count is very large. + /// + public int MaxTenants { get; set; } = 0; + + /// + /// Total seconds allowed for Phase 2 across all tenants before it is abandoned. + /// 0 means no timeout. Default: 0. + /// Remaining tenants are skipped gracefully when the timeout elapses. + /// + public int Phase2TimeoutSeconds { get; set; } = 0; +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantManagerDbWarmupService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantManagerDbWarmupService.cs new file mode 100644 index 000000000..723f7638e --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantManagerDbWarmupService.cs @@ -0,0 +1,225 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Unity.GrantManager.Applications; + +using Volo.Abp.EntityFrameworkCore; +using Volo.Abp.MultiTenancy; +using Volo.Abp.TenantManagement; +using Volo.Abp.Uow; + +namespace Unity.GrantManager.EntityFrameworkCore; + +/// +/// Background service that pre-warms the EF Core query pipeline after application startup. +/// +/// On first use, EF Core performs three expensive one-time operations: +/// 1. Model snapshot compilation — GrantTenantDbContext.OnModelCreating (30+ entity types) +/// 2. LINQ→SQL expression tree translation — especially costly for multi-JOIN includes +/// 3. Npgsql connection pool establishment + PostgreSQL query plan caching +/// +/// These costs are normally deferred to the first HTTP request, causing 6-8 second cold-start +/// latency for the GrantApplications DataTable. This service fires the most expensive query +/// shape (GetApplicationListRecordsAsync with typical date filters) shortly after startup so the +/// cache is warm before any user makes a request. +/// +/// Warmup is split into two independent phases: +/// Phase 1 (model compilation) — always succeeds; no DB connection required. +/// Phase 2 (per-tenant DB round-trip) — iterates tenants from the host database and warms +/// Npgsql's connection pool and PostgreSQL's query plan cache for each. +/// +/// Phase 2 behaviour is configurable via (appsettings "DbWarmup" section): +/// IsPhase2Enabled — set false to skip Phase 2 entirely (default: true). +/// MaxTenants — cap the number of tenants warmed; 0 = unlimited (default: 0). +/// Phase2TimeoutSeconds — abandon Phase 2 after N seconds; 0 = no timeout (default: 0). +/// +/// +public class GrantManagerDbWarmupService : BackgroundService +{ + private readonly IServiceScopeFactory _scopeFactory; + private readonly ILogger _logger; + private readonly IHostApplicationLifetime _hostApplicationLifetime; + private readonly DbWarmupOptions _options; + + public GrantManagerDbWarmupService( + IServiceScopeFactory scopeFactory, + ILogger logger, + IHostApplicationLifetime hostApplicationLifetime, + IOptions options) + { + _scopeFactory = scopeFactory; + _logger = logger; + _hostApplicationLifetime = hostApplicationLifetime; + _options = options.Value; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + // Wait until the host has fully started so ABP module initialization and startup hooks + // are complete before issuing any warmup queries. + if (!_hostApplicationLifetime.ApplicationStarted.IsCancellationRequested) + { + var applicationStartedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + using var applicationStartedRegistration = _hostApplicationLifetime.ApplicationStarted.Register( + static state => ((TaskCompletionSource)state!).TrySetResult(), + applicationStartedTcs); + using var cancellationRegistration = stoppingToken.Register( + static state => ((TaskCompletionSource)state!).TrySetCanceled(), + applicationStartedTcs); + + await applicationStartedTcs.Task; + } + + if (stoppingToken.IsCancellationRequested) return; + + _logger.LogInformation("[DbWarmup] Starting EF Core query pipeline warmup."); + + // Step 1: Model + // Accessing dbContext.Model forces EF Core to run OnModelCreating synchronously. + // This is a pure in-process operation; no DB connection is opened. + using (var phase1Scope = _scopeFactory.CreateScope()) + { + var unitOfWorkManager = phase1Scope.ServiceProvider.GetRequiredService(); + try + { + using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false); + var dbContextProvider = phase1Scope.ServiceProvider + .GetRequiredService>(); + var dbContext = await dbContextProvider.GetDbContextAsync(); + + // Accessing Model triggers OnModelCreating if not yet compiled. + // The result is cached for the lifetime of the application. + _ = dbContext.Model; + + await uow.CompleteAsync(); + _logger.LogInformation("[DbWarmup] Phase 1 complete — EF Core model compiled."); + } + catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { return; } + catch (Exception ex) + { + _logger.LogWarning(ex, "[DbWarmup] Phase 1 (model compilation) failed — this is unexpected."); + } + } + + // Step 2: Per-tenant DB connection + PostgreSQL query plan warmup + // Enumerates all tenants (ITenantRepository -> GrantManagerDbContext -> accessible without an active tenant scope). + // Foreach tenant, opens a new DI scope, activates the tenant via ICurrentTenant.Change, and issues a Take(1) query so that: + // - Opens and pools a connection to that tenant's database + // - PostgreSQL parses and caches the parameterised execution plan for the query shape + // - EFCore's compiled query cache is populated for this tenant + // Each tenant is isolated in its own scope to prevent UoW state from leaking between tenants. + // Uses GetApplicationListRecordsAsync — the same optimized projected query the DataTable endpoint calls. + if (!_options.IsPhase2Enabled) + { + _logger.LogInformation("[DbWarmup] Phase 2 disabled via configuration — skipping per-tenant warmup."); + return; + } + + IReadOnlyList tenants; + + using (var tenantListScope = _scopeFactory.CreateScope()) + { + var tenantUowManager = tenantListScope.ServiceProvider.GetRequiredService(); + try + { + using var uow = tenantUowManager.Begin(requiresNew: true, isTransactional: false); + var tenantRepository = tenantListScope.ServiceProvider.GetRequiredService(); + tenants = await tenantRepository.GetListAsync(); + await uow.CompleteAsync(); + } + catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { return; } + catch (Exception ex) + { + _logger.LogWarning(ex, "[DbWarmup] Phase 2 — could not retrieve tenant list from host database. Skipping per-tenant warmup."); + return; + } + } + + if (tenants.Count == 0) + { + _logger.LogDebug("[DbWarmup] Phase 2 — no tenants found in host database. Skipping per-tenant DB warmup."); + return; + } + + // Apply MaxTenants cap + var tenantsToWarm = _options.MaxTenants > 0 + ? tenants.Take(_options.MaxTenants).ToList() + : (IReadOnlyList)tenants; + + if (_options.MaxTenants > 0 && tenants.Count > _options.MaxTenants) + { + _logger.LogInformation( + "[DbWarmup] Phase 2 — capped at {MaxTenants} of {TotalTenants} tenant(s) (MaxTenants setting).", + _options.MaxTenants, tenants.Count); + } + + _logger.LogInformation("[DbWarmup] Phase 2 — warming {TenantCount} tenant(s).", tenantsToWarm.Count); + + // Apply Phase2TimeoutSeconds — link a deadline token with stoppingToken + using var phase2Cts = _options.Phase2TimeoutSeconds > 0 + ? CancellationTokenSource.CreateLinkedTokenSource(stoppingToken) + : null; + if (phase2Cts != null) + { + phase2Cts.CancelAfter(TimeSpan.FromSeconds(_options.Phase2TimeoutSeconds)); + _logger.LogDebug("[DbWarmup] Phase 2 — timeout set to {Seconds}s.", _options.Phase2TimeoutSeconds); + } + var phase2Token = phase2Cts?.Token ?? stoppingToken; + + var warmed = 0; + foreach (var tenant in tenantsToWarm) + { + if (phase2Token.IsCancellationRequested) + { + // Distinguish between a Phase 2 timeout and a host shutdown + if (!stoppingToken.IsCancellationRequested) + _logger.LogInformation( + "[DbWarmup] Phase 2 — timeout reached after {Warmed}/{Total} tenant(s).", + warmed, tenantsToWarm.Count); + return; + } + + using var tenantScope = _scopeFactory.CreateScope(); + var currentTenant = tenantScope.ServiceProvider.GetRequiredService(); + var tenantUowManager = tenantScope.ServiceProvider.GetRequiredService(); + + using (currentTenant.Change(tenant.Id)) + { + try + { + using var uow = tenantUowManager.Begin(requiresNew: true, isTransactional: false); + var repository = tenantScope.ServiceProvider.GetRequiredService(); + + await repository.GetApplicationListRecordsAsync( + skipCount: 0, + maxResultCount: 1, + sorting: null, + submittedFromDate: DateTime.UtcNow.AddMonths(-6), + submittedToDate: DateTime.UtcNow); + + await uow.CompleteAsync(); + warmed++; + _logger.LogDebug("[DbWarmup] Tenant '{TenantName}' ({TenantId}) warmed.", tenant.Name, tenant.Id); + } + catch (OperationCanceledException) when (phase2Token.IsCancellationRequested) { return; } + catch (Exception ex) + { + _logger.LogDebug(ex, + "[DbWarmup] Tenant '{TenantName}' ({TenantId}) — DB round-trip skipped. " + + "Tenant database may not be accessible in this environment.", + tenant.Name, tenant.Id); + } + } + } + + _logger.LogInformation("[DbWarmup] Phase 2 complete — {Warmed}/{Total} tenant(s) warmed.", warmed, tenantsToWarm.Count); + } +} \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantTenantDbContext.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantTenantDbContext.cs index 0d70b15a5..166e8cf1f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantTenantDbContext.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantTenantDbContext.cs @@ -68,6 +68,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) GrantManagerConsts.DbSchema); b.ConfigureByConvention(); b.HasIndex(x => x.OidcSub); + b.HasIndex(x => x.TenantId); }); modelBuilder.Entity(b => @@ -80,6 +81,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasMaxLength(600); b.HasIndex(x => x.ApplicantName); + b.HasIndex(x => x.TenantId); b.HasMany() .WithOne(s => s.Applicant) @@ -111,6 +113,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasForeignKey(x => x.ParentFormId) .IsRequired(false) .OnDelete(DeleteBehavior.NoAction); + + b.HasIndex(x => new { x.TenantId, x.IsDeleted }).HasFilter("\"IsDeleted\" = false"); }); modelBuilder.Entity(b => @@ -156,6 +160,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .WithMany(s => s.Applications) .HasForeignKey(x => x.ApplicationStatusId) .IsRequired(); + + b.HasIndex(x => new { x.TenantId, x.SubmissionDate }).HasFilter("\"IsDeleted\" = false"); + b.HasIndex(x => x.ReferenceNo); }); modelBuilder.Entity(b => @@ -181,6 +188,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.ConfigureByConvention(); //auto configure for the base class props b.HasOne().WithMany().HasForeignKey(x => x.ApplicantId).IsRequired(); + b.HasIndex(x => new { x.TenantId, x.ApplicationId }); }); modelBuilder.Entity(b => @@ -286,6 +294,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) GrantManagerConsts.DbSchema); b.ConfigureByConvention(); + b.HasIndex(x => new { x.TenantId, x.ApplicationId }); }); modelBuilder.Entity(b => @@ -298,8 +307,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasForeignKey(x => x.TagId) .IsRequired() .OnDelete(DeleteBehavior.NoAction); - - + b.HasIndex(x => new { x.TenantId, x.ApplicationId }); }); modelBuilder.Entity(b => @@ -327,7 +335,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .IsRequired() .HasDefaultValue(ApplicationLinkType.Related) .HasConversion(new EnumToStringConverter()); - + b.HasIndex(x => new { x.TenantId, x.ApplicationId }); }); modelBuilder.Entity(b => diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260507201003_Add_Applications_Performance_Indexes.Designer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260507201003_Add_Applications_Performance_Indexes.Designer.cs new file mode 100644 index 000000000..bbd75153b --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260507201003_Add_Applications_Performance_Indexes.Designer.cs @@ -0,0 +1,5031 @@ +// +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("20260507201003_Add_Applications_Performance_Indexes")] + partial class Add_Applications_Performance_Indexes + { + /// + 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("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.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("RenderFormIoToHtml") + .HasColumnType("boolean"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("SuffixType") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IntakeId"); + + b.HasIndex("ParentFormId"); + + b.ToTable("ApplicationForms", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormSubmission", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormVersionId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ChefsSubmissionGuid") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormVersionId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OidcSub") + .IsRequired() + .HasColumnType("text"); + + b.Property("RenderedHTML") + .HasColumnType("text"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Submission") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormSubmissions", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormVersion", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("AvailableChefsFields") + .HasColumnType("text"); + + b.Property("ChefsApplicationFormGuid") + .HasColumnType("text"); + + b.Property("ChefsFormVersionGuid") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormSchema") + .HasColumnType("jsonb"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SubmissionHeaderMapping") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormVersion", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationLink", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LinkType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("Related"); + + b.Property("LinkedApplicationId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.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("AuditTrackingNumber") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("AuditHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.FundingHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApprovedAmount") + .HasColumnType("numeric"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FundingNotes") + .HasColumnType("text"); + + b.Property("FundingYear") + .HasColumnType("text"); + + b.Property("GrantCategory") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OneTimeConsideration") + .HasColumnType("numeric"); + + b.Property("ReconsiderationAmount") + .HasColumnType("numeric"); + + b.Property("RenewedFunding") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TotalGrantAmount") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("FundingHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.IssueTracking", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IssueDescription") + .HasColumnType("text"); + + b.Property("IssueHeading") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ResolutionNote") + .HasColumnType("text"); + + b.Property("Resolved") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Year") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("IssueTrackings", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.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.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) + .IsRequired(); + + b.Navigation("AccountCoding"); + + b.Navigation("Site"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentTags.PaymentTag", b => + { + b.HasOne("Unity.Payments.Domain.PaymentRequests.PaymentRequest", null) + .WithMany("PaymentTags") + .HasForeignKey("PaymentRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.GlobalTag.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Site", b => + { + b.HasOne("Unity.Payments.Domain.Suppliers.Supplier", "Supplier") + .WithMany("Sites") + .HasForeignKey("SupplierId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.Navigation("Answers"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.Navigation("Answers"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Scoresheet", b => + { + b.Navigation("Instances"); + + b.Navigation("Sections"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.Navigation("Fields"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", b => + { + b.Navigation("Values"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.Worksheet", b => + { + b.Navigation("Links"); + + b.Navigation("Sections"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.Navigation("Fields"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Applicant", b => + { + b.Navigation("ApplicantAddresses"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.Navigation("ApplicantAddresses"); + + b.Navigation("ApplicantAgent"); + + b.Navigation("ApplicationAssignments"); + + b.Navigation("ApplicationLinks"); + + b.Navigation("ApplicationTags"); + + b.Navigation("Assessments"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationStatus", b => + { + b.Navigation("Applications"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.Navigation("ExpenseApprovals"); + + b.Navigation("PaymentTags"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Supplier", b => + { + b.Navigation("Sites"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260507201003_Add_Applications_Performance_Indexes.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260507201003_Add_Applications_Performance_Indexes.cs new file mode 100644 index 000000000..316d886af --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260507201003_Add_Applications_Performance_Indexes.cs @@ -0,0 +1,73 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + /// + public partial class Add_Applications_Performance_Indexes : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_ApplicationTags_TenantId_ApplicationId", + table: "ApplicationTags", + columns: new[] { "TenantId", "ApplicationId" }); + + migrationBuilder.CreateIndex( + name: "IX_Applications_ReferenceNo", + table: "Applications", + column: "ReferenceNo"); + + migrationBuilder.CreateIndex( + name: "IX_Applications_TenantId_SubmissionDate", + table: "Applications", + columns: new[] { "TenantId", "SubmissionDate" }, + filter: "\"IsDeleted\" = false"); + + migrationBuilder.CreateIndex( + name: "IX_ApplicationLinks_TenantId_ApplicationId", + table: "ApplicationLinks", + columns: new[] { "TenantId", "ApplicationId" }); + + migrationBuilder.CreateIndex( + name: "IX_ApplicationAssignments_TenantId_ApplicationId", + table: "ApplicationAssignments", + columns: new[] { "TenantId", "ApplicationId" }); + + migrationBuilder.CreateIndex( + name: "IX_ApplicantAgents_TenantId_ApplicationId", + table: "ApplicantAgents", + columns: new[] { "TenantId", "ApplicationId" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_ApplicationTags_TenantId_ApplicationId", + table: "ApplicationTags"); + + migrationBuilder.DropIndex( + name: "IX_Applications_ReferenceNo", + table: "Applications"); + + migrationBuilder.DropIndex( + name: "IX_Applications_TenantId_SubmissionDate", + table: "Applications"); + + migrationBuilder.DropIndex( + name: "IX_ApplicationLinks_TenantId_ApplicationId", + table: "ApplicationLinks"); + + migrationBuilder.DropIndex( + name: "IX_ApplicationAssignments_TenantId_ApplicationId", + table: "ApplicationAssignments"); + + migrationBuilder.DropIndex( + name: "IX_ApplicantAgents_TenantId_ApplicationId", + table: "ApplicantAgents"); + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260507201139_Add_Supporting_Table_TenantId_Indexes.Designer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260507201139_Add_Supporting_Table_TenantId_Indexes.Designer.cs new file mode 100644 index 000000000..663a51956 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260507201139_Add_Supporting_Table_TenantId_Indexes.Designer.cs @@ -0,0 +1,5038 @@ +// +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("20260507201139_Add_Supporting_Table_TenantId_Indexes")] + partial class Add_Supporting_Table_TenantId_Indexes + { + /// + 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("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("RenderFormIoToHtml") + .HasColumnType("boolean"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("SuffixType") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IntakeId"); + + b.HasIndex("ParentFormId"); + + b.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("RenderedHTML") + .HasColumnType("text"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Submission") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormSubmissions", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormVersion", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("AvailableChefsFields") + .HasColumnType("text"); + + b.Property("ChefsApplicationFormGuid") + .HasColumnType("text"); + + b.Property("ChefsFormVersionGuid") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormSchema") + .HasColumnType("jsonb"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SubmissionHeaderMapping") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormVersion", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationLink", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LinkType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("Related"); + + b.Property("LinkedApplicationId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.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("AuditTrackingNumber") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("AuditHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.FundingHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApprovedAmount") + .HasColumnType("numeric"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FundingNotes") + .HasColumnType("text"); + + b.Property("FundingYear") + .HasColumnType("text"); + + b.Property("GrantCategory") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OneTimeConsideration") + .HasColumnType("numeric"); + + b.Property("ReconsiderationAmount") + .HasColumnType("numeric"); + + b.Property("RenewedFunding") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TotalGrantAmount") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("FundingHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.IssueTracking", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IssueDescription") + .HasColumnType("text"); + + b.Property("IssueHeading") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ResolutionNote") + .HasColumnType("text"); + + b.Property("Resolved") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Year") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("IssueTrackings", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.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) + .IsRequired(); + + b.Navigation("AccountCoding"); + + b.Navigation("Site"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentTags.PaymentTag", b => + { + b.HasOne("Unity.Payments.Domain.PaymentRequests.PaymentRequest", null) + .WithMany("PaymentTags") + .HasForeignKey("PaymentRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.GlobalTag.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Site", b => + { + b.HasOne("Unity.Payments.Domain.Suppliers.Supplier", "Supplier") + .WithMany("Sites") + .HasForeignKey("SupplierId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.Navigation("Answers"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.Navigation("Answers"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Scoresheet", b => + { + b.Navigation("Instances"); + + b.Navigation("Sections"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.Navigation("Fields"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", b => + { + b.Navigation("Values"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.Worksheet", b => + { + b.Navigation("Links"); + + b.Navigation("Sections"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.Navigation("Fields"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Applicant", b => + { + b.Navigation("ApplicantAddresses"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.Navigation("ApplicantAddresses"); + + b.Navigation("ApplicantAgent"); + + b.Navigation("ApplicationAssignments"); + + b.Navigation("ApplicationLinks"); + + b.Navigation("ApplicationTags"); + + b.Navigation("Assessments"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationStatus", b => + { + b.Navigation("Applications"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.Navigation("ExpenseApprovals"); + + b.Navigation("PaymentTags"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Supplier", b => + { + b.Navigation("Sites"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260507201139_Add_Supporting_Table_TenantId_Indexes.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260507201139_Add_Supporting_Table_TenantId_Indexes.cs new file mode 100644 index 000000000..4cfd16f94 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260507201139_Add_Supporting_Table_TenantId_Indexes.cs @@ -0,0 +1,46 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + /// + public partial class Add_Supporting_Table_TenantId_Indexes : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_Persons_TenantId", + table: "Persons", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_ApplicationForms_TenantId_IsDeleted", + table: "ApplicationForms", + columns: new[] { "TenantId", "IsDeleted" }, + filter: "\"IsDeleted\" = false"); + + migrationBuilder.CreateIndex( + name: "IX_Applicants_TenantId", + table: "Applicants", + column: "TenantId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Persons_TenantId", + table: "Persons"); + + migrationBuilder.DropIndex( + name: "IX_ApplicationForms_TenantId_IsDeleted", + table: "ApplicationForms"); + + migrationBuilder.DropIndex( + name: "IX_Applicants_TenantId", + table: "Applicants"); + } + } +} 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 1722b5648..cff23c144 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 @@ -915,6 +915,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("ApplicantName"); + b.HasIndex("TenantId"); + b.ToTable("Applicants", (string)null); }); @@ -1103,6 +1105,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("ApplicationId") .IsUnique(); + b.HasIndex("TenantId", "ApplicationId"); + b.ToTable("ApplicantAgents", (string)null); }); @@ -1393,6 +1397,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("OwnerId"); + b.HasIndex("ReferenceNo"); + + b.HasIndex("TenantId", "SubmissionDate") + .HasFilter("\"IsDeleted\" = false"); + b.ToTable("Applications", (string)null); }); @@ -1448,6 +1457,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("AssigneeId"); + b.HasIndex("TenantId", "ApplicationId"); + b.ToTable("ApplicationAssignments", (string)null); }); @@ -1783,6 +1794,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("ParentFormId"); + b.HasIndex("TenantId", "IsDeleted") + .HasFilter("\"IsDeleted\" = false"); + b.ToTable("ApplicationForms", (string)null); }); @@ -1999,6 +2013,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("ApplicationId"); + b.HasIndex("TenantId", "ApplicationId"); + b.ToTable("ApplicationLinks", (string)null); }); @@ -2109,6 +2125,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("TagId"); + b.HasIndex("TenantId", "ApplicationId"); + b.ToTable("ApplicationTags", (string)null); }); @@ -2926,6 +2944,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("OidcSub"); + b.HasIndex("TenantId"); + b.ToTable("Persons", (string)null); }); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicationRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicationRepository.cs index a798964c6..4133f618b 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicationRepository.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicationRepository.cs @@ -64,13 +64,38 @@ private static (DateTime? FromUtc, DateTime? ToUtc) ConvertToUtcRange( return (fromUtc, toUtc); } - /// - /// Base query with all required includes - /// + private static readonly HashSet ApplicantAgentFields = new(StringComparer.OrdinalIgnoreCase) + { + "contactFullName", "contactTitle", "contactEmail", + "contactBusinessPhone", "contactCellPhone" + }; + + private static readonly HashSet TagFields = new(StringComparer.OrdinalIgnoreCase) + { + "applicationTag" + }; + + private static readonly HashSet OwnerFields = new(StringComparer.OrdinalIgnoreCase) + { + "Owner" + }; + + private static readonly HashSet ApplicationLinkFields = new(StringComparer.OrdinalIgnoreCase) + { + "applicationLinks" + }; + + private static readonly HashSet AssignmentFields = new(StringComparer.OrdinalIgnoreCase) + { + "assignees" + }; + + private async Task> BuildBaseQueryAsync() { return (await GetQueryableAsync()) .AsNoTracking() + .AsSplitQuery() .Include(a => a.ApplicationForm) .Include(a => a.ApplicationStatus) .Include(a => a.Applicant) @@ -141,7 +166,8 @@ public async Task GetCountAsync( DateTime? submittedFromDate, DateTime? submittedToDate) { - var query = await BuildBaseQueryAsync(); + // Dont use full query, run basic to just get count. + var query = (await GetQueryableAsync()).AsNoTracking(); var (fromUtc, toUtc) = ConvertToUtcRange( submittedFromDate, submittedToDate); @@ -197,6 +223,317 @@ public async Task> GetByApplicantIdAsync(Guid applicantId) .ToListAsync(); } + public async Task> GetApplicationListRecordsAsync( + int skipCount, + int maxResultCount, + string? sorting = null, + DateTime? submittedFromDate = null, + DateTime? submittedToDate = null, + string? searchTerm = null, + IReadOnlyList? requestedFields = null) + { + var fields = requestedFields != null + ? new HashSet(requestedFields, StringComparer.OrdinalIgnoreCase) + : null; // null = all fields included + + bool includeTags = fields == null || fields.Overlaps(TagFields); + bool includeAssignees = fields == null || fields.Overlaps(AssignmentFields); + bool includeLinks = fields == null || fields.Overlaps(ApplicationLinkFields); + bool includeApplicantAgent = fields == null || fields.Overlaps(ApplicantAgentFields); + bool includeOwner = fields == null || fields.Overlaps(OwnerFields); + + // Sorting is omitted: the DataTable operates in client-side mode and re-sorts locally. + // skipCount/maxResultCount are not applied while the DataTable is in client-side mode. + var query = (await GetQueryableAsync()).AsNoTracking(); + + var (fromUtc, toUtc) = ConvertToUtcRange(submittedFromDate, submittedToDate); + if (fromUtc.HasValue) + { + query = query.Where(a => a.SubmissionDate >= fromUtc.Value); + } + + if (toUtc.HasValue) + { + query = query.Where(a => a.SubmissionDate <= toUtc.Value); + } + + if (!string.IsNullOrWhiteSpace(searchTerm)) + { + query = query.Where(a => + a.ProjectName.Contains(searchTerm) || + a.ReferenceNo.Contains(searchTerm)); + } + + // Base query — always-required navigations only (ApplicationStatus, ApplicationForm, Applicant). + var baseData = await query + .Select(a => new + { + a.Id, + a.ProjectName, + a.ReferenceNo, + a.RequestedAmount, + a.TotalProjectBudget, + a.EconomicRegion, + a.City, + a.ProposalDate, + a.SubmissionDate, + a.FinalDecisionDate, + a.DueDate, + a.NotificationDate, + a.ProjectSummary, + a.TotalScore, + a.RecommendedAmount, + a.ApprovedAmount, + a.LikelihoodOfFunding, + a.DueDiligenceStatus, + a.SubStatus, + a.DeclineRational, + a.Notes, + a.AssessmentResultStatus, + a.AssessmentResultDate, + a.ProjectStartDate, + a.ProjectEndDate, + a.PercentageTotalProjectBudget, + a.ProjectFundingTotal, + a.Community, + a.CommunityPopulation, + a.Acquisition, + a.Forestry, + a.ForestryFocus, + a.ElectoralDistrict, + a.ApplicantElectoralDistrict, + a.Place, + a.RegionalDistrict, + a.OwnerId, + a.DefaultSiteId, + a.SigningAuthorityFullName, + a.SigningAuthorityTitle, + a.SigningAuthorityEmail, + a.SigningAuthorityBusinessPhone, + a.SigningAuthorityCellPhone, + a.ContractNumber, + a.ContractExecutionDate, + a.RiskRanking, + a.UnityApplicationId, + Status = a.ApplicationStatus.InternalStatus, // ApplicationStatus (always joined) + Category = a.ApplicationForm.Category ?? string.Empty, // ApplicationForm (always joined) + a.ApplicantId, + ApplicantName = a.Applicant.ApplicantName, // Applicant (always joined) + ApplicantSupplierId = a.Applicant.SupplierId, + ApplicantSector = a.Applicant.Sector, + ApplicantSubSector = a.Applicant.SubSector, + ApplicantOrgName = a.Applicant.OrgName, + ApplicantNonRegOrgName = a.Applicant.NonRegOrgName, + ApplicantOrganizationType = a.Applicant.OrganizationType, + ApplicantOrgNumber = a.Applicant.OrgNumber, + ApplicantOrgStatus = a.Applicant.OrgStatus, + ApplicantBusinessNumber = a.Applicant.BusinessNumber, + ApplicantOrganizationSize = a.Applicant.OrganizationSize, + ApplicantSectorSubSectorIndustryDesc = a.Applicant.SectorSubSectorIndustryDesc, + ApplicantRedStop = a.Applicant.RedStop, + ApplicantIndigenousOrgInd = a.Applicant.IndigenousOrgInd, + ApplicantFiscalDay = a.Applicant.FiscalDay, + ApplicantFiscalMonth = a.Applicant.FiscalMonth, + ApplicantUnityApplicantId = a.Applicant.UnityApplicantId, + }) + .ToListAsync(); + + if (baseData.Count == 0) + { + return []; + } + + // Conditionally join ApplicantAgent + var agentMap = new Dictionary(); + if (includeApplicantAgent) + { + var agentData = await query + .Where(a => a.ApplicantAgent != null) + .Select(a => new + { + a.Id, + a.ApplicantAgent!.Name, + a.ApplicantAgent.Title, + a.ApplicantAgent.Email, + Phone = a.ApplicantAgent.Phone, + Phone2 = a.ApplicantAgent.Phone2, + }) + .ToListAsync(); + + agentMap = agentData.ToDictionary( + x => x.Id, + x => ((string?)x.Name, x.Title, x.Email, x.Phone, x.Phone2)); + } + + // Conditionally join Owner + var ownerMap = new Dictionary(); + if (includeOwner) + { + var ownerData = await query + .Where(a => a.Owner != null) + .Select(a => new { a.Id, OwnerId = a.Owner!.Id, OwnerFullName = a.Owner.FullName }) + .ToListAsync(); + + ownerMap = ownerData.ToDictionary( + x => x.Id, + x => (x.OwnerId, (string?)x.OwnerFullName)); + } + + var dbContext = await GetDbContextAsync(); + + // matchingIds is kept as IQueryable so EF Core translates it as a SQL subquery + // (WHERE ApplicationId IN (SELECT Id FROM Applications WHERE ...)) rather than + // binding thousands of individual GUID parameters. + var matchingIds = query.Select(a => a.Id); + + // Conditionally join Tags + var tagsLookup = Enumerable.Empty().ToLookup(t => Guid.Empty); + if (includeTags) + { + var tags = await dbContext.Set() + .AsNoTracking() + .Where(t => matchingIds.Contains(t.ApplicationId)) + .Select(t => new ApplicationTagListItem + { + Id = t.Id, + ApplicationId = t.ApplicationId, + TagName = t.Tag != null ? t.Tag.Name : null, + }) + .ToListAsync(); + + tagsLookup = tags.ToLookup(t => t.ApplicationId); + } + + // Conditionally join Assignments + var assignmentsLookup = Enumerable.Empty().ToLookup(aa => Guid.Empty); + if (includeAssignees) + { + var assignments = await dbContext.Set() + .AsNoTracking() + .Where(aa => matchingIds.Contains(aa.ApplicationId)) + .Select(aa => new ApplicationAssignmentListItem + { + Id = aa.Id, + ApplicationId = aa.ApplicationId, + AssigneeId = aa.AssigneeId, + AssigneeName = aa.Assignee != null ? aa.Assignee.FullName : string.Empty, + Duty = aa.Duty, + }) + .ToListAsync(); + + assignmentsLookup = assignments.ToLookup(aa => aa.ApplicationId); + } + + // Conditionally join Links + var linksLookup = Enumerable.Empty().ToLookup(l => Guid.Empty); + if (includeLinks) + { + var links = await dbContext.Set() + .AsNoTracking() + .Where(l => matchingIds.Contains(l.ApplicationId)) + .Select(l => new ApplicationLinkListItem + { + Id = l.Id, + ApplicationId = l.ApplicationId, + LinkedApplicationId = l.LinkedApplicationId, + LinkType = l.LinkType, + }) + .ToListAsync(); + + linksLookup = links.ToLookup(l => l.ApplicationId); + } + + return baseData + .Select(a => + { + agentMap.TryGetValue(a.Id, out var agent); + ownerMap.TryGetValue(a.Id, out var owner); + var hasOwner = includeOwner && ownerMap.ContainsKey(a.Id); + + return new ApplicationListRecord + { + Id = a.Id, + ProjectName = a.ProjectName, + ReferenceNo = a.ReferenceNo, + RequestedAmount = a.RequestedAmount, + TotalProjectBudget = a.TotalProjectBudget, + EconomicRegion = a.EconomicRegion, + City = a.City, + ProposalDate = a.ProposalDate, + SubmissionDate = a.SubmissionDate, + FinalDecisionDate = a.FinalDecisionDate, + DueDate = a.DueDate, + NotificationDate = a.NotificationDate, + ProjectSummary = a.ProjectSummary, + TotalScore = a.TotalScore, + RecommendedAmount = a.RecommendedAmount, + ApprovedAmount = a.ApprovedAmount, + LikelihoodOfFunding = a.LikelihoodOfFunding, + DueDiligenceStatus = a.DueDiligenceStatus, + SubStatus = a.SubStatus, + DeclineRational = a.DeclineRational, + Notes = a.Notes, + AssessmentResultStatus = a.AssessmentResultStatus, + AssessmentResultDate = a.AssessmentResultDate, + ProjectStartDate = a.ProjectStartDate, + ProjectEndDate = a.ProjectEndDate, + PercentageTotalProjectBudget = a.PercentageTotalProjectBudget, + ProjectFundingTotal = a.ProjectFundingTotal, + Community = a.Community, + CommunityPopulation = a.CommunityPopulation, + Acquisition = a.Acquisition, + Forestry = a.Forestry, + ForestryFocus = a.ForestryFocus, + ElectoralDistrict = a.ElectoralDistrict, + ApplicantElectoralDistrict = a.ApplicantElectoralDistrict, + Place = a.Place, + RegionalDistrict = a.RegionalDistrict, + OwnerId = a.OwnerId, + DefaultSiteId = a.DefaultSiteId, + SigningAuthorityFullName = a.SigningAuthorityFullName, + SigningAuthorityTitle = a.SigningAuthorityTitle, + SigningAuthorityEmail = a.SigningAuthorityEmail, + SigningAuthorityBusinessPhone = a.SigningAuthorityBusinessPhone, + SigningAuthorityCellPhone = a.SigningAuthorityCellPhone, + ContractNumber = a.ContractNumber, + ContractExecutionDate = a.ContractExecutionDate, + RiskRanking = a.RiskRanking, + UnityApplicationId = a.UnityApplicationId, + Status = a.Status, + Category = a.Category, + ApplicantId = a.ApplicantId, + ApplicantName = a.ApplicantName, + ApplicantSupplierId = a.ApplicantSupplierId, + ApplicantSector = a.ApplicantSector, + ApplicantSubSector = a.ApplicantSubSector, + ApplicantOrgName = a.ApplicantOrgName, + ApplicantNonRegOrgName = a.ApplicantNonRegOrgName, + ApplicantOrganizationType = a.ApplicantOrganizationType, + ApplicantOrgNumber = a.ApplicantOrgNumber, + ApplicantOrgStatus = a.ApplicantOrgStatus, + ApplicantBusinessNumber = a.ApplicantBusinessNumber, + ApplicantOrganizationSize = a.ApplicantOrganizationSize, + ApplicantSectorSubSectorIndustryDesc = a.ApplicantSectorSubSectorIndustryDesc, + ApplicantRedStop = a.ApplicantRedStop, + ApplicantIndigenousOrgInd = a.ApplicantIndigenousOrgInd, + ApplicantFiscalDay = a.ApplicantFiscalDay, + ApplicantFiscalMonth = a.ApplicantFiscalMonth, + ApplicantUnityApplicantId = a.ApplicantUnityApplicantId, + ContactFullName = includeApplicantAgent ? agent.Name : null, + ContactTitle = includeApplicantAgent ? agent.Title : null, + ContactEmail = includeApplicantAgent ? agent.Email : null, + ContactBusinessPhone = includeApplicantAgent ? agent.Phone : null, + ContactCellPhone = includeApplicantAgent ? agent.Phone2 : null, + OwnerPersonId = hasOwner ? owner.PersonId : (Guid?)null, + OwnerFullName = hasOwner ? owner.FullName : null, + Tags = includeTags ? tagsLookup[a.Id].ToList() : [], + Assignments = includeAssignees ? assignmentsLookup[a.Id].ToList() : [], + Links = includeLinks ? linksLookup[a.Id].ToList() : [], + }; + }) + .ToList(); + } + public async Task> GetApplicationsBySiteIdAsync(Guid siteId) { return await (await GetQueryableAsync()) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebModule.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebModule.cs index e6f9d5eb1..081fa8dd7 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebModule.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebModule.cs @@ -135,6 +135,10 @@ public override void ConfigureServices(ServiceConfigurationContext context) var hostingEnvironment = context.Services.GetHostingEnvironment(); var configuration = context.Services.GetConfiguration(); + // Pre-warm the EF Core query pipeline after startup (web host only, not DbMigrator) + context.Services.Configure(configuration.GetSection(DbWarmupOptions.SectionName)); + context.Services.AddHostedService(); + ConfgureFormsApiAuhentication(context); ConfigureAuthentication(context, configuration); ConfigurePolicies(context); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenuContributor.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenuContributor.cs index f5fe33bf9..620938fe6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenuContributor.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenuContributor.cs @@ -1,13 +1,10 @@ -using Microsoft.Extensions.DependencyInjection; using System.Threading.Tasks; -using Unity.AI.Permissions; using Unity.GrantManager.Localization; using Unity.GrantManager.Permissions; using Unity.Identity.Web.Navigation; using Unity.Modules.Shared.Permissions; using Unity.TenantManagement; using Unity.TenantManagement.Web.Navigation; -using Volo.Abp.Features; using Volo.Abp.Identity; using Volo.Abp.UI.Navigation; @@ -29,7 +26,6 @@ public async Task ConfigureMenuAsync(MenuConfigurationContext context) private async static Task ConfigureMainMenuAsync(MenuConfigurationContext context) { var l = context.GetLocalizer(); - var featureChecker = context.ServiceProvider.GetRequiredService(); context.Menu.AddItem( new ApplicationMenuItem( @@ -117,20 +113,6 @@ private async static Task ConfigureMainMenuAsync(MenuConfigurationContext contex ) ); - if (await featureChecker.IsEnabledAsync("Unity.AIReporting")) - { - context.Menu.AddItem( - new ApplicationMenuItem( - GrantManagerMenus.AIReporting, - l["Menu:AIReporting"], - "~/AIReporting", - icon: "fl fl-view-dashboard", - requiredPermissionName: AIPermissions.Reporting.ReportingDefault, - order: 9 - ) - ); - } - // ******************** // Admin - Tenant Management context.Menu.AddItem( diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenus.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenus.cs index 872b27f47..f5fa7db19 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenus.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenus.cs @@ -17,7 +17,6 @@ public static class GrantManagerMenus public const string Intakes = Prefix + ".Intakes"; public const string ApplicationForms = Prefix + ".ApplicationForms"; public const string EndpointManagement = Prefix + ".EndpointManagement"; - public const string AIReporting = Prefix + ".AIReporting"; public const string Applicants = Prefix + ".Applicants"; public const string ConfigurationManagement = Prefix + ".ConfigurationManagement"; public const string UnityAdmin = Prefix + ".UnityAdmin"; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml index 1b2cd15d4..13133fe39 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml @@ -50,18 +50,19 @@ && formManualEnabled && await PermissionChecker.IsGrantedAsync(AIPermissions.Analysis.GenerateApplicationAnalysis); } -@section styles -{ - - -} -@section scripts -{ - - - - -} +@section styles +{ + + +} +@section scripts +{ + + + + + +}
@await Component.InvokeAsync("ApplicationBreadcrumbWidget", new { applicationId = @Model.ApplicationId }) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Index.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Index.js index e039b477e..7c10cc14d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Index.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Index.js @@ -9,6 +9,8 @@ $(function () { const l = abp.localization.getResource('GrantManager'); const defaultQuickDateRange = 'last6months'; const guidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + const canViewApplicants = abp.auth.isGranted('GrantApplicationManagement.Applicants.ViewList'); + const dtTextRenderer = $.fn.dataTable.render.text(); let dt = $('#GrantApplicationsTable'); let dataTable; @@ -89,6 +91,7 @@ $(function () { } }) dt.colReorder.order(orderedIndexes); + dt.columns.adjust(); if (typeof dt.filterRow === 'function') { const filterRowApi = dt.filterRow(); @@ -168,13 +171,15 @@ $(function () { }; let formatItems = function (items) { - const newData = items.map((item, index) => { - return { - ...item, - rowCount: index - }; + // Previously used + // const newData = items.map((item, index) => { return { ...item, rowCount: index }; }); + // return newData; + // While in clientside mode, we're always retrieving the full dataset. + // Can be reverted for server-side + items.forEach((item, index) => { + item.rowCount = index; }); - return newData; + return items; } init(); @@ -350,12 +355,10 @@ $(function () { UIElements.quickDateRange.val('custom'); localStorage.setItem('GrantApplications_QuickRange', 'custom'); - const dtInstance = $('#GrantApplicationsTable').DataTable(); - localStorage.setItem("GrantApplications_FromDate", grantTableFilters.submittedFromDate); localStorage.setItem("GrantApplications_ToDate", grantTableFilters.submittedToDate); - dtInstance.ajax.reload(null, true); + dataTable.ajax.reload(null, true); } function handleQuickDateRangeChange() { const selectedRange = $(this).val(); @@ -375,12 +378,13 @@ $(function () { setDateRangeLocalStorage(selectedRange, range); // Reload the table with new filters - const dtInstance = $('#GrantApplicationsTable').DataTable(); - dtInstance.ajax.reload(null, true); + dataTable.ajax.reload(null, true); } function initializeDataTableAndEvents() { let initialLoad = true; + let isRestoringState = false; + let refreshDataTimeout = null; dataTable = initializeDataTable({ dt, defaultVisibleColumns, @@ -392,13 +396,37 @@ $(function () { }, dataEndpoint: unity.grantManager.grantApplications.grantApplication.getList, data: function () { + let requestedFields; + if (dataTable) { + try { + const cols = dataTable.settings()[0].aoColumns; + requestedFields = cols + .filter(function (col, idx) { return dataTable.column(idx).visible(); }) + .map(function (col) { return col.sName; }) + .filter(function (name) { return !!name; }); + if (requestedFields.length > 0) { + localStorage.setItem('GrantApplications_RequestedFields', JSON.stringify(requestedFields)); + } + } catch { /* DataTable not yet fully initialised */ } + } + if (!requestedFields || requestedFields.length === 0) { + try { + const saved = localStorage.getItem('GrantApplications_RequestedFields'); + if (saved) requestedFields = JSON.parse(saved); + } catch { } + } + if (!requestedFields || requestedFields.length === 0) { + requestedFields = defaultVisibleColumns; + } return { submittedFromDate: grantTableFilters.submittedFromDate, - submittedToDate: grantTableFilters.submittedToDate + submittedToDate: grantTableFilters.submittedToDate, + requestedFields: requestedFields }; }, responseCallback, actionButtons, + deferRender: true, serverSideEnabled: false, pagingEnabled: true, reorderEnabled: true, @@ -415,20 +443,22 @@ $(function () { }; }, onStateLoadParams: function (settings, data) { - if (!initialLoad && data?.customFilters) { - // If there is any date change, this will refresh post load - // to ensure the correct data is shown based on the saved filters. - data.refreshTableWithDates = - data.customFilters.quickDateRange !== UIElements.quickDateRange.val() - || data.customFilters.submittedFromDate !== UIElements.submittedFromInput.val() - || data.customFilters.submittedToDate !== UIElements.submittedToInput.val(); - restoreCustomFilters(data.customFilters); + if (!initialLoad) { + // Mark that a state restore is in progress so column-visibility.dt + // events during restore don't trigger premature intermediate reloads. + isRestoringState = true; + if (data?.customFilters) { + restoreCustomFilters(data.customFilters); + } } }, onStateLoaded: function (dtApi, data) { // This needs to only reload when clicking on the load state not on initial page load // Otherwise it duplicates the data - if (!initialLoad && data?.refreshTableWithDates) { + if (!initialLoad) { + // All column-visibility changes are applied by this point. + // Clear the flag and fire a single reload with the correct column set. + isRestoringState = false; dtApi.ajax.reload(null, false); } initialLoad = false; // Reset flag after use @@ -460,11 +490,31 @@ $(function () { }); } }); + + dataTable.on('column-visibility.dt', function (e, settings, columnIdx) { + try { + const cols = dataTable.settings()[0].aoColumns; + const visibleFields = cols + .filter(function (col, idx) { return dataTable.column(idx).visible(); }) + .map(function (col) { return col.sName; }) + .filter(function (name) { return !!name; }); + if (visibleFields.length > 0) { + localStorage.setItem('GrantApplications_RequestedFields', JSON.stringify(visibleFields)); + } + // Only debounce on manual. During a saved-view restore, isRestoringState + // is true and onStateLoaded fires a single authoritative reload after all columns are applied + if (!isRestoringState && cols[columnIdx]?.refreshData) { + clearTimeout(refreshDataTimeout); + refreshDataTimeout = setTimeout(function () { + dataTable.ajax.reload(null, false); + }, 300); + } + } catch { } + }); } $('#search').on('input', function () { - let table = $('#GrantApplicationsTable').DataTable(); - table.search($(this).val()).draw(); + dataTable.search($(this).val()).draw(); }); //For savedStates @@ -574,7 +624,7 @@ $(function () { getNonRegisteredOrganizationNameColumn(columnIndex++), getUnityApplicationIdColumn(columnIndex++), getLinkRelationshipType(columnIndex++), - ].map((column) => ({ ...column, targets: [column.index], orderData: [column.index, 0] })) + ].map((column) => ({ ...column, targets: [column.index] })) .sort((a, b) => a.index - b.index); return sortedColumns; } @@ -589,13 +639,13 @@ $(function () { render: function(data, type, row) { let applicantName = (typeof data !== 'string' || data.trim() === '') ? 'Applicant Name' : data; - if (type === 'sort' || type === 'filter') { + if (type !== 'display') { return applicantName; } - const safeApplicantName = $.fn.dataTable.render.text().display(applicantName); + const safeApplicantName = dtTextRenderer.display(applicantName); - if (type === 'display' && abp.auth.isGranted('GrantApplicationManagement.Applicants.ViewList')) { + if (canViewApplicants) { const applicantId = row?.applicant?.id; const isGuid = applicantId && guidPattern.test(applicantId); @@ -618,6 +668,7 @@ $(function () { name: 'referenceNo', className: 'data-table-header text-nowrap', render: function (data, type, row) { + if (type !== 'display') return data ?? ''; return `${data}`; }, index: columnIndex @@ -702,6 +753,7 @@ $(function () { data: 'assignees', name: 'assignees', className: 'dt-editable', + refreshData: true, render: function (data, type, row) { let displayText = ' '; @@ -711,10 +763,13 @@ $(function () { displayText = getNames(data); } + if (type !== 'display') return displayText.trim(); + + const tooltipText = data?.length ? getNames(data) : ''; return ` ' + displayText + '' + + + tooltipText + '">' + displayText + '' + ``; }, index: columnIndex @@ -835,10 +890,8 @@ $(function () { name: 'projectStartDate', data: 'projectStartDate', className: 'data-table-header', - render: function (data) { - return data != null ? luxon.DateTime.fromISO(data, { - locale: abp.localization.currentCulture.name, - }).toUTC().toLocaleString() : ''; + render: function (data, type) { + return DateUtils.formatUtcDateToLocal(data, type); }, index: columnIndex } @@ -850,10 +903,8 @@ $(function () { name: 'projectEndDate', data: 'projectEndDate', className: 'data-table-header', - render: function (data) { - return data != null ? luxon.DateTime.fromISO(data, { - locale: abp.localization.currentCulture.name, - }).toUTC().toLocaleString() : ''; + render: function (data, type) { + return DateUtils.formatUtcDateToLocal(data, type); }, index: columnIndex } @@ -1056,12 +1107,12 @@ $(function () { name: 'applicationTag', data: 'applicationTag', className: '', + refreshData: true, render: function (data) { - - let tagNames = data - .filter(x => x?.tag?.name) - .map(x => x.tag.name); - return tagNames.join(', ') ?? ''; + return data + .filter(x => x?.tag?.name) + .map(x => x.tag.name) + .join(', '); }, index: columnIndex } @@ -1117,10 +1168,8 @@ $(function () { name: 'dueDate', data: 'dueDate', className: 'data-table-header', - render: function (data) { - return data != null ? luxon.DateTime.fromISO(data, { - locale: abp.localization.currentCulture.name, - }).toUTC().toLocaleString() : ''; + render: function (data, type) { + return DateUtils.formatUtcDateToLocal(data, type); }, index: columnIndex } @@ -1132,6 +1181,7 @@ $(function () { name: 'Owner', data: 'owner', className: 'data-table-header', + refreshData: true, render: function (data) { return data != null ? data.fullName : ''; }, @@ -1145,10 +1195,8 @@ $(function () { name: 'finalDecisionDate', data: 'finalDecisionDate', className: 'data-table-header', - render: function (data) { - return data != null ? luxon.DateTime.fromISO(data, { - locale: abp.localization.currentCulture.name, - }).toUTC().toLocaleString() : ''; + render: function (data, type) { + return DateUtils.formatUtcDateToLocal(data, type); }, index: columnIndex } @@ -1238,7 +1286,11 @@ $(function () { name: 'applicationLinks', data: 'applicationLinks', className: 'data-table-header', - render: function (data) { + refreshData: true, + render: function (data, type) { + if (type !== 'display' && type !== 'fullName') { + return (data || []).filter(x => x?.linkType).map(x => x.linkType).join(', '); + } const linkNames = Array.from(new Set((data || []) .filter(x => x?.linkType) .map(x => { @@ -1286,6 +1338,7 @@ $(function () { name: 'contactFullName', data: 'contactFullName', className: 'data-table-header', + refreshData: true, render: function (data) { return data ?? ''; }, @@ -1298,6 +1351,7 @@ $(function () { name: 'contactTitle', data: 'contactTitle', className: 'data-table-header', + refreshData: true, render: function (data) { return data ?? ''; }, @@ -1310,6 +1364,7 @@ $(function () { name: 'contactEmail', data: 'contactEmail', className: 'data-table-header', + refreshData: true, render: function (data) { return data ?? ''; }, @@ -1322,6 +1377,7 @@ $(function () { name: 'contactBusinessPhone', data: 'contactBusinessPhone', className: 'data-table-header', + refreshData: true, render: function (data) { return data ?? ''; }, @@ -1334,6 +1390,7 @@ $(function () { name: 'contactCellPhone', data: 'contactCellPhone', className: 'data-table-header', + refreshData: true, render: function (data) { return data ?? ''; }, @@ -1447,9 +1504,6 @@ $(function () { data: 'notes', className: 'data-table-header multi-line', width: "20rem", - createdCell: function (td) { - $(td).css('min-width', '20rem'); - }, render: function (data) { return data ?? ''; }, @@ -1543,50 +1597,47 @@ $(function () { return data.duty ? (" [" + data.duty + "]") : ''; } + const _companyTypeMap = new Map([ + ['BC', 'BC Company'], + ['CP', 'Cooperative'], + ['GP', 'General Partnership'], + ['S', 'Society'], + ['SP', 'Sole Proprietorship'], + ['A', 'Extraprovincial Company'], + ['B', 'Extraprovincial'], + ['BEN', 'Benefit Company'], + ['C', 'Continuation In'], + ['CC', 'BC Community Contribution Company'], + ['CS', 'Continued In Society'], + ['CUL', 'Continuation In as a BC ULC'], + ['EPR', 'Extraprovincial Registration'], + ['FI', 'Financial Institution'], + ['FOR', 'Foreign Registration'], + ['LIB', 'Public Library Association'], + ['LIC', 'Licensed (Extra-Pro)'], + ['LL', 'Limited Liability Partnership'], + ['LLC', 'Limited Liability Company'], + ['LP', 'Limited Partnership'], + ['MF', 'Miscellaneous Firm'], + ['PA', 'Private Act'], + ['PAR', 'Parish'], + ['QA', 'CO 1860'], + ['QB', 'CO 1862'], + ['QC', 'CO 1878'], + ['QD', 'CO 1890'], + ['QE', 'CO 1897'], + ['REG', 'Registraton (Extra-pro)'], + ['ULC', 'BC Unlimited Liability Company'], + ['XCP', 'Extraprovincial Cooperative'], + ['XL', 'Extrapro Limited Liability Partnership'], + ['XP', 'Extraprovincial Limited Partnership'], + ['XS', 'Extraprovincial Society'] + ]); + function getFullType(code) { - const companyTypes = [ - { code: "BC", name: "BC Company" }, - { code: "CP", name: "Cooperative" }, - { code: "GP", name: "General Partnership" }, - { code: "S", name: "Society" }, - { code: "SP", name: "Sole Proprietorship" }, - { code: "A", name: "Extraprovincial Company" }, - { code: "B", name: "Extraprovincial" }, - { code: "BEN", name: "Benefit Company" }, - { code: "C", name: "Continuation In" }, - { code: "CC", name: "BC Community Contribution Company" }, - { code: "CS", name: "Continued In Society" }, - { code: "CUL", name: "Continuation In as a BC ULC" }, - { code: "EPR", name: "Extraprovincial Registration" }, - { code: "FI", name: "Financial Institution" }, - { code: "FOR", name: "Foreign Registration" }, - { code: "LIB", name: "Public Library Association" }, - { code: "LIC", name: "Licensed (Extra-Pro)" }, - { code: "LL", name: "Limited Liability Partnership" }, - { code: "LLC", name: "Limited Liability Company" }, - { code: "LP", name: "Limited Partnership" }, - { code: "MF", name: "Miscellaneous Firm" }, - { code: "PA", name: "Private Act" }, - { code: "PAR", name: "Parish" }, - { code: "QA", name: "CO 1860" }, - { code: "QB", name: "CO 1862" }, - { code: "QC", name: "CO 1878" }, - { code: "QD", name: "CO 1890" }, - { code: "QE", name: "CO 1897" }, - { code: "REG", name: "Registraton (Extra-pro)" }, - { code: "ULC", name: "BC Unlimited Liability Company" }, - { code: "XCP", name: "Extraprovincial Cooperative" }, - { code: "XL", name: "Extrapro Limited Liability Partnership" }, - { code: "XP", name: "Extraprovincial Limited Partnership" }, - { code: "XS", name: "Extraprovincial Society" } - ]; - const match = companyTypes.find(entry => entry.code === code); - return match ? match.name : "Unknown"; - } - - - window.addEventListener('resize', () => { - }); + return _companyTypeMap.get(code) ?? 'Unknown'; + } + PubSub.subscribe( 'refresh_application_list', @@ -1598,22 +1649,17 @@ $(function () { ); function getNames(data) { - let name = ''; - data.forEach((d, index) => { - name = name + (' ' + d.fullName + getDutyText(d)); - if (index != (data.length - 1)) { - name = name + ','; - } - }); - - return name; + return data.map(d => d.fullName + getDutyText(d)).join(', '); } + const _titleCaseCache = new Map(); function titleCase(str) { - str = str.toLowerCase().split(' '); - for (let i = 0; i < str.length; i++) { - str[i] = str[i].charAt(0).toUpperCase() + str[i].slice(1); - } - return str.join(' '); + //This funciton is currently called by 6 columns, all of which are status types or predetermined values + //Caching the results in this case is to improve large data table loads while we're in client side. + //Columns: likelihoodOfFunding, assessmentResult, riskRanking, acquisition, fyeMonth, dueDiligenceStatus + if (_titleCaseCache.has(str)) return _titleCaseCache.get(str); + const result = str.toLowerCase().split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '); + _titleCaseCache.set(str, result); + return result; } function convertToYesNo(str) { 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 885a705ca..322acadab 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 @@ -10,6 +10,10 @@ const dismissedSectionVisibility = { recommendation: false }; +const aiAnalysisPollIntervalMs = 15000; +const aiAnalysisMaxPollFailures = 3; +let aiAnalysisMonitor = null; + function getAnalysisLabels() { const labels = document.getElementById('aiAnalysisLabels')?.dataset ?? {}; @@ -415,84 +419,58 @@ globalThis.queueApplicationAnalysis = function(triggerButton = null) { const applicationId = $('#DetailsViewApplicationId').val(); const $button = triggerButton ? $(triggerButton) : $('#regenerateApplicationAnalysis'); const existingHtml = $button.html(); - const aiAnalysisPollIntervalMs = 15000; - const aiAnalysisMaxPollFailures = 3; if (!applicationId || $button.prop('disabled')) { return; } - $button - .html('Generating...') - .prop('disabled', true); globalThis.AIGenerationButtonState?.setGenerating($button); - let aiAnalysisPollTimeoutId = null; - let aiAnalysisPollFailures = 0; - const stopAIAnalysisPolling = function() { - if (aiAnalysisPollTimeoutId) { - clearTimeout(aiAnalysisPollTimeoutId); - aiAnalysisPollTimeoutId = null; - } - }; - - const poll = function() { - unity.grantManager.grantApplications.grantApplication - .getAIGenerationStatus(applicationId, 'application-analysis') - .done(function(request) { - aiAnalysisPollFailures = 0; - const statusText = globalThis.AIGenerationButtonState?.resolveStatus(request?.status) ?? ''; - - if (statusText === 'Failed') { - stopAIAnalysisPolling(); - loadAIAnalysis(); - globalThis.AIGenerationButtonState?.restore($button); - $button.html(existingHtml).prop('disabled', false); - abp.message.error(request?.failureReason || 'AI analysis failed.'); - return; - } - - if (!request || request.isActive === false || statusText === 'Completed') { - stopAIAnalysisPolling(); - loadAIAnalysis(); - globalThis.AIGenerationButtonState?.setCompleted($button); - $button.html('Completed').prop('disabled', true); - return; - } - - aiAnalysisPollTimeoutId = setTimeout(poll, aiAnalysisPollIntervalMs); - }) - .fail(function(error) { - console.warn('Failed to poll AI analysis status.', error); - aiAnalysisPollFailures += 1; + unity.grantManager.grantApplications.grantApplication + .queueApplicationAnalysis(applicationId) + .done(function(request) { + const status = globalThis.AIGenerationButtonState?.resolveStatus(request?.status) ?? ''; - if (aiAnalysisPollFailures > aiAnalysisMaxPollFailures) { - stopAIAnalysisPolling(); - $button.html(existingHtml).prop('disabled', false); - abp.message.error('Unable to load AI analysis status. Please try again.'); + if (status === 'Completed') { + globalThis.AIGenerationButtonState?.restore($button); + $button.html(existingHtml).prop('disabled', false); + loadAIAnalysis(); + globalThis.refreshAIRateLimitState?.(); return; } - aiAnalysisPollTimeoutId = setTimeout(poll, aiAnalysisPollIntervalMs); - }); - }; - - unity.grantManager.grantApplications.grantApplication - .queueApplicationAnalysis(applicationId) - .done(function(request) { - aiAnalysisPollFailures = 0; - stopAIAnalysisPolling(); - aiAnalysisPollTimeoutId = setTimeout(poll, 500); + monitorAIAnalysisGeneration(applicationId, $button, existingHtml); }) .fail(function(error) { console.error('Failed to queue AI analysis.', error); - stopAIAnalysisPolling(); + aiAnalysisMonitor?.stop(); globalThis.AIGenerationButtonState?.restore($button); $button.html(existingHtml).prop('disabled', false); abp.message.error('Failed to queue AI analysis. Please try again.'); }); } +function monitorAIAnalysisGeneration(applicationId, $button, existingHtml) { + aiAnalysisMonitor?.stop(); + aiAnalysisMonitor = globalThis.AIGenerationButtonState.monitor({ + $button, + originalHtml: existingHtml, + intervalMs: aiAnalysisPollIntervalMs, + maxFailures: aiAnalysisMaxPollFailures, + getStatus: () => unity.grantManager.grantApplications.grantApplication + .getAIGenerationStatus(applicationId, 'application-analysis'), + onComplete: loadAIAnalysis, + onFailed: (request) => { + loadAIAnalysis(); + abp.message.error(request?.failureReason || 'AI analysis failed.'); + }, + onPollFailed: (error) => { + console.warn('Failed to poll AI analysis status.', error); + abp.message.error('Unable to load AI analysis status. Please try again.'); + } + }); +} + function loadAIAnalysis() { if ($('#AIAnalysisFeatureEnabled').val() === 'False') { return; @@ -531,5 +509,22 @@ $(function() { $regenerateButton.on('click', function() { queueApplicationAnalysis(); }); + + const applicationId = $('#DetailsViewApplicationId').val(); + if (!applicationId) { + return; + } + + unity.grantManager.grantApplications.grantApplication + .getAIGenerationStatus(applicationId, 'application-analysis') + .done(function(request) { + if (request?.isActive !== true) { + return; + } + + const existingHtml = $regenerateButton.html(); + globalThis.AIGenerationButtonState?.setGenerating($regenerateButton); + monitorAIAnalysisGeneration(applicationId, $regenerateButton, existingHtml); + }); } }); 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 a3c78ec4b..688d7bcac 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 @@ -6,16 +6,15 @@ opacity: '1', }; - const completedStyles = { - 'border-color': '#2e7d32', - color: '#2e7d32', - opacity: '1', - }; - function applyStyles($button, styles) { $button.css(styles); } + function restoreButton($button, html) { + global.AIGenerationButtonState.restore($button); + $button.html(html).prop('disabled', false); + } + global.AIGenerationButtonState = { resolveStatus(status) { switch (Number(status)) { @@ -32,12 +31,15 @@ } }, setGenerating($button) { + $button.removeAttr('data-ai-cooldown-active'); + $button.attr('data-ai-generating', '1'); + $button + .html('Generating...') + .prop('disabled', true); applyStyles($button, generatingStyles); }, - setCompleted($button) { - applyStyles($button, completedStyles); - }, restore($button) { + $button.removeAttr('data-ai-generating'); $button.css({ 'background-color': '', 'border-color': '', @@ -45,5 +47,58 @@ opacity: '', }).removeClass('disabled'); }, + monitor(options) { + const intervalMs = options.intervalMs || 15000; + const maxFailures = options.maxFailures || 3; + let timeoutId = null; + let failures = 0; + + const stop = () => { + if (timeoutId) { + clearTimeout(timeoutId); + timeoutId = null; + } + }; + + const poll = () => { + options.getStatus() + .done((request) => { + failures = 0; + const status = this.resolveStatus(request?.status); + + if (status === 'Failed') { + stop(); + restoreButton(options.$button, options.originalHtml); + options.onFailed?.(request); + return; + } + + if (!request || request.isActive === false || status === 'Completed') { + stop(); + restoreButton(options.$button, options.originalHtml); + options.onComplete?.(request); + global.refreshAIRateLimitState?.(); + return; + } + + timeoutId = setTimeout(poll, intervalMs); + }) + .fail((error) => { + failures += 1; + if (failures > maxFailures) { + stop(); + restoreButton(options.$button, options.originalHtml); + options.onPollFailed?.(error); + return; + } + + timeoutId = setTimeout(poll, intervalMs); + }); + }; + + stop(); + timeoutId = setTimeout(poll, options.initialDelayMs ?? 500); + return { stop }; + }, }; })(globalThis); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-rate-limit.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-rate-limit.js new file mode 100644 index 000000000..ad5a7221d --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-rate-limit.js @@ -0,0 +1,115 @@ +/* AB#32290 — per-user 60s cooldown for AI Generate buttons. + * Server stamps the cooldown; this module only mirrors that state in the UI. + * Strategy: on load, fetch the user's remaining seconds and disable every + * .ai-generate-btn with a countdown. After any generate click resolves we + * re-fetch (a successful click sets a new cooldown; a failed/blocked click + * may report the existing one). KISS — no per-button logic, no mutex. + */ +(function () { + const BUTTON_SELECTOR = '.ai-generate-btn'; + const ATTR_LABEL = 'data-original-label'; + const ATTR_COOLDOWN = 'data-ai-cooldown-active'; + + let countdownTimer = null; + let lastFetchAt = 0; + + function buttons() { + return document.querySelectorAll(BUTTON_SELECTOR); + } + + function rememberLabel(btn) { + if (!btn.getAttribute(ATTR_LABEL)) { + const label = btn.querySelector('.ai-button-content span:last-child') || btn; + btn.setAttribute(ATTR_LABEL, label.textContent.trim()); + } + } + + function setLabel(btn, text) { + const label = btn.querySelector('.ai-button-content span:last-child'); + if (label) { + label.textContent = text; + } else { + btn.textContent = text; + } + } + + function disable(btn, seconds) { + if (btn.getAttribute('data-ai-generating') === '1') { + return; + } + + rememberLabel(btn); + btn.setAttribute(ATTR_COOLDOWN, '1'); + btn.setAttribute('disabled', 'disabled'); + btn.classList.add('disabled'); + setLabel(btn, `Wait ${seconds}s`); + } + + function restore(btn) { + if (btn.getAttribute(ATTR_COOLDOWN) !== '1') return; + btn.removeAttribute(ATTR_COOLDOWN); + btn.removeAttribute('disabled'); + btn.classList.remove('disabled'); + const original = btn.getAttribute(ATTR_LABEL); + if (original) setLabel(btn, original); + } + + function clearTimer() { + if (countdownTimer) { + clearInterval(countdownTimer); + countdownTimer = null; + } + } + + function applyCooldown(seconds) { + clearTimer(); + if (!seconds || seconds <= 0) { + buttons().forEach(restore); + return; + } + let remaining = seconds; + buttons().forEach(b => disable(b, remaining)); + countdownTimer = setInterval(() => { + remaining -= 1; + if (remaining <= 0) { + clearTimer(); + buttons().forEach(restore); + return; + } + buttons().forEach(b => { + if (b.getAttribute(ATTR_COOLDOWN) === '1') setLabel(b, `Wait ${remaining}s`); + }); + }, 1000); + } + + async function fetchState(force = false) { + // Throttle to once per second to avoid hammering on chained clicks. + const now = Date.now(); + if (!force && now - lastFetchAt < 1000) return; + lastFetchAt = now; + try { + const res = await fetch('/api/app/ai-rate-limit/state', { + credentials: 'same-origin', + headers: { Accept: 'application/json' }, + }); + if (!res.ok) return; + const data = await res.json(); + applyCooldown(Number(data.retryAfterSeconds) || 0); + } catch (_) { + // Best-effort; the server is the source of truth. + } + } + + globalThis.refreshAIRateLimitState = () => fetchState(true); + + document.addEventListener('click', (e) => { + const btn = e.target.closest(BUTTON_SELECTOR); + if (!btn) return; + // Re-check shortly after the click so a successful generate immediately + // shows the fresh 60s cooldown. + setTimeout(fetchState, 250); + }); + + document.addEventListener('DOMContentLoaded', () => fetchState()); + if (document.readyState !== 'loading') fetchState(); +})(); 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 b697de046..23f509ab4 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 @@ -584,55 +584,21 @@ function queueApplicationScoring(triggerButton = null) { const applicationId = $('#DetailsViewApplicationId').val(); const $button = triggerButton ? $(triggerButton) : $('#regenerateAiScoresheetBtn'); const existingHtml = $button.html(); - const aiGenerationPollIntervalMs = 15000; - let aiGenerationPollTimeoutId = null; if (!applicationId || $button.prop('disabled')) { return; } - $button - .html( - 'Generating...' - ) - .prop('disabled', true); globalThis.AIGenerationButtonState?.setGenerating($button); - const stopPolling = function () { - if (aiGenerationPollTimeoutId) { - clearTimeout(aiGenerationPollTimeoutId); - aiGenerationPollTimeoutId = null; - } - }; - - const poll = function () { - unity.grantManager.grantApplications.grantApplication - .getAIGenerationStatus(applicationId, 'application-scoring') - .done(function (request) { - const status = globalThis.AIGenerationButtonState?.resolveStatus(request?.status) ?? ''; - - if (status === 'Failed') { - stopPolling(); - abp.message.error(request?.failureReason || 'AI scoring failed.'); - globalThis.AIGenerationButtonState?.restore($button); - $button.html(existingHtml).prop('disabled', false); - return; - } - - if (!request || request.isActive === false || status === 'Completed') { - stopPolling(); - globalThis.AIGenerationButtonState?.setCompleted($button); - $button.html('Completed').prop('disabled', true); - PubSub.publish('refresh_assessment_scores', null); - return; - } - - aiGenerationPollTimeoutId = setTimeout(poll, aiGenerationPollIntervalMs); - }) - .fail(function () { - aiGenerationPollTimeoutId = setTimeout(poll, aiGenerationPollIntervalMs); - }); - }; + const monitorScoring = () => globalThis.AIGenerationButtonState.monitor({ + $button, + originalHtml: existingHtml, + getStatus: () => unity.grantManager.grantApplications.grantApplication + .getAIGenerationStatus(applicationId, 'application-scoring'), + onComplete: () => PubSub.publish('refresh_assessment_scores', null), + onFailed: (request) => abp.message.error(request?.failureReason || 'AI scoring failed.') + }); unity.grantManager.grantApplications.grantApplication .queueApplicationScoring(applicationId) @@ -640,15 +606,16 @@ function queueApplicationScoring(triggerButton = null) { const status = globalThis.AIGenerationButtonState?.resolveStatus(request?.status) ?? ''; if (status === 'Completed') { - $button.html('Completed').prop('disabled', true); + globalThis.AIGenerationButtonState?.restore($button); + $button.html(existingHtml).prop('disabled', false); PubSub.publish('refresh_assessment_scores', null); + globalThis.refreshAIRateLimitState?.(); return; } - aiGenerationPollTimeoutId = setTimeout(poll, 500); + monitorScoring(); }) .fail(function () { - stopPolling(); abp.message.error( 'Failed to queue AI scoring. Please try again.' ); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.css index f259658c3..6d767358f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.css +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.css @@ -14,3 +14,9 @@ table.dataTable > tbody > tr.even.selected > * { display: inline-flex; align-items: center; } + +#AdjudicationTeamLeadActionBar .btn, +#ReviewListTable_wrapper .dt-buttons .btn { + text-transform: none; + background-color: var(--bs-btn-bg) !important; +} 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 0da7f980c..a75af55ef 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 @@ -4,20 +4,20 @@ const isAiScoringEnabled = document.querySelector("#ReviewListAIScoringEnabled") const canUseAiScoring = isAiScoringEnabled; const actionButtonConfigMap = { - Generate: { buttonType: 'generateAiButton', order: 1 }, - Clone: { buttonType: 'cloneButton', order: 2 }, - Create: { buttonType: 'createButton', order: 3 }, - SendBack: { buttonType: 'unityWorkflow', order: 4 }, - Complete: { buttonType: 'unityWorkflow', order: 5 }, + Generate: { buttonType: 'generateAiButton', order: 4 }, + Clone: { buttonType: 'cloneButton', order: 5 }, + Create: { buttonType: 'createButton', order: 1 }, + SendBack: { buttonType: 'unityWorkflow', order: 3 }, + Complete: { buttonType: 'unityWorkflow', order: 2 }, _Fallback: { buttonType: 'unityWorkflow', order: 100 } } const actionButtonLabelMap = { Generate: 'Generate', Clone: 'Clone', - Create: 'Create', + Create: 'Create Assessment', SendBack: 'Send Back', - Complete: 'Complete' + Complete: 'Complete Assessment' }; const finalApplicationStates = [ @@ -54,7 +54,7 @@ $(function () { $.extend(DataTable.ext.buttons, { unityWorkflow: { - className: 'btn unt-btn-outline-primary btn-outline-primary', + className: 'btn btn-light rounded-1', enabled: false, text: unityWorkflowButtonText, action: unityWorkflowButtonAction @@ -233,11 +233,12 @@ $(function () { CreateAssessmentButton(); } - function GenerateAiAssessmentButton() { - let generateButtons = new $.fn.dataTable.Buttons(reviewListTable, assessmentGenerateButtonGroup); - generateButtons.container().appendTo("#AdjudicationTeamLeadActionBar"); - reviewListTable.buttons('Generate:name').enable(); - } + function GenerateAiAssessmentButton() { + let generateButtons = new $.fn.dataTable.Buttons(reviewListTable, assessmentGenerateButtonGroup); + generateButtons.container().appendTo("#AdjudicationTeamLeadActionBar"); + reviewListTable.buttons('Generate:name').enable(); + resumeActiveReviewListAiButton(reviewListTable); + } async function CreateAssessmentButton() { let createButtons = new $.fn.dataTable.Buttons(reviewListTable, assessmentCreateButtonGroup); @@ -457,84 +458,68 @@ function unityWorkflowButtonAction(e, dt, button, config) { function generateAiButtonAction(e, dt, button, config) { const $button = button?.node ? $(button.node) : null; - const aiGenerationPollIntervalMs = 15000; - let aiGenerationPollTimeoutId = null; if ($button?.length) { - $button.prop('disabled', true); - $button.html('Generating...'); globalThis.AIGenerationButtonState?.setGenerating($button); } - const stopPolling = function () { - if (aiGenerationPollTimeoutId) { - clearTimeout(aiGenerationPollTimeoutId); - aiGenerationPollTimeoutId = null; - } - }; - - const poll = function () { - unity.grantManager.grantApplications.grantApplication - .getAIGenerationStatus(pageApplicationId, 'application-scoring') - .done(function (request) { - const status = globalThis.AIGenerationButtonState?.resolveStatus(request?.status) ?? ''; - - if (status === 'Failed') { - stopPolling(); - abp.message.error(request?.failureReason || 'AI scoring failed.'); - if ($button?.length) { - globalThis.AIGenerationButtonState?.restore($button); - $button.prop('disabled', false); - $button.html(generateAiButtonText(null, null, null)); - } - return; - } - - if (!request || request.isActive === false || status === 'Completed') { - stopPolling(); - setReviewListAiButtonCompleted($button); - refreshReviewListAfterAiScoring(); - return; - } - - aiGenerationPollTimeoutId = setTimeout(poll, aiGenerationPollIntervalMs); - }) - .fail(function () { - aiGenerationPollTimeoutId = setTimeout(poll, aiGenerationPollIntervalMs); - }); - }; - unity.grantManager.grantApplications.grantApplication.queueApplicationScoring(pageApplicationId) .done(function (request) { const status = globalThis.AIGenerationButtonState?.resolveStatus(request?.status) ?? ''; if (status === 'Completed') { - setReviewListAiButtonCompleted($button); + restoreReviewListAiButton($button); refreshReviewListAfterAiScoring(); + globalThis.refreshAIRateLimitState?.(); return; } - aiGenerationPollTimeoutId = setTimeout(poll, 500); + pollReviewListAiButton($button); }) .fail(function () { - stopPolling(); abp.message.error('Failed to queue AI scoring. Please try again.'); - if ($button?.length) { - globalThis.AIGenerationButtonState?.restore($button); - $button.prop('disabled', false); - $button.html(generateAiButtonText(null, null, null)); - } + restoreReviewListAiButton($button); }) ; } -function setReviewListAiButtonCompleted($button) { +function restoreReviewListAiButton($button) { if (!$button?.length) { return; } - globalThis.AIGenerationButtonState?.setCompleted($button); - $button.html('Completed').prop('disabled', true); + globalThis.AIGenerationButtonState?.restore($button); + $button.html(generateAiButtonText(null, null, null)).prop('disabled', false); +} + +function resumeActiveReviewListAiButton(reviewListTable) { + const button = reviewListTable.button('Generate:name'); + if (!button?.any()) { + return; + } + + const $button = $(button.node()); + unity.grantManager.grantApplications.grantApplication + .getAIGenerationStatus(pageApplicationId, 'application-scoring') + .done(function(request) { + if (request?.isActive !== true) { + return; + } + + globalThis.AIGenerationButtonState?.setGenerating($button); + pollReviewListAiButton($button); + }); +} + +function pollReviewListAiButton($button) { + globalThis.AIGenerationButtonState.monitor({ + $button, + originalHtml: generateAiButtonText(null, null, null), + getStatus: () => unity.grantManager.grantApplications.grantApplication + .getAIGenerationStatus(pageApplicationId, 'application-scoring'), + onComplete: refreshReviewListAfterAiScoring, + onFailed: (request) => abp.message.error(request?.failureReason || 'AI scoring failed.') + }); } function refreshReviewListAfterAiScoring() { @@ -546,11 +531,11 @@ function executeAssessmentAction(assessmentId, triggerAction) { unity.grantManager.assessments.assessment.executeAssessmentAction(assessmentId, triggerAction, {}) .then(function (result) { PubSub.publish('assessment_action_completed'); - PubSub.publish('refresh_review_list', assessmentId); - abp.notify.success( - "Completed Successfully", - l(`Enum:AssessmentAction.${triggerAction}`) - ); + PubSub.publish('refresh_review_list', assessmentId); + abp.notify.success( + "Completed Successfully", + l(`Enum:AssessmentAction.${triggerAction}`) + ); }); } 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 5d556e0d1..0a5c618e2 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 @@ -4,6 +4,7 @@ using Shouldly; using System; using System.IO; +using System.Threading; using System.Threading.Tasks; using Unity.AI; using Unity.AI.Extraction; @@ -13,16 +14,11 @@ using Unity.GrantManager.Applications; using Unity.GrantManager.Intakes; using Xunit; -using Xunit.Abstractions; namespace Unity.GrantManager.AI.Operations; -public class AttachmentSummaryServiceTests : GrantManagerApplicationTestBase +public class AttachmentSummaryServiceTests { - public AttachmentSummaryServiceTests(ITestOutputHelper outputHelper) : base(outputHelper) - { - } - [Fact] public async Task GenerateAndSaveAsync_Uses_Streamed_Attachment_Text() { @@ -76,4 +72,41 @@ public async Task GenerateAndSaveAsync_Uses_Streamed_Attachment_Text() await attachmentRepository.Received(1).UpdateAsync(attachment); stream.CanRead.ShouldBeFalse(); } + + [Fact] + public async Task GenerateAndSaveAsync_Should_Propagate_Cancellation() + { + var attachmentId = Guid.NewGuid(); + var submissionId = Guid.NewGuid(); + var fileId = Guid.NewGuid(); + var stream = new MemoryStream([1, 2, 3]); + using var cancellationTokenSource = new CancellationTokenSource(); + await cancellationTokenSource.CancelAsync(); + + var attachment = new ApplicationChefsFileAttachment + { + ApplicationId = Guid.NewGuid(), + FileName = "test.txt", + ChefsSubmissionId = submissionId.ToString(), + ChefsFileId = fileId.ToString() + }; + + var attachmentRepository = Substitute.For(); + attachmentRepository.GetAsync(attachmentId).Returns(attachment); + + var streamProvider = Substitute.For(); + streamProvider.OpenAsync(submissionId, fileId, "test.txt") + .Returns(new ChefsFileAttachmentStream(stream, "text/plain")); + + var service = new AttachmentSummaryService( + attachmentRepository, + streamProvider, + Substitute.For(), + Substitute.For(), + new AIExecutionModeResolver(new ConfigurationBuilder().Build()), + NullLogger.Instance); + + await Should.ThrowAsync(() => + service.GenerateAndSaveAsync(attachmentId, "v1", cancellationTokenSource.Token)); + } } 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 new file mode 100644 index 000000000..9d07ef896 --- /dev/null +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/RateLimit/AIRateLimiterTests.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Medallion.Threading; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; +using NSubstitute; +using Shouldly; +using Unity.AI.RateLimit; +using Volo.Abp; +using Volo.Abp.Users; +using Xunit; + +namespace Unity.GrantManager.AI.RateLimit; + +public class AIRateLimiterTests +{ + private readonly Guid _userId = Guid.NewGuid(); + private readonly IDistributedCache _cache = new MemoryDistributedCache( + Options.Create(new MemoryDistributedCacheOptions())); + private readonly ICurrentUser _currentUser = Substitute.For(); + private readonly IConfiguration _configuration; + + public AIRateLimiterTests() + { + _currentUser.Id.Returns(_userId); + _configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["AI:RateLimit:CooldownSeconds"] = "60" + }).Build(); + } + + private AIRateLimiter NewLimiter() => new(_cache, _currentUser, _configuration, new TestDistributedLockProvider()); + + [Fact] + public async Task GetStateAsync_Returns_Zero_When_NoCooldown() + { + var state = await NewLimiter().GetStateAsync(); + state.RetryAfterSeconds.ShouldBe(0); + } + + [Fact] + public async Task EnsureAsync_AllowsThrough_When_NoCooldown() + { + await NewLimiter().EnsureAsync(); + var state = await NewLimiter().GetStateAsync(); + state.RetryAfterSeconds.ShouldBe(0); + } + + [Fact] + public async Task StampAsync_Starts_Cooldown() + { + var limiter = NewLimiter(); + await limiter.StampAsync(); + var state = await limiter.GetStateAsync(); + state.RetryAfterSeconds.ShouldBeInRange(1, 60); + } + + [Fact] + public async Task EnsureAsync_Throws_When_Cooldown_Exists() + { + var limiter = NewLimiter(); + await limiter.StampAsync(); + + var ex = await Should.ThrowAsync(() => limiter.EnsureAsync()); + ex.Message.ShouldContain("rate limited"); + ex.Message.ShouldMatch(@"\d+ second"); + } + + [Fact] + public async Task GetStateAsync_Returns_Zero_For_AnonymousUser() + { + _currentUser.Id.Returns((Guid?)null); + var state = await NewLimiter().GetStateAsync(); + state.RetryAfterSeconds.ShouldBe(0); + } + + [Fact] + public async Task EnsureAsync_IsNoOp_For_AnonymousUser() + { + _currentUser.Id.Returns((Guid?)null); + await NewLimiter().EnsureAsync(); // Should not throw. + var state = await NewLimiter().GetStateAsync(); + state.RetryAfterSeconds.ShouldBe(0); + } + + [Fact] + public async Task DifferentUsers_Have_IndependentCooldowns() + { + await NewLimiter().StampAsync(); + + _currentUser.Id.Returns(Guid.NewGuid()); + await NewLimiter().EnsureAsync(); // Should not throw. + } + + [Fact] + public async Task StampAsync_ForSuppliedUser_Starts_Cooldown_ForThatUser() + { + var otherUserId = Guid.NewGuid(); + + await NewLimiter().StampAsync(otherUserId); + + await NewLimiter().EnsureAsync(); // Current user should not be blocked. + + _currentUser.Id.Returns(otherUserId); + var ex = await Should.ThrowAsync(() => NewLimiter().EnsureAsync()); + ex.Message.ShouldContain("rate limited"); + } + + [Fact] + public async Task ExpiredCooldown_AllowsNewStamp() + { + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["AI:RateLimit:CooldownSeconds"] = "1" + }).Build(); + var limiter = new AIRateLimiter(_cache, _currentUser, config, new TestDistributedLockProvider()); + + await limiter.StampAsync(); + await Task.Delay(TimeSpan.FromSeconds(1.2), CancellationToken.None); + await limiter.EnsureAsync(); // Should not throw. + } + + private sealed class TestDistributedLockProvider : IDistributedLockProvider + { + public IDistributedLock CreateLock(string name) => new TestDistributedLock(name); + } + + private sealed class TestDistributedLock(string name) : IDistributedLock + { + private static readonly SemaphoreSlim Gate = new(1, 1); + + public string Name => name; + + public IDistributedSynchronizationHandle Acquire(TimeSpan? timeout = null, CancellationToken cancellationToken = default) + { + Gate.Wait(cancellationToken); + return new TestDistributedSynchronizationHandle(Gate); + } + + public async ValueTask AcquireAsync(TimeSpan? timeout = null, CancellationToken cancellationToken = default) + { + await Gate.WaitAsync(cancellationToken); + return new TestDistributedSynchronizationHandle(Gate); + } + + public IDistributedSynchronizationHandle? TryAcquire(TimeSpan timeout = default, CancellationToken cancellationToken = default) => + Gate.Wait(timeout, cancellationToken) ? new TestDistributedSynchronizationHandle(Gate) : null; + + public async ValueTask TryAcquireAsync(TimeSpan timeout = default, CancellationToken cancellationToken = default) => + await Gate.WaitAsync(timeout, cancellationToken) + ? new TestDistributedSynchronizationHandle(Gate) + : null; + } + + private sealed class TestDistributedSynchronizationHandle(SemaphoreSlim gate) : IDistributedSynchronizationHandle + { + public CancellationToken HandleLostToken => CancellationToken.None; + + public void Dispose() => gate.Release(); + + public ValueTask DisposeAsync() + { + Dispose(); + return ValueTask.CompletedTask; + } + } +} diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/AIProviderPayloadValidatorTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/AIProviderPayloadValidatorTests.cs new file mode 100644 index 000000000..d84d2e55a --- /dev/null +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/AIProviderPayloadValidatorTests.cs @@ -0,0 +1,123 @@ +using Shouldly; +using Unity.AI.Runtime; +using Xunit; + +namespace Unity.GrantManager.AI.Runtime; + +public class AIProviderPayloadValidatorTests +{ + [Theory] + [InlineData(null, false)] + [InlineData("", false)] + [InlineData(" ", false)] + [InlineData("Some summary text", true)] + public void IsValidAttachmentSummaryText_Should_RejectBlankAndAcceptContent(string? input, bool expected) + { + AIProviderPayloadValidator.IsValidAttachmentSummaryText(input!).ShouldBe(expected); + } + + [Fact] + public void IsValidApplicationAnalysisJson_Should_ReturnTrue_ForWellFormedPayload() + { + var json = """ + { + "decision": "Approved", + "errors": [], + "warnings": [], + "summaries": [], + "recommendations": [] + } + """; + AIProviderPayloadValidator.IsValidApplicationAnalysisJson(json).ShouldBeTrue(); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("not json")] + [InlineData("[]")] + public void IsValidApplicationAnalysisJson_Should_ReturnFalse_ForInvalidInput(string? input) + { + AIProviderPayloadValidator.IsValidApplicationAnalysisJson(input!).ShouldBeFalse(); + } + + [Fact] + public void IsValidApplicationAnalysisJson_Should_ReturnFalse_WhenDecisionMissing() + { + var json = """{"errors":[],"warnings":[],"summaries":[],"recommendations":[]}"""; + AIProviderPayloadValidator.IsValidApplicationAnalysisJson(json).ShouldBeFalse(); + } + + [Fact] + public void IsValidApplicationAnalysisJson_Should_ReturnFalse_WhenErrorsIsNotArray() + { + var json = """{"decision":"ok","errors":"bad","warnings":[],"summaries":[],"recommendations":[]}"""; + AIProviderPayloadValidator.IsValidApplicationAnalysisJson(json).ShouldBeFalse(); + } + + [Fact] + public void IsValidApplicationAnalysisJson_Should_AcceptMarkdownWrappedJson() + { + var json = "```json\n{\"decision\":\"ok\",\"errors\":[],\"warnings\":[],\"summaries\":[],\"recommendations\":[]}\n```"; + AIProviderPayloadValidator.IsValidApplicationAnalysisJson(json).ShouldBeTrue(); + } + + [Fact] + public void IsValidApplicationScoringJson_Should_ReturnTrue_ForWellFormedPayload() + { + var sectionJson = """[{"id":"q1"},{"id":"q2"}]"""; + var response = """ + { + "q1": {"answer": "Yes", "confidence": 85}, + "q2": {"answer": "No", "confidence": 42} + } + """; + AIProviderPayloadValidator.IsValidApplicationScoringJson(response, sectionJson).ShouldBeTrue(); + } + + [Fact] + public void IsValidApplicationScoringJson_Should_ReturnFalse_WhenSectionJsonIsEmpty() + { + AIProviderPayloadValidator.IsValidApplicationScoringJson("{}", "[]").ShouldBeFalse(); + } + + [Fact] + public void IsValidApplicationScoringJson_Should_ReturnFalse_WhenAnswerMissing() + { + var sectionJson = """[{"id":"q1"}]"""; + var response = """{"q1": {"confidence": 50}}"""; + AIProviderPayloadValidator.IsValidApplicationScoringJson(response, sectionJson).ShouldBeFalse(); + } + + [Fact] + public void IsValidApplicationScoringJson_Should_ReturnFalse_WhenConfidenceOutOfRange() + { + var sectionJson = """[{"id":"q1"}]"""; + var response = """{"q1": {"answer": "Yes", "confidence": 150}}"""; + AIProviderPayloadValidator.IsValidApplicationScoringJson(response, sectionJson).ShouldBeFalse(); + } + + [Fact] + public void IsValidApplicationScoringJson_Should_ReturnFalse_WhenQuestionMissingFromResponse() + { + var sectionJson = """[{"id":"q1"},{"id":"q2"}]"""; + var response = """{"q1": {"answer": "Yes", "confidence": 80}}"""; + AIProviderPayloadValidator.IsValidApplicationScoringJson(response, sectionJson).ShouldBeFalse(); + } + + [Fact] + public void IsValidApplicationScoringJson_Should_AcceptQuestionsWrappedInObject() + { + var sectionJson = """{"questions":[{"id":"q1"}]}"""; + var response = """{"q1": {"answer": "Yes", "confidence": 75}}"""; + AIProviderPayloadValidator.IsValidApplicationScoringJson(response, sectionJson).ShouldBeTrue(); + } + + [Fact] + public void IsValidApplicationScoringJson_Should_ReturnFalse_WhenConfidenceIsNegative() + { + var sectionJson = """[{"id":"q1"}]"""; + var response = """{"q1": {"answer": "Yes", "confidence": -1}}"""; + AIProviderPayloadValidator.IsValidApplicationScoringJson(response, sectionJson).ShouldBeFalse(); + } +} diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/AIResponseJsonTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/AIResponseJsonTests.cs new file mode 100644 index 000000000..24daba630 --- /dev/null +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/AIResponseJsonTests.cs @@ -0,0 +1,53 @@ +using Shouldly; +using Unity.AI.Runtime; +using Xunit; + +namespace Unity.GrantManager.AI.Runtime; + +public class AIResponseJsonTests +{ + [Theory] + [InlineData(null, "")] + [InlineData("", "")] + [InlineData(" ", "")] + public void CleanJsonResponse_Should_ReturnEmpty_ForNullOrWhitespace(string? input, string expected) + { + AIResponseJson.CleanJsonResponse(input!).ShouldBe(expected); + } + + [Fact] + public void CleanJsonResponse_Should_StripMarkdownJsonFence_WithNewline() + { + var input = "```json\n{\"key\":\"value\"}\n```"; + AIResponseJson.CleanJsonResponse(input).ShouldBe("{\"key\":\"value\"}"); + } + + [Fact] + public void CleanJsonResponse_Should_StripPlainFence_WithNewline() + { + var input = "```\n{\"key\":\"value\"}\n```"; + AIResponseJson.CleanJsonResponse(input).ShouldBe("{\"key\":\"value\"}"); + } + + [Fact] + public void CleanJsonResponse_Should_StripTrailingFence_OnlyAfterContent() + { + var input = "{\"key\":\"value\"}```"; + AIResponseJson.CleanJsonResponse(input).ShouldBe("{\"key\":\"value\"}"); + } + + [Fact] + public void CleanJsonResponse_Should_ReturnTrimmedJson_WithNoFences() + { + var input = " {\"key\":\"value\"} "; + AIResponseJson.CleanJsonResponse(input).ShouldBe("{\"key\":\"value\"}"); + } + + [Fact] + public void CleanJsonResponse_Should_HandleFenceWithoutNewline_UsingFirstJsonToken() + { + var input = "```json{\"key\":\"value\"}```"; + var result = AIResponseJson.CleanJsonResponse(input); + result.ShouldBe("{\"key\":\"value\"}"); + } +} diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/ApplicationListRecordAlignmentTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/ApplicationListRecordAlignmentTests.cs new file mode 100644 index 000000000..6229d9ef0 --- /dev/null +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/ApplicationListRecordAlignmentTests.cs @@ -0,0 +1,421 @@ +using Shouldly; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using Unity.GrantManager.Applications; + +using Volo.Abp.Uow; + +using Xunit; +using Xunit.Abstractions; + +namespace Unity.GrantManager.GrantApplications; + +/// +/// Verifies that returns +/// field values consistent with the full-entity data returned by +/// for the same filter inputs. +/// +public class ApplicationListRecordAlignmentTests : GrantManagerApplicationTestBase +{ + private readonly IApplicationRepository _applicationRepository; + private readonly IUnitOfWorkManager _unitOfWorkManager; + + public ApplicationListRecordAlignmentTests(ITestOutputHelper outputHelper) : base(outputHelper) + { + _applicationRepository = GetRequiredService(); + _unitOfWorkManager = GetRequiredService(); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetApplicationListRecordsAsync_WithNullRequestedFields_ReturnsSameCount_As_WithFullDetailsAsync() + { + using var uow = _unitOfWorkManager.Begin(); + + var fullDetails = await _applicationRepository.WithFullDetailsAsync( + skipCount: 0, + maxResultCount: int.MaxValue); + + var listRecords = await _applicationRepository.GetApplicationListRecordsAsync( + skipCount: 0, + maxResultCount: int.MaxValue, + requestedFields: null); + + listRecords.Count.ShouldBe(fullDetails.Count); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetApplicationListRecordsAsync_ScalarFields_Match_WithFullDetailsAsync() + { + using var uow = _unitOfWorkManager.Begin(); + + var fullDetails = await _applicationRepository.WithFullDetailsAsync( + skipCount: 0, + maxResultCount: int.MaxValue); + + var listRecords = await _applicationRepository.GetApplicationListRecordsAsync( + skipCount: 0, + maxResultCount: int.MaxValue, + requestedFields: null); + + fullDetails.ShouldNotBeEmpty(); + + foreach (var app in fullDetails) + { + var rec = listRecords.FirstOrDefault(r => r.Id == app.Id); + rec.ShouldNotBeNull($"No ApplicationListRecord found for Application Id={app.Id}"); + + rec.ProjectName.ShouldBe(app.ProjectName, $"ProjectName mismatch for Id={app.Id}"); + rec.ReferenceNo.ShouldBe(app.ReferenceNo, $"ReferenceNo mismatch for Id={app.Id}"); + rec.RequestedAmount.ShouldBe(app.RequestedAmount, $"RequestedAmount mismatch for Id={app.Id}"); + rec.TotalProjectBudget.ShouldBe(app.TotalProjectBudget, $"TotalProjectBudget mismatch for Id={app.Id}"); + rec.EconomicRegion.ShouldBe(app.EconomicRegion, $"EconomicRegion mismatch for Id={app.Id}"); + rec.City.ShouldBe(app.City, $"City mismatch for Id={app.Id}"); + rec.SubmissionDate.ShouldBe(app.SubmissionDate, $"SubmissionDate mismatch for Id={app.Id}"); + rec.OwnerId.ShouldBe(app.OwnerId, $"OwnerId mismatch for Id={app.Id}"); + rec.ApplicantId.ShouldBe(app.ApplicantId, $"ApplicantId mismatch for Id={app.Id}"); + } + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetApplicationListRecordsAsync_StatusAndCategory_Match_WithFullDetailsAsync() + { + using var uow = _unitOfWorkManager.Begin(); + + var fullDetails = await _applicationRepository.WithFullDetailsAsync( + skipCount: 0, + maxResultCount: int.MaxValue); + + var listRecords = await _applicationRepository.GetApplicationListRecordsAsync( + skipCount: 0, + maxResultCount: int.MaxValue, + requestedFields: null); + + foreach (var app in fullDetails) + { + var rec = listRecords.First(r => r.Id == app.Id); + + rec.Status.ShouldBe(app.ApplicationStatus.InternalStatus, + $"Status (InternalStatus) mismatch for Id={app.Id}"); + rec.Category.ShouldBe(app.ApplicationForm.Category ?? string.Empty, + $"Category mismatch for Id={app.Id}"); + } + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetApplicationListRecordsAsync_ApplicantFields_Match_WithFullDetailsAsync() + { + using var uow = _unitOfWorkManager.Begin(); + + var fullDetails = await _applicationRepository.WithFullDetailsAsync( + skipCount: 0, + maxResultCount: int.MaxValue); + + var listRecords = await _applicationRepository.GetApplicationListRecordsAsync( + skipCount: 0, + maxResultCount: int.MaxValue, + requestedFields: null); + + foreach (var app in fullDetails) + { + var rec = listRecords.First(r => r.Id == app.Id); + var applicant = app.Applicant; + + rec.ApplicantName.ShouldBe(applicant.ApplicantName, + $"ApplicantName mismatch for Id={app.Id}"); + rec.ApplicantOrgName.ShouldBe(applicant.OrgName, + $"ApplicantOrgName mismatch for Id={app.Id}"); + rec.ApplicantOrgNumber.ShouldBe(applicant.OrgNumber, + $"ApplicantOrgNumber mismatch for Id={app.Id}"); + rec.ApplicantOrgStatus.ShouldBe(applicant.OrgStatus, + $"ApplicantOrgStatus mismatch for Id={app.Id}"); + rec.ApplicantSector.ShouldBe(applicant.Sector, + $"ApplicantSector mismatch for Id={app.Id}"); + rec.ApplicantSubSector.ShouldBe(applicant.SubSector, + $"ApplicantSubSector mismatch for Id={app.Id}"); + rec.ApplicantOrganizationType.ShouldBe(applicant.OrganizationType, + $"ApplicantOrganizationType mismatch for Id={app.Id}"); + rec.ApplicantOrganizationSize.ShouldBe(applicant.OrganizationSize, + $"ApplicantOrganizationSize mismatch for Id={app.Id}"); + rec.ApplicantIndigenousOrgInd.ShouldBe(applicant.IndigenousOrgInd, + $"ApplicantIndigenousOrgInd mismatch for Id={app.Id}"); + rec.ApplicantRedStop.ShouldBe(applicant.RedStop, + $"ApplicantRedStop mismatch for Id={app.Id}"); + rec.ApplicantUnityApplicantId.ShouldBe(applicant.UnityApplicantId, + $"ApplicantUnityApplicantId mismatch for Id={app.Id}"); + } + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetApplicationListRecordsAsync_CollectionCounts_Match_WithFullDetailsAsync() + { + using var uow = _unitOfWorkManager.Begin(); + + var fullDetails = await _applicationRepository.WithFullDetailsAsync( + skipCount: 0, + maxResultCount: int.MaxValue); + + var listRecords = await _applicationRepository.GetApplicationListRecordsAsync( + skipCount: 0, + maxResultCount: int.MaxValue, + requestedFields: null); + + foreach (var app in fullDetails) + { + var rec = listRecords.First(r => r.Id == app.Id); + + rec.Tags.Count.ShouldBe( + app.ApplicationTags?.Count ?? 0, + $"Tag count mismatch for Id={app.Id}"); + rec.Assignments.Count.ShouldBe( + app.ApplicationAssignments?.Count ?? 0, + $"Assignment count mismatch for Id={app.Id}"); + rec.Links.Count.ShouldBe( + app.ApplicationLinks?.Count ?? 0, + $"Link count mismatch for Id={app.Id}"); + } + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetApplicationListRecordsAsync_ContactFields_Match_WithFullDetailsAsync() + { + using var uow = _unitOfWorkManager.Begin(); + + var fullDetails = await _applicationRepository.WithFullDetailsAsync( + skipCount: 0, + maxResultCount: int.MaxValue); + + var listRecords = await _applicationRepository.GetApplicationListRecordsAsync( + skipCount: 0, + maxResultCount: int.MaxValue, + requestedFields: null); + + foreach (var app in fullDetails) + { + var rec = listRecords.First(r => r.Id == app.Id); + + rec.ContactFullName.ShouldBe(app.ApplicantAgent?.Name, + $"ContactFullName mismatch for Id={app.Id}"); + rec.ContactTitle.ShouldBe(app.ApplicantAgent?.Title, + $"ContactTitle mismatch for Id={app.Id}"); + rec.ContactEmail.ShouldBe(app.ApplicantAgent?.Email, + $"ContactEmail mismatch for Id={app.Id}"); + rec.ContactBusinessPhone.ShouldBe(app.ApplicantAgent?.Phone, + $"ContactBusinessPhone mismatch for Id={app.Id}"); + rec.ContactCellPhone.ShouldBe(app.ApplicantAgent?.Phone2, + $"ContactCellPhone mismatch for Id={app.Id}"); + } + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetApplicationListRecordsAsync_OwnerFields_Match_WithFullDetailsAsync() + { + using var uow = _unitOfWorkManager.Begin(); + + var fullDetails = await _applicationRepository.WithFullDetailsAsync( + skipCount: 0, + maxResultCount: int.MaxValue); + + var listRecords = await _applicationRepository.GetApplicationListRecordsAsync( + skipCount: 0, + maxResultCount: int.MaxValue, + requestedFields: null); + + foreach (var app in fullDetails) + { + var rec = listRecords.First(r => r.Id == app.Id); + + if (app.Owner != null) + { + rec.OwnerPersonId.ShouldBe(app.Owner.Id, + $"OwnerPersonId mismatch for Id={app.Id}"); + rec.OwnerFullName.ShouldBe(app.Owner.FullName, + $"OwnerFullName mismatch for Id={app.Id}"); + } + else + { + rec.OwnerPersonId.ShouldBeNull($"Expected null OwnerPersonId for Id={app.Id}"); + rec.OwnerFullName.ShouldBeNull($"Expected null OwnerFullName for Id={app.Id}"); + } + } + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetApplicationListRecordsAsync_KnownApplication1_HasExpectedFieldValues() + { + using var uow = _unitOfWorkManager.Begin(); + + var listRecords = await _applicationRepository.GetApplicationListRecordsAsync( + skipCount: 0, + maxResultCount: int.MaxValue, + requestedFields: null); + + var rec = listRecords.FirstOrDefault(r => r.Id == GrantManagerTestData.Application1_Id); + rec.ShouldNotBeNull("Application1 seed data should be present in list records"); + + rec.ProjectName.ShouldBe("Application For Integration Test Funding"); + rec.ReferenceNo.ShouldBe("TEST12345"); + rec.RequestedAmount.ShouldBe(3456.13m); + rec.ApplicantId.ShouldBe(GrantManagerTestData.Applicant1_Id); + rec.ApplicantName.ShouldBe("Integration Tester 1"); + rec.Status.ShouldBe("Submitted"); + rec.SubmissionDate.ShouldBe(new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc)); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetApplicationListRecordsAsync_KnownApplication2_HasExpectedFieldValues() + { + using var uow = _unitOfWorkManager.Begin(); + + var listRecords = await _applicationRepository.GetApplicationListRecordsAsync( + skipCount: 0, + maxResultCount: int.MaxValue, + requestedFields: null); + + var rec = listRecords.FirstOrDefault(r => r.Id == GrantManagerTestData.Application2_Id); + rec.ShouldNotBeNull("Application2 seed data should be present in list records"); + + rec.ProjectName.ShouldBe("Application 2 For Integration Test Funding"); + rec.ReferenceNo.ShouldBe("TEST67890"); + rec.RequestedAmount.ShouldBe(5000m); + rec.ApplicantId.ShouldBe(GrantManagerTestData.Applicant1_Id); + rec.ApplicantName.ShouldBe("Integration Tester 1"); + rec.Status.ShouldBe("Submitted"); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetApplicationListRecordsAsync_WithTagsRequestedField_ExcludesContactAndOwnerData() + { + using var uow = _unitOfWorkManager.Begin(); + + var listRecords = await _applicationRepository.GetApplicationListRecordsAsync( + skipCount: 0, + maxResultCount: int.MaxValue, + requestedFields: new List { "applicationTag" }); + + listRecords.ShouldNotBeEmpty(); + + foreach (var rec in listRecords) + { + rec.ContactFullName.ShouldBeNull( + $"ContactFullName should not be loaded when only 'applicationTag' is requested for Id={rec.Id}"); + rec.OwnerPersonId.ShouldBeNull( + $"OwnerPersonId should not be loaded when only 'applicationTag' is requested for Id={rec.Id}"); + } + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetApplicationListRecordsAsync_WithAssigneesRequestedField_ExcludesContactAndOwnerData() + { + using var uow = _unitOfWorkManager.Begin(); + + var listRecords = await _applicationRepository.GetApplicationListRecordsAsync( + skipCount: 0, + maxResultCount: int.MaxValue, + requestedFields: new List { "assignees" }); + + listRecords.ShouldNotBeEmpty(); + + foreach (var rec in listRecords) + { + rec.ContactFullName.ShouldBeNull( + $"ContactFullName should not be loaded when only 'assignees' is requested for Id={rec.Id}"); + rec.OwnerPersonId.ShouldBeNull( + $"OwnerPersonId should not be loaded when only 'assignees' is requested for Id={rec.Id}"); + } + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetApplicationListRecordsAsync_WithContactFieldRequested_ExcludesTagsAssignmentsLinks() + { + using var uow = _unitOfWorkManager.Begin(); + + var listRecords = await _applicationRepository.GetApplicationListRecordsAsync( + skipCount: 0, + maxResultCount: int.MaxValue, + requestedFields: new List { "contactEmail" }); + + listRecords.ShouldNotBeEmpty(); + + foreach (var rec in listRecords) + { + rec.Tags.ShouldBeEmpty( + $"Tags should not be loaded when only contact fields are requested for Id={rec.Id}"); + rec.Assignments.ShouldBeEmpty( + $"Assignments should not be loaded when only contact fields are requested for Id={rec.Id}"); + rec.Links.ShouldBeEmpty( + $"Links should not be loaded when only contact fields are requested for Id={rec.Id}"); + rec.OwnerPersonId.ShouldBeNull( + $"OwnerPersonId should not be loaded when only contact fields are requested for Id={rec.Id}"); + } + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetApplicationListRecordsAsync_WithOwnerRequestedField_ExcludesTagsAssignmentsLinks() + { + using var uow = _unitOfWorkManager.Begin(); + + var listRecords = await _applicationRepository.GetApplicationListRecordsAsync( + skipCount: 0, + maxResultCount: int.MaxValue, + requestedFields: new List { "Owner" }); + + listRecords.ShouldNotBeEmpty(); + + foreach (var rec in listRecords) + { + rec.Tags.ShouldBeEmpty( + $"Tags should not be loaded when only 'Owner' is requested for Id={rec.Id}"); + rec.Assignments.ShouldBeEmpty( + $"Assignments should not be loaded when only 'Owner' is requested for Id={rec.Id}"); + rec.Links.ShouldBeEmpty( + $"Links should not be loaded when only 'Owner' is requested for Id={rec.Id}"); + rec.ContactFullName.ShouldBeNull( + $"ContactFullName should not be loaded when only 'Owner' is requested for Id={rec.Id}"); + } + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetApplicationListRecordsAsync_WithEmptyRequestedFields_ExcludesAllOptionalData() + { + using var uow = _unitOfWorkManager.Begin(); + + var listRecords = await _applicationRepository.GetApplicationListRecordsAsync( + skipCount: 0, + maxResultCount: int.MaxValue, + requestedFields: new List()); + + listRecords.ShouldNotBeEmpty(); + + foreach (var rec in listRecords) + { + rec.Tags.ShouldBeEmpty( + $"Tags should not be loaded when requestedFields is empty for Id={rec.Id}"); + rec.Assignments.ShouldBeEmpty( + $"Assignments should not be loaded when requestedFields is empty for Id={rec.Id}"); + rec.Links.ShouldBeEmpty( + $"Links should not be loaded when requestedFields is empty for Id={rec.Id}"); + rec.ContactFullName.ShouldBeNull( + $"ContactFullName should not be loaded when requestedFields is empty for Id={rec.Id}"); + rec.OwnerPersonId.ShouldBeNull( + $"OwnerPersonId should not be loaded when requestedFields is empty for Id={rec.Id}"); + } + } +} \ No newline at end of file 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 45dcdf556..f499604bc 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 @@ -7,12 +7,14 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Unity.AI.RateLimit; 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.Users; using Xunit; using Xunit.Abstractions; @@ -50,6 +52,7 @@ public async Task QueueAllAIStagesAsync_Should_Enqueue_Pipeline_Job_When_None_Ex capturedArgs!.ApplicationId.ShouldBe(applicationId); capturedArgs.TenantId.ShouldBe(tenantId); capturedArgs.PromptVersion.ShouldBe("v1"); + capturedArgs.RequestedByUserId.ShouldBe(CreateQueueCurrentUserId); capturedArgs.RequestKey.ShouldBe(AIGenerationRequestKeyHelper.BuildRequestKey(tenantId, applicationId, AIGenerationRequestKeyHelper.PipelineOperationType)); await backgroundJobManager.Received(1).EnqueueAsync(Arg.Any(), Arg.Any(), Arg.Any()); await repository.Received(1).InsertAsync(Arg.Is(r => @@ -78,12 +81,15 @@ public async Task QueueApplicationAnalysisAsync_Should_Not_Enqueue_When_An_Activ repository.GetQueryableAsync().Returns(Task.FromResult>(new[] { request }.AsQueryable())); var backgroundJobManager = Substitute.For(); - var queue = CreateQueue(backgroundJobManager, repository); + var rateLimiter = Substitute.For(); + rateLimiter.EnsureAsync().Returns(Task.CompletedTask); + var queue = CreateQueue(backgroundJobManager, repository, rateLimiter); await queue.QueueApplicationAnalysisAsync(applicationId, tenantId, promptVersion); await backgroundJobManager.DidNotReceive().EnqueueAsync(Arg.Any()); await repository.DidNotReceive().InsertAsync(Arg.Any(), Arg.Any(), Arg.Any()); + await rateLimiter.DidNotReceive().EnsureAsync(); } [Fact] @@ -117,6 +123,7 @@ public async Task QueueApplicationAnalysisAsync_Should_Enqueue_New_Request_When_ capturedArgs!.ApplicationId.ShouldBe(applicationId); capturedArgs.TenantId.ShouldBe(tenantId); capturedArgs.PromptVersion.ShouldBe(promptVersion); + capturedArgs.RequestedByUserId.ShouldBe(CreateQueueCurrentUserId); capturedArgs.RequestKey.ShouldBe(AIGenerationRequestKeyHelper.BuildRequestKey(tenantId, applicationId, AIGenerationRequestKeyHelper.ApplicationAnalysisOperationType)); await repository.Received(1).InsertAsync(Arg.Is(r => r.ApplicationId == applicationId && @@ -126,6 +133,48 @@ await repository.Received(1).InsertAsync(Arg.Is(r => r.Status == AIGenerationRequestStatus.Queued), Arg.Any(), Arg.Any()); } + [Fact] + public async Task QueueApplicationAnalysisAsync_Should_Check_Rate_Limit_Before_Enqueueing_New_Request() + { + 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 rateLimiter = Substitute.For(); + rateLimiter.EnsureAsync().Returns(Task.CompletedTask); + var queue = CreateQueue(backgroundJobManager, repository, rateLimiter); + + await queue.QueueApplicationAnalysisAsync(applicationId, tenantId); + + await rateLimiter.Received(1).EnsureAsync(); + 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 QueueApplicationAnalysisAsync_Should_Not_Insert_Or_Enqueue_When_Rate_Limited() + { + 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(_ => throw new InvalidOperationException("rate limited")); + var queue = CreateQueue(backgroundJobManager, repository, rateLimiter); + + await Should.ThrowAsync(() => queue.QueueApplicationAnalysisAsync(applicationId, tenantId)); + + await rateLimiter.Received(1).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() { @@ -183,6 +232,7 @@ public async Task QueueAttachmentSummaryAsync_Should_Enqueue_New_Request_When_No capturedArgs!.ApplicationId.ShouldBe(applicationId); capturedArgs.TenantId.ShouldBe(tenantId); capturedArgs.PromptVersion.ShouldBe(promptVersion); + capturedArgs.RequestedByUserId.ShouldBe(CreateQueueCurrentUserId); capturedArgs.RequestKey.ShouldBe(AIGenerationRequestKeyHelper.BuildRequestKey(tenantId, applicationId, AIGenerationRequestKeyHelper.AttachmentSummaryOperationType)); await repository.Received(1).InsertAsync(Arg.Is(r => r.ApplicationId == applicationId && @@ -249,6 +299,7 @@ public async Task QueueApplicationScoringAsync_Should_Enqueue_New_Request_When_N capturedArgs!.ApplicationId.ShouldBe(applicationId); capturedArgs.TenantId.ShouldBe(tenantId); capturedArgs.PromptVersion.ShouldBe(promptVersion); + capturedArgs.RequestedByUserId.ShouldBe(CreateQueueCurrentUserId); capturedArgs.RequestKey.ShouldBe(AIGenerationRequestKeyHelper.BuildRequestKey(tenantId, applicationId, AIGenerationRequestKeyHelper.ApplicationScoringOperationType)); await repository.Received(1).InsertAsync(Arg.Is(r => r.ApplicationId == applicationId && @@ -297,12 +348,30 @@ public ValueTask DisposeAsync() private static ApplicationAIGenerationQueue CreateQueue( IBackgroundJobManager backgroundJobManager, - IRepository repository) + IRepository repository, + Unity.AI.RateLimit.IAIRateLimiter? rateLimiter = null) { + if (rateLimiter == null) + { + rateLimiter = Substitute.For(); + rateLimiter.EnsureAsync().Returns(Task.CompletedTask); + } + return new ApplicationAIGenerationQueue( backgroundJobManager, repository, new TestDistributedLockProvider(), + rateLimiter, + CreateCurrentUser(), Substitute.For>()); } + + private static readonly Guid CreateQueueCurrentUserId = Guid.NewGuid(); + + private static ICurrentUser CreateCurrentUser() + { + var currentUser = Substitute.For(); + currentUser.Id.Returns(CreateQueueCurrentUserId); + return currentUser; + } } diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/ApplicationContentAppServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/ApplicationContentAppServiceTests.cs index 366018a28..84a14dfe3 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/ApplicationContentAppServiceTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/ApplicationContentAppServiceTests.cs @@ -1,7 +1,10 @@ +using Microsoft.Extensions.Localization; using NSubstitute; using Shouldly; using System; using System.Threading.Tasks; +using Unity.AI.Localization; +using Unity.AI.Settings; using Unity.GrantManager.GrantApplications; using Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; using Unity.AI.Automation; @@ -20,6 +23,8 @@ public async Task GenerateContentAsync_Should_Return_Completed_Result() featureChecker.IsEnabledAsync("Unity.AI.AttachmentSummaries").Returns(true); featureChecker.IsEnabledAsync("Unity.AI.ApplicationAnalysis").Returns(true); featureChecker.IsEnabledAsync("Unity.AI.Scoring").Returns(true); + var localizer = Substitute.For>(); + var featureGuard = new AIFeatureGuard(featureChecker, localizer); var queue = Substitute.For(); queue.QueueAllAIStagesAsync(Arg.Any(), Arg.Any(), Arg.Any()) @@ -27,7 +32,7 @@ public async Task GenerateContentAsync_Should_Return_Completed_Result() var currentTenant = Substitute.For(); currentTenant.Id.Returns(Guid.NewGuid()); - var service = new ApplicationContentAppService(queue, featureChecker, currentTenant); + var service = new ApplicationContentAppService(queue, featureGuard, currentTenant); var result = await service.GenerateContentAsync(Guid.NewGuid()); @@ -36,3 +41,4 @@ public async Task GenerateContentAsync_Should_Return_Completed_Result() await queue.Received(1).QueueAllAIStagesAsync(Arg.Any(), Arg.Any(), Arg.Any()); } } + 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 c8ef48d05..bc24754ba 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 @@ -9,6 +9,7 @@ using Unity.GrantManager.Applications; using Unity.GrantManager.Attachments; using Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; +using Unity.AI.RateLimit; using Volo.Abp.Domain.Repositories; using Volo.Abp.EventBus.Local; using Volo.Abp.Features; @@ -32,11 +33,13 @@ public async Task ExecuteAsync_Should_Mark_Request_Completed_When_Features_Disab requests.Add(request); var job = BuildJob(featureChecker, repository); + var requestedByUserId = Guid.NewGuid(); await job.ExecuteAsync(new RunApplicationAIPipelineJobArgs { ApplicationId = request.ApplicationId!.Value, RequestKey = request.RequestKey, + RequestedByUserId = requestedByUserId, TenantId = request.TenantId }); @@ -60,21 +63,26 @@ public async Task ExecuteAsync_Should_Publish_Event_When_Pipeline_Scoring_Comple .Returns(new ApplicationScoringResultDto { Completed = true }); var localEventBus = Substitute.For(); + var rateLimiter = Substitute.For(); + var requestedByUserId = Guid.NewGuid(); var job = BuildJob( featureChecker, repository, localEventBus: localEventBus, + rateLimiter: rateLimiter, applicationScoringAppService: scoringAppService); await job.ExecuteAsync(new RunApplicationAIPipelineJobArgs { ApplicationId = request.ApplicationId!.Value, RequestKey = request.RequestKey, + RequestedByUserId = requestedByUserId, TenantId = request.TenantId }); request.Status.ShouldBe(AIGenerationRequestStatus.Completed); + await rateLimiter.Received(1).StampAsync(requestedByUserId); await localEventBus.Received(1).PublishAsync( Arg.Is(x => x.ApplicationId == request.ApplicationId)); } @@ -85,7 +93,8 @@ private RunApplicationAIPipelineJob BuildJob( ILocalEventBus? localEventBus = null, IAttachmentSummaryAppService? attachmentSummaryAppService = null, IApplicationAnalysisAppService? applicationAnalysisAppService = null, - IApplicationScoringAppService? applicationScoringAppService = null) + IApplicationScoringAppService? applicationScoringAppService = null, + IAIRateLimiter? rateLimiter = null) { return new RunApplicationAIPipelineJob( Substitute.For(), @@ -97,6 +106,7 @@ private RunApplicationAIPipelineJob BuildJob( generationRequestRepository, Substitute.For(), GetRequiredService(), + rateLimiter ?? Substitute.For(), NullLogger.Instance); } diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/GrantApplicationListTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/GrantApplicationListTests.cs new file mode 100644 index 000000000..730926281 --- /dev/null +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/GrantApplicationListTests.cs @@ -0,0 +1,296 @@ +using Shouldly; + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +using Unity.GrantManager.Applications; + +using Volo.Abp.Uow; + +using Xunit; +using Xunit.Abstractions; + +namespace Unity.GrantManager.GrantApplications; + +public class GrantApplicationListTests : GrantManagerApplicationTestBase +{ + private readonly IGrantApplicationAppService _appService; + private readonly IApplicationRepository _applicationRepository; + private readonly IUnitOfWorkManager _unitOfWorkManager; + + public GrantApplicationListTests(ITestOutputHelper outputHelper) : base(outputHelper) + { + _appService = GetRequiredService(); + _applicationRepository = GetRequiredService(); + _unitOfWorkManager = GetRequiredService(); + } + + + // GetApplicationListRecordsAsync -- requestedFields flag logic + [Fact] + [Trait("Category", "Integration")] + public async Task GetApplicationListRecordsAsync_NullRequestedFields_ReturnsSeededApplicationsWithBaseFields() + { + using var uow = _unitOfWorkManager.Begin(); + + var result = await _applicationRepository.GetApplicationListRecordsAsync( + skipCount: 0, + maxResultCount: int.MaxValue, + requestedFields: null); + + result.ShouldNotBeNull(); + result.Count.ShouldBeGreaterThanOrEqualTo(1); + result.ShouldAllBe(r => r.Status != null); + result.ShouldAllBe(r => r.ApplicantName != null); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetApplicationListRecordsAsync_WithNonContactNonOwnerFields_OmitsAgentAndOwnerData() + { + using var uow = _unitOfWorkManager.Begin(); + + var result = await _applicationRepository.GetApplicationListRecordsAsync( + skipCount: 0, + maxResultCount: int.MaxValue, + requestedFields: ["projectName", "referenceNo"]); + + result.ShouldNotBeNull(); + result.Count.ShouldBeGreaterThanOrEqualTo(1); + + // Agent fields should be null -- not requested + result.ShouldAllBe(r => r.ContactFullName == null); + result.ShouldAllBe(r => r.ContactEmail == null); + result.ShouldAllBe(r => r.ContactTitle == null); + + // Owner fields should be null -- not requested + result.ShouldAllBe(r => r.OwnerFullName == null); + + // Collections should be empty -- not requested + result.ShouldAllBe(r => r.Tags.Count == 0); + result.ShouldAllBe(r => r.Assignments.Count == 0); + result.ShouldAllBe(r => r.Links.Count == 0); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetApplicationListRecordsAsync_WithContactFields_RunsAgentPath_AssignmentsAndLinksOmitted() + { + using var uow = _unitOfWorkManager.Begin(); + + var result = await _applicationRepository.GetApplicationListRecordsAsync( + skipCount: 0, + maxResultCount: int.MaxValue, + requestedFields: ["contactFullName", "contactEmail"]); + + result.ShouldNotBeNull(); + result.Count.ShouldBeGreaterThanOrEqualTo(1); + result.ShouldAllBe(r => r.ApplicantName != null); + result.ShouldAllBe(r => r.Tags.Count == 0); + result.ShouldAllBe(r => r.Assignments.Count == 0); + result.ShouldAllBe(r => r.Links.Count == 0); + result.ShouldAllBe(r => r.OwnerFullName == null); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetApplicationListRecordsAsync_WithTagField_RunsTagsPath_AgentAndOwnerOmitted() + { + using var uow = _unitOfWorkManager.Begin(); + + var result = await _applicationRepository.GetApplicationListRecordsAsync( + skipCount: 0, + maxResultCount: int.MaxValue, + requestedFields: ["applicationTag"]); + + result.ShouldNotBeNull(); + result.Count.ShouldBeGreaterThanOrEqualTo(1); + result.ShouldAllBe(r => r.Tags != null); + result.ShouldAllBe(r => r.ContactFullName == null); + result.ShouldAllBe(r => r.OwnerFullName == null); + result.ShouldAllBe(r => r.Assignments.Count == 0); + result.ShouldAllBe(r => r.Links.Count == 0); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetApplicationListRecordsAsync_WithOwnerField_RunsOwnerPath_AgentAndTagsOmitted() + { + using var uow = _unitOfWorkManager.Begin(); + + var result = await _applicationRepository.GetApplicationListRecordsAsync( + skipCount: 0, + maxResultCount: int.MaxValue, + requestedFields: ["Owner"]); + + result.ShouldNotBeNull(); + result.Count.ShouldBeGreaterThanOrEqualTo(1); + result.ShouldAllBe(r => r.ContactFullName == null); + result.ShouldAllBe(r => r.Tags.Count == 0); + result.ShouldAllBe(r => r.Assignments.Count == 0); + result.ShouldAllBe(r => r.Links.Count == 0); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetApplicationListRecordsAsync_WithAssigneesField_RunsAssignmentsPath_TagsOmitted() + { + using var uow = _unitOfWorkManager.Begin(); + + var result = await _applicationRepository.GetApplicationListRecordsAsync( + skipCount: 0, + maxResultCount: int.MaxValue, + requestedFields: ["assignees"]); + + result.ShouldNotBeNull(); + result.Count.ShouldBeGreaterThanOrEqualTo(1); + result.ShouldAllBe(r => r.Assignments != null); + result.ShouldAllBe(r => r.Tags.Count == 0); + result.ShouldAllBe(r => r.Links.Count == 0); + result.ShouldAllBe(r => r.ContactFullName == null); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetApplicationListRecordsAsync_WithApplicationLinksField_RunsLinksPath_TagsOmitted() + { + using var uow = _unitOfWorkManager.Begin(); + + var result = await _applicationRepository.GetApplicationListRecordsAsync( + skipCount: 0, + maxResultCount: int.MaxValue, + requestedFields: ["applicationLinks"]); + + result.ShouldNotBeNull(); + result.Count.ShouldBeGreaterThanOrEqualTo(1); + result.ShouldAllBe(r => r.Links != null); + result.ShouldAllBe(r => r.Tags.Count == 0); + result.ShouldAllBe(r => r.Assignments.Count == 0); + result.ShouldAllBe(r => r.ContactFullName == null); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetApplicationListRecordsAsync_WithFutureDateFilter_ReturnsEmpty() + { + using var uow = _unitOfWorkManager.Begin(); + + var result = await _applicationRepository.GetApplicationListRecordsAsync( + skipCount: 0, + maxResultCount: int.MaxValue, + submittedFromDate: DateTime.UtcNow.AddYears(10)); + + result.ShouldNotBeNull(); + result.Count.ShouldBe(0); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetApplicationListRecordsAsync_WithDateRangeMatchingSeededData_ReturnsApplications() + { + // Seeded applications have SubmissionDate = 2023-01-01 + using var uow = _unitOfWorkManager.Begin(); + + var result = await _applicationRepository.GetApplicationListRecordsAsync( + skipCount: 0, + maxResultCount: int.MaxValue, + submittedFromDate: new DateTime(2022, 1, 1, 0, 0, 0, DateTimeKind.Utc), + submittedToDate: new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc)); + + result.ShouldNotBeNull(); + result.Count.ShouldBeGreaterThanOrEqualTo(1); + } + + + // GetListAsync -- totalCount == items.Count and requestedFields pass-through + [Fact] + [Trait("Category", "Integration")] + public async Task GetListAsync_TotalCount_EqualsItemsCount() + { + var result = await _appService.GetListAsync(new GrantApplicationListInputDto()); + + result.ShouldNotBeNull(); + result.TotalCount.ShouldBe(result.Items.Count); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetListAsync_ReturnsAtLeastOneItem() + { + var result = await _appService.GetListAsync(new GrantApplicationListInputDto()); + + result.Items.Count.ShouldBeGreaterThanOrEqualTo(1); + result.TotalCount.ShouldBeGreaterThanOrEqualTo(1); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetListAsync_WithFutureDateFilter_ReturnsEmptyPagedResult() + { + var result = await _appService.GetListAsync(new GrantApplicationListInputDto + { + SubmittedFromDate = DateTime.UtcNow.AddYears(10) + }); + + result.ShouldNotBeNull(); + result.Items.Count.ShouldBe(0); + result.TotalCount.ShouldBe(0); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetListAsync_WithNullRequestedFields_AllItemsHaveBaseFields() + { + var result = await _appService.GetListAsync(new GrantApplicationListInputDto + { + RequestedFields = null + }); + + result.ShouldNotBeNull(); + result.Items.Count.ShouldBeGreaterThanOrEqualTo(1); + result.TotalCount.ShouldBe(result.Items.Count); + result.Items.ShouldAllBe(dto => !string.IsNullOrEmpty(dto.Status)); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetListAsync_WithContactRequestedFields_TotalCountMatchesItems() + { + var result = await _appService.GetListAsync(new GrantApplicationListInputDto + { + RequestedFields = new List { "contactFullName", "contactEmail" } + }); + + result.ShouldNotBeNull(); + result.TotalCount.ShouldBe(result.Items.Count); + result.Items.ShouldAllBe(dto => !string.IsNullOrEmpty(dto.Status)); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetListAsync_WithoutContactRequestedFields_ContactFieldsAreNull() + { + var result = await _appService.GetListAsync(new GrantApplicationListInputDto + { + RequestedFields = new List { "projectName", "referenceNo" } + }); + + result.ShouldNotBeNull(); + result.Items.ShouldAllBe(dto => dto.ContactFullName == null); + result.Items.ShouldAllBe(dto => dto.ContactEmail == null); + result.Items.ShouldAllBe(dto => dto.ContactTitle == null); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetListAsync_TotalCountEqualsItemsCount_IsConsistentAcrossMultipleCalls() + { + var result1 = await _appService.GetListAsync(new GrantApplicationListInputDto()); + var result2 = await _appService.GetListAsync(new GrantApplicationListInputDto()); + + result1.TotalCount.ShouldBe(result1.Items.Count); + result2.TotalCount.ShouldBe(result2.Items.Count); + result1.TotalCount.ShouldBe(result2.TotalCount); + } +} \ No newline at end of file