From 317bcf6efa65bc225837ee607b63a7c94f647a0c Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 20 Feb 2026 16:42:10 -0800 Subject: [PATCH 01/19] AB#31813 Improve analysis prompt structure and output constraints --- .../AI/OpenAIService.cs | 55 +++++++++++++------ 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs index 1ea3e9360..ebc4d1e53 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs @@ -187,21 +187,29 @@ public async Task AnalyzeApplicationAsync(string applicationContent, Lis EVALUATION RUBRIC: {rubric} -Analyze this grant application comprehensively across all five rubric categories (Eligibility, Completeness, Financial Review, Risk Assessment, and Quality Indicators). Identify issues, concerns, and areas for improvement. Return your findings in the following JSON format: +SEVERITY +ERROR: Issue that would likely prevent the application from being approved. +WARNING: Issue that could negatively affect the application's approval. +RECOMMENDATION: Reviewer-facing improvement or follow-up consideration. + +SCORE +HIGH: Application demonstrates strong evidence across most rubric areas with few or no issues. +MEDIUM: Application has some gaps or weaknesses that require reviewer attention. +LOW: Application has significant gaps or risks across key rubric areas. + +OUTPUT {{ ""overall_score"": ""HIGH/MEDIUM/LOW"", ""warnings"": [ {{ ""category"": ""Brief summary of the warning"", - ""message"": ""Detailed warning message with full context and explanation"", - ""severity"": ""WARNING"" + ""message"": ""Detailed warning message with full context and explanation"" }} ], ""errors"": [ {{ ""category"": ""Brief summary of the error"", - ""message"": ""Detailed error message with full context and explanation"", - ""severity"": ""ERROR"" + ""message"": ""Detailed error message with full context and explanation"" }} ], ""recommendations"": [ @@ -212,19 +220,30 @@ public async Task AnalyzeApplicationAsync(string applicationContent, Lis ] }} -Important: The 'category' field should be a concise summary (3-6 words) that captures the essence of the issue, while the 'message' field should contain the detailed explanation."; - - var systemPrompt = @"You are an expert grant application reviewer for the BC Government. - -Conduct a thorough, comprehensive analysis across all rubric categories. Identify substantive issues, concerns, and opportunities for improvement. - -Classify findings based on their impact on the application's evaluation and fundability: -- ERRORS: Important missing information, significant gaps in required content, compliance issues, or major concerns affecting eligibility -- WARNINGS: Areas needing clarification, moderate issues, or concerns that should be addressed - -Evaluate the quality, clarity, and appropriateness of all application content. Be thorough but fair - identify real issues while avoiding nitpicking. - -Respond only with valid JSON in the exact format requested."; +RULES +- Use only APPLICATION CONTENT, ATTACHMENT SUMMARIES, EVALUATION RUBRIC, and form field context as evidence. +- Do not invent fields, documents, requirements, or facts. +- Treat missing or empty values as findings only when they weaken rubric evidence. +- Prefer material issues; avoid nitpicking. +- Each error/warning/recommendation must describe one concrete issue or consideration and why it matters. +- Use 3-6 words for category. +- Each message must be 1-2 complete sentences. +- Each message must be grounded in concrete evidence from provided inputs. +- If attachment evidence is used, reference the attachment explicitly in the message. +- Do not provide applicant-facing advice. +- Do not mention rubric section names in findings. +- If no findings exist, return empty arrays. +- overall_score must be HIGH, MEDIUM, or LOW. +- Return values exactly as specified in OUTPUT. +- Do not return keys outside OUTPUT. +- Return valid JSON only. +- Return plain JSON only (no markdown)."; + + var systemPrompt = @"ROLE +You are an expert grant analyst assistant for human reviewers. + +TASK +Using APPLICATION CONTENT, ATTACHMENT SUMMARIES, EVALUATION RUBRIC, SEVERITY, SCORE, OUTPUT, and RULES, return review findings."; await LogPromptInputAsync("ApplicationAnalysis", systemPrompt, analysisContent); var rawAnalysis = await GenerateSummaryAsync(analysisContent, systemPrompt, 1000); From 6b46d5a819288940b0e32d8f1f411673485e1de4 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 20 Feb 2026 16:47:03 -0800 Subject: [PATCH 02/19] AB#32004 Improve attachment summary prompt structure and evidence rules --- .../AI/OpenAIService.cs | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs index 1ea3e9360..0175753a6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs @@ -129,21 +129,47 @@ public async Task GenerateAttachmentSummaryAsync(string fileName, byte[] var extractedText = await _textExtractionService.ExtractTextAsync(fileName, fileContent, contentType); string contentToAnalyze; - string prompt; + var prompt = @"ROLE +You are a professional grant analyst for the BC Government. + +TASK +Produce a concise reviewer-facing summary of the provided attachment context. + +OUTPUT +- Plain text only +- 1-2 complete sentences + +RULES +- Use only the provided attachment context as evidence. +- If text content is present, summarize the actual content. +- If text content is missing or empty, provide a conservative metadata-based summary. +- Do not invent missing details. +- Keep the summary specific, concrete, and reviewer-facing. +- Return plain text only (no markdown, bullets, or JSON)."; if (!string.IsNullOrWhiteSpace(extractedText)) { _logger.LogDebug("Extracted {TextLength} characters from {FileName}", extractedText.Length, fileName); - contentToAnalyze = $"Document: {fileName}\nType: {contentType}\nContent:\n{extractedText}"; - prompt = "Please analyze this document and provide a concise summary of its content, purpose, and key information, for use by your fellow grant analysts. It should be 1-2 sentences long and about 46 tokens."; + contentToAnalyze = $@"ATTACHMENT +{{ + ""name"": ""{fileName}"", + ""contentType"": ""{contentType}"", + ""sizeBytes"": {fileContent.Length}, + ""text"": {JsonSerializer.Serialize(extractedText)} +}}"; } else { _logger.LogDebug("No text extracted from {FileName}, analyzing metadata only", fileName); - contentToAnalyze = $"File: {fileName}, Type: {contentType}, Size: {fileContent.Length} bytes"; - prompt = "Please analyze this document and provide a concise summary of its content, purpose, and key information, for use by your fellow grant analysts. It should be 1-2 sentences long and about 46 tokens."; + contentToAnalyze = $@"ATTACHMENT +{{ + ""name"": ""{fileName}"", + ""contentType"": ""{contentType}"", + ""sizeBytes"": {fileContent.Length}, + ""text"": null +}}"; } await LogPromptInputAsync("AttachmentSummary", prompt, contentToAnalyze); From 72065d717b3081d355fdff69f8a7c34f31c39cb1 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 20 Feb 2026 16:53:04 -0800 Subject: [PATCH 03/19] AB#32005 Improve scoresheet prompt contract and response reliability --- .../AI/OpenAIService.cs | 69 +++++++++---------- 1 file changed, 32 insertions(+), 37 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs index 1ea3e9360..5ecee640f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs @@ -378,50 +378,45 @@ public async Task GenerateScoresheetSectionAnswersAsync(string applicati ATTACHMENT SUMMARIES: - {attachmentSummariesText} -SCORESHEET SECTION: {sectionName} +SECTION: {sectionName} {sectionJson} -Please analyze this grant application and provide appropriate answers for each question in the ""{sectionName}"" section only. - -For each question, provide: -1. Your answer based on the application content -2. A brief cited description (1-2 sentences) explaining your reasoning with specific references to the application content -3. A confidence score from 0-100 indicating how confident you are in your answer based on available information - -Guidelines for answers: -- For numeric questions, provide a numeric value within the specified range -- For yes/no questions, provide either 'Yes' or 'No' -- For text questions, provide a concise, relevant response -- For select list questions, respond with ONLY the number from the 'number' field (1, 2, 3, etc.) of your chosen option. NEVER return 0 - the lowest valid answer is 1. For example: if you want '(0 pts) No outcomes provided', choose the option where number=1, not 0. -- For text area questions, provide a detailed but concise response -- Base your confidence score on how clearly the application content supports your answer - -Return your response as a JSON object where each key is the question ID and the value contains the answer, citation, and confidence: +OUTPUT {{ - ""question-id-1"": {{ - ""answer"": ""your-answer-here"", - ""citation"": ""Brief explanation with specific reference to application content"", + """": {{ + ""answer"": """", + ""citation"": """", ""confidence"": 85 - }}, - ""question-id-2"": {{ - ""answer"": ""3"", - ""citation"": ""Based on the project budget of $50,000 mentioned in the application, this falls into the medium budget category"", - ""confidence"": 90 }} }} -IMPORTANT FOR SELECT LIST QUESTIONS: If a question has availableOptions like: -[{{""number"":1,""value"":""Low (Under $25K)""}}, {{""number"":2,""value"":""Medium ($25K-$75K)""}}, {{""number"":3,""value"":""High (Over $75K)""}}] -Then respond with ONLY the number (e.g., ""3"" for ""High (Over $75K)""), not the text value. - -Do not return any markdown formatting, just the JSON by itself"; - - var systemPrompt = @"You are an expert grant application reviewer for the BC Government. -Analyze the provided application and generate appropriate answers for the scoresheet section questions based on the application content. -Be thorough, objective, and fair in your assessment. Base your answers strictly on the provided application content. -Always provide citations that reference specific parts of the application content to support your answers. -Be honest about your confidence level - if information is missing or unclear, reflect this in a lower confidence score. -Respond only with valid JSON in the exact format requested."; +RULES +- Use only APPLICATION CONTENT, ATTACHMENT SUMMARIES, and SECTION as evidence. +- Do not invent missing application details. +- Return exactly one answer object per question ID in SECTION. +- Do not omit any question IDs from SECTION. +- Do not add keys that are not question IDs from SECTION. +- Each answer object must include: answer, citation, confidence. +- answer type must match question type: Number => numeric; YesNo/SelectList/Text/TextArea => string. +- For yes/no questions, answer must be exactly ""Yes"" or ""No"". +- For numeric questions, answer must be a numeric value within the allowed range. +- For select list questions, answer must be the selected availableOptions.number encoded as a string. +- For select list questions, never return option label text (for example: ""Yes"", ""No"", or ""N/A""); return the option number string. +- For text and text area questions, answer must be concise, grounded in evidence, and non-empty. +- citation must be 1-2 complete sentences grounded in concrete evidence from provided inputs. +- If evidence is insufficient, give a conservative answer and state uncertainty in citation. +- confidence must be an integer from 0 to 100. +- Confidence reflects certainty in the selected answer given available evidence, not application quality. +- Return values exactly as specified in OUTPUT. +- Do not return keys outside OUTPUT. +- Return valid JSON only. +- Return plain JSON only (no markdown)."; + + var systemPrompt = @"ROLE +You are an expert grant application reviewer for the BC Government. + +TASK +Using APPLICATION CONTENT, ATTACHMENT SUMMARIES, SECTION, OUTPUT, and RULES, answer only the questions in SECTION."; await LogPromptInputAsync("ScoresheetSection", systemPrompt, analysisContent); var modelOutput = await GenerateSummaryAsync(analysisContent, systemPrompt, 2000); From a8662586bef2ecc83a21154b2da6e2f2dda20a7b Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 20 Feb 2026 18:05:01 -0800 Subject: [PATCH 04/19] AB#32005 Align scoresheet AI rationale/confidence parsing and integer confidence display --- .../AssessmentScoresWidgetViewComponent.cs | 44 ++++++++++++++++--- .../AssessmentScoresWidget/Default.cshtml | 6 +-- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/AssessmentScoresWidgetViewComponent.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/AssessmentScoresWidgetViewComponent.cs index e07ce701b..5ba221959 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/AssessmentScoresWidgetViewComponent.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/AssessmentScoresWidgetViewComponent.cs @@ -5,6 +5,7 @@ using Volo.Abp.AspNetCore.Mvc.UI.Bundling; using System; using System.Threading.Tasks; +using System.Globalization; using Unity.GrantManager.Assessments; using Unity.Flex.Domain.ScoresheetInstances; using Unity.Flex.Domain.Scoresheets; @@ -113,10 +114,10 @@ private static void ResolveAiAnswer(Dictionary aiAnswers, Q { question.IsHumanConfirmed = false; // Mark as AI generated - // Handle enhanced AI response format with answer, citation, and confidence + // Handle AI response format with answer, rationale, and confidence. if (aiAnswerValue.ValueKind == JsonValueKind.Object) { - // New format with citations and confidence scores + // New format with rationale and confidence if (aiAnswerValue.TryGetProperty("answer", out var answerProp)) { var rawAnswer = answerProp.ToString(); @@ -131,14 +132,14 @@ private static void ResolveAiAnswer(Dictionary aiAnswers, Q question.Answer = rawAnswer; } } - if (aiAnswerValue.TryGetProperty("citation", out var citationProp)) + if (aiAnswerValue.TryGetProperty("rationale", out var rationaleProp) || + aiAnswerValue.TryGetProperty("citation", out rationaleProp)) { - question.AICitation = citationProp.ToString(); + question.AICitation = rationaleProp.ToString(); } - if (aiAnswerValue.TryGetProperty("confidence", out var confidenceProp) && - confidenceProp.TryGetInt32(out var confidenceScore)) + if (aiAnswerValue.TryGetProperty("confidence", out var confidenceProp)) { - question.AIConfidence = confidenceScore; + question.AIConfidence = ParseAiConfidence(confidenceProp); } } else @@ -238,6 +239,35 @@ private static string ConvertNumericAnswerToSelectListValue(string numericAnswer return numericAnswer; } + private static int ParseAiConfidence(JsonElement confidenceProp) + { + int confidence = 0; + + if (confidenceProp.ValueKind == JsonValueKind.Number) + { + if (confidenceProp.TryGetInt32(out var intValue)) + { + confidence = intValue; + } + else if (confidenceProp.TryGetDouble(out var doubleValue)) + { + confidence = (int)Math.Round(doubleValue, MidpointRounding.AwayFromZero); + } + } + else if (confidenceProp.ValueKind == JsonValueKind.String) + { + var raw = confidenceProp.GetString(); + if (!int.TryParse(raw, out confidence) && + double.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsedDouble)) + { + confidence = (int)Math.Round(parsedDouble, MidpointRounding.AwayFromZero); + } + } + + var rounded = (int)Math.Round(confidence / 5.0, MidpointRounding.AwayFromZero) * 5; + return Math.Clamp(rounded, 0, 100); + } + } public class AssessmentScoresWidgetStyleBundleContributor : BundleContributor diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.cshtml index 0839196de..5e9142ac5 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.cshtml @@ -59,8 +59,8 @@ } @if (question.AIConfidence.HasValue && question.AIConfidence.Value < 85) { - - @question.AIConfidence.Value.ToString("F1")% + + @question.AIConfidence.Value.ToString("F0")% } @@ -229,5 +229,3 @@ else } - - From e70e47c16027c72d4ef29f690601ef55a7130986 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Tue, 24 Feb 2026 18:33:59 -0800 Subject: [PATCH 05/19] AB#31813 Align analysis prompt with structured schema/data/attachments format --- .../AI/OpenAIService.cs | 52 +++++++++++++------ 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs index ebc4d1e53..f4af6e74a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs @@ -168,23 +168,45 @@ public async Task AnalyzeApplicationAsync(string applicationContent, Lis try { - var attachmentSummariesText = attachmentSummaries?.Count > 0 - ? string.Join("\n- ", attachmentSummaries.Select((s, i) => $"Attachment {i + 1}: {s}")) - : "No attachments provided."; + object schemaPayload = new { }; + if (!string.IsNullOrWhiteSpace(formFieldConfiguration)) + { + try + { + using var schemaDoc = JsonDocument.Parse(formFieldConfiguration); + schemaPayload = schemaDoc.RootElement.Clone(); + } + catch (JsonException) + { + _logger.LogWarning("Invalid form field configuration JSON. Using empty schema payload."); + } + } - var fieldConfigurationSection = !string.IsNullOrEmpty(formFieldConfiguration) - ? $@" -{formFieldConfiguration}" - : string.Empty; + var dataPayload = new + { + applicationContent + }; - var analysisContent = $@"APPLICATION CONTENT: -{applicationContent} + var attachmentsPayload = attachmentSummaries?.Count > 0 + ? attachmentSummaries + .Select((summary, index) => new + { + name = $"Attachment {index + 1}", + summary = summary + }) + .Cast() + : Enumerable.Empty(); -ATTACHMENT SUMMARIES: -- {attachmentSummariesText} -{fieldConfigurationSection} + var analysisContent = $@"SCHEMA +{JsonSerializer.Serialize(schemaPayload, JsonLogOptions)} + +DATA +{JsonSerializer.Serialize(dataPayload, JsonLogOptions)} + +ATTACHMENTS +{JsonSerializer.Serialize(attachmentsPayload, JsonLogOptions)} -EVALUATION RUBRIC: +RUBRIC {rubric} SEVERITY @@ -221,7 +243,7 @@ public async Task AnalyzeApplicationAsync(string applicationContent, Lis }} RULES -- Use only APPLICATION CONTENT, ATTACHMENT SUMMARIES, EVALUATION RUBRIC, and form field context as evidence. +- Use only SCHEMA, DATA, ATTACHMENTS, and RUBRIC as evidence. - Do not invent fields, documents, requirements, or facts. - Treat missing or empty values as findings only when they weaken rubric evidence. - Prefer material issues; avoid nitpicking. @@ -243,7 +265,7 @@ public async Task AnalyzeApplicationAsync(string applicationContent, Lis You are an expert grant analyst assistant for human reviewers. TASK -Using APPLICATION CONTENT, ATTACHMENT SUMMARIES, EVALUATION RUBRIC, SEVERITY, SCORE, OUTPUT, and RULES, return review findings."; +Using SCHEMA, DATA, ATTACHMENTS, RUBRIC, SEVERITY, SCORE, OUTPUT, and RULES, return review findings."; await LogPromptInputAsync("ApplicationAnalysis", systemPrompt, analysisContent); var rawAnalysis = await GenerateSummaryAsync(analysisContent, systemPrompt, 1000); From 34a03cb64cc56dd48358a5ff7d0bc0ce4b446a42 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Wed, 25 Feb 2026 09:49:30 -0800 Subject: [PATCH 06/19] AB#32004 Simplify attachment summary prompt payload construction --- .../AI/OpenAIService.cs | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs index 0175753a6..212dc5bf7 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs @@ -128,7 +128,6 @@ public async Task GenerateAttachmentSummaryAsync(string fileName, byte[] { var extractedText = await _textExtractionService.ExtractTextAsync(fileName, fileContent, contentType); - string contentToAnalyze; var prompt = @"ROLE You are a professional grant analyst for the BC Government. @@ -147,31 +146,25 @@ Produce a concise reviewer-facing summary of the provided attachment context. - Keep the summary specific, concrete, and reviewer-facing. - Return plain text only (no markdown, bullets, or JSON)."; - if (!string.IsNullOrWhiteSpace(extractedText)) + var attachmentText = string.IsNullOrWhiteSpace(extractedText) ? null : extractedText; + if (attachmentText != null) { _logger.LogDebug("Extracted {TextLength} characters from {FileName}", extractedText.Length, fileName); - - contentToAnalyze = $@"ATTACHMENT -{{ - ""name"": ""{fileName}"", - ""contentType"": ""{contentType}"", - ""sizeBytes"": {fileContent.Length}, - ""text"": {JsonSerializer.Serialize(extractedText)} -}}"; } else { _logger.LogDebug("No text extracted from {FileName}, analyzing metadata only", fileName); - - contentToAnalyze = $@"ATTACHMENT -{{ - ""name"": ""{fileName}"", - ""contentType"": ""{contentType}"", - ""sizeBytes"": {fileContent.Length}, - ""text"": null -}}"; } + var attachmentPayload = new + { + name = fileName, + contentType, + sizeBytes = fileContent.Length, + text = attachmentText + }; + var contentToAnalyze = $"ATTACHMENT\n{JsonSerializer.Serialize(attachmentPayload, JsonLogOptions)}"; + await LogPromptInputAsync("AttachmentSummary", prompt, contentToAnalyze); var modelOutput = await GenerateSummaryAsync(contentToAnalyze, prompt, 150); await LogPromptOutputAsync("AttachmentSummary", modelOutput); From 92a5839f15cb3cb1850c4e36d46943b6f7e19e50 Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Wed, 25 Feb 2026 11:57:25 -0800 Subject: [PATCH 07/19] AB#30430 update documentation --- ...ion.md => applicant-portal-integration.md} | 61 +- .../docs/applicant-profile-data-providers.md | 540 ++++++++++++++++++ 2 files changed, 582 insertions(+), 19 deletions(-) rename applications/Unity.GrantManager/docs/{ApplicantPortalIntegration.md => applicant-portal-integration.md} (94%) create mode 100644 applications/Unity.GrantManager/docs/applicant-profile-data-providers.md diff --git a/applications/Unity.GrantManager/docs/ApplicantPortalIntegration.md b/applications/Unity.GrantManager/docs/applicant-portal-integration.md similarity index 94% rename from applications/Unity.GrantManager/docs/ApplicantPortalIntegration.md rename to applications/Unity.GrantManager/docs/applicant-portal-integration.md index c47190351..4595cffcd 100644 --- a/applications/Unity.GrantManager/docs/ApplicantPortalIntegration.md +++ b/applications/Unity.GrantManager/docs/applicant-portal-integration.md @@ -69,7 +69,7 @@ X-Api-Key: {your-api-key} ### 1. Get Applicant Profile -Retrieves basic profile information for an applicant. +Retrieves applicant profile data based on the specified key. The response `data` property is polymorphic and varies by key. See [Applicant Profile Data Providers](./applicant-profile-data-providers.md) for full details on each provider. **Endpoint**: `GET /api/app/applicant-profiles/profile` @@ -79,20 +79,33 @@ Retrieves basic profile information for an applicant. | `ProfileId` | `Guid` | Yes | Unique identifier for the applicant profile | | `Subject` | `string` | Yes | OIDC subject identifier (e.g., `user@idp`) | | `TenantId` | `Guid` | Yes | The tenant ID to query within | +| `Key` | `string` | Yes | The data type to retrieve: `CONTACTINFO`, `ADDRESSINFO`, `SUBMISSIONINFO`, `ORGINFO`, `PAYMENTINFO` | **Request Example**: ```http -GET /api/app/applicant-profiles/profile?ProfileId=3fa85f64-5717-4562-b3fc-2c963f66afa6&Subject=smzfrrla7j5hw6z7wzvyzdrtq6dj6fbr@chefs-frontend-5299&TenantId=7c9e6679-7425-40de-944b-e07fc1f90ae7 +GET /api/app/applicant-profiles/profile?ProfileId=3fa85f64-5717-4562-b3fc-2c963f66afa6&Subject=smzfrrla7j5hw6z7wzvyzdrtq6dj6fbr@chefs-frontend-5299&TenantId=7c9e6679-7425-40de-944b-e07fc1f90ae7&Key=CONTACTINFO X-Api-Key: your-api-key-here ``` -**Response Example** (200 OK): +**Response Example** (200 OK — `Key=CONTACTINFO`): ```json { "profileId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "subject": "smzfrrla7j5hw6z7wzvyzdrtq6dj6fbr@chefs-frontend-5299", - "email": "applicant@example.com", - "displayName": "John Doe" + "key": "CONTACTINFO", + "tenantId": "7c9e6679-7425-40de-944b-e07fc1f90ae7", + "data": { + "dataType": "CONTACTINFO", + "contacts": [ + { + "contactId": "a1b2c3d4-...", + "name": "John Doe", + "email": "applicant@example.com", + "contactType": "ApplicantProfile", + "isEditable": true + } + ] + } } ``` @@ -102,11 +115,14 @@ public class ApplicantProfileDto { public Guid ProfileId { get; set; } public string Subject { get; set; } - public string Email { get; set; } - public string DisplayName { get; set; } + public string Key { get; set; } + public Guid TenantId { get; set; } + public ApplicantProfileDataDto? Data { get; set; } // Polymorphic — varies by Key } ``` +The `Data` property uses a JSON discriminator (`dataType`) for polymorphic deserialization. See [Applicant Profile Data Providers](./applicant-profile-data-providers.md) for the complete schema of each data type. + --- ### 2. Get Applicant Tenants @@ -159,25 +175,32 @@ public class ApplicantTenantDto ## Subject Identifier Format -The system extracts and normalizes OIDC subject identifiers as follows: +The system extracts and normalizes OIDC subject identifiers as follows. For full extraction logic including the CHEFS form prerequisite and token structure, see [OIDC Subject Ingestion from CHEFS](./applicant-profile-data-providers.md#oidc-subject-ingestion-from-chefs). ### Input Formats Supported -1. **From CHEFS Submission**: + +Search paths are checked in priority order until a non-empty value is found: + +1. **From CHEFS Submission (primary)**: + - Path: `submission.data.applicantAgent.sub` + - Example: `"smzfrrla7j5hw6z7wzvyzdrtq6dj6fbr@chefs-frontend-5299"` + +2. **From CHEFS Submission (alternate)**: - Path: `submission.data.hiddenApplicantAgent.sub` - Example: `"smzfrrla7j5hw6z7wzvyzdrtq6dj6fbr@chefs-frontend-5299"` -2. **From CreatedBy Field**: - - Path: `submission.createdBy` +3. **From CreatedBy Field (fallback)**: + - Path: `createdBy` - Example: `"anonymous@bcservicescard"` ### Normalization Rules 1. Extract the identifier **before** the `@` symbol 2. Convert to **UPPERCASE** -3. Store in `AppApplicantTenantMaps.OidcSubUsername` +3. Store in `AppApplicantTenantMaps.OidcSubUsername` and `ApplicationFormSubmission.OidcSub` **Examples**: -- `smzfrrla7j5hw6z7wzvyzdrtq6dj6fbr@chefs-frontend-5299` ? `SMZFRRLA7J5HW6Z7WZVYZDRTQ6DJ6FBR` -- `anonymous@bcservicescard` ? `ANONYMOUS` +- `smzfrrla7j5hw6z7wzvyzdrtq6dj6fbr@chefs-frontend-5299` --> `SMZFRRLA7J5HW6Z7WZVYZDRTQ6DJ6FBR` +- `anonymous@bcservicescard` --> `ANONYMOUS` **Implementation**: See `IntakeSubmissionHelper.ExtractOidcSub(dynamic submission)` @@ -472,7 +495,7 @@ graph TB ### Message Flow Patterns -#### 1. Commands (Applicant Portal ? Unity) +#### 1. Commands (Applicant Portal --> Unity) Commands represent requests from the Applicant Portal for Unity to perform an action. @@ -513,7 +536,7 @@ sequenceDiagram - Trigger: User requests status update - Action: Unity publishes current status event -#### 2. Events (Unity ? Applicant Portal) +#### 2. Events (Unity --> Applicant Portal) Events represent notifications about things that have happened in Unity. @@ -1087,13 +1110,13 @@ public class EncryptedMessage #### Planned Message Types -**Commands** (Portal ? Unity): +**Commands** (Portal --> Unity): - `CreateApplicationDraftCommand` - `UploadDocumentCommand` - `WithdrawApplicationCommand` - `RequestApplicationReviewCommand` -**Events** (Unity ? Portal): +**Events** (Unity --> Portal): - `ReviewerAssignedEvent` - `AssessmentCompletedEvent` - `FundingAgreementGeneratedEvent` @@ -1549,7 +1572,7 @@ For issues or questions: ## Related Documentation -- [Applicant Tenant Mapping Implementation](./ApplicantTenantMapping.md) - Technical implementation details +- [Applicant Profile Data Providers](./applicant-profile-data-providers.md) - Provider strategy, data flow diagrams, and OIDC subject extraction details - [API Key Authentication](../src/Unity.GrantManager.HttpApi/Controllers/Authentication/README.md) - Authentication setup - [Background Jobs](../src/Unity.GrantManager.Application/HealthChecks/BackgroundWorkers/README.md) - Background worker configuration diff --git a/applications/Unity.GrantManager/docs/applicant-profile-data-providers.md b/applications/Unity.GrantManager/docs/applicant-profile-data-providers.md new file mode 100644 index 000000000..93c919dbf --- /dev/null +++ b/applications/Unity.GrantManager/docs/applicant-profile-data-providers.md @@ -0,0 +1,540 @@ +# Applicant Profile Data Providers + +## Overview + +The Applicant Profile system exposes a single polymorphic API endpoint that returns different data shapes depending on a **key** parameter. The controller delegates to `ApplicantProfileAppService`, which resolves the correct `IApplicantProfileDataProvider` implementation using a strategy/dictionary pattern. + +All providers are registered via ABP's `[ExposeServices]` attribute and collected as `IEnumerable` in the app service constructor, where they are indexed by their `Key` property. + +--- + +## Entry Point + +**Endpoint:** `GET /api/app/applicant-profiles/profile` + +**Authentication:** API Key (via `ApiKeyAuthorizationFilter`) + +**Query Parameters** (`ApplicantProfileInfoRequest`): + +| Parameter | Type | Description | +|-------------|--------|--------------------------------------------------------------| +| `ProfileId` | `Guid` | The applicant profile identifier | +| `Subject` | `string` | The OIDC subject (e.g. `user@idir`) | +| `TenantId` | `Guid` | The tenant to scope the query to | +| `Key` | `string` | The provider key — determines which data type is returned | + +**Supported Keys:** + +| Key | Provider Class | DTO Returned | Status | +|------------------|------------------------------|-------------------------------|-----------------| +| `CONTACTINFO` | `ContactInfoDataProvider` | `ApplicantContactInfoDto` | ✅ Implemented | +| `ADDRESSINFO` | `AddressInfoDataProvider` | `ApplicantAddressInfoDto` | ✅ Implemented | +| `SUBMISSIONINFO` | `SubmissionInfoDataProvider` | `ApplicantSubmissionInfoDto` | ✅ Implemented | +| `ORGINFO` | `OrgInfoDataProvider` | `ApplicantOrgInfoDto` | ⬜ Placeholder | +| `PAYMENTINFO` | `PaymentInfoDataProvider` | `ApplicantPaymentInfoDto` | ⬜ Placeholder | + +**Response:** `ApplicantProfileDto` with a polymorphic `Data` property (JSON discriminator: `dataType`). + +--- + +## High-Level Architecture + +```mermaid +graph TB + Client([External Client]) + Controller["ApplicantProfileController
GET /api/app/applicant-profiles/profile"] + Filter["ApiKeyAuthorizationFilter"] + AppService["ApplicantProfileAppService"] + ProviderDict["Provider Dictionary
key to IApplicantProfileDataProvider"] + + Client -->|"HTTP GET ?Key=..."| Controller + Controller --> Filter + Filter -->|Authorized| AppService + AppService -->|"Lookup by Key"| ProviderDict + + ProviderDict --> ContactProvider["ContactInfoDataProvider
CONTACTINFO"] + ProviderDict --> AddressProvider["AddressInfoDataProvider
ADDRESSINFO"] + ProviderDict --> SubmissionProvider["SubmissionInfoDataProvider
SUBMISSIONINFO"] + ProviderDict --> OrgProvider["OrgInfoDataProvider
ORGINFO
placeholder"] + ProviderDict --> PaymentProvider["PaymentInfoDataProvider
PAYMENTINFO
placeholder"] + + style OrgProvider fill:#f5f5f5,stroke:#bbb,stroke-dasharray:5 + style PaymentProvider fill:#f5f5f5,stroke:#bbb,stroke-dasharray:5 +``` + +--- + +## Dispatch Flow + +The `ApplicantProfileAppService.GetApplicantProfileAsync` method is the central orchestrator. It: + +1. Creates a new `ApplicantProfileDto` and copies request fields (`ProfileId`, `Subject`, `TenantId`, `Key`). +2. Looks up the matching `IApplicantProfileDataProvider` by `Key` in an in-memory dictionary (case-insensitive). +3. Calls `provider.GetDataAsync(request)` if found; otherwise logs a warning. +4. Returns the DTO with the polymorphic `Data` property populated. + +```mermaid +sequenceDiagram + participant C as Client + participant Ctrl as ApplicantProfileController + participant Svc as ApplicantProfileAppService + participant Dict as Provider Dictionary + participant P as IApplicantProfileDataProvider + + C->>Ctrl: GET /api/app/applicant-profiles/profile?Key=X&... + Ctrl->>Svc: GetApplicantProfileAsync(request) + Svc->>Dict: TryGetValue(request.Key) + alt Key found + Dict-->>Svc: provider + Svc->>P: GetDataAsync(request) + P-->>Svc: ApplicantProfileDataDto (concrete subclass) + else Key not found + Svc->>Svc: Log warning + end + Svc-->>Ctrl: ApplicantProfileDto { Data = ... } + Ctrl-->>C: 200 OK (JSON) +``` + +--- + +## Provider Details + +### 1. ContactInfoDataProvider (`CONTACTINFO`) + +**Purpose:** Aggregates contact information from two sources — profile-linked contacts and application-level contacts. + +**Dependencies:** +- `ICurrentTenant` — for multi-tenant scoping +- `IApplicantProfileContactService` — encapsulates contact query logic + +**Logic:** + +1. Switches to the requested tenant context. +2. Retrieves **profile contacts** — contacts linked to the applicant profile via `ContactLink` records where `RelatedEntityType == "ApplicantProfile"` and `RelatedEntityId == profileId`. These are **editable** (`IsEditable = true`). +3. Retrieves **application contacts** — contacts on applications whose form submissions match the normalized OIDC subject. These are **read-only** (`IsEditable = false`). +4. Merges both lists into a single `ApplicantContactInfoDto.Contacts` collection. + +**Subject Normalization:** The OIDC subject (e.g. `user@idir`) is normalized by stripping everything after `@` and converting to uppercase. + +```mermaid +flowchart TD + Start([GetDataAsync called]) + Tenant["Switch to request.TenantId"] + + subgraph ProfileContacts["Profile Contacts - Editable"] + PC1["Query ContactLink
WHERE RelatedEntityType = 'ApplicantProfile'
AND RelatedEntityId = profileId
AND IsActive = true"] + PC2["JOIN Contact ON ContactId"] + PC3["Map to ContactInfoItemDto
IsEditable = true"] + PC1 --> PC2 --> PC3 + end + + subgraph AppContacts["Application Contacts - Read-Only"] + AC1["Normalize Subject
strip domain, uppercase"] + AC2["Query ApplicationFormSubmission
WHERE OidcSub = normalizedSubject"] + AC3["JOIN ApplicationContact
ON ApplicationId"] + AC4["Map to ContactInfoItemDto
IsEditable = false"] + AC1 --> AC2 --> AC3 --> AC4 + end + + Start --> Tenant + Tenant --> PC1 + Tenant --> AC1 + PC3 --> Merge["Merge into Contacts list"] + AC4 --> Merge + Merge --> Return([Return ApplicantContactInfoDto]) +``` + +**Data Sources:** + +| Source | Entity | Join Path | Editable | +|--------|--------|-----------|----------| +| Profile Contacts | `ContactLink` → `Contact` | `ContactLink.RelatedEntityId = profileId` | ✅ Yes | +| Application Contacts | `ApplicationFormSubmission` → `ApplicationContact` | `Submission.OidcSub = normalizedSubject` | ❌ No | + +--- + +### 2. AddressInfoDataProvider (`ADDRESSINFO`) + +**Purpose:** Retrieves applicant addresses by querying address records linked to the applicant's form submissions. Addresses are resolved via two join paths and deduplicated. + +**Dependencies:** +- `ICurrentTenant` — for multi-tenant scoping +- `IRepository` — form submissions +- `IRepository` — address records +- `IRepository` — applications (for `ReferenceNo`) + +**Logic:** + +1. Normalizes the OIDC subject. +2. Switches to the requested tenant context. +3. Queries addresses through **two join paths**: + - **By ApplicationId:** `Submission → Address (on ApplicationId) → Application` — these are **not editable** (owned by an application). + - **By ApplicantId:** `Submission → Address (on ApplicantId) → Application (LEFT JOIN)` — these are **editable** (owned by the applicant directly). +4. Concatenates both result sets. +5. **Deduplicates** by `Address.Id` — if the same address appears in both sets, the application-linked (non-editable) version takes priority. +6. Maps `AddressType` enum values to human-readable names (`Physical`, `Mailing`, `Business`). +7. Checks the `isPrimary` extended property on addresses; if no address is marked primary, the most recently created address is auto-promoted. + +```mermaid +flowchart TD + Start([GetDataAsync called]) + Norm["Normalize Subject
strip domain, uppercase"] + Tenant["Switch to request.TenantId"] + + Start --> Norm --> Tenant + + subgraph ByAppId["Join Path: By ApplicationId - Read-Only"] + A1["ApplicationFormSubmission
WHERE OidcSub = normalized"] + A2["JOIN ApplicantAddress
ON Submission.ApplicationId = Address.ApplicationId"] + A3["JOIN Application
ON Address.ApplicationId = Application.Id"] + A4["IsEditable = false"] + A1 --> A2 --> A3 --> A4 + end + + subgraph ByApplicantId["Join Path: By ApplicantId - Editable"] + B1["ApplicationFormSubmission
WHERE OidcSub = normalized"] + B2["JOIN ApplicantAddress
ON Submission.ApplicantId = Address.ApplicantId"] + B3["LEFT JOIN Application
ON Address.ApplicationId = Application.Id"] + B4["IsEditable = true"] + B1 --> B2 --> B3 --> B4 + end + + Tenant --> A1 + Tenant --> B1 + + A4 --> Concat["CONCAT both result sets"] + B4 --> Concat + Concat --> Dedup["Deduplicate by Address.Id
prefer IsEditable = false"] + Dedup --> Map["Map to AddressInfoItemDto
AddressType to display name
Check isPrimary extended property"] + Map --> Primary{"Any address
marked primary?"} + Primary -->|Yes| Return([Return ApplicantAddressInfoDto]) + Primary -->|No| AutoPrimary["Mark most recent
address as primary"] + AutoPrimary --> Return +``` + +**Deduplication Rule:** When the same address ID appears in both join paths, the application-linked record (`IsEditable = false`) wins. This is achieved by grouping on `Address.Id` and ordering by `IsEditable` ascending (`false` < `true`). + +--- + +### 3. SubmissionInfoDataProvider (`SUBMISSIONINFO`) + +**Purpose:** Lists all form submissions associated with the applicant's OIDC subject, along with application metadata and a link to view the form in CHEFS. + +**Dependencies:** +- `ICurrentTenant` — for multi-tenant scoping +- `IRepository` — form submissions +- `IRepository` — applications +- `IRepository` — status records +- `IEndpointManagementAppService` — resolves the CHEFS API base URL +- `ILogger` — logging + +**Logic:** + +1. Normalizes the OIDC subject. +2. Resolves the **CHEFS form view URL** from the `INTAKE_API_BASE` dynamic URL setting: + - Fetches the base URL (e.g. `https://chefs-dev.apps.silver.devops.gov.bc.ca/app/api/v1`) + - Strips the trailing `/api/v1` segment + - Appends `/form/view?s=` to create the view link template + - Falls back to an empty string on failure. +3. Switches to the requested tenant context. +4. Queries `ApplicationFormSubmission` → `Application` → `ApplicationStatus` where `OidcSub` matches. +5. Maps each result to a `SubmissionInfoItemDto`: + - `ReceivedTime` = the submission's `CreationTime` in the system. + - `SubmissionTime` = the `createdAt` timestamp parsed from the CHEFS JSON payload; falls back to `CreationTime` if parsing fails. + - `Status` = the `ExternalStatus` from the application status record. + - `LinkId` = the `ChefsSubmissionGuid` used to build a direct link to the form. + +```mermaid +flowchart TD + Start([GetDataAsync called]) + Norm["Normalize Subject
strip domain, uppercase"] + + Start --> Norm + Norm --> ResolveUrl["ResolveFormViewUrlAsync"] + Norm --> Tenant["Switch to request.TenantId"] + + subgraph URLResolution["CHEFS Form View URL Resolution"] + U1["Fetch INTAKE_API_BASE
via IEndpointManagementAppService"] + U2["Strip trailing /api/v1"] + U3["Append /form/view?s="] + U4["Set as dto.LinkSource"] + U1 --> U2 --> U3 --> U4 + end + + ResolveUrl --> U1 + + subgraph Query["Submission Query"] + Q1["ApplicationFormSubmission
WHERE OidcSub = normalized"] + Q2["JOIN Application
ON Submission.ApplicationId = Application.Id"] + Q3["JOIN ApplicationStatus
ON Application.ApplicationStatusId = Status.Id"] + Q4["SELECT Id, ChefsSubmissionGuid,
CreationTime, Submission JSON,
ReferenceNo, ProjectName, ExternalStatus"] + Q1 --> Q2 --> Q3 --> Q4 + end + + Tenant --> Q1 + + Q4 --> MapItems["Map to SubmissionInfoItemDto
ReceivedTime = CreationTime
SubmissionTime = parse JSON createdAt
Status = ExternalStatus
LinkId = ChefsSubmissionGuid"] + + U4 --> Result + MapItems --> Result([Return ApplicantSubmissionInfoDto]) +``` + +**Submission Time Resolution:** + +```mermaid +flowchart LR + JSON["Submission JSON"] + Parse{"Parse JSON?"} + HasField{"Has 'createdAt'
field?"} + ValidDate{"Valid DateTime?"} + Use["Use parsed DateTime"] + Fallback["Use CreationTime
(fallback)"] + + JSON --> Parse + Parse -->|Success| HasField + Parse -->|JsonException| Fallback + HasField -->|Yes| ValidDate + HasField -->|No| Fallback + ValidDate -->|Yes| Use + ValidDate -->|No| Fallback +``` + +--- + +### 4. OrgInfoDataProvider (`ORGINFO`) — Placeholder + +**Purpose:** Will provide organization information for the applicant profile. + +**Current Status:** Returns an empty `ApplicantOrgInfoDto` with no data fields populated. No dependencies or query logic implemented yet. + +--- + +### 5. PaymentInfoDataProvider (`PAYMENTINFO`) — Placeholder + +**Purpose:** Will provide payment information for the applicant profile. + +**Current Status:** Returns an empty `ApplicantPaymentInfoDto` with no data fields populated. No dependencies or query logic implemented yet. + +--- + +## Common Patterns + +### Subject Normalization + +All providers that query by OIDC subject apply the same normalization: + +``` +Input: "5ay5pewjqddncvlzlukm3gn2r7vdzq6q@chefs-frontend-5299" → Output: "5AY5PEWJQDDNCVLZLUKM3GN2R7VDZQ6Q" +Input: "user@idir" → Output: "USER" +Input: "USER" → Output: "USER" +``` + +The portion after `@` is stripped and the remainder is uppercased. This matches the format stored in `ApplicationFormSubmission.OidcSub`, which is populated during intake import (see [OIDC Subject Ingestion from CHEFS](#oidc-subject-ingestion-from-chefs) below). + +### Multi-Tenancy + +Every provider switches to the requested `TenantId` using `ICurrentTenant.Change(request.TenantId)` before querying tenant-scoped data. This ensures queries hit the correct tenant database. + +### Polymorphic Serialization + +The `ApplicantProfileDataDto` base class uses `System.Text.Json` polymorphic attributes: + +``` +[JsonPolymorphic(TypeDiscriminatorPropertyName = "dataType")] +[JsonDerivedType(typeof(ApplicantContactInfoDto), "CONTACTINFO")] +[JsonDerivedType(typeof(ApplicantOrgInfoDto), "ORGINFO")] +[JsonDerivedType(typeof(ApplicantAddressInfoDto), "ADDRESSINFO")] +[JsonDerivedType(typeof(ApplicantSubmissionInfoDto), "SUBMISSIONINFO")] +[JsonDerivedType(typeof(ApplicantPaymentInfoDto), "PAYMENTINFO")] +``` + +The JSON response includes a `dataType` discriminator field so consumers can deserialize the correct concrete type. + +### Editability + +Providers distinguish between **editable** and **read-only** data: + +| Provider | Editable Source | Read-Only Source | +|----------|----------------|-----------------| +| ContactInfo | Profile-linked contacts | Application-level contacts | +| AddressInfo | Addresses linked via ApplicantId | Addresses linked via ApplicationId | + +--- + +## OIDC Subject Ingestion from CHEFS + +The `OidcSub` field stored on `ApplicationFormSubmission` is the key that links submissions to an applicant across the profile system. It is populated **at intake import time** by `IntakeFormSubmissionManager.ProcessFormSubmissionAsync`, which calls `IntakeSubmissionHelper.ExtractOidcSub`. + +### CHEFS Form Prerequisite + +For the OIDC subject to be available, the CHEFS form **must** include a **hidden form control** whose value is set to the authenticated user's JWT token. When the form is submitted, CHEFS includes this token payload in the submission JSON, making the `sub` claim accessible to the import process. + +If this hidden control is not configured, the `sub` field will be absent and `ExtractOidcSub` will fall back to `Guid.Empty`. + +### Token Structure in CHEFS Submission JSON + +When set up correctly, the submission JSON received from CHEFS contains the decoded token as a nested object. Example: + +```json +{ + "submission": { + "data": { + "applicantAgent": { + "aud": "chefs-frontend-5299", + "azp": "chefs-frontend-5299", + "exp": 1770327585, + "iat": 1770327285, + "iss": "https://dev.loginproxy.gov.bc.ca/auth/realms/standard", + "jti": "onrtac:b2571d2d-ebbf-4f50-aaf8-5d603aa6a171", + "sub": "5ay5pewjqddncvlzlukm3gn2r7vdzq6q@chefs-frontend-5299", + "typ": "Bearer", + "scope": "openid chefs-frontend-5299 idir bceidbusiness email profile bceidbasic", + "family_name": "SURFACE", + "given_names": "PRISCILA", + "identity_provider": "chefs-frontend-5299", + "preferred_username": "5ay5pewjqddncvlzlukm3gn2r7vdzq6q@chefs-frontend-5299" + } + } + } +} +``` + +### Extraction Logic (`IntakeSubmissionHelper.ExtractOidcSub`) + +The helper searches the dynamic submission object through **multiple configured paths** in priority order until a non-empty value is found: + +| Priority | Search Path | Description | +|----------|------------|-------------| +| 1 | `submission→data→applicantAgent→sub` | Primary path — standard hidden control name | +| 2 | `submission→data→hiddenApplicantAgent→sub` | Alternate hidden control name | +| 3 | `createdBy` | Top-level CHEFS fallback field | + +Once the raw `sub` value is found (e.g. `5ay5pewjqddncvlzlukm3gn2r7vdzq6q@chefs-frontend-5299`), it is normalized: +- Everything after `@` is stripped → `5ay5pewjqddncvlzlukm3gn2r7vdzq6q` +- Converted to uppercase → `5AY5PEWJQDDNCVLZLUKM3GN2R7VDZQ6Q` +- If no value is found, returns `Guid.Empty` as a string + +```mermaid +flowchart TD + Start([CHEFS Submission Received]) + Import["IntakeFormSubmissionManager
ProcessFormSubmissionAsync"] + Extract["IntakeSubmissionHelper.ExtractOidcSub"] + P1{"Try: submission / data /
applicantAgent / sub"} + P2{"Try: submission / data /
hiddenApplicantAgent / sub"} + P3{"Try: createdBy"} + Strip["Strip domain suffix"] + Upper["Convert to uppercase"] + Empty["Use Guid.Empty"] + Store["Store as ApplicationFormSubmission.OidcSub"] + Used(["Used by all providers to
match submissions to the applicant"]) + + Start --> Import --> Extract + Extract --> P1 + P1 -->|found| Strip + P1 -->|empty| P2 + P2 -->|found| Strip + P2 -->|empty| P3 + P3 -->|found| Strip + P3 -->|empty| Empty + Strip --> Upper --> Store + Empty --> Store + Store --> Used +``` + +### Import Call Site + +In `IntakeFormSubmissionManager.ProcessFormSubmissionAsync`: + +```csharp +var newSubmission = new ApplicationFormSubmission +{ + OidcSub = IntakeSubmissionHelper.ExtractOidcSub(formSubmission.submission), + ApplicantId = application.ApplicantId, + ApplicationFormId = applicationForm.Id, + ChefsSubmissionGuid = intakeMap.SubmissionId ?? $"{Guid.Empty}", + ApplicationId = application.Id, + Submission = dataNode?.ToString() ?? string.Empty +}; +``` + +The `formSubmission.submission` object passed to `ExtractOidcSub` is the `submission` node from the CHEFS JSON payload. The helper traverses into `data→applicantAgent→sub` to reach the token's `sub` claim. + +--- + +## Full Request Lifecycle + +```mermaid +sequenceDiagram + participant Client + participant Controller as ApplicantProfileController + participant AuthFilter as ApiKeyAuthorizationFilter + participant AppService as ApplicantProfileAppService + participant Provider as IApplicantProfileDataProvider + participant TenantCtx as ICurrentTenant + participant DB as Tenant Database + + Client->>Controller: GET /api/app/applicant-profiles/profile
?ProfileId=...&Subject=...&TenantId=...&Key=CONTACTINFO + Controller->>AuthFilter: Validate API Key + AuthFilter-->>Controller: ✅ Authorized + Controller->>AppService: GetApplicantProfileAsync(request) + + Note over AppService: Build ApplicantProfileDto shell
with ProfileId, Subject, TenantId, Key + + AppService->>AppService: _providersByKey.TryGetValue("CONTACTINFO") + AppService->>Provider: GetDataAsync(request) + + Provider->>TenantCtx: Change(request.TenantId) + TenantCtx-->>Provider: Scoped to tenant + + Provider->>DB: Query contacts / addresses / submissions + DB-->>Provider: Raw data + + Provider->>Provider: Normalize, deduplicate, map to DTOs + Provider-->>AppService: ApplicantContactInfoDto + + Note over AppService: dto.Data = contactInfoDto + + AppService-->>Controller: ApplicantProfileDto + Controller-->>Client: 200 OK
{ profileId, subject, tenantId, key,
data: { dataType: "CONTACTINFO", contacts: [...] } } +``` + +--- + +## Project Structure + +``` +src/ +├── Unity.GrantManager.Application.Contracts/ApplicantProfile/ +│ ├── ApplicantProfileDto.cs # Response wrapper DTO +│ ├── ApplicantProfileRequest.cs # Request models (base + info) +│ ├── IApplicantProfileAppService.cs # App service interface +│ ├── IApplicantProfileContactService.cs # Contact service interface +│ ├── IApplicantProfileDataProvider.cs # Provider strategy interface +│ └── ProfileData/ +│ ├── ApplicantProfileDataDto.cs # Polymorphic base (discriminator) +│ ├── ApplicantContactInfoDto.cs # CONTACTINFO response +│ ├── ApplicantOrgInfoDto.cs # ORGINFO response (placeholder) +│ ├── ApplicantAddressInfoDto.cs # ADDRESSINFO response +│ ├── ApplicantSubmissionInfoDto.cs # SUBMISSIONINFO response +│ ├── ApplicantPaymentInfoDto.cs # PAYMENTINFO response (placeholder) +│ ├── ContactInfoItemDto.cs # Individual contact item +│ ├── AddressInfoItemDto.cs # Individual address item +│ └── SubmissionInfoItemDto.cs # Individual submission item +│ +├── Unity.GrantManager.Application/ApplicantProfile/ +│ ├── ApplicantProfileAppService.cs # Central orchestrator +│ ├── ApplicantProfileContactService.cs # Contact query logic +│ ├── ApplicantProfileKeys.cs # Key constants +│ ├── AddressInfoDataProvider.cs # ADDRESSINFO provider +│ ├── ContactInfoDataProvider.cs # CONTACTINFO provider +│ ├── SubmissionInfoDataProvider.cs # SUBMISSIONINFO provider +│ ├── OrgInfoDataProvider.cs # ORGINFO provider (placeholder) +│ └── PaymentInfoDataProvider.cs # PAYMENTINFO provider (placeholder) +│ +├── Unity.GrantManager.Application/Intakes/ +│ ├── IntakeFormSubmissionManager.cs # Import orchestrator (calls ExtractOidcSub) +│ └── IntakeSubmissionHelper.cs # OidcSub extraction from CHEFS token +│ +└── Unity.GrantManager.HttpApi/Controllers/ + └── ApplicantProfileController.cs # API controller entry point +``` From 876958203f54a74d8da0f5eef7e622fa26199411 Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Wed, 25 Feb 2026 12:11:16 -0800 Subject: [PATCH 08/19] AB#30430 move docs to correct place --- .../applicant-portal}/applicant-portal-integration.md | 0 .../applicant-portal}/applicant-profile-data-providers.md | 0 .../reporting/get_worksheet_data_specification.md | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename {applications/Unity.GrantManager/docs => documentation/applicant-portal}/applicant-portal-integration.md (100%) rename {applications/Unity.GrantManager/docs => documentation/applicant-portal}/applicant-profile-data-providers.md (100%) rename {applications/Unity.GrantManager/docs => documentation}/reporting/get_worksheet_data_specification.md (100%) diff --git a/applications/Unity.GrantManager/docs/applicant-portal-integration.md b/documentation/applicant-portal/applicant-portal-integration.md similarity index 100% rename from applications/Unity.GrantManager/docs/applicant-portal-integration.md rename to documentation/applicant-portal/applicant-portal-integration.md diff --git a/applications/Unity.GrantManager/docs/applicant-profile-data-providers.md b/documentation/applicant-portal/applicant-profile-data-providers.md similarity index 100% rename from applications/Unity.GrantManager/docs/applicant-profile-data-providers.md rename to documentation/applicant-portal/applicant-profile-data-providers.md diff --git a/applications/Unity.GrantManager/docs/reporting/get_worksheet_data_specification.md b/documentation/reporting/get_worksheet_data_specification.md similarity index 100% rename from applications/Unity.GrantManager/docs/reporting/get_worksheet_data_specification.md rename to documentation/reporting/get_worksheet_data_specification.md From dd34287308678e62562fa7b68bd8e71b65834c3d Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Wed, 25 Feb 2026 15:15:53 -0800 Subject: [PATCH 09/19] AB#32005 Further scoresheet alignment --- .../AI/OpenAIService.cs | 54 +++++++++++++------ 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs index 5ecee640f..6661f08fe 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs @@ -372,43 +372,65 @@ public async Task GenerateScoresheetSectionAnswersAsync(string applicati ? string.Join("\n- ", attachmentSummaries.Select((s, i) => $"Attachment {i + 1}: {s}")) : "No attachments provided."; - var analysisContent = $@"APPLICATION CONTENT: + object sectionQuestionsPayload = sectionJson; + if (!string.IsNullOrWhiteSpace(sectionJson)) + { + try + { + using var sectionDoc = JsonDocument.Parse(sectionJson); + sectionQuestionsPayload = sectionDoc.RootElement.Clone(); + } + catch (JsonException) + { + // Keep raw string payload when JSON parsing fails. + } + } + + var sectionPayload = new + { + name = sectionName, + questions = sectionQuestionsPayload + }; + + var analysisContent = $@"DATA {applicationContent} -ATTACHMENT SUMMARIES: +ATTACHMENTS - {attachmentSummariesText} -SECTION: {sectionName} -{sectionJson} +SECTION +{JsonSerializer.Serialize(sectionPayload, JsonLogOptions)} -OUTPUT +RESPONSE {{ """": {{ ""answer"": """", - ""citation"": """", + ""rationale"": """", ""confidence"": 85 }} }} RULES -- Use only APPLICATION CONTENT, ATTACHMENT SUMMARIES, and SECTION as evidence. +- Use only DATA and ATTACHMENTS as evidence. - Do not invent missing application details. -- Return exactly one answer object per question ID in SECTION. -- Do not omit any question IDs from SECTION. -- Do not add keys that are not question IDs from SECTION. -- Each answer object must include: answer, citation, confidence. +- Return exactly one answer object per question ID in SECTION.questions. +- Do not omit any question IDs from SECTION.questions. +- Do not add keys that are not question IDs from SECTION.questions. +- Use RESPONSE as the output contract and fill every placeholder value. +- Each answer object must include: answer, rationale, confidence. - answer type must match question type: Number => numeric; YesNo/SelectList/Text/TextArea => string. - For yes/no questions, answer must be exactly ""Yes"" or ""No"". - For numeric questions, answer must be a numeric value within the allowed range. - For select list questions, answer must be the selected availableOptions.number encoded as a string. - For select list questions, never return option label text (for example: ""Yes"", ""No"", or ""N/A""); return the option number string. - For text and text area questions, answer must be concise, grounded in evidence, and non-empty. -- citation must be 1-2 complete sentences grounded in concrete evidence from provided inputs. -- If evidence is insufficient, give a conservative answer and state uncertainty in citation. +- rationale must be 1-2 complete sentences grounded in concrete DATA/ATTACHMENTS evidence. +- For every question, rationale must justify both the selected answer and confidence level based on evidence strength. +- If evidence is insufficient, choose the most conservative valid answer and state uncertainty in rationale. - confidence must be an integer from 0 to 100. - Confidence reflects certainty in the selected answer given available evidence, not application quality. -- Return values exactly as specified in OUTPUT. -- Do not return keys outside OUTPUT. +- Return values exactly as specified in RESPONSE. +- Do not return keys outside RESPONSE. - Return valid JSON only. - Return plain JSON only (no markdown)."; @@ -416,7 +438,7 @@ public async Task GenerateScoresheetSectionAnswersAsync(string applicati You are an expert grant application reviewer for the BC Government. TASK -Using APPLICATION CONTENT, ATTACHMENT SUMMARIES, SECTION, OUTPUT, and RULES, answer only the questions in SECTION."; +Using DATA, ATTACHMENTS, SECTION, RESPONSE, and RULES, answer only the questions in SECTION."; await LogPromptInputAsync("ScoresheetSection", systemPrompt, analysisContent); var modelOutput = await GenerateSummaryAsync(analysisContent, systemPrompt, 2000); From 34d97ea076612b9d6024aba03d6d9f0ec0eb9e2f Mon Sep 17 00:00:00 2001 From: "Todosichuk, Daryl JEDI:EX" Date: Wed, 25 Feb 2026 15:33:51 -0800 Subject: [PATCH 10/19] AB#32063 Remove obsolete openshift yaml files moved to GitOps --- .github/workflows/docker-build-dev.yml | 4 +- .github/workflows/docker-build-main.yml | 4 +- .github/workflows/docker-build-test.yml | 4 +- .github/workflows/pr-check-dev-branch.yml | 2 +- .github/workflows/pr-check-main-branch.yml | 2 +- README.md | 5 +- applications/Unity.Tools/README.md | 2 +- .../Unity.Tools/Unity.Metabase/README.md | 211 ++++++- .../Unity.Metabase/docker-compose.yml | 62 +++ .../Unity.Tools/Unity.RabbitMQ/README.md | 226 ++++++-- .../Unity.RabbitMQ/docker-compose.yml | 33 ++ .../rabbit@unity-rabbitmq-1-feature_flags | 23 - database/.gitkeep | 0 database/crunchy-postgres/.helmignore | 23 - database/crunchy-postgres/Chart.yaml | 28 - database/crunchy-postgres/README.md | 202 ------- .../custom-values-example.yaml | 72 --- .../templates/PostgresCluster.yaml | 254 --------- .../crunchy-postgres/templates/_helpers.tpl | 66 --- database/crunchy-postgres/templates/_s3.tpl | 18 - .../templates/data-restore-configmap.yaml | 35 -- .../templates/data-restore-cronjob.yaml | 190 ------- .../templates/data-restore-secret.yaml | 16 - .../crunchy-postgres/templates/s3Secret.yaml | 11 - database/crunchy-postgres/values.yaml | 196 ------- .../metabase-setup-database-readonly.sql | 98 ---- .../metabase-setup-database-readwrite.sql | 71 --- .../metabase-setup-metabaseuploaddb.sql | 76 --- database/scripts/metabase-setup-readme.md | 110 ---- database/scripts/metabase-setup-roles.sql | 70 --- database/unity-backup-cronjob.yaml | 167 ------ database/unity-database.yaml | 239 -------- openshift/Readme.md | 95 ---- openshift/SSL_CERTIFICATE.md | 94 ---- openshift/redis-sentinel/.helmignore | 23 - openshift/redis-sentinel/Chart.lock | 6 - openshift/redis-sentinel/Chart.yaml | 29 - .../redis-sentinel/charts/redis-21.1.11.tgz | Bin 111679 -> 0 bytes openshift/redis-sentinel/values-dev.yaml | 17 - openshift/redis-sentinel/values-dev2.yaml | 17 - openshift/redis-sentinel/values-prod.yaml | 17 - openshift/redis-sentinel/values-test.yaml | 17 - openshift/redis-sentinel/values-uat.yaml | 17 - openshift/redis-sentinel/values.yaml | 49 -- openshift/tools-networkpolicy.yaml | 46 -- openshift/unity-app-data-build.json | 130 ----- openshift/unity-app-data-web.json | 241 -------- openshift/unity-applicantportal-build.yaml | Bin 7938 -> 0 bytes openshift/unity-applicantportal-web.yaml | 271 --------- openshift/unity-chefs-data-web.json | 117 ---- openshift/unity-grantmanager-build.yaml | Bin 8976 -> 0 bytes .../unity-grantmanager-dbmigrator-job.yaml | 119 ---- .../unity-grantmanager-pgbackup-job.yaml | 141 ----- openshift/unity-grantmanager-web.yaml | 517 ------------------ openshift/unity-image-puller.yaml | 20 - openshift/unity-imagestream.yaml | Bin 3532 -> 0 bytes openshift/unity-metabase.yaml | Bin 18934 -> 0 bytes openshift/unity-networkpolicy.yaml | 80 --- openshift/unity-rabbitmq.yaml | Bin 16700 -> 0 bytes openshift/unity-s3-object-storage.yaml | 94 ---- openshift/unity-sysdig-team.yaml | 15 - 61 files changed, 505 insertions(+), 4187 deletions(-) create mode 100644 applications/Unity.Tools/Unity.Metabase/docker-compose.yml create mode 100644 applications/Unity.Tools/Unity.RabbitMQ/docker-compose.yml delete mode 100644 applications/Unity.Tools/Unity.RabbitMQ/rabbit@unity-rabbitmq-1-feature_flags create mode 100644 database/.gitkeep delete mode 100644 database/crunchy-postgres/.helmignore delete mode 100644 database/crunchy-postgres/Chart.yaml delete mode 100644 database/crunchy-postgres/README.md delete mode 100644 database/crunchy-postgres/custom-values-example.yaml delete mode 100644 database/crunchy-postgres/templates/PostgresCluster.yaml delete mode 100644 database/crunchy-postgres/templates/_helpers.tpl delete mode 100644 database/crunchy-postgres/templates/_s3.tpl delete mode 100644 database/crunchy-postgres/templates/data-restore-configmap.yaml delete mode 100644 database/crunchy-postgres/templates/data-restore-cronjob.yaml delete mode 100644 database/crunchy-postgres/templates/data-restore-secret.yaml delete mode 100644 database/crunchy-postgres/templates/s3Secret.yaml delete mode 100644 database/crunchy-postgres/values.yaml delete mode 100644 database/scripts/metabase-setup-database-readonly.sql delete mode 100644 database/scripts/metabase-setup-database-readwrite.sql delete mode 100644 database/scripts/metabase-setup-metabaseuploaddb.sql delete mode 100644 database/scripts/metabase-setup-readme.md delete mode 100644 database/scripts/metabase-setup-roles.sql delete mode 100644 database/unity-backup-cronjob.yaml delete mode 100644 database/unity-database.yaml delete mode 100644 openshift/Readme.md delete mode 100644 openshift/SSL_CERTIFICATE.md delete mode 100644 openshift/redis-sentinel/.helmignore delete mode 100644 openshift/redis-sentinel/Chart.lock delete mode 100644 openshift/redis-sentinel/Chart.yaml delete mode 100644 openshift/redis-sentinel/charts/redis-21.1.11.tgz delete mode 100644 openshift/redis-sentinel/values-dev.yaml delete mode 100644 openshift/redis-sentinel/values-dev2.yaml delete mode 100644 openshift/redis-sentinel/values-prod.yaml delete mode 100644 openshift/redis-sentinel/values-test.yaml delete mode 100644 openshift/redis-sentinel/values-uat.yaml delete mode 100644 openshift/redis-sentinel/values.yaml delete mode 100644 openshift/tools-networkpolicy.yaml delete mode 100644 openshift/unity-app-data-build.json delete mode 100644 openshift/unity-app-data-web.json delete mode 100644 openshift/unity-applicantportal-build.yaml delete mode 100644 openshift/unity-applicantportal-web.yaml delete mode 100644 openshift/unity-chefs-data-web.json delete mode 100644 openshift/unity-grantmanager-build.yaml delete mode 100644 openshift/unity-grantmanager-dbmigrator-job.yaml delete mode 100644 openshift/unity-grantmanager-pgbackup-job.yaml delete mode 100644 openshift/unity-grantmanager-web.yaml delete mode 100644 openshift/unity-image-puller.yaml delete mode 100644 openshift/unity-imagestream.yaml delete mode 100644 openshift/unity-metabase.yaml delete mode 100644 openshift/unity-networkpolicy.yaml delete mode 100644 openshift/unity-rabbitmq.yaml delete mode 100644 openshift/unity-s3-object-storage.yaml delete mode 100644 openshift/unity-sysdig-team.yaml diff --git a/.github/workflows/docker-build-dev.yml b/.github/workflows/docker-build-dev.yml index 70596aa88..5f33222ea 100644 --- a/.github/workflows/docker-build-dev.yml +++ b/.github/workflows/docker-build-dev.yml @@ -10,8 +10,8 @@ on: - '.gitignore' - 'database/**' - 'documentation/**' - - 'openshift/**' - - 'tests/**' + - '**/docs/**' + - '**/README*' - 'CODE_OF_CONDUCT.md' - 'COMPLIANCE.yaml' - 'CONTRIBUTING.md' diff --git a/.github/workflows/docker-build-main.yml b/.github/workflows/docker-build-main.yml index f583143dc..c0294fe06 100644 --- a/.github/workflows/docker-build-main.yml +++ b/.github/workflows/docker-build-main.yml @@ -10,8 +10,8 @@ on: - '.gitignore' - 'database/**' - 'documentation/**' - - 'openshift/**' - - 'tests/**' + - '**/docs/**' + - '**/README*' - 'CODE_OF_CONDUCT.md' - 'COMPLIANCE.yaml' - 'CONTRIBUTING.md' diff --git a/.github/workflows/docker-build-test.yml b/.github/workflows/docker-build-test.yml index c061a34c6..3b7e9d91f 100644 --- a/.github/workflows/docker-build-test.yml +++ b/.github/workflows/docker-build-test.yml @@ -10,8 +10,8 @@ on: - '.gitignore' - 'database/**' - 'documentation/**' - - 'openshift/**' - - 'tests/**' + - '**/docs/**' + - '**/README*' - 'CODE_OF_CONDUCT.md' - 'COMPLIANCE.yaml' - 'CONTRIBUTING.md' diff --git a/.github/workflows/pr-check-dev-branch.yml b/.github/workflows/pr-check-dev-branch.yml index f08a1749d..aaadd9726 100644 --- a/.github/workflows/pr-check-dev-branch.yml +++ b/.github/workflows/pr-check-dev-branch.yml @@ -1,4 +1,4 @@ -name: Dev - CI & Unit Tests +name: Dev - Branch Protection - CI & Unit Tests permissions: contents: read diff --git a/.github/workflows/pr-check-main-branch.yml b/.github/workflows/pr-check-main-branch.yml index 8199f1aba..205a2c9d8 100644 --- a/.github/workflows/pr-check-main-branch.yml +++ b/.github/workflows/pr-check-main-branch.yml @@ -1,4 +1,4 @@ -name: Main - Branch Protection +name: Main - Branch Protection - CI & Unit Tests permissions: contents: read pull-requests: write diff --git a/README.md b/README.md index 4425d1952..46240d8e9 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,8 @@ The project is in a reliable state and major changes are unlikely to happen. ├── Unity.NginxData/ - Nginx HTTP server and reference files ├── Unity.RabbitMQ/ - RabbitMQ message broker configuration └── Unity.RedisSentinel/- Redis Sentinel high-availability setup - database/ - Database configuration and scripts - documentation/ - Solution documentation and assets - openshift/ - OpenShift deployment files and configs + database/ - Database configuration scripts + documentation/ - Solution documentation COMPLIANCE.yaml - BCGov PIA/STRA compliance status CONTRIBUTING.md - How to contribute LICENSE - License diff --git a/applications/Unity.Tools/README.md b/applications/Unity.Tools/README.md index 7adc5859f..856a9d6c7 100644 --- a/applications/Unity.Tools/README.md +++ b/applications/Unity.Tools/README.md @@ -4,5 +4,5 @@ This directory contains supporting tools and services for the Unity platform: - [Unity.Metabase](Unity.Metabase/README.md): Reserved for Metabase integration or related resources. - [Unity.NginxData](Unity.NginxData/README.md): Nginx HTTP server and reverse proxy S2I application, with reference files for forms and reporting. -- [Unity.RabbitMQ](Unity.RabbitMQ/README.md): RabbitMQ message broker setup for OpenShift, including user and vhost configuration. +- [Unity.RabbitMQ](Unity.RabbitMQ/README.md): RabbitMQ message broker user and vhost configuration. - [Unity.RedisSentinel](Unity.RedisSentinel/README.md): Docker Compose configuration for Redis with Sentinel for high availability. diff --git a/applications/Unity.Tools/Unity.Metabase/README.md b/applications/Unity.Tools/Unity.Metabase/README.md index b0359d7ce..9dddbdd79 100644 --- a/applications/Unity.Tools/Unity.Metabase/README.md +++ b/applications/Unity.Tools/Unity.Metabase/README.md @@ -1,3 +1,210 @@ -# Unity Metabase +# Metabase Configuration -This directory is reserved for the Unity Metabase integration resources. +This directory contains a Docker Compose configuration for setting up Metabase for local development and analytics. + +## Overview + +The setup provides: + +- **Metabase Analytics Platform**: Business intelligence and data visualization +- **PostgreSQL Database**: Metabase application database for storing dashboards, users, etc. +- **Persistent Storage**: Data persistence across container restarts + +## Getting Started + +### Basic Usage + +Start Metabase: + +```bash +docker-compose up +``` + +To run in detached mode: + +```bash +docker-compose up -d +``` + +### Configuration Options + +This setup supports environment variables for customization: + +| Variable | Default | Description | +|----------|---------|-------------| +| `MB_DB_DBNAME` | `metabase` | Metabase application database name | +| `MB_DB_USER` | `metabase` | Metabase database username | +| `MB_DB_PASS` | `metabase123` | Metabase database password | +| `POSTGRES_USER` | `metabase` | PostgreSQL superuser username | +| `POSTGRES_PASSWORD` | `metabase123` | PostgreSQL superuser password | + +#### Custom Database Configuration + +You can set custom database credentials: + +```bash +# PowerShell +$env:MB_DB_PASS="mysecurepassword"; $env:POSTGRES_PASSWORD="mysecurepassword"; docker-compose up + +# Bash/CMD +MB_DB_PASS=mysecurepassword POSTGRES_PASSWORD=mysecurepassword docker-compose up +``` + +Alternatively, create a `.env` file in the same directory: + +```config +MB_DB_DBNAME=metabase +MB_DB_USER=metabase +MB_DB_PASS=mysecurepassword +POSTGRES_USER=metabase +POSTGRES_PASSWORD=mysecurepassword +``` + +### Accessing Metabase + +#### Web Interface + +- **URL**: http://localhost:3000 +- **First-time setup**: You'll be prompted to create an admin account +- **Default admin**: Create during initial setup + +#### Database Connection for Unity Data + +When setting up data sources in Metabase to connect to your Unity databases: + +**For Unity PostgreSQL (from Unity.GrantManager docker-compose):** +- **Host**: `host.docker.internal` (Windows/Mac) or your machine's IP +- **Port**: `5432` (or your Unity DB port) +- **Database**: Your Unity database name +- **Username/Password**: Your Unity database credentials + +## Initial Setup + +### First-Time Configuration + +1. Start Metabase: `docker-compose up` +2. Wait for services to fully start (check logs) +3. Navigate to http://localhost:3000 +4. Complete the initial setup wizard: + - Create admin account + - Skip adding data source (or add Unity database) + - Finish setup + +### Connecting to Unity Data + +To analyze Unity application data: + +1. **Add Database** in Metabase +2. **Select PostgreSQL** +3. **Connection details**: + ``` + Host: host.docker.internal + Port: 5432 (or your Unity DB port) + Database name: [Your Unity DB name] + Username: [Your Unity DB user] + Password: [Your Unity DB password] + ``` + +### Example Unity Database Connection + +If using the Unity.GrantManager docker-compose setup: + +```json +{ + "host": "host.docker.internal", + "port": 5432, + "database": "postgres", + "username": "postgres", + "password": "admin" +} +``` + +## Verifying the Setup + +Check Metabase status: + +```bash +# Check if services are running +docker ps | grep metabase + +# Check Metabase logs +docker-compose logs metabase + +# Check PostgreSQL logs +docker-compose logs metabase-db +``` + +Test web interface: + +```bash +curl http://localhost:3000/api/health +``` + +## Stopping and Cleanup + +Stop Metabase: + +```bash +docker-compose down +``` + +Remove volumes (this will delete all dashboards and configuration): + +```bash +docker-compose down -v +``` + +## Notes & Limitations + +- This setup is designed for local development and testing +- For production deployments, use proper secrets management +- The first startup takes longer as Metabase initializes its database +- Dashboards and questions are stored in the PostgreSQL database + +## Troubleshooting + +### Common Issues + +1. **Slow startup**: Metabase can take 2-3 minutes to fully initialize on first run +2. **Port conflicts**: If port 3000 is in use, modify the port mapping in `docker-compose.yml` +3. **Database connection issues**: Ensure your Unity database is accessible from Docker + +### Useful Commands + +```bash +# Check Metabase initialization status +docker-compose logs -f metabase | grep -i "metabase initialization" + +# Reset Metabase (removes all dashboards/config) +docker-compose down -v && docker-compose up + +# Access PostgreSQL directly +docker exec -it metabase-db psql -U metabase -d metabase +``` + +## Integration with Unity Applications + +This Metabase setup is designed to work with Unity applications for: + +- **Analytics Dashboards**: Visualize Unity application data +- **Business Intelligence**: Generate reports from Unity databases +- **Data Monitoring**: Track application metrics and KPIs +- **User Insights**: Analyze user behavior and application usage + +### Common Unity Analytics Use Cases + +- Grant application metrics and trends +- User engagement and portal usage +- Application performance monitoring +- Business process analytics +- Compliance and audit reporting + +## Production Considerations + +When deploying to production environments: + +- Use external PostgreSQL database instead of containerized one +- Implement proper backup strategies for dashboards and configuration +- Set up proper authentication integration (LDAP, SAML, etc.) +- Configure SSL/TLS for secure connections +- Use environment-specific connection strings diff --git a/applications/Unity.Tools/Unity.Metabase/docker-compose.yml b/applications/Unity.Tools/Unity.Metabase/docker-compose.yml new file mode 100644 index 000000000..c758c444a --- /dev/null +++ b/applications/Unity.Tools/Unity.Metabase/docker-compose.yml @@ -0,0 +1,62 @@ +services: + metabase: + image: metabase/metabase:v0.51.4 + container_name: unity-metabase + hostname: unity-metabase + environment: + - MB_DB_TYPE=postgres + - MB_DB_DBNAME=${MB_DB_DBNAME:-metabase} + - MB_DB_PORT=5432 + - MB_DB_USER=${MB_DB_USER:-metabase} + - MB_DB_PASS=${MB_DB_PASS:-metabase123} + - MB_DB_HOST=metabase-db + - JAVA_OPTS=-Xmx1024m -Xss1m -Dfile.encoding=UTF-8 -Dlogfile.path=target/log -server + ports: + - "3000:3000" + - "8443:8443" + depends_on: + metabase-db: + condition: service_healthy + networks: + - metabase-network + volumes: + - metabase-data:/metabase-data + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 120s + restart: unless-stopped + + metabase-db: + image: postgres:15 + container_name: metabase-db + hostname: metabase-db + environment: + - POSTGRES_DB=${MB_DB_DBNAME:-metabase} + - POSTGRES_USER=${POSTGRES_USER:-metabase} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-metabase123} + ports: + - "5433:5432" # Different port to avoid conflicts with Unity DB + networks: + - metabase-network + volumes: + - postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-metabase} -d ${MB_DB_DBNAME:-metabase}"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + restart: unless-stopped + +networks: + metabase-network: + driver: bridge + +volumes: + metabase-data: + driver: local + postgres-data: + driver: local \ No newline at end of file diff --git a/applications/Unity.Tools/Unity.RabbitMQ/README.md b/applications/Unity.Tools/Unity.RabbitMQ/README.md index ec86d535d..d5f3f6708 100644 --- a/applications/Unity.Tools/Unity.RabbitMQ/README.md +++ b/applications/Unity.Tools/Unity.RabbitMQ/README.md @@ -1,61 +1,215 @@ -# Unity RabbitMQ +# RabbitMQ Configuration -This directory contains the setup for RabbitMQ message broker in an OpenShift container. It includes configuration for administrator and client users, as well as virtual hosts for development environments. +This directory contains a Docker Compose configuration for setting up RabbitMQ for local development. -## Contents -- RabbitMQ configuration files -- User and vhost setup instructions +## Overview -See the README for setup and usage instructions. +The setup provides: -Setup of RabbitMQ message broker in an OpenShift container requires an administrator user (`unity-admin`) and two client users each associated with their own virtual hosts (`/dev` and `/dev2`). +- **RabbitMQ Server**: Message broker for Unity applications +- **Management Interface**: Web-based management and monitoring +- **Persistent Storage**: Data persistence across container restarts -## Prerequisites +## Getting Started -- OpenShift cluster access -- RabbitMQ installed on your OpenShift cluster -- RabbitMQ CLI tools (`rabbitmqctl`) +### Basic Usage -## Setup +Start RabbitMQ: -### Creating Virtual Hosts +```bash +docker-compose up +``` + +To run in detached mode: + +```bash +docker-compose up -d +``` + +### Configuration Options + +This setup supports environment variables for customization: + +| Variable | Default | Description | +|----------|---------|-------------| +| `RABBITMQ_DEFAULT_USER` | `admin` | Default RabbitMQ username | +| `RABBITMQ_DEFAULT_PASS` | `admin` | Default RabbitMQ password | + +#### Custom Credentials + +You can set custom RabbitMQ credentials: + +```bash +# PowerShell +$env:RABBITMQ_DEFAULT_USER="myuser"; $env:RABBITMQ_DEFAULT_PASS="mypassword"; docker-compose up + +# Bash/CMD +RABBITMQ_DEFAULT_USER=myuser RABBITMQ_DEFAULT_PASS=mypassword docker-compose up +``` + +Alternatively, create a `.env` file in the same directory: + +```config +RABBITMQ_DEFAULT_USER=myuser +RABBITMQ_DEFAULT_PASS=mypassword +``` + +### Accessing RabbitMQ + +#### Management Interface + +- **URL**: http://localhost:15672 +- **Username**: `admin` (or your custom user) +- **Password**: `admin` (or your custom password) + +#### AMQP Connection + +- **Host**: localhost +- **Port**: 5672 +- **Username**: `admin` (or your custom user) +- **Password**: `admin` (or your custom password) + +### Client Application Configuration + +For Unity applications, configure your `appsettings.json` as follows: + +```json +{ + "RabbitMQ": { + "Host": "localhost", + "Port": 5672, + "Username": "admin", + "Password": "admin", + "VirtualHost": "/", + "ExchangeName": "unity.exchange", + "QueueName": "unity.queue" + } +} +``` + +#### Configuration Examples -To create the virtual hosts `/dev` and `/dev2`, use the following commands: +For local development: -```sh -rabbitmqctl add_vhost /dev -rabbitmqctl add_vhost /dev2 +```json +{ + "RabbitMQ": { + "Host": "localhost", + "Port": 5672, + "Username": "admin", + "Password": "admin" + } +} ``` -### Adding Users and Setting Permissions +For Docker network communication: + +```json +{ + "RabbitMQ": { + "Host": "rabbitmq", + "Port": 5672, + "Username": "admin", + "Password": "admin" + } +} +``` + +For Kubernetes deployment: + +```json +{ + "RabbitMQ": { + "Host": "unity-rabbitmq.namespace.svc.cluster.local", + "Port": 5672, + "Username": "admin", + "Password": "your-secure-password" + } +} +``` + +## Verifying the Setup -Create the administrator user `unity-admin`: +Check RabbitMQ status: -```sh -rabbitmqctl add_user unity-admin 'your_admin_password' -rabbitmqctl set_permissions -p / unity-admin ".*" ".*" ".*" -rabbitmqctl set_user_tags unity-admin administrator +```bash +# Check if RabbitMQ is running +docker ps | grep rabbitmq + +# Check logs +docker-compose logs rabbitmq ``` -Create the client user `unity-rabbitmq-user-dev` for the `/dev` vhost: +Test connection using management API: -```sh -rabbitmqctl add_user unity-rabbitmq-user-dev 'your_dev_password' -rabbitmqctl set_permissions -p /dev unity-rabbitmq-user-dev ".*" ".*" ".*" +```bash +curl -u admin:admin http://localhost:15672/api/overview ``` -Create the client user `unity-rabbitmq-user-dev2` for the `/dev2` vhost: +## Stopping and Cleanup + +Stop RabbitMQ: -```sh -rabbitmqctl add_user unity-rabbitmq-user-dev2 'your_dev2_password' -rabbitmqctl set_permissions -p /dev2 unity-rabbitmq-user-dev2 ".*" ".*" ".*" +```bash +docker-compose down ``` -## Volume Mounts +Remove volumes (this will delete all data): + +```bash +docker-compose down -v +``` + +## Notes & Limitations + +- This setup is designed for local development and testing +- For production deployments, use proper secrets management +- The management interface is exposed on all interfaces (0.0.0.0) +- Data persists in Docker volumes between restarts + +## Troubleshooting + +### Common Issues + +1. **Port Already in Use**: If ports 5672 or 15672 are already in use, modify the port mappings in `docker-compose.yml` -To persist RabbitMQ data a container volume mount is required with backup to offsite S3 storage. +2. **Permission Issues**: Ensure Docker has proper permissions to create volumes -```yaml -volumeMounts: - - mountPath: /var/lib/rabbitmq +3. **Connection Refused**: Check that RabbitMQ has fully started by monitoring the logs: + ```bash + docker-compose logs -f rabbitmq + ``` + +### RabbitMQ Management Commands + +Useful management commands via the web interface or CLI: + +```bash +# List queues +docker exec rabbitmq rabbitmqctl list_queues + +# List exchanges +docker exec rabbitmq rabbitmqctl list_exchanges + +# List users +docker exec rabbitmq rabbitmqctl list_users + +# Add user +docker exec rabbitmq rabbitmqctl add_user newuser newpassword + +# Set permissions +docker exec rabbitmq rabbitmqctl set_permissions -p / newuser ".*" ".*" ".*" ``` + +## Integration with Unity Applications + +This RabbitMQ setup is designed to work seamlessly with Unity applications that require message queuing capabilities. The configuration matches the OpenShift deployment specifications for consistency across development and production environments. + +### Message Patterns + +Common RabbitMQ patterns used in Unity applications: + +- **Work Queues**: Distributing tasks among workers +- **Publish/Subscribe**: Broadcasting messages to multiple consumers +- **Routing**: Selective message routing based on criteria +- **Topics**: Complex routing patterns with wildcards \ No newline at end of file diff --git a/applications/Unity.Tools/Unity.RabbitMQ/docker-compose.yml b/applications/Unity.Tools/Unity.RabbitMQ/docker-compose.yml new file mode 100644 index 000000000..fe0f05e3e --- /dev/null +++ b/applications/Unity.Tools/Unity.RabbitMQ/docker-compose.yml @@ -0,0 +1,33 @@ +services: + rabbitmq: + image: rabbitmq:4.2-management + container_name: unity-rabbitmq + hostname: unity-rabbitmq + environment: + - RABBITMQ_DEFAULT_USER=${RABBITMQ_DEFAULT_USER:-admin} + - RABBITMQ_DEFAULT_PASS=${RABBITMQ_DEFAULT_PASS:-admin} + - RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS=-rabbit log_levels [{connection,error},{default,info}] + ports: + - "5672:5672" # AMQP port + - "15672:15672" # Management interface port + - "15692:15692" # Prometheus metrics port (optional) + - "25672:25672" # Inter-node and CLI tool communication port + volumes: + - rabbitmq-data:/var/lib/rabbitmq + networks: + - rabbitmq-network + healthcheck: + test: ["CMD", "rabbitmq-diagnostics", "ping"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 40s + restart: unless-stopped + +networks: + rabbitmq-network: + driver: bridge + +volumes: + rabbitmq-data: + driver: local \ No newline at end of file diff --git a/applications/Unity.Tools/Unity.RabbitMQ/rabbit@unity-rabbitmq-1-feature_flags b/applications/Unity.Tools/Unity.RabbitMQ/rabbit@unity-rabbitmq-1-feature_flags deleted file mode 100644 index 073f94dde..000000000 --- a/applications/Unity.Tools/Unity.RabbitMQ/rabbit@unity-rabbitmq-1-feature_flags +++ /dev/null @@ -1,23 +0,0 @@ -[classic_mirrored_queue_version, - classic_queue_type_delivery_support, - detailed_queues_endpoint, - direct_exchange_routing_v2, - drop_unroutable_metric, - empty_basic_get_metric, - feature_flags_v2, - implicit_default_bindings, - listener_records_in_ets, - maintenance_mode_status, - message_containers, - message_containers_deaths_v2, - quorum_queue, - quorum_queue_non_voters, - restart_streams, - stream_filtering, - stream_queue, - stream_sac_coordinator_unblock_group, - stream_single_active_consumer, - stream_update_config_command, - tracking_records_in_ets, - user_limits, - virtual_host_metadata]. \ No newline at end of file diff --git a/database/.gitkeep b/database/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/database/crunchy-postgres/.helmignore b/database/crunchy-postgres/.helmignore deleted file mode 100644 index 0e8a0eb36..000000000 --- a/database/crunchy-postgres/.helmignore +++ /dev/null @@ -1,23 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*.orig -*~ -# Various IDEs -.project -.idea/ -*.tmproj -.vscode/ diff --git a/database/crunchy-postgres/Chart.yaml b/database/crunchy-postgres/Chart.yaml deleted file mode 100644 index ec6ceaee2..000000000 --- a/database/crunchy-postgres/Chart.yaml +++ /dev/null @@ -1,28 +0,0 @@ -apiVersion: v2 -name: crunchy-postgres -description: High Availability CrunchyDB Operator Chart - -icon: https://www.postgresql.org/media/img/about/press/elephant.png - -# A chart can be either an 'application' or a 'library' chart. -# -# Application charts are a collection of templates that can be packaged into versioned archives -# to be deployed. -# -# Library charts provide useful utilities or functions for the chart developer. They're included as -# a dependency of application charts to inject those utilities and functions into the rendering -# pipeline. Library charts do not define any templates and therefore cannot be deployed. -type: application - -# This is the chart version. This version number should be incremented each time you make changes -# to the chart and its templates, including the app version. -# Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.4 - -# This is the version number of the application being deployed. This version number should be -# incremented each time you make changes to the application. Versions are not expected to -# follow Semantic Versioning. They should reflect the version the application is using. -# It is recommended to use it with quotes. - -# Crunchy Postgres Operator version -appVersion: "5.0.4" diff --git a/database/crunchy-postgres/README.md b/database/crunchy-postgres/README.md deleted file mode 100644 index 3302e44b0..000000000 --- a/database/crunchy-postgres/README.md +++ /dev/null @@ -1,202 +0,0 @@ -# Crunchy Postgres chart - -A chart to provision a [Crunchy Postgres](https://www.crunchydata.com/) cluster. - -## Configuration -Apply base configuration from values.yaml and make the necessary overrides in custom-values-example.yaml -```Bash -helm upgrade --install new-hippo-ha . -f values.yaml -f custom-values-example.yaml -``` -### Crunchy Options - -| Parameter | Description | Default | -| ------------------ | ---------------------- | ------------------ | -| `fullnameOverride` | Override release name | `crunchy-postgres` | -| `crunchyImage` | Crunchy Postgres image | | -| `postgresVersion` | Postgres version | `15` | - ---- - -### Instances - -| Parameter | Description | Default | -| ------------------------------------------- | ------------------------------ | ------------------------ | -| `instances.name` | Instance name | `ha` (high availability) | -| `instances.replicas` | Number of replicas | `2` | -| `instances.dataVolumeClaimSpec.storage` | Amount of storage for each PVC | `256Mi` | -| `instances.requests.cpu` | CPU requests | `1m` | -| `instances.requests.memory` | Memory requests | `256Mi` | -| `instances.limits.cpu` | CPU limits | `100m` | -| `instances.limits.memory` | Memory limits | `512Mi` | -| `instances.replicaCertCopy.requests.cpu` | replicaCertCopy CPU requests | `1m` | -| `instances.replicaCertCopy.requests.memory` | replicaCertCopyMemory requests | `32Mi` | -| `instances.replicaCertCopy.limits.cpu` | replicaCertCopyCPU limits | `50m` | -| `instances.replicaCertCopy.limits.memory` | replicaCertCopy Memory limits | `64Mi` | - ---- - -### pgBackRest - Reliable PostgreSQL Backup & Restore - -[pgBackRest site](https://pgbackrest.org/) -[Crunchy pgBackRest docs](https://access.crunchydata.com/documentation/pgbackrest/latest/) - -| Parameter | Description | Default | -| ---------------------------------------------------- | ------------------------------------------------------------- | ---------------------- | -| `pgBackRest.image` | Crunchy pgBackRest | | -| `pgBackRest.retention` | Number of backups/days to keep depending on retentionFullType | `2` | -| `pgBackRest.retentionFullType` | Either 'count' or 'time' | `count` | -| `pgBackRest.repos.schedules.full` | Full backup schedule | `0 8 * * *` | -| `pgBackRest.repos.schedules.incremental` | Incremental backup schedule | `0 0,4,12,16,20 * * *` | -| `pgBackRest.repos.schedules.volume.addessModes` | Access modes | `ReadWriteOnce` | -| `pgBackRest.repos.schedules.volume.storage` | PVC size | `128Mi` | -| `pgBackRest.repos.schedules.volume.storageClassName` | Storage class name modes | `netapp-file-backup` | -| `pgBackRest.repoHost.requests.cpu` | CPU requests | `1m` | -| `pgBackRest.repoHost.requests.memory` | Memory requests | `64Mi` | -| `pgBackRest.repoHost.limits.cpu` | CPU limits | `50m` | -| `pgBackRest.repoHost.limits.memory` | Memory limits | `128Mi` | -| `pgBackRest.sidecars.requests.cpu` | sidecars CPU requests | `1m` | -| `pgBackRest.sidecars.requests.memory` | sidecars Memory requests | `64Mi` | -| `pgBackRest.sidecars.limits.cpu` | sidecars CPU limits | `50m` | -| `pgBackRest.sidecars.limits.memory` | sidecars Memory limits | `128Mi` | -| `pgBackRest.s3.enabled` | Enables the s3 repo backups | `false` | -| `pgBackRest.s3.createS3Secret` | Creates the s3 secret based on key and keySecret | `true` | -| `pgBackRest.s3.s3Secret` | The secret name to be created or read from | `s3-pgbackrest` | -| `pgBackRest.s3.s3Path` | The path inside the bucket where the backups will be saved to, set it to `/` to use the root of the bucket. | `/dbbackup` | -| `pgBackRest.s3.s3UriStyle` | Style of URL to use for S3 communication. [More Info](https://pgbackrest.org/configuration.html#section-repository/option-repo-s3-uri-style) | `path` | -| `pgBackRest.s3.bucket` | The bucket to use for backups | `bucketName` | -| `pgBackRest.s3.endpoint` | The endpoint to use, for example s3.ca-central-1.amazonaws.com | `endpointName` | -| `pgBackRest.s3.region` | The region to use, not necessary if your S3 system does not specify one | `ca-central-1` | -| `pgBackRest.s3.key` | The key to use to access the bucket. MUST BE KEPT SECRET | `s3KeyValue` | -| `pgBackRest.s3.keySecret` | The key secret for the key set above. MUST BE KEPT SECRET | `s3SecretValue` | ---- - -### Patroni - -[Patroni docs](https://patroni.readthedocs.io/en/latest/) -[Crunchy Patroni docs](https://access.crunchydata.com/documentation/patroni/latest/) - -| Parameter | Description | Default | -| ------------------------------------------- | ------------------------------------------------------------------- | --------------------------------- | -| `patroni.postgresql.pg_hba` | pg_hba permissions | `"host all all 0.0.0.0/0 md5"` | -| `crunchyImage` | Crunchy Postgres image | `...crunchy-postgres:ubi8-14.7-0` | -| `patroni.parameters.shared_buffers` | The number of shared memory buffers used by the server | `16MB` | -| `patroni.parameters.wal_buffers` | The number of disk-page buffers in shared memory for WAL | `64KB` | -| `patroni.parameters.min_wal_size` | The minimum size to shrink the WAL to | `32MB` | -| `patroni.parameters.max_wal_size` | Sets the WAL size that triggers a checkpoint | `64MB` | -| `patroni.parameters.max_slot_wal_keep_size` | Sets the maximum WAL size that can be reserved by replication slots | `128MB` | - ---- - -### pgBouncer - -A lightweight connection pooler for PostgreSQL - -[pgBouncer site](https://www.pgbouncer.org/) -[Crunchy Postgres pgBouncer docs](https://access.crunchydata.com/documentation/pgbouncer/latest/) - -| Parameter | Description | Default | -| --------------------------------- | ----------------------- | ------- | -| `proxy.pgBouncer.image` | Crunchy pgBouncer image | | -| `proxy.pgBouncer.replicas` | Number of replicas | `2` | -| `proxy.pgBouncer.requests.cpu` | CPU requests | `1m` | -| `proxy.pgBouncer.requests.memory` | Memory requests | `64Mi` | -| `proxy.pgBouncer.limits.cpu` | CPU limits | `50m` | -| `proxy.pgBouncer.limits.memory` | Memory limits | `128Mi` | - ---- - -## PG Monitor - -[Crunchy Postgres PG Monitor docs](https://access.crunchydata.com/documentation/pgmonitor/latest/) - -| Parameter | Description | Default | -| ------------------------------------ | ---------------------------------------------- | ------- | -| `pgmonitor.enabled` | Enable PG Monitor (currently only PG exporter) | `false` | -| `pgmonitor.exporter.requests.cpu` | PG Monitor CPU requests | `1m` | -| `pgmonitor.exporter.requests.memory` | PG Monitor Memory requests | `64Mi` | -| `pgmonitor.exporter.limits.cpu` | PG Monitor CPU limits | `50m` | -| `pgmonitor.exporter.limits.memory` | PG Monitor Memory limits | `128Mi` | - -#### Postgres Exporter - -A [Prometheus](https://prometheus.io/) exporter for PostgreSQL - -[Postgres Exporter](https://github.com/prometheus-community/postgres_exporter) - -| Parameter | Description | Default | -| ------------------------------------ | ------------------------- | ------- | -| `pgmonitor.exporter.image` | Crunchy PG Exporter image | | -| `pgmonitor.exporter.requests.cpu` | CPU requests | `1m` | -| `pgmonitor.exporter.requests.memory` | Memory requests | `64Mi` | -| `pgmonitor.exporter.limits.cpu` | CPU limits | `50m` | -| `pgmonitor.exporterr.limits.memory` | Memory limits | `128Mi` | - ---- - -## Data Restore CronJob - -This feature allows you to set up a daily CronJob that restores data from a source S3 repository (e.g., from another database instance) into the current PostgreSQL cluster. This is useful for change data capture scenarios where you need to regularly sync data from a source database. The configuration reuses the same structure as `dataSource` and `pgBackRest.s3` for consistency. - -### Configuration - -| Parameter | Description | Default | -| ---------------------------------------------- | ----------------------------------------------------- | ---------------------- | -| `dataRestore.enabled` | Enable the data restore CronJob | `false` | -| `dataRestore.schedule` | Cron schedule for the restore job | `"0 2 * * *"` | -| `dataRestore.image` | pgBackRest image to use for restore | `crunchy-pgbackrest` | -| `dataRestore.secretName` | K8s secret containing S3 credentials (reuse existing)| `s3-pgbackrest` | -| `dataRestore.repo.name` | Repository name (repo1, repo2, etc.) | `repo2` | -| `dataRestore.repo.path` | S3 path prefix | `/habackup` | -| `dataRestore.repo.s3.bucket` | Source S3 bucket name | `bucketName` | -| `dataRestore.repo.s3.endpoint` | S3 endpoint URL | Object store endpoint | -| `dataRestore.repo.s3.region` | S3 region | `not-used` | -| `dataRestore.repo.s3.uriStyle` | S3 URI style (path or host) | `path` | -| `dataRestore.stanza` | pgBackRest stanza name | `db` | -| `dataRestore.target.clusterName` | Target cluster name (defaults to current cluster) | `""` | -| `dataRestore.target.database` | Target database name | `postgres` | -| `dataRestore.resources.requests.cpu` | CPU requests for restore job | `100m` | -| `dataRestore.resources.requests.memory` | Memory requests for restore job | `256Mi` | -| `dataRestore.resources.limits.cpu` | CPU limits for restore job | `500m` | -| `dataRestore.resources.limits.memory` | Memory limits for restore job | `512Mi` | -| `dataRestore.successfulJobsHistoryLimit` | Number of successful jobs to keep in history | `3` | -| `dataRestore.failedJobsHistoryLimit` | Number of failed jobs to keep in history | `1` | -| `dataRestore.restartPolicy` | Pod restart policy for failed jobs | `OnFailure` | -| `dataRestore.additionalArgs` | Additional pgbackrest arguments | `[]` | - -### Usage Example - -The configuration reuses existing S3 secrets and follows the same patterns as `dataSource`: - -```yaml -dataRestore: - enabled: true - schedule: "0 2 * * *" # Daily at 2 AM - # Reuse existing S3 secret from dataSource or pgBackRest.s3 - secretName: "dev-s3-pgbackrest" - repo: - name: repo2 - path: "/habackup-source-database" - s3: - bucket: "source-database-backups" - endpoint: "https://sector.objectstore.gov.bc.ca" - region: "not-used" - uriStyle: "path" - stanza: db - target: - database: "myapp" - additionalArgs: - - "--log-level-console=debug" - - "--process-max=2" -``` - -### Important Notes - -- The restore uses `--delta` mode, which only restores changed files for efficiency -- Reuses existing S3 secrets from `dataSource` or `pgBackRest.s3` configuration -- The job runs with the specified S3 repository as the source -- Ensure the source S3 repository contains valid pgBackRest backups -- The target cluster must be accessible and have proper credentials -- Monitor CronJob logs for restore status and any errors -- Configuration follows the same patterns as `dataSource` for consistency - ---- diff --git a/database/crunchy-postgres/custom-values-example.yaml b/database/crunchy-postgres/custom-values-example.yaml deleted file mode 100644 index f8646e241..000000000 --- a/database/crunchy-postgres/custom-values-example.yaml +++ /dev/null @@ -1,72 +0,0 @@ -# Apply base configuration from values.yaml and make the necessary overrides in custom-values-example.yaml -# helm upgrade --install new-hippo-ha . -f values.yaml -f custom-values-example.yaml - -fullnameOverride: new-crunchy-postgres - -labels: - app.kubernetes.io/part-of: new-crunchydb-postgres - -dataSource: - enabled: false - # should have the same name and contain the same keys as the pgbackrest secret - secretName: new-s3-pgbackrest - repo: - path: "/habackup-new" - bucket: "sector-project-new" - endpoint: "https://sector.objectstore.gov.bc.ca" - -pgBackRest: - repos: - schedules: - full: 10 10 * * * - incremental: 10 3,15,19,23 * * * - s3: - enabled: false - createS3Secret: false - # the s3 secret name - s3Secret: new-s3-pgbackrest - # the path start with /, it will be created under bucket if it doesn't exist - s3Path: "/habackup-new" - # bucket specifies the S3 bucket to use, - bucket: "sector-project-new" - # endpoint specifies the S3 endpoint to use. - endpoint: "https://sector.objectstore.gov.bc.ca" - # key is the S3 key. This is stored in a Secret. - # Please DO NOT push this value to GitHub - key: "s3keyValue" - # keySecret is the S3 key secret. This is stored in a Secret. - # Please DO NOT push this value to GitHub - keySecret: "s3SecretValue" - # set the default schedule to avoid conflicts - fullSchedule: 30 11 * * * - incrementalSchedule: 30 3,15,19,23 * * * - -# Data restore cronjob configuration example -# Uncomment and configure to enable daily restore from source database -# Reuses the same structure as dataSource for consistency -# dataRestore: -# enabled: true -# schedule: "0 2 * * *" # Daily at 2 AM -# image: "artifacts.developer.gov.bc.ca/bcgov-docker-local/crunchy-pgbackrest:ubi8-2.47-1" -# secretName: "new-s3-pgbackrest" -# repo: -# name: repo2 -# path: "/habackup-source" -# bucket: "source-database-backups" -# endpoint: "https://sector.objectstore.gov.bc.ca" -# region: "not-used" -# uriStyle: "path" -# stanza: db -# target: -# clusterName: "" -# database: "myapp" -# resources: -# requests: -# cpu: 200m -# memory: 512Mi -# limits: -# cpu: 1000m -# memory: 1Gi -# additionalArgs: -# - "--log-level-console=debug" -# - "--process-max=2" \ No newline at end of file diff --git a/database/crunchy-postgres/templates/PostgresCluster.yaml b/database/crunchy-postgres/templates/PostgresCluster.yaml deleted file mode 100644 index cb6d0f61b..000000000 --- a/database/crunchy-postgres/templates/PostgresCluster.yaml +++ /dev/null @@ -1,254 +0,0 @@ -apiVersion: postgres-operator.crunchydata.com/v1beta1 -kind: PostgresCluster -metadata: - name: {{ template "crunchy-postgres.fullname" . }} - labels: - helm.sh/chart: {{ include "crunchy-postgres.chart" . }} - app.kubernetes.io/name: {{ include "crunchy-postgres.name" . }} - app.kubernetes.io/instance: {{ include "crunchy-postgres.fullname" . }} - {{- if .Chart.AppVersion }} - app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} - {{- end }} - app.kubernetes.io/managed-by: {{ .Release.Service }} - {{- range $key, $value := .Values.labels }} - {{ $key }}: {{ $value | quote }} - {{- end }} - app.kubernetes.io/component: "database" - {{- if .Values.annotations }} - annotations: - {{- range $key, $value := .Values.annotations }} - {{ $key }}: {{ $value | quote }} - {{- end }} - {{- end }} -spec: - openshift: {{ .Values.openshift | default false }} - {{- if .Values.shutdown }} - shutdown: {{ .Values.shutdown }} - {{- end }} - metadata: - labels: - helm.sh/chart: {{ include "crunchy-postgres.chart" . }} - app.kubernetes.io/name: {{ include "crunchy-postgres.name" . }} - app.kubernetes.io/instance: {{ include "crunchy-postgres.fullname" . }} - {{- if .Chart.AppVersion }} - app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} - {{- end }} - app.kubernetes.io/managed-by: {{ .Release.Service }} - {{- range $key, $value := .Values.labels }} - {{ $key }}: {{ $value | quote }} - {{- end }} - app.kubernetes.io/component: "database" - {{ if .Values.crunchyImage }} - image: {{ .Values.crunchyImage }} - {{ end }} - imagePullPolicy: {{.Values.imagePullPolicy}} - postgresVersion: {{ .Values.postgresVersion }} - {{ if .Values.postGISVersion }} - postGISVersion: {{ .Values.postGISVersion | quote }} - {{ end }} - postgresVersion: {{ .Values.postgresVersion }} - - {{ if .Values.pgmonitor.enabled }} - - monitoring: - pgmonitor: - # this stuff is for the "exporter" container in the "postgres-cluster-ha" set of pods - exporter: - {{ if .Values.pgmonitor.exporter.image}} - image: {{ .Values.pgmonitor.exporter.image}} - {{ end }} - resources: - requests: - cpu: {{ .Values.pgmonitor.exporter.requests.cpu }} - memory: {{ .Values.pgmonitor.exporter.requests.memory }} - limits: - cpu: {{ .Values.pgmonitor.exporter.limits.cpu }} - memory: {{ .Values.pgmonitor.exporter.limits.memory }} - - {{ end }} - - instances: - - name: {{ .Values.instances.name }} - replicas: {{ .Values.instances.replicas }} - resources: - requests: - cpu: {{ .Values.instances.requests.cpu }} - memory: {{ .Values.instances.requests.memory }} - sidecars: - replicaCertCopy: - resources: - requests: - cpu: {{ .Values.instances.replicaCertCopy.requests.cpu }} - memory: {{ .Values.instances.replicaCertCopy.requests.memory }} - limits: - cpu: {{ .Values.instances.replicaCertCopy.limits.cpu }} - memory: {{ .Values.instances.replicaCertCopy.limits.memory }} - dataVolumeClaimSpec: - accessModes: - - "ReadWriteOnce" - resources: - requests: - storage: {{ .Values.instances.dataVolumeClaimSpec.storage }} - storageClassName: {{ .Values.instances.dataVolumeClaimSpec.storageClassName }} - affinity: - podAntiAffinity: - preferredDuringSchedulingIgnoredDuringExecution: - - weight: 1 - podAffinityTerm: - topologyKey: topology.kubernetes.io/zone - labelSelector: - matchLabels: - postgres-operator.crunchydata.com/cluster: - {{ template "crunchy-postgres.fullname" . }} - postgres-operator.crunchydata.com/instance-set: {{ .Values.instances.name }}-ha - - users: - - name: {{ template "crunchy-postgres.fullname" . }} - databases: - - {{ template "crunchy-postgres.fullname" . }} - options: "CREATEROLE" - - name: postgres - databases: - - {{ template "crunchy-postgres.fullname" . }} - - {{ if .Values.dataSource.enabled }} - dataSource: - pgbackrest: - configuration: - - secret: - name: {{ .Values.dataSource.secretName }} - global: - repo2-s3-uri-style: {{ .Values.dataSource.repo.s3.uriStyle | quote }} - repo2-path: {{ .Values.dataSource.repo.path }} - repo: - name: {{ .Values.dataSource.repo.name }} - s3: - bucket: {{ .Values.dataSource.repo.s3.bucket }} - endpoint: {{ .Values.dataSource.repo.s3.endpoint }} - region: {{ .Values.dataSource.repo.s3.region }} - stanza: {{ .Values.dataSource.stanza }} - {{ end }} - - backups: - pgbackrest: - {{ if .Values.pgBackRest.image }} - image: {{ .Values.pgBackRest.image }} - {{ end }} - {{- if .Values.pgBackRest.s3.enabled }} - configuration: - - secret: - name: {{ .Values.pgBackRest.s3.s3Secret }} - {{- end }} - global: - # Support both PVC and s3 backups - repo1-retention-full: {{ .Values.pgBackRest.retention | quote }} - repo1-retention-full-type: {{ .Values.pgBackRest.retentionFullType }} - repo1-retention-archive: {{ .Values.pgBackRest.retentionArchive | quote }} - repo1-retention-archive-type: {{ .Values.pgBackRest.retentionArchiveType }} - {{- if .Values.pgBackRest.s3.enabled }} - repo2-retention-full: {{ .Values.pgBackRest.retentionS3 | quote }} - repo2-retention-full-type: {{ .Values.pgBackRest.retentionFullTypeS3 }} - repo2-path: {{ .Values.pgBackRest.s3.s3Path }} - repo2-s3-uri-style: {{ .Values.pgBackRest.s3.s3UriStyle }} - {{- end }} - repos: - # hardcoding repo1 until we solution allowing multiple repos - - name: repo1 - schedules: - full: {{ .Values.pgBackRest.repos.schedules.full }} - incremental: {{ .Values.pgBackRest.repos.schedules.incremental }} - volume: - volumeClaimSpec: - accessModes: - - {{ .Values.pgBackRest.repos.volume.accessModes }} - resources: - requests: - storage: {{ .Values.pgBackRest.repos.volume.storage }} - storageClassName: {{ .Values.pgBackRest.repos.volume.storageClassName }} - {{- if .Values.pgBackRest.s3.enabled }} - - name: repo2 - schedules: - full: {{ if .Values.pgBackRest.s3.fullSchedule }}{{ .Values.pgBackRest.s3.fullSchedule }}{{ else }}{{ .Values.pgBackRest.repos.schedules.full }}{{ end }} - incremental: {{ if .Values.pgBackRest.s3.incrementalSchedule }}{{ .Values.pgBackRest.s3.incrementalSchedule }}{{ else }}{{ .Values.pgBackRest.repos.schedules.incremental }}{{ end }} - s3: - bucket: {{ .Values.pgBackRest.s3.bucket }} - endpoint: {{ .Values.pgBackRest.s3.endpoint }} - region: {{ .Values.pgBackRest.s3.region }} - {{- end }} - # this stuff is for the "pgbackrest" container (the only non-init container) in the "postgres-crunchy-repo-host" pod - repoHost: - resources: - requests: - cpu: {{ .Values.pgBackRest.repoHost.requests.cpu }} - memory: {{ .Values.pgBackRest.repoHost.requests.memory }} - limits: - cpu: {{ .Values.pgBackRest.repoHost.limits.cpu }} - memory: {{ .Values.pgBackRest.repoHost.limits.memory }} - sidecars: - # this stuff is for the "pgbackrest" container in the "postgres-crunchy-ha" set of pods - pgbackrest: - resources: - requests: - cpu: {{ .Values.pgBackRest.sidecars.requests.cpu }} - memory: {{ .Values.pgBackRest.sidecars.requests.memory }} - limits: - cpu: {{ .Values.pgBackRest.sidecars.limits.cpu }} - memory: {{ .Values.pgBackRest.sidecars.limits.memory }} - pgbackrestConfig: - resources: - requests: - cpu: {{ .Values.pgBackRest.sidecars.requests.cpu }} - memory: {{ .Values.pgBackRest.sidecars.requests.memory }} - limits: - cpu: {{ .Values.pgBackRest.sidecars.limits.cpu }} - memory: {{ .Values.pgBackRest.sidecars.limits.memory }} - standby: - enabled: {{ .Values.standby.enabled }} - repoName: {{ .Values.standby.repoName }} - - patroni: - dynamicConfiguration: - postgresql: - pg_hba: - {{- range .Values.patroni.postgresql.pg_hba }} - - {{ . | quote }} - {{- end }} - parameters: - shared_buffers: {{ .Values.patroni.postgresql.parameters.shared_buffers }} - wal_buffers: {{ .Values.patroni.postgresql.parameters.wal_buffers }} - min_wal_size: {{ .Values.patroni.postgresql.parameters.min_wal_size }} - max_wal_size: {{ .Values.patroni.postgresql.parameters.max_wal_size }} - max_slot_wal_keep_size: {{ .Values.patroni.postgresql.parameters.max_slot_wal_keep_size }} - temp_file_limit: {{ .Values.patroni.postgresql.parameters.temp_file_limit }} - checkpoint_timeout: {{ .Values.patroni.postgresql.parameters.checkpoint_timeout }} - checkpoint_completion_target: {{ .Values.patroni.postgresql.parameters.checkpoint_completion_target }} - - proxy: - pgBouncer: - config: - global: - client_tls_sslmode: disable - {{ if .Values.proxy.pgBouncer.image }} - image: {{ .Values.proxy.pgBouncer.image }} - {{ end }} - replicas: {{ .Values.proxy.pgBouncer.replicas }} - # these resources are for the "pgbouncer" container in the "postgres-crunchy-ha-pgbouncer" set of pods - # there is a sidecar in these pods which are not mentioned here, but the requests/limits are teeny weeny by default so no worries there. - resources: - requests: - cpu: {{ .Values.proxy.pgBouncer.requests.cpu }} - memory: {{ .Values.proxy.pgBouncer.requests.memory }} - limits: - cpu: {{ .Values.proxy.pgBouncer.limits.cpu }} - memory: {{ .Values.proxy.pgBouncer.limits.memory }} - affinity: - podAntiAffinity: - preferredDuringSchedulingIgnoredDuringExecution: - - weight: 1 - podAffinityTerm: - topologyKey: topology.kubernetes.io/zone - labelSelector: - matchLabels: - postgres-operator.crunchydata.com/cluster: - {{ .Values.instances.name }} - postgres-operator.crunchydata.com/role: pgbouncer diff --git a/database/crunchy-postgres/templates/_helpers.tpl b/database/crunchy-postgres/templates/_helpers.tpl deleted file mode 100644 index 1a758b08e..000000000 --- a/database/crunchy-postgres/templates/_helpers.tpl +++ /dev/null @@ -1,66 +0,0 @@ -{{/* -Expand the name of the chart. -*/}} -{{- define "crunchy-postgres.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -If release name contains chart name it will be used as a full name. -*/}} -{{- define "crunchy-postgres.fullname" -}} -{{- if .Values.fullnameOverride }} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- $name := default .Chart.Name .Values.nameOverride }} -{{- if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} -{{- end }} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "crunchy-postgres.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Common labels -*/}} -{{- define "crunchy-postgres.labels" -}} -helm.sh/chart: {{ include "crunchy-postgres.chart" . }} -{{ include "crunchy-postgres.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- range $key, $value := .Values.labels }} -{{ $key }}: {{ $value | quote }} -{{- end }} -app.kubernetes.io/component: "database" -{{- end }} - -{{/* -Selector labels -*/}} -{{- define "crunchy-postgres.selectorLabels" -}} -app.kubernetes.io/name: {{ include "crunchy-postgres.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - -{{/* -Create the name of the service account to use -*/}} -{{- define "crunchy-postgres.serviceAccountName" -}} -{{- if .Values.serviceAccount.create }} -{{- default (include "crunchy-postgres.fullname" .) .Values.serviceAccount.name }} -{{- else }} -{{- default "default" .Values.serviceAccount.name }} -{{- end }} -{{- end }} diff --git a/database/crunchy-postgres/templates/_s3.tpl b/database/crunchy-postgres/templates/_s3.tpl deleted file mode 100644 index 9f71811f7..000000000 --- a/database/crunchy-postgres/templates/_s3.tpl +++ /dev/null @@ -1,18 +0,0 @@ -{{/* Allow for S3 secret information to be stored in a Secret */}} -{{- define "postgres.s3" }} -[global] -{{- if .s3 }} - {{- if .s3.key }} -repo{{ add .index 1 }}-s3-key={{ .s3.key }} - {{- end }} - {{- if .s3.keySecret }} -repo{{ add .index 1 }}-s3-key-secret={{ .s3.keySecret }} - {{- end }} - {{- if .s3.keyType }} -repo{{ add .index 1 }}-s3-key-type={{ .s3.keyType }} - {{- end }} - {{- if .s3.encryptionPassphrase }} -repo{{ add .index 1 }}-cipher-pass={{ .s3.encryptionPassphrase }} - {{- end }} -{{- end }} -{{ end }} \ No newline at end of file diff --git a/database/crunchy-postgres/templates/data-restore-configmap.yaml b/database/crunchy-postgres/templates/data-restore-configmap.yaml deleted file mode 100644 index d60ad8ea6..000000000 --- a/database/crunchy-postgres/templates/data-restore-configmap.yaml +++ /dev/null @@ -1,35 +0,0 @@ -{{- if .Values.dataRestore.enabled }} -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ include "crunchy-postgres.fullname" . }}-data-restore-config - labels: - helm.sh/chart: {{ include "crunchy-postgres.chart" . }} - app.kubernetes.io/name: {{ include "crunchy-postgres.name" . }} - app.kubernetes.io/instance: {{ include "crunchy-postgres.fullname" . }} - {{- if .Chart.AppVersion }} - app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} - {{- end }} - app.kubernetes.io/managed-by: {{ .Release.Service }} - {{- range $key, $value := .Values.labels }} - {{ $key }}: {{ $value | quote }} - {{- end }} - app.kubernetes.io/component: "data-restore-config" -data: - pgbackrest.conf: | - [global] - repo{{ .Values.dataRestore.repo.name | replace "repo" "" }}-type=s3 - repo{{ .Values.dataRestore.repo.name | replace "repo" "" }}-s3-bucket={{ .Values.dataRestore.repo.bucket }} - repo{{ .Values.dataRestore.repo.name | replace "repo" "" }}-s3-endpoint={{ .Values.dataRestore.repo.endpoint }} - repo{{ .Values.dataRestore.repo.name | replace "repo" "" }}-s3-region={{ .Values.dataRestore.repo.s3.region | default "not-used" }} - repo{{ .Values.dataRestore.repo.name | replace "repo" "" }}-path={{ .Values.dataRestore.repo.path }} - repo{{ .Values.dataRestore.repo.name | replace "repo" "" }}-s3-uri-style={{ .Values.dataRestore.repo.s3.uriStyle | default "path" }} - log-level-console=info - log-level-file=debug - - [{{ .Values.dataRestore.stanza }}] - pg1-host={{ if .Values.dataRestore.target.clusterName }}{{ .Values.dataRestore.target.clusterName }}{{ else }}{{ include "crunchy-postgres.fullname" . }}{{ end }}-primary.{{ .Release.Namespace }}.svc.cluster.local - pg1-port=5432 - pg1-user=postgres - pg1-database={{ .Values.dataRestore.target.database }} -{{- end }} diff --git a/database/crunchy-postgres/templates/data-restore-cronjob.yaml b/database/crunchy-postgres/templates/data-restore-cronjob.yaml deleted file mode 100644 index b22a6b2fa..000000000 --- a/database/crunchy-postgres/templates/data-restore-cronjob.yaml +++ /dev/null @@ -1,190 +0,0 @@ -{{- if .Values.dataRestore.enabled }} -apiVersion: batch/v1 -kind: CronJob -metadata: - name: {{ include "crunchy-postgres.fullname" . }}-data-restore - annotations: - app.openshift.io/connects-to: {{ include "crunchy-postgres.fullname" . }} - app.openshift.io/vcs-ref: main - app.openshift.io/runtime-namespace: {{ .Release.Namespace }} - app.openshift.io/runtime: postgresql - labels: - helm.sh/chart: {{ include "crunchy-postgres.chart" . }} - app.kubernetes.io/name: {{ include "crunchy-postgres.name" . }} - app.kubernetes.io/instance: {{ include "crunchy-postgres.fullname" . }} - {{- if .Chart.AppVersion }} - app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} - {{- end }} - app.kubernetes.io/managed-by: {{ .Release.Service }} - {{- range $key, $value := .Values.labels }} - {{ $key }}: {{ $value | quote }} - {{- end }} - app.kubernetes.io/component: "database" -spec: - schedule: {{ .Values.dataRestore.schedule | quote }} - successfulJobsHistoryLimit: {{ .Values.dataRestore.successfulJobsHistoryLimit }} - failedJobsHistoryLimit: {{ .Values.dataRestore.failedJobsHistoryLimit }} - jobTemplate: - metadata: - labels: - helm.sh/chart: {{ include "crunchy-postgres.chart" . }} - app.kubernetes.io/name: {{ include "crunchy-postgres.name" . }} - app.kubernetes.io/instance: {{ include "crunchy-postgres.fullname" . }} - {{- if .Chart.AppVersion }} - app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} - {{- end }} - app.kubernetes.io/managed-by: {{ .Release.Service }} - {{- range $key, $value := .Values.labels }} - {{ $key }}: {{ $value | quote }} - {{- end }} - app.kubernetes.io/component: "database" - spec: - template: - metadata: - labels: - helm.sh/chart: {{ include "crunchy-postgres.chart" . }} - app.kubernetes.io/name: {{ include "crunchy-postgres.name" . }} - app.kubernetes.io/instance: {{ include "crunchy-postgres.fullname" . }} - {{- if .Chart.AppVersion }} - app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} - {{- end }} - app.kubernetes.io/managed-by: {{ .Release.Service }} - {{- range $key, $value := .Values.labels }} - {{ $key }}: {{ $value | quote }} - {{- end }} - app.kubernetes.io/component: "database" - spec: - restartPolicy: {{ .Values.dataRestore.restartPolicy }} - containers: - - name: pgbackrest-restore - image: {{ .Values.dataRestore.image }} - command: ["/bin/bash"] - args: - - "-c" - - | - set -e - echo "=== Change Data Capture with S3 Restore Started ===" - echo "Timestamp: $(date)" - echo "Namespace: $NAMESPACE" - echo "Pod: $PODNAME" - - # Set connection parameters - LOCAL_DB_HOST="$PGBACKREST_DB_HOST" - LOCAL_DB_PORT="$PGBACKREST_DB_PORT" - - echo "Target Database: $LOCAL_DB_HOST:$LOCAL_DB_PORT" - echo "S3 Bucket: {{ .Values.dataRestore.repo.bucket }}" - echo "S3 Path: {{ .Values.dataRestore.repo.path }}" - echo "Stanza: $PGBACKREST_STANZA" - echo "Repo: $PGBACKREST_REPO" - - # Merge configuration files to create a complete pgbackrest.conf - echo "=== Setting up pgBackRest Configuration ===" - echo "Creating merged configuration file..." - cat /etc/pgbackrest/pgbackrest.conf > /tmp/pgbackrest.conf - echo "" >> /tmp/pgbackrest.conf - echo "# S3 Credentials from secret" >> /tmp/pgbackrest.conf - cat /etc/pgbackrest/s3.conf >> /tmp/pgbackrest.conf - echo "Configuration created successfully" - - # Set the environment variable to use our merged config - export PGBACKREST_CONFIG=/tmp/pgbackrest.conf - - # Step 1: Query S3 for latest backup info (using pgbackrest info) - echo "=== Step 1: Checking S3 Backup Information ===" - echo "Querying S3 for latest backup..." - - # Use pgbackrest info to check what's available in S3 - - echo "Available backups in S3:" - PGBACKREST_INFO_OUTPUT=$(pgbackrest info --stanza="$PGBACKREST_STANZA" --repo="$PGBACKREST_REPO" --log-level-console=info 2>&1) - echo "$PGBACKREST_INFO_OUTPUT" - - if echo "$PGBACKREST_INFO_OUTPUT" | grep -q "status: error"; then - echo "ERROR: pgBackRest reported an error status. Check S3 credentials and permissions." - exit 1 - fi - - if echo "$PGBACKREST_INFO_OUTPUT" | grep -q "SignatureDoesNotMatch"; then - echo "ERROR: S3 authentication failed (SignatureDoesNotMatch). Check your Secret Access Key." - exit 1 - fi - - echo "✓ S3 backup information retrieved" - - # Step 2: Implement change data capture logic - echo "=== Step 2: Change Data Capture Operations ===" - echo "Note: Full restore cannot be performed on a running cluster" - echo "Implementing incremental sync approach instead..." - - # Wait for database to be ready - echo "Checking database connectivity..." - for i in {1..10}; do - if pg_isready -h "$LOCAL_DB_HOST" -p "$LOCAL_DB_PORT" 2>/dev/null; then - echo "✓ Database is ready" - break - fi - echo "Waiting for database... ($i/10)" - sleep 5 - done - - # Simulate CDC operations that would use the S3 backup data - echo "CDC Operations would:" - echo "1. Compare current database state with latest S3 backup" - echo "2. Identify data differences and changes" - echo "3. Apply incremental updates to maintain consistency" - echo "4. Update tracking tables with sync status" - - # Update last sync timestamp - CURRENT_TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S') - echo "=== Restore and CDC Completed Successfully ===" - echo "Completion timestamp: $CURRENT_TIMESTAMP" - echo "=== Change Data Capture with S3 Restore Completed ===" - env: - - name: NAMESPACE - value: {{ .Release.Namespace | quote }} - - name: PODNAME - valueFrom: - fieldRef: - fieldPath: metadata.name - - name: PGBACKREST_STANZA - value: {{ .Values.dataRestore.stanza | quote }} - - name: PGBACKREST_REPO - value: {{ .Values.dataRestore.repo.name | replace "repo" "" | quote }} - - name: PGBACKREST_DB_HOST - value: {{ if .Values.dataRestore.target.clusterName }}{{ .Values.dataRestore.target.clusterName }}{{ else }}{{ include "crunchy-postgres.fullname" . }}{{ end }}-primary.{{ .Release.Namespace }}.svc.cluster.local - - name: PGBACKREST_DB_PORT - value: "5432" - - name: PGUSER - value: "postgres" - - name: PGDATABASE - value: "postgres" - - name: CDC_JOB_NAME - value: {{ include "crunchy-postgres.fullname" . }}-data-restore - - name: CDC_SCHEDULE - value: {{ .Values.dataRestore.schedule | quote }} - resources: - requests: - cpu: {{ .Values.dataRestore.resources.requests.cpu }} - memory: {{ .Values.dataRestore.resources.requests.memory }} - limits: - cpu: {{ .Values.dataRestore.resources.limits.cpu }} - memory: {{ .Values.dataRestore.resources.limits.memory }} - volumeMounts: - - name: pgbackrest-config - mountPath: /etc/pgbackrest - readOnly: true - - name: tmp - mountPath: /tmp - volumes: - - name: pgbackrest-config - projected: - sources: - - secret: - name: {{ .Values.dataRestore.secretName }} - - configMap: - name: {{ include "crunchy-postgres.fullname" . }}-data-restore-config - optional: true - - name: tmp - emptyDir: {} -{{- end }} diff --git a/database/crunchy-postgres/templates/data-restore-secret.yaml b/database/crunchy-postgres/templates/data-restore-secret.yaml deleted file mode 100644 index e2e2c0803..000000000 --- a/database/crunchy-postgres/templates/data-restore-secret.yaml +++ /dev/null @@ -1,16 +0,0 @@ -{{- if and .Values.dataRestore.enabled .Values.dataRestore.createS3Secret }} -apiVersion: v1 -kind: Secret -metadata: - name: {{ .Values.dataRestore.secretName | default "dev-s3-restore" }} - namespace: {{ .Release.Namespace }} - labels: - {{- include "crunchy-postgres.labels" . | nindent 4 }} -type: Opaque -stringData: - # Same format as pgBackRest secret - using s3.conf key name to match - s3.conf: | - [global] - repo2-s3-key={{ .Values.dataRestore.s3.key }} - repo2-s3-key-secret={{ .Values.dataRestore.s3.keySecret }} -{{- end }} diff --git a/database/crunchy-postgres/templates/s3Secret.yaml b/database/crunchy-postgres/templates/s3Secret.yaml deleted file mode 100644 index 5c1aef224..000000000 --- a/database/crunchy-postgres/templates/s3Secret.yaml +++ /dev/null @@ -1,11 +0,0 @@ -{{- if and .Values.pgBackRest.s3.enabled .Values.pgBackRest.s3.createS3Secret }} -apiVersion: v1 -kind: Secret -metadata: - name: {{ .Values.pgBackRest.s3.s3Secret }} -type: Opaque -data: - {{- $args := dict "s3" .Values.pgBackRest.s3 "index" 1 }} - s3.conf: |- - {{ include "postgres.s3" $args | b64enc }} -{{- end }} \ No newline at end of file diff --git a/database/crunchy-postgres/values.yaml b/database/crunchy-postgres/values.yaml deleted file mode 100644 index 78ccf662c..000000000 --- a/database/crunchy-postgres/values.yaml +++ /dev/null @@ -1,196 +0,0 @@ -fullnameOverride: crunchy-postgres - -# Set this to true for OpenShift deployments to avoid incompatible securityContext values -openshift: true - -labels: - app.kubernetes.io/part-of: crunchydb-postgres - -crunchyImage: # it's not necessary to specify an image as the images specified in the Crunchy Postgres Operator will be pulled by default -#crunchyImage: artifacts.developer.gov.bc.ca/bcgov-docker-local/crunchy-postgres-gis:ubi8-15.2-3.3-0 # use this image for POSTGIS -postgresVersion: 16 -#postGISVersion: '3.3' # use this version of POSTGIS. both crunchyImage and this property needs to have valid values for POSTGIS to be enabled. -imagePullPolicy: IfNotPresent - -# enable to bootstrap a standby cluster from backup. Then disable to promote this standby to primary -standby: - enabled: false - # If you want to recover from PVC, use repo1. If you want to recover from S3, use repo2 - repoName: repo1 - -instances: - name: ha # high availability - replicas: 2 - dataVolumeClaimSpec: - storage: 512Mi - storageClassName: netapp-block-standard - requests: - cpu: 10m - memory: 256Mi - replicaCertCopy: - requests: - cpu: 1m - memory: 32Mi - limits: - cpu: 50m - memory: 64Mi - -# If we need to restore the cluster from a backup, we need to set the following values -# assuming restore from repo2 (s3), adjust as needed if your S3 repo is different -dataSource: - enabled: false - # should have the same name and contain the same keys as the pgbackrest secret - secretName: s3-pgbackrest - repo: - name: repo2 - path: "/habackup" - s3: - bucket: "bucketName" - endpoint: "https://sector.objectstore.gov.bc.ca" - region: "not-used" - uriStyle: "path" - stanza: db - -pgBackRest: - image: # it's not necessary to specify an image as the images specified in the Crunchy Postgres Operator will be pulled by default - # If retention-full-type set to 'count' then the oldest backups will expire when the number of backups reach the number defined in retention - # If retention-full-type set to 'time' then the number defined in retention will take that many days worth of full backups before expiration - retention: "2" # Ideally a number to keep backups for 2 working days - retentionS3: "30" # Ideally a larger number such as backups for 30 days - retentionFullType: count # Type of retention for full backups - retentionFullTypeS3: time # Type of retention for full backups - retentionArchive: "2" # Number of backups worth of continuous WAL to retain - retentionArchiveType: full # Type of retention for WAL archives - repos: - schedules: - full: 0 6 * * 0 # Full backup every Sunday at 10:00 PM PST - incremental: 15 */8 * * * # Incremental every 8 hours - volume: - accessModes: "ReadWriteOnce" - storage: 256Mi - storageClassName: netapp-file-backup - repoHost: - requests: - cpu: 1m - memory: 64Mi - limits: - cpu: 50m - memory: 128Mi - sidecars: - requests: - cpu: 1m - memory: 64Mi - limits: - cpu: 50m - memory: 128Mi - s3: - enabled: true - createS3Secret: true - # the s3 secret name - s3Secret: s3-pgbackrest - # the path start with /, it will be created under bucket if it doesn't exist - s3Path: "/habackup" - # s3UriStyle is host or path - s3UriStyle: path - # bucket specifies the S3 bucket to use, - bucket: "bucketName" - # endpoint specifies the S3 endpoint to use. - endpoint: "https://sector.objectstore.gov.bc.ca" - # region specifies the S3 region to use. If your S3 storage system does not - # use "region", fill this in with a random value. - region: "not-used" - # key is the S3 key. This is stored in a Secret. - # Please DO NOT push this value to GitHub - key: "s3keyValue" - # keySecret is the S3 key secret. This is stored in a Secret. - # Please DO NOT push this value to GitHub - keySecret: "s3SecretValue" - # set the default schedule to avoid conflicts - fullSchedule: 30 5 * * 0 # Full backup every Monday at 9:30 PM PST - incrementalSchedule: 45 */8 * * * # Incremental every 8 hours - -patroni: - postgresql: - pg_hba: - - "local all postgres trust" # trust local system socket connections user postgres - - "host all all 127.0.0.1/32 trust" # trust IPv4 local connections includes port forwarding - - "host all all ::1/128 trust" # trust IPv6 local connections includes port forwarding - - "host all all 10.0.0.0/8 md5" # Allow any users to connect to any database from 10.x.x.x private subnet range if password is correctly supplied - parameters: - shared_buffers: 256MB # default is 128MB; a good tuned default for shared_buffers is 25% of the memory allocated to the pod - wal_buffers: "-1" # this can be set to -1 to automatically set as 1/32 of shared_buffers or 64kB, whichever is larger - min_wal_size: 64MB # Sets the minimum size to shrink the WAL files to - max_wal_size: 256MB # default is 1GB make sure the mounted volume is large enough for the logging - max_slot_wal_keep_size: 256MB # default is -1, allowing unlimited wal growth when replicas fall behind - temp_file_limit: 512MB # Prevent temp files from filling PVC - checkpoint_timeout: 15min # Reduce checkpoint frequency - checkpoint_completion_target: 0.9 # Smooth checkpointing - -proxy: - pgBouncer: - image: # it's not necessary to specify an image as the images specified in the Crunchy Postgres Operator will be pulled by default - replicas: 2 - requests: - cpu: 1m - memory: 64Mi - limits: - cpu: 50m - memory: 128Mi - -# Postgres Cluster resource values: -pgmonitor: - enabled: false - exporter: - image: # it's not necessary to specify an image as the images specified in the Crunchy Postgres Operator will be pulled by default - requests: - cpu: 1m - memory: 64Mi - limits: - cpu: 50m - memory: 128Mi - -# Data restore cronjob configuration - reuses dataSource and pgBackRest.s3 patterns -dataRestore: - enabled: false - createS3Secret: true - schedule: "0 2 * * *" # Run every day at 2 AM - image: "artifacts.developer.gov.bc.ca/bcgov-docker-local/crunchy-pgbackrest:ubi8-2.53.1-0" - secretName: s3-pgbackrest - repo: - name: repo2 - path: "/habackup" - bucket: "bucketName" - endpoint: "https://sector.objectstore.gov.bc.ca" - region: "not-used" - uriStyle: "path" - stanza: db - # S3 credentials for data restore (only used if createS3Secret: true) - s3: - # key is the S3 key. This is stored in a Secret. - # Please DO NOT push this value to GitHub - key: "s3keyValue" - # keySecret is the S3 key secret. This is stored in a Secret. - # Please DO NOT push this value to GitHub - keySecret: "s3SecretValue" - # Target database configuration - target: - # The PostgreSQL cluster name to restore into (defaults to current cluster if empty) - clusterName: "" - # Database name to restore - database: "postgres" - # Resource limits for the cronjob - resources: - requests: - cpu: 100m - memory: 256Mi - limits: - cpu: 500m - memory: 512Mi - # Job settings - successfulJobsHistoryLimit: 3 - failedJobsHistoryLimit: 1 - restartPolicy: OnFailure - # Additional pgbackrest arguments - additionalArgs: [] - # - "--log-level-console=debug" - # - "--process-max=2" diff --git a/database/scripts/metabase-setup-database-readonly.sql b/database/scripts/metabase-setup-database-readonly.sql deleted file mode 100644 index daf367a1b..000000000 --- a/database/scripts/metabase-setup-database-readonly.sql +++ /dev/null @@ -1,98 +0,0 @@ --- This script ensures that the metabase_readonly role has the necessary CONNECT, USAGE, and SELECT privileges --- on the database, schemas, tables, and sequences. It also sets default privileges for any new tables and sequences --- created in the specified schemas. This should allow the metabase_readonly role to access the schemas and tables --- with read-only permissions -DO $$ -DECLARE - db_name TEXT; - schema TEXT; - schema_list TEXT[] := ARRAY['public', 'Flex', 'Notifications', 'Payments', 'Reporting']; - existing_schemas TEXT := ''; -BEGIN - -- Get the name of the current database - SELECT current_database() INTO db_name; - - -- Grant CONNECT privilege on the database - EXECUTE format('GRANT CONNECT ON DATABASE %I TO metabase_readonly;', db_name); - RAISE NOTICE 'Granted CONNECT on database % to role metabase_readonly', db_name; - - -- List schemas in the current database - RAISE NOTICE 'Listing schemas in the current database %:', db_name; - FOREACH schema IN ARRAY schema_list LOOP - IF EXISTS (SELECT 1 FROM information_schema.schemata s WHERE s.schema_name = schema) THEN - existing_schemas := existing_schemas || schema || ', '; - END IF; - END LOOP; - - -- Remove the trailing comma and space - IF existing_schemas <> '' THEN - existing_schemas := substring(existing_schemas FROM 1 FOR length(existing_schemas) - 2); - END IF; - - RAISE NOTICE 'Schemas in the current database %: %', db_name, existing_schemas; - - -- Grant schema usage and set default privileges for metabase_readonly - FOREACH schema IN ARRAY schema_list LOOP - IF EXISTS (SELECT 1 FROM information_schema.schemata s WHERE s.schema_name = schema) THEN - EXECUTE format('GRANT USAGE ON SCHEMA %I TO metabase_readonly;', schema); - RAISE NOTICE 'Granted USAGE on schema % to role metabase_readonly', schema; - - -- Grant SELECT on all existing tables in the schema - EXECUTE format('GRANT SELECT ON ALL TABLES IN SCHEMA %I TO metabase_readonly;', schema); - RAISE NOTICE 'Granted SELECT on all tables in schema % to role metabase_readonly', schema; - - -- Grant USAGE and SELECT on all sequences in the schema - EXECUTE format('GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA %I TO metabase_readonly;', schema); - RAISE NOTICE 'Granted USAGE and SELECT on all sequences in schema % to role metabase_readonly', schema; - - -- Set default privileges for metabase_readonly - EXECUTE format('ALTER DEFAULT PRIVILEGES IN SCHEMA %I GRANT SELECT ON TABLES TO metabase_readonly;', schema); - EXECUTE format('ALTER DEFAULT PRIVILEGES IN SCHEMA %I GRANT USAGE, SELECT ON SEQUENCES TO metabase_readonly;', schema); - RAISE NOTICE 'Set default privileges for role metabase_readonly in schema %', schema; - ELSE - RAISE NOTICE 'Schema % does not exist in the current database', schema; - END IF; - END LOOP; -END $$; - --- Combined Query to List Schema Privileges and Default Privileges for All Schemas, Sorted by Schema Name -WITH schema_privileges AS ( - SELECT - 'SCHEMA' AS object_type, - nspname AS schema, - pg_catalog.pg_get_userbyid(nspowner) AS owner, - array_agg(acl) AS privileges - FROM - pg_namespace - LEFT JOIN - pg_roles ON pg_roles.oid = pg_namespace.nspowner - LEFT JOIN - unnest(nspacl) AS acl ON true - WHERE - nspname NOT LIKE 'pg_%' AND nspname <> 'information_schema' - GROUP BY - nspname, nspowner -), -default_privileges AS ( - SELECT - CASE defaclobjtype - WHEN 'r' THEN 'TABLE' - WHEN 'S' THEN 'SEQUENCE' - WHEN 'f' THEN 'FUNCTION' - WHEN 'T' THEN 'TYPE' - END AS object_type, - nspname AS schema, - pg_catalog.pg_get_userbyid(defaclrole) AS role, - defaclacl AS privileges - FROM - pg_default_acl - JOIN - pg_namespace ON pg_namespace.oid = pg_default_acl.defaclnamespace - WHERE - defaclobjtype IN ('r', 'S', 'f', 'T') - AND nspname NOT LIKE 'pg_%' AND nspname <> 'information_schema' -) -SELECT * FROM schema_privileges -UNION ALL -SELECT * FROM default_privileges -ORDER BY schema; \ No newline at end of file diff --git a/database/scripts/metabase-setup-database-readwrite.sql b/database/scripts/metabase-setup-database-readwrite.sql deleted file mode 100644 index b7156b55f..000000000 --- a/database/scripts/metabase-setup-database-readwrite.sql +++ /dev/null @@ -1,71 +0,0 @@ --- This script ensures that the metabase_readwrite role has the necessary CONNECT, USAGE, SELECT, INSERT, UPDATE, and DELETE privileges --- on the public schema. It also sets default privileges for any new tables and sequences created in the public schema. - -DO $$ -DECLARE - db_name TEXT := 'metabaseuploaddb'; - schema TEXT := 'public'; -BEGIN - -- Grant CONNECT and TEMPORARY on the database to metabase_readwrite - EXECUTE format('GRANT CONNECT, TEMPORARY ON DATABASE %I TO metabase_readwrite;', db_name); - RAISE NOTICE 'Granted CONNECT, TEMPORARY on database % to role metabase_readwrite', db_name; - - -- Grant USAGE and CREATE on the public schema to metabase_readwrite - EXECUTE format('GRANT USAGE, CREATE ON SCHEMA %I TO metabase_readwrite;', schema); - RAISE NOTICE 'Granted USAGE, CREATE on schema % to role metabase_readwrite', schema; - - -- Grant SELECT, INSERT, UPDATE, DELETE on all existing tables in the public schema - EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA %I TO metabase_readwrite;', schema); - RAISE NOTICE 'Granted SELECT, INSERT, UPDATE, DELETE on all tables in schema % to role metabase_readwrite', schema; - - -- Grant USAGE and SELECT, UPDATE on all sequences in the public schema - EXECUTE format('GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA %I TO metabase_readwrite;', schema); - RAISE NOTICE 'Granted USAGE, SELECT, UPDATE on all sequences in schema % to role metabase_readwrite', schema; - - -- Set default privileges for metabase_readwrite - EXECUTE format('ALTER DEFAULT PRIVILEGES IN SCHEMA %I GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO metabase_readwrite;', schema); - EXECUTE format('ALTER DEFAULT PRIVILEGES IN SCHEMA %I GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO metabase_readwrite;', schema); - RAISE NOTICE 'Set default privileges for role metabase_readwrite in schema %', schema; -END $$; - --- Combined Query to List Schema Privileges and Default Privileges for All Schemas, Sorted by Schema Name -WITH schema_privileges AS ( - SELECT - 'SCHEMA' AS object_type, - nspname AS schema, - pg_catalog.pg_get_userbyid(nspowner) AS owner, - array_agg(acl) AS privileges - FROM - pg_namespace - LEFT JOIN - pg_roles ON pg_roles.oid = pg_namespace.nspowner - LEFT JOIN - unnest(nspacl) AS acl ON true - WHERE - nspname NOT LIKE 'pg_%' AND nspname <> 'information_schema' - GROUP BY - nspname, nspowner -), -default_privileges AS ( - SELECT - CASE defaclobjtype - WHEN 'r' THEN 'TABLE' - WHEN 'S' THEN 'SEQUENCE' - WHEN 'f' THEN 'FUNCTION' - WHEN 'T' THEN 'TYPE' - END AS object_type, - nspname AS schema, - pg_catalog.pg_get_userbyid(defaclrole) AS role, - defaclacl AS privileges - FROM - pg_default_acl - JOIN - pg_namespace ON pg_namespace.oid = pg_default_acl.defaclnamespace - WHERE - defaclobjtype IN ('r', 'S', 'f', 'T') - AND nspname NOT LIKE 'pg_%' AND nspname <> 'information_schema' -) -SELECT * FROM schema_privileges -UNION ALL -SELECT * FROM default_privileges -ORDER BY schema; \ No newline at end of file diff --git a/database/scripts/metabase-setup-metabaseuploaddb.sql b/database/scripts/metabase-setup-metabaseuploaddb.sql deleted file mode 100644 index f1069148c..000000000 --- a/database/scripts/metabase-setup-metabaseuploaddb.sql +++ /dev/null @@ -1,76 +0,0 @@ --- This script sets up the metabaseuploaddb with the necessary privileges for the metabase_dbuser role. -DO $$ -DECLARE - db_name TEXT := 'metabaseuploaddb'; -BEGIN - -- Check if the database exists and print the appropriate message - IF NOT EXISTS (SELECT FROM pg_database WHERE datname = db_name) THEN - RAISE NOTICE 'Database does not exist. You need to create it manually: CREATE DATABASE %;', db_name; - ELSE - RAISE NOTICE 'Database "%" already exists.', db_name; - END IF; -END $$; - -DO $$ -DECLARE - db_name TEXT := 'metabaseuploaddb'; - schema TEXT := 'public'; -BEGIN - -- Grant ALL PRIVILEGES on the database to metabase_dbuser - EXECUTE format('GRANT ALL PRIVILEGES ON DATABASE %I TO metabase_dbuser;', db_name); - RAISE NOTICE 'Granted ALL PRIVILEGES on database % to role metabase_dbuser', db_name; - - -- Grant ALL on the public schema to metabase_dbuser - EXECUTE format('GRANT ALL ON SCHEMA %I TO metabase_dbuser;', schema); - RAISE NOTICE 'Granted ALL on schema % to role metabase_dbuser', schema; - - -- Alter the database owner to metabase_dbuser - EXECUTE format('ALTER DATABASE %I OWNER TO metabase_dbuser;', db_name); - RAISE NOTICE 'Changed owner of database % to metabase_dbuser', db_name; - - -- Grant USAGE and CREATE on the public schema to metabase_dbuser - EXECUTE format('GRANT USAGE, CREATE ON SCHEMA %I TO metabase_dbuser;', schema); - RAISE NOTICE 'Granted USAGE, CREATE on schema % to role metabase_dbuser', schema; -END $$; - --- Combined Query to List Schema Privileges and Default Privileges for All Schemas, Sorted by Schema Name -WITH schema_privileges AS ( - SELECT - 'SCHEMA' AS object_type, - nspname AS schema, - pg_catalog.pg_get_userbyid(nspowner) AS owner, - array_agg(acl) AS privileges - FROM - pg_namespace - LEFT JOIN - pg_roles ON pg_roles.oid = pg_namespace.nspowner - LEFT JOIN - unnest(nspacl) AS acl ON true - WHERE - nspname NOT LIKE 'pg_%' AND nspname <> 'information_schema' - GROUP BY - nspname, nspowner -), -default_privileges AS ( - SELECT - CASE defaclobjtype - WHEN 'r' THEN 'TABLE' - WHEN 'S' THEN 'SEQUENCE' - WHEN 'f' THEN 'FUNCTION' - WHEN 'T' THEN 'TYPE' - END AS object_type, - nspname AS schema, - pg_catalog.pg_get_userbyid(defaclrole) AS role, - defaclacl AS privileges - FROM - pg_default_acl - JOIN - pg_namespace ON pg_namespace.oid = pg_default_acl.defaclnamespace - WHERE - defaclobjtype IN ('r', 'S', 'f', 'T') - AND nspname NOT LIKE 'pg_%' AND nspname <> 'information_schema' -) -SELECT * FROM schema_privileges -UNION ALL -SELECT * FROM default_privileges -ORDER BY schema; diff --git a/database/scripts/metabase-setup-readme.md b/database/scripts/metabase-setup-readme.md deleted file mode 100644 index d7ccfdbf5..000000000 --- a/database/scripts/metabase-setup-readme.md +++ /dev/null @@ -1,110 +0,0 @@ -## Metabase Read-Only Permissions in PostgreSQL - -The script applies **read-only permissions** to the schemas relevant to Metabase reporting (`public`, `Flex`, `Notifications`, `Payments`). It does the following: -1. **Checks if each schema exists** before granting permissions. -2. **Grants USAGE on schemas** to `metabase_readonly`. -3. **Sets default privileges** for `metabase_readonly`: - - **TABLES:** Grants `SELECT` - - **SEQUENCES:** Grants `USAGE, SELECT` -4. **Lists existing privileges** for schemas, tables, and sequences. - ---- - -## Database Setup Roles - -### 1. Running `metabase-setup-roles.sql` - -This script creates the necessary roles and users for Metabase with read-only and read/write permissions. - -#### Steps: -1. **Create Readonly and Read/Write Group Roles**: - - `metabase_readonly` - - `metabase_readwrite` -2. **Create Users and Assign Them to the Correct Roles**: - - `ugm_readonly` - - `ugt_readonly` - - `ugm_uploads` -3. **Cleanup Roles**: - - Drop unnecessary roles. -4. **Verify Role Assignments**: - - List all custom roles excluding default PostgreSQL roles. -5. **Verify Role Memberships**: - - List role memberships excluding default PostgreSQL roles. - -### 2. Applying `metabase_readonly` to All Tenant Databases - -After running `metabase-setup-roles.sql`, apply the `metabase_readonly` role to all tenant databases to ensure read-only access. - -#### Steps: -1. **Grant CONNECT privilege on the database**. -2. **Grant USAGE on schemas**. -3. **Grant SELECT on all existing tables in the schemas**. -4. **Grant USAGE and SELECT on all sequences in the schemas**. -5. **Set default privileges for `metabase_readonly`**: - - **TABLES:** Grants `SELECT` - - **SEQUENCES:** Grants `USAGE, SELECT` - -### 3. Running `metabase-setup-metabaseuploaddb.sql` - -This script sets up the `metabaseuploaddb` with the necessary privileges for the `metabase_dbuser` role. - -#### Steps: -1. **Grant ALL PRIVILEGES on the database to `metabase_dbuser`**. -2. **Grant ALL on the public schema to `metabase_dbuser`**. -3. **Alter the database owner to `metabase_dbuser`**. -4. **Grant USAGE and CREATE on the public schema to `metabase_dbuser`**. - -### 4. Applying `metabase_readwrite` Role - -After setting up the `metabaseuploaddb`, apply the `metabase_readwrite` role to ensure the necessary privileges. - -#### Steps: -1. **Grant CONNECT and TEMPORARY on the database to `metabase_readwrite`**. -2. **Grant USAGE and CREATE on the public schema to `metabase_readwrite`**. -3. **Grant SELECT, INSERT, UPDATE, DELETE on all existing tables in the public schema**. -4. **Grant USAGE and SELECT, UPDATE on all sequences in the public schema**. -5. **Set default privileges for `metabase_readwrite`**: - - **TABLES:** Grants `SELECT, INSERT, UPDATE, DELETE` - - **SEQUENCES:** Grants `USAGE, SELECT, UPDATE` - ---- - -### Explanation of Query Results -Each row in the output represents a privilege assignment for a specific schema and object type (`SCHEMA`, `TABLE`, `SEQUENCE`). - -#### **Key Terms** -- **`object_type`**: Type of object (SCHEMA, TABLE, SEQUENCE). -- **`schema`**: The schema name. -- **`owner`**: The user who owns the schema. -- **`privileges`**: The privileges assigned in `[role=permissions/owner]` format. - - `r` = SELECT (read) - - `U` = USAGE - - `C` = CREATE (for schemas) - - `rU` = SELECT + USAGE (for sequences) - - `UC` = USAGE + CREATE (for schemas) - -#### **Results Breakdown** -| Object Type | Schema | Owner | Privileges | -|-------------|--------------|-----------|------------| -| **TABLE** | `Flex` | `postgres` | `["metabase_readonly=r/postgres"]` → Read-only access on tables | -| **SCHEMA** | `Flex` | `postgres` | `["postgres=UC/postgres", "metabase_readonly=U/postgres"]` → `metabase_readonly` can use this schema, but not create objects. | -| **SEQUENCE**| `Flex` | `postgres` | `["metabase_readonly=rU/postgres"]` → Read and use sequences. | -| **SCHEMA** | `Notifications` | `postgres` | `["postgres=UC/postgres", "metabase_readonly=U/postgres"]` | -| **SEQUENCE**| `Notifications` | `postgres` | `["metabase_readonly=rU/postgres"]` | -| **TABLE** | `Notifications` | `postgres` | `["metabase_readonly=r/postgres"]` | -| **SCHEMA** | `Payments` | `postgres` | `["postgres=UC/postgres", "metabase_readonly=U/postgres"]` | -| **TABLE** | `Payments` | `postgres` | `["metabase_readonly=r/postgres"]` | -| **SEQUENCE**| `Payments` | `postgres` | `["metabase_readonly=rU/postgres"]` | -| **SCHEMA** | `public` | `pg_database_owner` | `["pg_database_owner=UC/pg_database_owner", "=U/pg_database_owner", "metabase_readonly=U/pg_database_owner"]` | -| **TABLE** | `public` | `postgres` | `["metabase_readonly=r/postgres"]` | -| **SEQUENCE**| `public` | `postgres` | `["metabase_readonly=rU/postgres"]` | - ---- - -### **Key Takeaways** -- `metabase_readonly` **has access to all specified schemas** (`USAGE` granted). -- `metabase_readonly` **can query tables (`SELECT`) but cannot modify them**. -- `metabase_readonly` **can use sequences (`USAGE, SELECT`) but cannot modify them**. -- **No `CREATE` privileges were granted**, ensuring Metabase remains read-only. - -This configuration ensures **secure, repeatable, and limited** readonly PostgreSQL access for Metabase reporting. diff --git a/database/scripts/metabase-setup-roles.sql b/database/scripts/metabase-setup-roles.sql deleted file mode 100644 index 3b0797bfa..000000000 --- a/database/scripts/metabase-setup-roles.sql +++ /dev/null @@ -1,70 +0,0 @@ --- 1. Create Readonly and Read/Write Group Roles -DO $$ -BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'metabase_readonly') THEN - CREATE ROLE metabase_readonly NOLOGIN; - END IF; - - IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'metabase_readwrite') THEN - CREATE ROLE metabase_readwrite NOLOGIN; - END IF; -END $$; - --- 2. Create Users and Assign Them to the Correct Roles -DO $$ -DECLARE - ugm_readonly_password TEXT := (SELECT string_agg(substring('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' FROM floor(random() * 62 + 1)::int FOR 1), '') FROM generate_series(1, 16)); - ugt_readonly_password TEXT := (SELECT string_agg(substring('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' FROM floor(random() * 62 + 1)::int FOR 1), '') FROM generate_series(1, 16)); - ugm_uploads_password TEXT := (SELECT string_agg(substring('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' FROM floor(random() * 62 + 1)::int FOR 1), '') FROM generate_series(1, 16)); -BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'ugm_readonly') THEN - EXECUTE format('CREATE ROLE ugm_readonly WITH LOGIN PASSWORD %L INHERIT', ugm_readonly_password); - GRANT metabase_readonly TO ugm_readonly; - RAISE NOTICE 'Role ugm_readonly created and assigned to metabase_readonly successfully. Password: %', ugm_readonly_password; - ELSE - RAISE NOTICE 'Role ugm_readonly already exists.'; - END IF; - - IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'ugt_readonly') THEN - EXECUTE format('CREATE ROLE ugt_readonly WITH LOGIN PASSWORD %L INHERIT', ugt_readonly_password); - GRANT metabase_readonly TO ugt_readonly; - RAISE NOTICE 'Role ugt_readonly created and assigned to metabase_readonly successfully. Password: %', ugt_readonly_password; - ELSE - RAISE NOTICE 'Role ugt_readonly already exists.'; - END IF; - - IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'ugm_uploads') THEN - EXECUTE format('CREATE ROLE ugm_uploads WITH LOGIN PASSWORD %L INHERIT', ugm_uploads_password); - GRANT metabase_readwrite TO ugm_uploads; - RAISE NOTICE 'Role ugm_uploads created and assigned to metabase_readwrite successfully. Password: %', ugm_uploads_password; - ELSE - RAISE NOTICE 'Role ugm_uploads already exists.'; - END IF; -END $$; - --- 3. Cleanup Roles -DO $$ -BEGIN - -- Role: metabase_grant_name - DROP ROLE IF EXISTS metabase_grant_name; - -- Role: grant_name - DROP ROLE IF EXISTS grant_name; - -- Role: pg_read_all_data - REVOKE pg_read_all_data FROM metabase_readonly; - REVOKE pg_write_all_data FROM metabase_readwrite; -END $$; - --- 4. Verify Role Assignments --- List all custom roles excluding default PostgreSQL roles -SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls -FROM pg_roles -WHERE rolname NOT LIKE 'pg_%'; - --- 5. Verify Role Memberships: --- List role memberships excluding default PostgreSQL roles -SELECT pg_roles.rolname AS role_name, member.rolname AS member_name -FROM pg_auth_members -JOIN pg_roles ON pg_roles.oid = pg_auth_members.roleid -JOIN pg_roles AS member ON member.oid = pg_auth_members.member -WHERE pg_roles.rolname NOT LIKE 'pg_%' -AND member.rolname NOT LIKE 'pg_%'; \ No newline at end of file diff --git a/database/unity-backup-cronjob.yaml b/database/unity-backup-cronjob.yaml deleted file mode 100644 index e0ed02caf..000000000 --- a/database/unity-backup-cronjob.yaml +++ /dev/null @@ -1,167 +0,0 @@ -apiVersion: template.openshift.io/v1 -kind: Template -message: |- - A scheduled cronjob has been created in your project: unity-backup. - For more information about using this template, including OpenShift considerations, - see template usage guide found in the project readme.md and wiki documents. -metadata: - name: unity-backup-cronjob - # This template uses a separate parameter .env file to override the default values defined in this section. - # oc process -f .\database\unity-backup-cronjob.yaml --param-file=backup-cronjob.env | oc create -f - - labels: - template: unity-backup-cronjob - annotations: - description: |- - Template for running a recurring backup script in OpenShift. - iconClass: icon-build - openshift.io/display-name: Database Backup Cronjob - template.openshift.io/long-description: |- - This template defines resources needed to run a Postgres-16 container application. - tags: database,postgresql -parameters: -# Project namespace parameters -- description: The name of the backup application. - displayName: Application Name - name: APPLICATION_NAME - required: true - value: unity-backup-cronjob -- description: The name of the application grouping. - displayName: Application Group - name: APPLICATION_GROUP - required: true - value: unity-grantmanager -# Additional parameters for project database provisioning. -- description: The name of the OpenShift Service exposed for the database. - displayName: Database Service Name - name: DATABASE_SERVICE_NAME - required: true - value: unity-data-postgres -- name: DATABASE_BACKUP_KEEP - description: 'Number of backups to keep' - value: '1' -- name: DATABASE_BACKUP_SCHEDULE - description: 'Cron-like schedule expression m h D M DayOfWeek add +7/8 hours for UTC conversions' - required: true - value: '0 14 * * *' -- name: DATABASE_BACKUP_VOLUME_CLAIM - description: 'Name of the volume claim to be used as storage' - required: true - value: unity-data-backup -- description: The name of the storage object. - displayName: Object Storage Name - name: STORAGE_OBJECT_NAME - required: true - value: s3-object-storage -- description: The Namespace where the container image resides default=project-tools cluster=openshift, source=registry.redhat.io/rhel9/postgresql-16 - displayName: Registry Namespace - name: IMAGEPULL_NAMESPACE - from: '[a-zA-Z0-9]{5}-tools' - generate: expression -- description: The Openshift ImageStream Name - displayName: Registry imagestream name - name: IMAGESTREAM_NAME - required: true - value: postgresql-16 -- description: The version of the postgresql container image to use. - displayName: Registry container image to pull - name: IMAGESTREAM_TAG - required: true - value: latest -- description: The registry path of the postgresql container image to use. - displayName: Registry container image to pull - name: IMAGEPULL_REGISTRY - required: true - value: image-registry.apps.silver.devops.gov.bc.ca -# Resource limits -- description: The minimum amount of CPU the container is guaranteed. - displayName: CPU Request - name: CPU_REQUEST - required: true - value: 50m -- description: The minimum amount of memory the container is guaranteed. - displayName: Memory Request - name: MEMORY_REQUEST - required: true - value: 64Mi -# Template objects to instantiate the project. -objects: -# Recurring cronjob for Database Backups -- apiVersion: batch/v1 - kind: CronJob - metadata: - name: ${APPLICATION_NAME} - labels: - job-name: ${APPLICATION_NAME} - app.kubernetes.io/component: ${APPLICATION_NAME} - app.kubernetes.io/instance: ${APPLICATION_NAME}-1 - app.kubernetes.io/name: ${APPLICATION_NAME} - app.kubernetes.io/part-of: ${APPLICATION_GROUP} - # Cronjob script works with both database or instance backup commands - # pg_dump --username=$UNITY_POSTGRES_USER --host=$UNITY_DB_HOST --port=$UNITY_DB_PORT --column-inserts --clean --create ${DATABASE_SERVICE_NAME} - # pg_dumpall --username=$UNITY_POSTGRES_USER --host=$UNITY_DB_HOST --port=$UNITY_DB_PORT --column-inserts --clean - spec: - schedule: ${DATABASE_BACKUP_SCHEDULE} - concurrencyPolicy: Forbid - successfulJobsHistoryLimit: 1 - failedJobsHistoryLimit: 1 - jobTemplate: - spec: - template: - spec: - volumes: - - name: ${APPLICATION_NAME} - persistentVolumeClaim: - claimName: ${DATABASE_BACKUP_VOLUME_CLAIM} - containers: - - name: ${APPLICATION_NAME} - image: ${IMAGEPULL_REGISTRY}/${IMAGEPULL_NAMESPACE}/${IMAGESTREAM_NAME}:${IMAGESTREAM_TAG} - command: - - 'bash' - - '-eo' - - 'pipefail' - - '-c' - - > - trap "echo Backup failed; exit 0" ERR; date; - FILENAME=dumpall-${DATABASE_SERVICE_NAME}-`date +%Y-%m-%d_%H%M%S`.sql.gz; - time (find /var/lib/pgsql/backups -type f -name "*-${DATABASE_SERVICE_NAME}-*" -exec ls -1tr "{}" + | head -n -$DATABASE_BACKUP_KEEP | xargs rm -fr; - PGPASSWORD="$UNITY_POSTGRES_PASSWORD" pg_dumpall --username=$UNITY_POSTGRES_USER --host=$UNITY_DB_HOST --port=$UNITY_DB_PORT --column-inserts --clean | gzip > /var/lib/pgsql/backups/$FILENAME); - echo "";echo "Backup successful";du -h /var/lib/pgsql/backups/$FILENAME; - echo "to restore the backup use: $ psql --username=$UNITY_POSTGRES_USER --password --host=$UNITY_DB_HOST --port=$UNITY_DB_PORT --username postgres < /var/lib/pgsql/backups/ (unpacked with gunzip)"; - echo "";/var/lib/pgsql/backups/bin/mc alias set $AccessKeyID $RestEndpoint $AccessKeyID $SecretKey;/var/lib/pgsql/backups/bin/mc mirror --remove --summary /var/lib/pgsql/backups $AccessKeyID/$BucketDisplayName/Unity/Backups; - echo "";ls -lR /var/lib/pgsql/backups - env: - - name: RestEndpoint - valueFrom: - configMapKeyRef: - name: ${STORAGE_OBJECT_NAME} - key: S3__Endpoint - - name: AccessKeyID - valueFrom: - secretKeyRef: - name: ${STORAGE_OBJECT_NAME} - key: S3__AccessKeyId - - name: BucketDisplayName - valueFrom: - secretKeyRef: - name: ${STORAGE_OBJECT_NAME} - key: S3__Bucket - - name: SecretKey - valueFrom: - secretKeyRef: - name: ${STORAGE_OBJECT_NAME} - key: S3__SecretAccessKey - - name: DATABASE_BACKUP_KEEP - value: ${DATABASE_BACKUP_KEEP} - - name: TZ - value: Canada/Pacific - envFrom: - - secretRef: - name: ${DATABASE_SERVICE_NAME} - volumeMounts: - - name: ${APPLICATION_NAME} - mountPath: /var/lib/pgsql/backups - resources: - requests: - cpu: ${CPU_REQUEST} - memory: ${MEMORY_REQUEST} - restartPolicy: Never diff --git a/database/unity-database.yaml b/database/unity-database.yaml deleted file mode 100644 index d7555a68e..000000000 --- a/database/unity-database.yaml +++ /dev/null @@ -1,239 +0,0 @@ -apiVersion: template.openshift.io/v1 -kind: Template -message: |- - A new application been created in your project: unity-database - For more information about using this template, including OpenShift considerations, - see template usage guide found in the project readme.md and wiki documents. -metadata: - name: unity-database - # This template uses a separate parameter .env file to override the default values defined in this section. - # oc process -f .\database\unity-database.yaml --param-file=unity-database.env | oc create -f - - labels: - template: unity-database - annotations: - description: |- - PostgreSQL database service with persistent storage. - NOTE: Scaling to more than one replica is not supported. - iconClass: icon-postgresql - openshift.io/display-name: PostgreSQL - openshift.io/long-description: This template provides a standalone PostgreSQL - server with an initial database created. The database is stored on persistent storage. - The database name, username, and password are selected through parameters during provisioning. - tags: database,postgresql -parameters: -# Project namespace parameters -- description: The name of the backup application. - displayName: Application Name - name: APPLICATION_NAME - required: false - value: unity-databaase -- description: The name of the application grouping. - displayName: Application Group - name: APPLICATION_GROUP - required: true - value: unity-tools -# Additional parameters for project database provisioning. -- description: The name of the OpenShift Service exposed for the database. - displayName: Database Service Name - name: DATABASE_SERVICE_NAME - required: true - value: unity-database -- description: The port exposed for the database. - displayName: Database Service Port - name: DATABASE_PORT - required: true - value: "5432" -- description: Username for PostgreSQL user that will be used for accessing the database. - displayName: PostgreSQL Connection Username - name: POSTGRESQL_USER - required: false - value: "postgres" -- description: Password for the PostgreSQL connection user. - displayName: PostgreSQL Connection Password - name: POSTGRESQL_PASSWORD - required: false - from: '[a-zA-Z0-9]{26}' - generate: expression -- description: Name of the PostgreSQL database accessed. - displayName: PostgreSQL Database Name - name: POSTGRESQL_DATABASE - required: true - value: postgres -- description: Volume space for data directory. - displayName: Volume Capacity - name: VOLUME_CAPACITY - required: true - value: 256Mi -- description: The Namespace where the container image resides default=project-tools cluster=openshift, source=registry.redhat.io/rhel9/postgresql-16 - displayName: Registry Namespace - name: IMAGEPULL_NAMESPACE - from: '[a-zA-Z0-9]{5}-tools' - generate: expression -- description: The Openshift ImageStream Name - displayName: Registry imagestream name - name: IMAGESTREAM_NAME - required: true - value: postgresql-16 -- description: The version of the postgresql container image to use. - displayName: Registry container image to pull - name: IMAGESTREAM_TAG - required: true - value: latest -- description: The registry path of the postgresql container image to use. - displayName: Registry container image to pull - name: IMAGEPULL_REGISTRY - required: true - value: image-registry.apps.silver.devops.gov.bc.ca -# Resource limits -- description: The minimum amount of CPU the container is guaranteed. - displayName: CPU Request - name: CPU_REQUEST - required: true - value: 50m -- description: The minimum amount of memory the container is guaranteed. - displayName: Memory Request - name: MEMORY_REQUEST - required: true - value: 64Mi -# Template objects to instantiate the project. -objects: -# Secrets -- apiVersion: v1 - kind: Secret - metadata: - annotations: - template.openshift.io/expose-database_name: '{.data[''POSTGRES_DATABASE'']}' - template.openshift.io/expose-password: '{.data[''POSTGRES_PASSWORD'']}' - template.openshift.io/expose-username: '{.data[''POSTGRES_USER'']}' - name: ${DATABASE_SERVICE_NAME} - labels: - app: ${DATABASE_SERVICE_NAME} - app.kubernetes.io/component: ${DATABASE_SERVICE_NAME} - app.kubernetes.io/instance: ${DATABASE_SERVICE_NAME}-1 - app.kubernetes.io/name: ${DATABASE_SERVICE_NAME} - app.kubernetes.io/part-of: ${APPLICATION_GROUP} - stringData: - POSTGRES_USER: ${POSTGRESQL_USER} - POSTGRES_DATABASE: ${POSTGRESQL_DATABASE} - POSTGRES_PASSWORD: ${POSTGRESQL_PASSWORD} - type: Opaque -# Service -- apiVersion: v1 - kind: Service - metadata: - annotations: - template.openshift.io/expose-uri: postgres://{.spec.clusterIP}:{.spec.ports[?(.name=="postgresql")].port} - name: ${DATABASE_SERVICE_NAME} - labels: - app: ${DATABASE_SERVICE_NAME} - app.kubernetes.io/component: ${DATABASE_SERVICE_NAME} - app.kubernetes.io/instance: ${DATABASE_SERVICE_NAME}-1 - app.kubernetes.io/name: ${DATABASE_SERVICE_NAME} - app.kubernetes.io/part-of: ${APPLICATION_GROUP} - spec: - ports: - - name: ${DATABASE_SERVICE_NAME} - nodePort: 0 - protocol: TCP - port: ${{DATABASE_PORT}} - targetPort: ${{DATABASE_PORT}} - selector: - app: ${DATABASE_SERVICE_NAME} - sessionAffinity: None - type: ClusterIP - status: - loadBalancer: {} -# Persistent storage for database backups -- apiVersion: v1 - kind: PersistentVolumeClaim - metadata: - name: unity-data-backup - labels: - app: ${DATABASE_SERVICE_NAME} - app.kubernetes.io/component: ${DATABASE_SERVICE_NAME} - app.kubernetes.io/instance: ${DATABASE_SERVICE_NAME}-1 - app.kubernetes.io/name: ${DATABASE_SERVICE_NAME} - app.kubernetes.io/part-of: ${APPLICATION_GROUP} - spec: - accessModes: - - ReadWriteMany - resources: - requests: - storage: ${VOLUME_CAPACITY} - storageClassName: netapp-file-backup - volumeMode: Filesystem -# Deployment -- apiVersion: apps/v1 - kind: Deployment - metadata: - annotations: - template.alpha.openshift.io/wait-for-ready: "true" - # Add the trigger annotation - image.openshift.io/triggers: >- - [{"from":{"kind":"ImageStreamTag","name":"${IMAGESTREAM_NAME}:${IMAGESTREAM_TAG}","namespace":"${IMAGEPULL_NAMESPACE}"},"fieldPath":"spec.template.spec.containers[?(@.name==\"${{DATABASE_SERVICE_NAME}\")].image","pause":"false"}] - name: ${DATABASE_SERVICE_NAME} - labels: - app: ${DATABASE_SERVICE_NAME} - app.kubernetes.io/component: ${DATABASE_SERVICE_NAME} - app.kubernetes.io/instance: ${DATABASE_SERVICE_NAME}-1 - app.kubernetes.io/name: ${DATABASE_SERVICE_NAME} - app.kubernetes.io/part-of: ${APPLICATION_GROUP} - spec: - replicas: 1 - selector: - matchLabels: - app: ${DATABASE_SERVICE_NAME} - template: - metadata: - labels: - app: ${DATABASE_SERVICE_NAME} - app.kubernetes.io/component: ${DATABASE_SERVICE_NAME} - app.kubernetes.io/instance: ${DATABASE_SERVICE_NAME}-1 - app.kubernetes.io/name: ${DATABASE_SERVICE_NAME} - app.kubernetes.io/part-of: ${APPLICATION_GROUP} - spec: - containers: - - name: ${DATABASE_SERVICE_NAME} - image: ${IMAGEPULL_REGISTRY}/${IMAGEPULL_NAMESPACE}/${IMAGESTREAM_NAME}:${IMAGESTREAM_TAG} - ports: - - containerPort: ${{DATABASE_PORT}} - protocol: TCP - env: - - name: POSTGRESQL_ADMIN_PASSWORD - valueFrom: - secretKeyRef: - name: ${DATABASE_SERVICE_NAME} - key: POSTGRES_PASSWORD - livenessProbe: - exec: - command: - - /usr/libexec/check-container - - --live - initialDelaySeconds: 120 - timeoutSeconds: 10 - readinessProbe: - exec: - command: - - /usr/libexec/check-container - initialDelaySeconds: 5 - periodSeconds: 60 - timeoutSeconds: 1 - resources: - requests: - cpu: ${CPU_REQUEST} - memory: ${MEMORY_REQUEST} - terminationMessagePath: /dev/termination-log - envFrom: - - secretRef: - name: ${DATABASE_SERVICE_NAME} - volumeMounts: - - mountPath: /var/lib/pgsql/backups - name: unity-data-backups - dnsPolicy: ClusterFirst - restartPolicy: Always - volumes: - - name: unity-data-backups - persistentVolumeClaim: - claimName: unity-data-backup - strategy: - type: Recreate diff --git a/openshift/Readme.md b/openshift/Readme.md deleted file mode 100644 index eb9eb63c2..000000000 --- a/openshift/Readme.md +++ /dev/null @@ -1,95 +0,0 @@ -# Instructions to Install Unity Project - -## Step 1: Create templates from code - -You can create the required templates using the web OpenShift console or the oc CLI. -``` -# Delete build templates -oc delete templates --all - -# Create build templates -oc create -f $repository\database\unity-backup-cronjob.yaml -oc create -f $repository\database\unity-database.yaml -oc create -f $repository\openshift\unity-imagestream.yaml -oc create -f $repository\openshift\unity-grantmanager-dbmigrator-job.yaml -oc create -f $repository\openshift\unity-grantmanager-web.yaml -oc create -f $repository\openshift\unity-networkpolicy.yaml -oc create -f $repository\openshift\unity-rabbitmq.yaml -oc create -f $repository\openshift\unity-s3-object-storage.yaml -oc create -f $repository\openshift\unity-app-data-web.json -oc create -f $repository\openshift\unity-chefs-data-web.json -oc create -f $repository\openshift\unity-metabase.yaml -``` - -## Step 2: Create .env paramater files - -As a best practice, store copies of these files in a secure location. -``` -"database.env" -"dbmigrator-job.env" -"grantmanager-web.env" -"S3-storage.env" -"metabase.env" -"rabbitmq.env" -``` - -Use oc get templates to find all available parameters of a project template. -oc get templates - -| **NAME** | **DESCRIPTION** | -|-----------|-----------------| -| unity-app-data-web | An example Nginx HTTP server and a reverse proxy (nginx) application that serves web content. | -| unity-backup-cronjob | Template for running a recurring backup script in OpenShift. | -| unity-chefs-data-web | An example Nginx HTTP server and a reverse proxy (nginx) application that serves web content. | -| unity-database | PostgreSQL database service with persistent storage. | -| unity-grantmanager-dbmigrator-job | Template for running a dotnet console application once in OpenShift. | -| unity-grantmanager-pgbackup-job | Template for running a dotnet console application once in OpenShift. | -| unity-grantmanager-web | Template for running a DotNet web application on OpenShift. | -| unity-imagestream | Template for tracking of changes in the application image. | -| unity-metabase | Template for running a DotNet web application on OpenShift. | -| unity-networkpolicy | Template for communications rules in OpenShift. | -| unity-rabbitmq | Template for running RabbitMQ message queue application on OpenShift. | -| unity-s3-object-storage | Template for S3 connection information in OpenShift. | - -## Step 3: Create or replace project resources - -You can create OpenShift resources using the web OpenShift console or the oc CLI. - -Using the command line, -``` -# Replace the running network and namespace policy -oc delete networkpolicies --all -oc process unity-networkpolicy | oc create -f - -oc policy add-role-to-user system:image-puller system:serviceaccount:${project}:default --namespace=${tools} -oc policy add-role-to-group system:image-puller system:serviceaccounts:${project} --namespace=${tools} - -# Create Database objects from templates with parameters -oc process unity-database --param-file=${params}-database.env | oc create -f - -helm upgrade --install ${release}-hippo-ha . -f $repository\database\crunchy-postgres\values.yaml -f ${params}-pgo-custom-values.yaml -oc process unity-backup-cronjob --param-file=${params}-database.env | oc create -f - - -# Create DbMigraitor objects from templates with parameters -oc process unity-grantmanager-imagestream -p APPLICATION_GROUP=${release}-unity-grantmanager -p APPLICATION_NAME=${release}-unity-dbmigrator | oc create -f - -oc import-image ${release}-unity-dbmigrator:$tag --confirm --from=image-registry.openshift-image-registry.svc:5000/${tools}/${release}-unity-dbmigrator-build:$tag -oc process unity-grantmanager-dbmigrator-job --param-file=${params}-dbmigrator-job.env | oc create -f - -oc wait jobs/${release}-unity-dbmigrator --for condition=complete --timeout=120s - -# Create S3 storage objects from templates with parameters -oc process unity-s3-object-storage --param-file=${params}-S3.env | oc create -f - - -# Create GrantManager objects from templates with parameters -oc process unity-grantmanager-imagestream -p APPLICATION_GROUP=${release}-unity-grantmanager -p APPLICATION_NAME=${release}-unity-grantmanager | oc create -f - -oc import-image ${release}-unity-grantmanager:$tag --confirm --from=image-registry.openshift-image-registry.svc:5000/${tools}/${release}-unity-grantmanager-build:$tag -oc process unity-grantmanager-web --param-file=${params}-grantmanager-web.env | oc create -f - -oc wait dc/${release}-unity-grantmanager-web --for condition=available=true --timeout=120s - -# Create RabbitMQ objects from templates with parameters -oc process unity-rabbitmq --param-file=${project}-rabbitmq.env | oc create -f - -oc wait dc/${namespace}unity-rabbitmq --for condition=available - -# Deployment for app-data-web -oc process unity-app-data-web -p IMAGEPULL_NAMESPACE=${tools} -p IMAGESTREAM_NAME=${namespace}-unity-app-data-build -p IMAGESTREAM_TAG=latest | oc create -f - - -# Deployment for reporting -oc process unity-metabase --param-file=${project}-metabase.env | oc create -f - -``` \ No newline at end of file diff --git a/openshift/SSL_CERTIFICATE.md b/openshift/SSL_CERTIFICATE.md deleted file mode 100644 index 10e8e9218..000000000 --- a/openshift/SSL_CERTIFICATE.md +++ /dev/null @@ -1,94 +0,0 @@ -# Instructions to Install Unity SSL Certificate - -## Step 1: Submit a CSR - -A Certificate Signing Request (CSR) is necessary for new certificates or certificate renewals. Contact the ISB Operations team they will make an iStore request with associated approved funding and generate the required .csr file, then provide the SSL certificate files when they are ready. - - -## Step 2: Install SSL certificates - -As a best practice, store copies of these files in the ISB Operations SSL certificate store (e.g. Zone-B server filesystem). That way, the keys can be retrieved when needed. Only project namespace administrators can edit OpenShift certificate objects. - -Ensure you have all four (4) required files: - -- Certificate: unity.gov.bc.ca.txt -- Private Key: unity.gov.bc.ca.key -- CA Certificate: L1KChain.txt -- CA Root Certificate: G2Root.txt - -## Step 3: Create route for unity.gov.bc.ca - -You can create network routes using the web OpenShift console or the oc CLI. - -Using the command line, the following example creates a secured HTTPS route named `unity-gov-bc-ca` that directs traffic to the `unity-grantmanager-web` service: - -```bash -oc create route edge unity-gov-bc-ca \ - --service=unity-grantmanager-web \ - --cert=unity.gov.bc.ca.txt \ - --key=unity.gov.bc.ca.key \ - --ca-cert=L1KChain.txt \ - --hostname=unity.gov.bc.ca \ - --insecure-policy=Redirect -``` - -Using the web console, you can navigate to the **Administrator > Networking > Routes** section of the conaole. - -Click **Create Route** to define and create a route in the project. - -Use the following settings: - -- Name: unity-gov-bc-ca -- Hostname: unity.gov.bc.ca -- Path: `/` -- Service: unity-grantmanager-web -- Secure Route: (yes) -- TLS Termination: Edge -- Insecure Traffic: Redirect - -| Route field | Source file | -| -------------------------- | ------------------- | -| Certificate | unity.gov.bc.ca.txt | -| Private Key | unity.gov.bc.ca.key | -| CA Certificate | L1KChain.txt | - -## Step 4: Verify new route - -The site should work immediately after saving these route settings. - -- Check that https://unity.gov.bc.ca is live and that the application landing page loads correctly. -- Verify SSO (Keycloak) settings - https://bcgov.github.io/sso-requests - -## Optional steps to generate a local CSR -Run the openssl utility with the CSR and private key options **these do not need to be created on the intended machine or containers**. - -```bashs -openssl req -new -newkey rsa:2048 -nodes -out unity.gov.bc.ca.csr \ - -keyout unity.gov.bc.ca.key \ - -subj "/C=CA/ST=British Columbia/L=Victoria/O=Government of the Province of British Columbia/OU=CITZ/CN=unity.gov.bc.ca" -``` - -Response should be: - -``` -Generating a RSA private key -.........+++++ -...............................+++++ -writing new private key to 'unity.gov.bc.ca.key' ------ -You are about to be asked to enter information that will be incorporated -into your certificate request. -What you are about to enter is what is called a Distinguished Name or a DN. -There are quite a few fields but you can leave some blank -For some fields there will be a default value, -If you enter '.', the field will be left blank. ------ -Country Name (2 letter code) [AU]:CA -State or Province Name (full name) [Some-State]:British Columbia -Locality Name (eg, city) []:Victoria -Organization Name (eg, company) [Internet Widgits Pty Ltd]:Government of the Province of British Columbia -Organizational Unit Name (eg, section) []:JEDI -Common Name (e.g. server FQDN or YOUR name) []:unity.gov.bc.ca -Email Address []: - -Keep the secret key and send the `.csr` file to the OCIO Access and Directory Management Services team they will require an iStore order to process the `.csr` file and will provide the SSL certificates when they are ready. \ No newline at end of file diff --git a/openshift/redis-sentinel/.helmignore b/openshift/redis-sentinel/.helmignore deleted file mode 100644 index 0e8a0eb36..000000000 --- a/openshift/redis-sentinel/.helmignore +++ /dev/null @@ -1,23 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*.orig -*~ -# Various IDEs -.project -.idea/ -*.tmproj -.vscode/ diff --git a/openshift/redis-sentinel/Chart.lock b/openshift/redis-sentinel/Chart.lock deleted file mode 100644 index 55e55af74..000000000 --- a/openshift/redis-sentinel/Chart.lock +++ /dev/null @@ -1,6 +0,0 @@ -dependencies: -- name: redis - repository: https://charts.bitnami.com/bitnami - version: 21.1.11 -digest: sha256:98f3d6fdc3360f0ea929a647528658e8693cf22ea503a099dc77af35e46af99f -generated: "2025-06-03T11:36:36.363955-07:00" diff --git a/openshift/redis-sentinel/Chart.yaml b/openshift/redis-sentinel/Chart.yaml deleted file mode 100644 index dc01835b7..000000000 --- a/openshift/redis-sentinel/Chart.yaml +++ /dev/null @@ -1,29 +0,0 @@ -apiVersion: v2 -name: redis -description: High Availability Redis Chart - -# A chart can be either an 'application' or a 'library' chart. -# -# Application charts are a collection of templates that can be packaged into versioned archives -# to be deployed. -# -# Library charts provide useful utilities or functions for the chart developer. They're included as -# a dependency of application charts to inject those utilities and functions into the rendering -# pipeline. Library charts do not define any templates and therefore cannot be deployed. -type: application - -# This is the chart version. This version number should be incremented each time you make changes -# to the chart and its templates, including the app version. -# Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.2 - -# This is the version number of the application being deployed. This version number should be -# incremented each time you make changes to the application. Versions are not expected to -# follow Semantic Versioning. They should reflect the version the application is using. -# It is recommended to use it with quotes. -appVersion: "8.2.1" - -dependencies: - - name: redis - version: "21.1.11" # Specify the version you want - repository: "https://charts.bitnami.com/bitnami" diff --git a/openshift/redis-sentinel/charts/redis-21.1.11.tgz b/openshift/redis-sentinel/charts/redis-21.1.11.tgz deleted file mode 100644 index 78e1f139e38bcab1bec212715e8edc087c831e68..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 111679 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

@J+WgQrFQtpQUQYRr|ys zuA-+IH^rVEr!MLeq3}raIeHM+rkaO6z`Ur?`iP_SB+AXynp@3c#!JbVg`YI8vxEXT zBbQO&&sDue%XWi(J@p4yBIzt@GE;1Sn8=v?)}xwpd`mMnrE}Wu?~BmwJg zM~!du4h<*`uu984THU)wr}90{M-;b4IWJpW5L z^dsXfmz(LDJ=^vJy=$fZKVsozKD@Dp`8(2&xpG)z)koUIa&xsB7 z)^{Cmk1Q^|C3}&JVq%NshI-o3RZrK~x_+Z*Y`OOOxOTKwGNrEK`Htp;SGI#* z2otoXE&2U=Sk{l-6hUx(vB19^aoEYPW7}%}G_@l3q#{t!SRPKk<(99;k>$+bLMVW> zRTy*08|VW+e6OROo!v8cuQ%E8Erycu8>`mw***28Iyw)3N2&WuQk>a2qI+WAk@$BI ze%J4bvbaMr(0i8A+tDiO_S9*KMGbj+-V2?YaUE8%?TDLXSa-ASOIV?PMP9$Y6PlAn zW%Fe%voFi+g`Z`amiT5PiVc-zn^+AgPjcF-_&p}<5W_qA*l93$VrmJ>LGHs9NxhG)uuh7XL`Ypozz@9}Ue&vQ3=c--sgjpPORm#c%tUQCA?`%odkK&V`;u>@P z6xY23sdcB$=`3Z{If9=z!_@rDInA5T>P*E;S@&67bAD!7W&AhcqvCUY1?lzDKM{FG z@*%ISKj)&=Fr>cRTyh%U&|1m=h(kmp?8bR4r-?f;R&hQ>_F$3nap%i%01(-Bj%0EkKGnQ{{8)$)4@M(p|#i z`;vuthhXRIXZMuAiOfhbitL7y;17|j$I^}SZ0eL8+Z_32o^>T9)f`rFI6C!}MHv^+ zwF-4vHf>ln`O-S|vsHh~vmlL=-F{pxQD7ZuR*f%9 ka;xN(P>w#9;k_{Lk$G=jUOB7HrR3Sbp0aRqA4Veae~w)SY5)KL diff --git a/openshift/unity-applicantportal-web.yaml b/openshift/unity-applicantportal-web.yaml deleted file mode 100644 index ba55149ac..000000000 --- a/openshift/unity-applicantportal-web.yaml +++ /dev/null @@ -1,271 +0,0 @@ -apiVersion: template.openshift.io/v1 -kind: Template -message: |- - A new application been created in your project: unity-applicantportal-web - For more information about using this template, including OpenShift considerations, - see template usage guide found in the project readme.md and wiki documents. -metadata: - name: unity-applicantportal-web - # This template uses a separate parameter .env file to override the default values defined in this section. - # oc process -f .\openshift\unity-applicantportal-web.yaml --param-file=namespace.env | oc create -f - - labels: - template: unity-applicantportal-web - annotations: - description: |- - Template for running a DotNet web application on OpenShift. - iconClass: icon-dotnet - openshift.io/display-name: DotNet web application - template.openshift.io/long-description: |- - This template defines resources needed to build and deploy a GitHub DotNet core base web application. - tags: dotnet,unity-applicantportal-web -parameters: -# Project namespace parameters -- description: The name of the application grouping. - displayName: Application Group - name: APPLICATION_GROUP - value: unity-applicantportal -- description: The name of the application. - displayName: Application Name - name: APPLICATION_NAME - required: true - value: unity-applicantportal-web -# Additional parameters for project application provisioning. -- description: The name of the OpenShift Service exposed for the database. - displayName: Database Service Name - name: DATABASE_SERVICE_NAME - required: true - value: unity-data-postgres -- description: Volume space available for data, e.g. 512Mi, 2Gi. - displayName: Volume Capacity - name: VOLUME_CAPACITY - required: true - value: 40Mi -- description: Git source URI for application - displayName: Git Repository URL - name: SOURCE_REPOSITORY_URL - required: true - value: https://github.com/bcgov/Unity -- description: Git branch/tag reference - displayName: Git Reference - name: SOURCE_REPOSITORY_REF - value: dev -- description: 'Custom hostname for http service route. Leave blank for default hostname, - e.g.: -.' - displayName: Custom http Route Hostname - name: HOSTNAME_HTTP - value: dev2-grants.apps.silver.devops.gov.bc.ca -- description: ASPNETCORE_ENVIRONMENT - displayName: ASPNETCORE_ENVIRONMENT - name: ASPNETCORE_ENVIRONMENT - value: Development -- description: ASPNETCORE_URLS - displayName: ASPNETCORE_URLS - name: ASPNETCORE_URLS - value: 'http://*:8080' -# Base image location -- description: The Namespace where the container image resides - displayName: Registry Namespace - name: IMAGEPULL_NAMESPACE - from: '[a-zA-Z0-9]{5}-tools' - generate: expression -- description: The ImageStream Name - displayName: Registry imagestream name - name: IMAGESTREAM_NAME - value: unity-applicantportal-build -- description: The version of the image to use, e.g. v1.0.0, v0.1.0, latest the ImageStream tag. - displayName: Application Version - name: IMAGESTREAM_TAG - required: true - value: latest -- description: The registry path of the container image used. - displayName: Registry location to pull from - name: IMAGEPULL_REGISTRY - value: image-registry.openshift-image-registry.svc:5000 -# Resource limits control how much CPU and memory a container will consume -- description: The minimum amount of CPU the Container is guaranteed. - displayName: CPU Request - name: CPU_REQUEST - required: true - value: 50m -- description: The minimum amount of Memory the Container is guaranteed. - displayName: Memory Request - name: MEMORY_REQUEST - required: true - value: 64Mi -# Template objects to instantiate the project application. -objects: -# Configmap -- apiVersion: v1 - kind: ConfigMap - metadata: - name: ${APPLICATION_NAME} - labels: - app: ${APPLICATION_NAME} - app.kubernetes.io/component: ${APPLICATION_NAME} - app.kubernetes.io/instance: ${APPLICATION_NAME}-1 - app.kubernetes.io/name: ${APPLICATION_NAME} - app.kubernetes.io/part-of: ${APPLICATION_GROUP} - data: - # Configuration values can be set as key-value properties - ASPNETCORE_ENVIRONMENT: ${ASPNETCORE_ENVIRONMENT} - ASPNETCORE_URLS: ${ASPNETCORE_URLS} -# Service -- apiVersion: v1 - kind: Service - metadata: - annotations: - description: The application's http port. - name: ${APPLICATION_NAME} - labels: - app: ${APPLICATION_NAME} - app.kubernetes.io/component: ${APPLICATION_NAME} - app.kubernetes.io/instance: ${APPLICATION_NAME}-1 - app.kubernetes.io/name: ${APPLICATION_NAME} - app.kubernetes.io/part-of: ${APPLICATION_GROUP} - spec: - ports: - - name: 80-tcp - protocol: TCP - port: 80 - targetPort: 8080 - selector: - app: ${APPLICATION_NAME} -# Route -- apiVersion: route.openshift.io/v1 - id: ${APPLICATION_NAME}-http - kind: Route - metadata: - annotations: - description: Route for application's http service. - haproxy.router.openshift.io/balance: roundrobin - haproxy.router.openshift.io/hsts_header: max-age=31536000;includeSubDomains;preload - haproxy.router.openshift.io/ip_whitelist: 142.22.0.0/15 142.24.0.0/13 142.32.0.0/14 142.36.0.0/16 - router.openshift.io/cookie-same-site: Strict - router.openshift.io/cookie_name: haproxy-uap - name: ${APPLICATION_NAME} - labels: - app: ${APPLICATION_NAME} - app.kubernetes.io/component: ${APPLICATION_NAME} - app.kubernetes.io/instance: ${APPLICATION_NAME}-1 - app.kubernetes.io/name: ${APPLICATION_NAME} - app.kubernetes.io/part-of: ${APPLICATION_GROUP} - spec: - host: ${HOSTNAME_HTTP} - path: / - to: - kind: Service - name: ${APPLICATION_NAME} - weight: 100 - port: - targetPort: 80-tcp - tls: - termination: edge - insecureEdgeTerminationPolicy: Redirect - wildcardPolicy: None - httpHeaders: - actions: - response: - - name: X-Frame-Options - action: - type: Set - set: - value: SAMEORIGIN - - name: X-Content-Type-Options - action: - type: Set - set: - value: no-sniff - - name: Referrer-Policy - action: - type: Set - set: - value: strict-origin-when-cross-origin - - name: Content-Security-Policy - action: - type: Set - set: - value: object-src 'none'; frame-ancestors 'none' -# Persistent storage for the application logfiles. -- apiVersion: v1 - kind: PersistentVolumeClaim - metadata: - name: ${APPLICATION_NAME}-logfiles - labels: - app: ${APPLICATION_NAME} - app.kubernetes.io/component: ${APPLICATION_NAME} - app.kubernetes.io/instance: ${APPLICATION_NAME}-1 - app.kubernetes.io/name: ${APPLICATION_NAME} - app.kubernetes.io/part-of: ${APPLICATION_GROUP} - spec: - accessModes: - - ReadWriteMany - resources: - requests: - storage: ${VOLUME_CAPACITY} - storageClassName: netapp-file-standard - volumeMode: Filesystem -# Deployment -- apiVersion: apps/v1 - kind: Deployment - metadata: - name: ${APPLICATION_NAME} - annotations: - app.openshift.io/route-disabled: "false" - app.openshift.io/vcs-ref: ${SOURCE_REPOSITORY_REF} - app.openshift.io/vcs-uri: ${SOURCE_REPOSITORY_URL} - # Add the trigger annotation - image.openshift.io/triggers: >- - [{"from":{"kind":"ImageStreamTag","name":"${IMAGESTREAM_NAME}:${IMAGESTREAM_TAG}","namespace":"${IMAGEPULL_NAMESPACE}"},"fieldPath":"spec.template.spec.containers[?(@.name==\"${APPLICATION_NAME}\")].image","pause":"false"}] - labels: - app: ${APPLICATION_NAME} - app.openshift.io/runtime: dotnet - app.kubernetes.io/component: ${APPLICATION_NAME} - app.kubernetes.io/instance: ${APPLICATION_NAME}-1 - app.kubernetes.io/name: ${APPLICATION_NAME} - app.kubernetes.io/part-of: ${APPLICATION_GROUP} - spec: - replicas: 1 - selector: - matchLabels: - app: ${APPLICATION_NAME} - strategy: - type: Recreate - template: - metadata: - labels: - app: ${APPLICATION_NAME} - spec: - volumes: - - name: ${APPLICATION_NAME}-logfiles - persistentVolumeClaim: - claimName: ${APPLICATION_NAME}-logfiles - containers: - - name: ${APPLICATION_NAME} - image: ${IMAGEPULL_REGISTRY}/${IMAGEPULL_NAMESPACE}/${IMAGESTREAM_NAME}:${IMAGESTREAM_TAG} - imagePullPolicy: Always - resources: - requests: - cpu: ${CPU_REQUEST} - memory: ${MEMORY_REQUEST} - ports: - - containerPort: 443 - protocol: TCP - - containerPort: 80 - protocol: TCP - env: - - name: ConnectionStrings__Default - value: >- - Host=$(UNITY_DB_HOST);port=$(UNITY_DB_PORT);Database=$(UNITY_POSTGRES_DB);Username=$(UNITY_POSTGRES_USER);Password=$(UNITY_POSTGRES_PASSWORD) - envFrom: - - configMapRef: - name: ${APPLICATION_NAME} - - configMapRef: - name: ${DATABASE_SERVICE_NAME} - - secretRef: - name: ${DATABASE_SERVICE_NAME} - volumeMounts: - - mountPath: /app/logs - name: ${APPLICATION_NAME}-logfiles - restartPolicy: Always - terminationGracePeriodSeconds: 30 - dnsPolicy: ClusterFirst diff --git a/openshift/unity-chefs-data-web.json b/openshift/unity-chefs-data-web.json deleted file mode 100644 index 1494e8e12..000000000 --- a/openshift/unity-chefs-data-web.json +++ /dev/null @@ -1,117 +0,0 @@ -{ - "kind": "Template", - "apiVersion": "template.openshift.io/v1", - "metadata": { - "name": "unity-chefs-data-web", - "annotations": { - "openshift.io/display-name": "Nginx HTTP server and a reverse proxy", - "description": "An example Nginx HTTP server and a reverse proxy (nginx) application that serves static content.", - "tags": "${APPLICATION_NAME}", - "iconClass": "icon-nginx", - "openshift.io/long-description": "This template defines resources needed to develop a static application served by Nginx HTTP server and a reverse proxy (nginx), including a build configuration and application deployment configuration." - } - }, - "message": "The following service(s) have been created in your project: ${APPLICATION_NAME}.", - "labels": { - "template": "${APPLICATION_NAME}", - "app": "${APPLICATION_NAME}" - }, - "objects": [ - { - "kind": "Route", - "apiVersion": "route.openshift.io/v1", - "metadata": { - "name": "${APPLICATION_NAME}", - "labels": { - "app.kubernetes.io/part-of": "${APPLICATION_GROUP}" - }, - "annotations": { - "haproxy.router.openshift.io/hsts_header": "max-age=31536000;includeSubDomains;preload", - "haproxy.router.openshift.io/ip_whitelist": "142.22.0.0/15 142.24.0.0/13 142.32.0.0/14 142.36.0.0/16", - "template.openshift.io/expose-uri": "http://{.spec.host}{.spec.path}" - } - }, - "spec": { - "host": "${APPLICATION_DOMAIN}", - "to": { - "kind": "Service", - "name": "${APPLICATION_SERVICE}" - }, - "httpHeaders": { - "actions": { - "request": null, - "response": [ - { - "action": { - "set": { - "value": "SAMEORIGIN" - }, - "type": "Set" - }, - "name": "X-Frame-Options" - }, - { - "action": { - "set": { - "value": "no-sniff" - }, - "type": "Set" - }, - "name": "X-Content-Type-Options" - }, - { - "action": { - "set": { - "value": "strict-origin-when-cross-origin" - }, - "type": "Set" - }, - "name": "Referrer-Policy" - }, - { - "action": { - "set": { - "value": "object-src 'none'; frame-ancestors 'none'" - }, - "type": "Set" - }, - "name": "Content-Security-Policy" - } - ] - } - }, - "tls": { - "termination": "edge", - "insecureEdgeTerminationPolicy": "Redirect" - } - } - } - ], - "parameters": [ - { - "description": "The name of the application grouping.", - "displayName": "Application Group", - "name": "APPLICATION_GROUP", - "value": "unity-tools" - }, - { - "description": "The name of the application.", - "displayName": "Application Name", - "name": "APPLICATION_NAME", - "required": true, - "value": "unity-chefs-data-web" - }, - { - "description": "The name of the service.", - "displayName": "Application Seevice", - "name": "APPLICATION_SERVICE", - "value": "unity-app-data-web" - }, - { - "name": "APPLICATION_DOMAIN", - "displayName": "Application Hostname", - "description": "The exposed hostname that will route to the nginx service, if left blank a value will be defaulted.", - "value": "dev-unity-chefs-data.apps.silver.devops.gov.bc.ca" - } - ] -} diff --git a/openshift/unity-grantmanager-build.yaml b/openshift/unity-grantmanager-build.yaml deleted file mode 100644 index 3e01660897788fcb596daf750d9ddbf41649d4c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8976 zcmchdT~8ZF6o%)zQvbtBRLV^;O{&~P>IDIkx=pBSNZLw~5H?_Fu%TX?Hck51+rG~n zAMfmXZPt)PmhtY+%$alE^F7Od{~m|ya1maI(=ZOXuCBr;JPBEt=*duHm*F^Gw={CC zXCuuz*Ss@5ZN>Rp;U+u`UxqKjTiuVsNnC$eST zdpcWl7Vn0UlF6Uxx~Hd8t;_TmTt<INz_S8e_3%nTluHx zq03B9uY}$-&Kil2Tsn76PU3p-Kb9_C&XMlWGunld3yr^%mIqNjXn-`cuz|5g3Z17K zcX}J5bGQ_BLjtRUdqeD6SB9m@ZFlelj_ALo-;t~qys$RI_)7Ozddi%!o*SZ$A9rV> zJk@-x;zrkK+V-|lbbcCPvrw+*HAxFM4nCU2d{6$Rn_y}tc`6; zT0k?Gk@BV}Ixg&ED0~<7VqVHPycAe{&`RSp^*raZ@M9uIPb)8_mBKFYfvO#UEE~a| zM&b=ibd0NeqoG_9q1o&CtZEE`OFoC1oklY+{ePIswla8*{g zj8Y6_$LnHGeyNMG#3Q`W{EQhyv0VEQ@y&BGAxG3?CNXWI(Nt>|C!UMOBz&)RJ&l%8 z0B7j33jDdM*JxR9Sl>YN(H3!Y5;>VDlHWHD7QX5+%&mJvdv(NZk>`-#A}~5PA11cA zw-*%9>t^J2nP(7pk=`j{WyBF^mUCl=doXWPiKjRd{wl%xXXP(b#xg9z; zwyGofUxc>2Wm{g`mUr!kqwqX*b@fVPzN(1VtZSl?PleBY&~U_DswP@U-^4F9+fBT-XWV=JoC}4*bJ}y-2YPO` z*f5S0L+)cCk5 zUHNcf4fWc09+yWGr{0ymuC)U?CKHU60d1ETb4gQd4r0{kDw^yG5wPj#$$@w})NeQZ zrl;^&tX?0db8#{eSH$oweFl-YbNOFSnkV}V^|Ys}fv#_K{Ynwo>geXU_9QEHQC~59 zOZ#D0mM_nQ30yN)cziP~>({P}AhCNhF=Nm9{LOXB*r^~2MsW9#<%M(MSH!KWt^#BtY*30#+GA?wP9!EOG9@%{H?qOVwvo* zExX>47Ko>G;po+RIWiM9IlTs_O`IQ$!+r3o^0E$M%hr}Ld!S0M_!zGnlAs&4pH?+U zInbsKsoNQhxhjA)7joBKcI0PO~5E-p(w^PMkN`Sparr_;E5p9wdYL zXdaQY48J;;9?tqac%;&R6Y?T`By!+$UL8Aco+CY{rJPvz&bg#@otE?H$D3tIlO7!D zQ}8A8dCoafi#J}ZPWxGiSw-ub#_HDVa~_4q(&(^3d|3yvmnH3eI^4?p+>Rc3;AM<< z#7J!ZV_^7LapaNcUPKf(q_M@bv@7|y*HU)Q!x;^>ho-T5I!5X{>%Hnvu8@_6_&0K&M48jm^s2>w z72d1Xs6X@YKL4M2mSOes(^y6gLrz52r(CRWNmjZSVm7C5=#4i8bp2Z~R&ka}?Q8k_ z)9#nr5gktR-8N_Mv5?F^hR|vW)0p(>aI`bLiTY|z;cklzk0K*666NkJyuFp{vg>3Q5_=YP5;6DP5lBUr>7=%Jaw* z=_!Pr;JR-*$DU0r8~ORGkNR&v;^?}m&rjvuePx>Cef_EkU8X{=@9P71xvYb?R7K3m zRmGB%y;psqy8@4IOB&)Gm%R--xg!Ou?~=KXWp6OtH^36 z8mT>^Nq@sR(9A{Ic>mzt*nWoX;Ex<%#Yy>Y!PMGdem{Ow&&s+V;Cs$A=JXDKDWUX-XA&3aj&+LXs-{g88({x;{b nO0}NP)DzM(kra2lf&UV*|0t-&W~5B1q!rkXIv?syRzmTAINX@w diff --git a/openshift/unity-grantmanager-dbmigrator-job.yaml b/openshift/unity-grantmanager-dbmigrator-job.yaml deleted file mode 100644 index 4a1b27035..000000000 --- a/openshift/unity-grantmanager-dbmigrator-job.yaml +++ /dev/null @@ -1,119 +0,0 @@ -apiVersion: template.openshift.io/v1 -kind: Template -message: |- - A job has been created in your project: unity-grantmanager-dbmigrator-job. - For more information about using this template, including OpenShift considerations, - see template usage guide found in the project readme.md and wiki documents. -metadata: - name: unity-grantmanager-dbmigrator-job - # This template uses a separate parameter .env file to override the default values defined in this section. - # oc process -f .\openshift\unity-grantmanager-dbmigrator-job.yaml --param-file=.env | oc create -f - - labels: - template: unity-grantmanager-dbmigrator-job - annotations: - description: |- - Template for running a dotnet console application once in OpenShift. - iconClass: icon-build - openshift.io/display-name: Database Migrator Job - template.openshift.io/long-description: |- - This template defines resources needed to build and deploy a container application. - tags: dotnet,unity-grantmanager-dbmigrator -parameters: -# Project namespace parameters -- description: The name of the application. - displayName: Application Name - name: APPLICATION_NAME - required: true - value: unity-grantmanager-dbmigrator -- description: The name of the application grouping. - displayName: Application Group - name: APPLICATION_GROUP - value: Triggers -# Additional parameters for project application provisioning. -- description: The name of the OpenShift Service exposed for the database. - displayName: Database Service Name - name: DATABASE_SERVICE_NAME - required: true - value: unity-data-postgres -- description: Git source URI for application - displayName: Git Repository URL - name: SOURCE_REPOSITORY_URL - required: true - value: 'https://github.com/bcgov/Unity' -# Base image location -- description: The Namespace where the container image resides - displayName: Registry Namespace - name: IMAGEPULL_NAMESPACE - from: '[a-zA-Z0-9]{5}-tools' - generate: expression -- description: The ImageStream Name - displayName: Registry imagestream name - name: IMAGESTREAM_NAME - value: unity-dbmigrator-build -- description: The version of the image to use, e.g. v1.0.0, v0.1.0, latest the ImageStream tag. - displayName: Application Version - name: IMAGESTREAM_TAG - required: true - value: latest -- description: The registry path of the container image used. - displayName: Registry location to pull from - name: IMAGEPULL_REGISTRY - value: image-registry.openshift-image-registry.svc:5000 -# Resource limits -- description: The minimum amount of CPU the container is guaranteed. - displayName: CPU Request - name: CPU_REQUEST - required: true - value: 50m -- description: The minimum amount of memory the container is guaranteed. - displayName: Memory Request - name: MEMORY_REQUEST - required: true - value: 64Mi -# Template objects to instantiate the project. -objects: -# RunOnce Job for Database Migrator -- apiVersion: batch/v1 - kind: Job - metadata: - name: ${APPLICATION_NAME} - labels: - job-name: ${APPLICATION_NAME} - app.openshift.io/runtime: build - app.kubernetes.io/component: ${APPLICATION_NAME} - app.kubernetes.io/instance: ${APPLICATION_NAME}-1 - app.kubernetes.io/name: ${APPLICATION_NAME} - app.kubernetes.io/part-of: ${APPLICATION_GROUP} - annotations: - app.openshift.io/vcs-uri: ${SOURCE_REPOSITORY_URL} - spec: - parallelism: 1 - completions: 1 - backoffLimit: 1 - selector: {} - successfulJobsHistoryLimit: 1 - failedJobsHistoryLimit: 1 - template: - metadata: - name: ${APPLICATION_NAME} - labels: - application: ${APPLICATION_NAME} - spec: - containers: - - name: ${APPLICATION_NAME} - image: ${IMAGEPULL_REGISTRY}/${IMAGEPULL_NAMESPACE}/${IMAGESTREAM_NAME}:${IMAGESTREAM_TAG} - env: - - name: ConnectionStrings__Default - value: >- - Host=$(UNITY_DB_HOST);port=$(UNITY_DB_PORT);Database=$(UNITY_POSTGRES_DB);Username=$(UNITY_POSTGRES_USER);Password=$(UNITY_POSTGRES_PASSWORD) - - name: ConnectionStrings__Tenant - value: >- - Host=$(UNITY_DB_HOST);port=$(UNITY_DB_PORT);Database=$(UNITY_TENANT_DB);Username=$(UNITY_POSTGRES_USER);Password=$(UNITY_POSTGRES_PASSWORD) - envFrom: - - secretRef: - name: ${DATABASE_SERVICE_NAME} - resources: - requests: - cpu: ${CPU_REQUEST} - memory: ${MEMORY_REQUEST} - restartPolicy: Never diff --git a/openshift/unity-grantmanager-pgbackup-job.yaml b/openshift/unity-grantmanager-pgbackup-job.yaml deleted file mode 100644 index 8a4b50bb5..000000000 --- a/openshift/unity-grantmanager-pgbackup-job.yaml +++ /dev/null @@ -1,141 +0,0 @@ -apiVersion: template.openshift.io/v1 -kind: Template -message: |- - A job has been created in your project: unity-grantmanager-pgbackup-job. - For more information about using this template, including OpenShift considerations, - see template usage guide found in the project readme.md and wiki documents. -metadata: - name: unity-grantmanager-pgbackup-job - # This template uses a separate parameter .env file to override the default values defined in this section. - # oc process -f .\openshift\unity-grantmanager-pgbackup-job.yaml --param-file=pgbackup-job.env | oc create -f - - labels: - template: unity-grantmanager-pgbackup-job - annotations: - description: |- - Template for running a dotnet console application once in OpenShift. - iconClass: icon-build - openshift.io/display-name: Database Backup Job - template.openshift.io/long-description: |- - This template defines resources needed to run a Postgres-16 container application. - tags: database,postgresql -parameters: -# Project namespace parameters -- description: The name of the application. - displayName: Application Name - name: APPLICATION_NAME - required: true - value: unity-grantmanager-pgbackup -- description: The name of the application grouping. - displayName: Application Group - name: APPLICATION_GROUP - required: true - value: unity-grantmanager -# Additional parameters for project database provisioning. -- description: The name of the OpenShift Service exposed for the database. - displayName: Database Service Name - name: DATABASE_SERVICE_NAME - required: true - value: unity-data-postgres -- name: DATABASE_BACKUP_KEEP - description: 'Number of backups to keep' - value: '1' -- name: DATABASE_BACKUP_VOLUME_CLAIM - description: 'Name of the volume claim to be used as storage' - required: true - value: unity-data-backup -- description: The Namespace where the container image resides default=project-tools cluster=openshift, source=registry.redhat.io/rhel9/postgresql-16 - displayName: Registry Namespace - name: IMAGEPULL_NAMESPACE - from: '[a-zA-Z0-9]{5}-tools' - generate: expression -- description: The Openshift ImageStream Name - displayName: Registry imagestream name - name: IMAGESTREAM_NAME - required: true - value: postgresql-16 -- description: The version of the postgresql container image to use. - displayName: Registry container image to pull - name: IMAGESTREAM_TAG - required: true - value: latest -- description: The registry path of the postgresql container image to use. - displayName: Registry container image to pull - name: IMAGEPULL_REGISTRY - required: true - value: image-registry.apps.silver.devops.gov.bc.ca -# Resource limits -- description: The minimum amount of CPU the container is guaranteed. - displayName: CPU Request - name: CPU_REQUEST - required: true - value: 50m -- description: The minimum amount of memory the container is guaranteed. - displayName: Memory Request - name: MEMORY_REQUEST - required: true - value: 64Mi -# Template objects to instantiate the project. -objects: -# RunOnce Job for Database Backups -- apiVersion: batch/v1 - kind: Job - metadata: - name: ${APPLICATION_NAME} - labels: - job-name: ${APPLICATION_NAME} - app.openshift.io/runtime: build - app.kubernetes.io/component: ${APPLICATION_NAME} - app.kubernetes.io/instance: ${APPLICATION_NAME}-1 - app.kubernetes.io/name: ${APPLICATION_NAME} - app.kubernetes.io/part-of: ${APPLICATION_GROUP} - spec: - parallelism: 1 - completions: 1 - backoffLimit: 1 - selector: {} - successfulJobsHistoryLimit: 1 - failedJobsHistoryLimit: 1 - template: - metadata: - name: ${APPLICATION_NAME} - labels: - application: ${APPLICATION_NAME} - spec: - volumes: - - name: ${APPLICATION_NAME} - persistentVolumeClaim: - claimName: ${DATABASE_BACKUP_VOLUME_CLAIM} - containers: - - name: ${APPLICATION_NAME} - image: ${IMAGEPULL_REGISTRY}/${IMAGEPULL_NAMESPACE}/${IMAGESTREAM_NAME}:${IMAGESTREAM_TAG} - command: - - 'bash' - - '-eo' - - 'pipefail' - - '-c' - - > - trap "echo Backup failed; exit 0" ERR; date; - FILENAME=dumpall-${DATABASE_SERVICE_NAME}-`date +%Y-%m-%d_%H%M%S`.sql.gz; - time (find /var/lib/pgsql/backups -type f -name "*-${DATABASE_SERVICE_NAME}-*" -exec ls -1tr "{}" + | head -n -$DATABASE_BACKUP_KEEP | xargs rm -fr; - PGPASSWORD="$UNITY_POSTGRES_PASSWORD" pg_dumpall --username=$UNITY_POSTGRES_USER --host=$UNITY_DB_HOST --port=$UNITY_DB_PORT --column-inserts --clean | gzip > /var/lib/pgsql/backups/$FILENAME); - echo "";echo "Backup successful";du -h /var/lib/pgsql/backups/$FILENAME; - echo "";echo "to restore the backup use: $ psql --username=$UNITY_POSTGRES_USER --password --host=$UNITY_DB_HOST --port=$UNITY_DB_PORT --username postgres < /var/lib/pgsql/backups/ (unpacked with gunzip)"; - echo "";ls -lR /var/lib/pgsql/backups - ## Add single and mapped environment values - env: - - name: DATABASE_BACKUP_KEEP - value: ${DATABASE_BACKUP_KEEP} - - name: TZ - value: Canada/Pacific - envFrom: - ## Add all from ${DATABASE_SERVICE_NAME} - - secretRef: - name: ${DATABASE_SERVICE_NAME} - volumeMounts: - - name: ${APPLICATION_NAME} - mountPath: /var/lib/pgsql/backups - resources: - requests: - cpu: ${CPU_REQUEST} - memory: ${MEMORY_REQUEST} - restartPolicy: Never diff --git a/openshift/unity-grantmanager-web.yaml b/openshift/unity-grantmanager-web.yaml deleted file mode 100644 index 8c37fc3e2..000000000 --- a/openshift/unity-grantmanager-web.yaml +++ /dev/null @@ -1,517 +0,0 @@ -apiVersion: template.openshift.io/v1 -kind: Template -message: |- - A new application been created in your project: unity-grantmanager-web - For more information about using this template, including OpenShift considerations, - see template usage guide found in the project readme.md and wiki documents. -metadata: - name: unity-grantmanager-web - # This template uses a separate parameter .env file to override the default values defined in this section. - # oc process -f .\openshift\unity-grantmanager-web.yaml --param-file=namespace.env | oc create -f - - labels: - template: unity-grantmanager-web - annotations: - description: |- - Template for running a DotNet web application on OpenShift. - iconClass: icon-dotnet - openshift.io/display-name: DotNet web application - template.openshift.io/long-description: |- - This template defines resources needed to build and deploy a GitHub DotNet core base web application. - tags: dotnet,unity-grantmanager-web -parameters: -# Project namespace parameters -- description: The name of the application grouping. - displayName: Application Group - name: APPLICATION_GROUP - value: unity-grantmanager -- description: The name of the application. - displayName: Application Name - name: APPLICATION_NAME - required: true - value: unity-grantmanager-web -# Additional parameters for project application provisioning. -- description: The name of the OpenShift Service exposed for the database. - displayName: Database Service Name - name: DATABASE_SERVICE_NAME - required: true - value: unity-data-postgres -- description: The name of the storage object. - displayName: Object Storage Name - name: STORAGE_OBJECT_NAME - required: true - value: s3-object-storage -- description: Volume space available for data, e.g. 512Mi, 2Gi. - displayName: Volume Capacity - name: VOLUME_CAPACITY - required: true - value: 128Mi -- description: Git source URI for application - displayName: Git Repository URL - name: SOURCE_REPOSITORY_URL - required: true - value: https://github.com/bcgov/Unity -- description: Git branch/tag reference - displayName: Git Reference - name: SOURCE_REPOSITORY_REF - value: dev -- description: 'Custom hostname for http service route. Leave blank for default hostname, - e.g.: -.' - displayName: Custom http Route Hostname - name: HOSTNAME_HTTP - value: develop-unity.apps.silver.devops.gov.bc.ca -- description: ASPNETCORE_ENVIRONMENT - displayName: ASPNETCORE_ENVIRONMENT - name: ASPNETCORE_ENVIRONMENT - value: Development -- description: ASPNETCORE_URLS - displayName: ASPNETCORE_URLS - name: ASPNETCORE_URLS - value: 'http://*:8080' -- description: StringEncryption__DefaultPassPhrase - displayName: StringEncryption__DefaultPassPhrase - from: '[a-zA-Z0-9]{16}' - generate: expression - name: StringEncryption__DefaultPassPhrase - required: true -- description: AuthServer__ClientId - displayName: AuthServer__ClientId - from: '[a-zA-Z0-9]{16}' - generate: expression - name: AuthServer__ClientId - required: true -- description: AuthServer__ClientSecret - displayName: AuthServer__ClientSecret - from: 'unity-[0-9]{4}' - generate: expression - name: AuthServer__ClientSecret - required: true -- description: AuthServer__Audience - displayName: AuthServer__Audience - from: 'unity-[0-9]{4}' - generate: expression - name: AuthServer__Audience - required: true -- description: AuthServer__ServerAddress - displayName: AuthServer__ServerAddress - name: AuthServer__ServerAddress - value: 'https://dev.loginproxy.gov.bc.ca/auth' -- description: Intake__BaseUri - displayName: Intake__BaseUri - name: Intake__BaseUri - value: 'https://submit.digital.gov.bc.ca/app/api/v1' -- description: CssApi__ClientId - displayName: CssApi__ClientId - name: CssApi__ClientId - from: 'service-account-[0-9]{4}-[0-9]{4}' - generate: expression -- description: CssApi__ClientSecret - displayName: CssApi__ClientSecret - name: CssApi__ClientSecret - from: '[a-zA-Z0-9]{32}' - generate: expression - required: true -- description: CssApi__TokenUrl - displayName: CssApi__TokenUrl - name: CssApi__TokenUrl - value: 'https://loginproxy.gov.bc.ca/auth/realms/standard/protocol/openid-connect/token' -- description: CssApi__Url - displayName: CssApi__Url - name: CssApi__Url - value: 'https://api.loginproxy.gov.bc.ca/api/v1' -- description: CssApi__Env - displayName: CssApi__Env - name: CssApi__Env - value: dev -- description: Notifications__TeamsNotificationsWebhook - displayName: Notifications__TeamsNotificationsWebhook - name: Notifications__TeamsNotificationsWebhook -- description: Notifications__ChesClientSecret - displayName: Notifications__ChesClientSecret - name: Notifications__ChesClientSecret - from: '[a-zA-Z0-9]{32}' - generate: expression - required: true -- description: Notifications__ChesClientId - displayName: Notifications__ChesClientId - from: '[a-zA-Z0-9]{16}' - generate: expression - name: Notifications__ChesClientId - required: true -- description: Notifications__ChesTokenUrl - displayName: Notifications__ChesTokenUrl - name: Notifications__ChesTokenUrl - value: 'https://dev.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token' -- description: Notifications__ChesUrl - displayName: Notifications__ChesUrl - name: Notifications__ChesUrl - value: 'https://ches-dev.api.gov.bc.ca/api/v1' -- description: Notifications__ChesFromEmail - displayName: Notifications__ChesFromEmail - name: Notifications__ChesFromEmail - value: 'unity-noreply@gov.bc.ca' -- description: Payments__CasBaseUrl - displayName: Payments__CasBaseUrl - name: Payments__CasBaseUrl - value: 'https://cfs-systws.cas.gov.bc.ca:7025/ords/cas' -- description: Payments__CasClientSecret - displayName: Payments__CasClientSecret - from: '[a-zA-Z0-9]{22}..' - generate: expression - name: Payments__CasClientSecret -- description: Payments__CasClientId - displayName: Payments__CasClientId - from: '[a-zA-Z0-9]{22}..' - generate: expression - name: Payments__CasClientId -- description: RabbitMQ__Password - displayName: RabbitMQ__Password - from: '[a-zA-Z0-9]{26}' - generate: expression - name: RabbitMQ__Password -- description: RabbitMQ__UserName - displayName: RabbitMQ__UserName - value: 'unity-rabbitmq-user-dev' - name: RabbitMQ__UserName -- description: RabbitMQ__VirtualHost - displayName: RabbitMQ__VirtualHost - value: 'dev' - name: RabbitMQ__VirtualHost -- description: RabbitMQ__HostName - displayName: RabbitMQ__HostName - value: 'unity-rabbitmq' - name: RabbitMQ__HostName -- description: Redis__Configuration - displayName: Redis__Configuration - from: 'dev-redis-ha.[a-zA-Z0-9]{5}-dev.svc.cluster.local:26379' - generate: expression - name: Redis__Configuration -- description: Redis__HostName - displayName: Redis__HostName - value: 'dev-redis-ha' - name: Redis__HostName -- description: Redis__IsEnabled - displayName: Redis__IsEnabled - value: 'false' - name: Redis__IsEnabled -# Base image location -- description: The Namespace where the container image resides - displayName: Registry Namespace - name: IMAGEPULL_NAMESPACE - from: '[a-zA-Z0-9]{5}-tools' - generate: expression -- description: The ImageStream Name - displayName: Registry imagestream name - name: IMAGESTREAM_NAME - value: unity-grantmanager-build -- description: The version of the image to use, e.g. v1.0.0, v0.1.0, latest the ImageStream tag. - displayName: Application Version - name: IMAGESTREAM_TAG - required: true - value: latest -- description: The registry path of the container image used. - displayName: Registry location to pull from - name: IMAGEPULL_REGISTRY - value: image-registry.openshift-image-registry.svc:5000 -# Resources control how much CPU and memory a container will consume -- description: The minimum amount of CPU the Container is guaranteed. - displayName: CPU Request - name: CPU_REQUEST - required: true - value: 50m -- description: The minimum amount of Memory the Container is guaranteed. - displayName: Memory Request - name: MEMORY_REQUEST - required: true - value: 128Mi -# Template objects to instantiate the project application. -objects: -# Secrets -- apiVersion: v1 - kind: Secret - metadata: - name: ${APPLICATION_NAME} - labels: - app: ${APPLICATION_NAME} - app.kubernetes.io/component: ${APPLICATION_NAME} - app.kubernetes.io/instance: ${APPLICATION_NAME}-1 - app.kubernetes.io/name: ${APPLICATION_NAME} - app.kubernetes.io/part-of: ${APPLICATION_GROUP} - stringData: - StringEncryption__DefaultPassPhrase: ${StringEncryption__DefaultPassPhrase} - AuthServer__ClientId: ${AuthServer__ClientId} - AuthServer__ClientSecret: ${AuthServer__ClientSecret} - AuthServer__Audience: ${AuthServer__Audience} - CssApi__ClientId: ${CssApi__ClientId} - CssApi__ClientSecret: ${CssApi__ClientSecret} - Notifications__TeamsNotificationsWebhook: ${Notifications__TeamsNotificationsWebhook} - Notifications__ChesClientId: ${Notifications__ChesClientId} - Notifications__ChesClientSecret: ${Notifications__ChesClientSecret} - Payments__CasClientSecret: ${Payments__CasClientSecret} - Payments__CasClientId: ${Payments__CasClientId} - RabbitMQ__Password: ${RabbitMQ__Password} - type: Opaque -# Configmap -- apiVersion: v1 - kind: ConfigMap - metadata: - name: ${APPLICATION_NAME} - labels: - app: ${APPLICATION_NAME} - app.kubernetes.io/component: ${APPLICATION_NAME} - app.kubernetes.io/instance: ${APPLICATION_NAME}-1 - app.kubernetes.io/name: ${APPLICATION_NAME} - app.kubernetes.io/part-of: ${APPLICATION_GROUP} - data: - # Configuration values can be set as key-value properties - ASPNETCORE_ENVIRONMENT: ${ASPNETCORE_ENVIRONMENT} - ASPNETCORE_URLS: ${ASPNETCORE_URLS} - AuthServer__IsBehindTlsTerminationProxy: 'true' - AuthServer__Realm: standard - AuthServer__RequireHttpsMetadata: 'false' - AuthServer__ServerAddress: ${AuthServer__ServerAddress} - BackgroundJobs__CasPaymentsReconciliation__ConsumerExpression: '0 0 14 1/1 * ? *' - BackgroundJobs__CasPaymentsReconciliation__ProducerExpression: '0 0 13 1/1 * ? *' - BackgroundJobs__EmailResend__Expression: '0 0/5 * * * ?' - BackgroundJobs__EmailResend__RetryAttemptsMaximum: '2' - BackgroundJobs__IsJobExecutionEnabled: 'true' - BackgroundJobs__Quartz__IsAutoRegisterEnabled: 'true' - BackgroundJobs__IntakeResync__NumDaysToCheck: '-2' - BackgroundJobs__IntakeResync__Expression: '0 0 23 1/1 * ? *' - BackgroundJobs__Quartz__UseCluster: ${Redis__IsEnabled} - CssApi__TokenUrl: ${CssApi__TokenUrl} - CssApi__Url: ${CssApi__Url} - CssApi__Env: ${CssApi__Env} - Intake__BaseUri: ${Intake__BaseUri} - Notifications__ChesTokenUrl: ${Notifications__ChesTokenUrl} - Notifications__ChesUrl: ${Notifications__ChesUrl} - Notifications__ChesFromEmail: ${Notifications__ChesFromEmail} - Payments__CasBaseUrl: ${Payments__CasBaseUrl} - RabbitMQ__UserName: ${RabbitMQ__UserName} - RabbitMQ__VirtualHost: ${RabbitMQ__VirtualHost} - RabbitMQ__HostName: ${RabbitMQ__HostName} - DataProtection__IsEnabled: ${Redis__IsEnabled} - Redis__Configuration: ${Redis__Configuration} - Redis__DatabaseId: '0' - Redis__Host: ${Redis__HostName} - Redis__InstanceName: ${Redis__HostName} - Redis__IsEnabled: ${Redis__IsEnabled} - Redis__KeyPrefix: unity - Redis__Port: '6379' - Redis__SentinelMasterName: redisMasterSet - Redis__UseSentinel: ${Redis__IsEnabled} - Serilog__MinimumLevel__Override__Quartz.Impl: Information - Serilog__MinimumLevel__Override__Quartz.SQL: Information -# Services -- apiVersion: v1 - kind: Service - metadata: - annotations: - description: The application's http port. - name: ${APPLICATION_NAME} - labels: - app: ${APPLICATION_NAME} - app.kubernetes.io/component: ${APPLICATION_NAME} - app.kubernetes.io/instance: ${APPLICATION_NAME}-1 - app.kubernetes.io/name: ${APPLICATION_NAME} - app.kubernetes.io/part-of: ${APPLICATION_GROUP} - spec: - ports: - - name: 80-tcp - protocol: TCP - port: 80 - targetPort: 8080 - selector: - app: ${APPLICATION_NAME} -# Route ingress -- apiVersion: route.openshift.io/v1 - id: ${APPLICATION_NAME}-http - kind: Route - metadata: - annotations: - description: Route for application's http service. - haproxy.router.openshift.io/balance: roundrobin - haproxy.router.openshift.io/hsts_header: max-age=31536000;includeSubDomains;preload - router.openshift.io/cookie-same-site: Strict - router.openshift.io/cookie_name: haproxy-ugm - name: ${APPLICATION_NAME} - labels: - app: ${APPLICATION_NAME} - app.kubernetes.io/component: ${APPLICATION_NAME} - app.kubernetes.io/instance: ${APPLICATION_NAME}-1 - app.kubernetes.io/name: ${APPLICATION_NAME} - app.kubernetes.io/part-of: ${APPLICATION_GROUP} - spec: - host: ${HOSTNAME_HTTP} - path: / - to: - kind: Service - name: ${APPLICATION_NAME} - weight: 100 - port: - targetPort: 80-tcp - tls: - termination: edge - insecureEdgeTerminationPolicy: Redirect - wildcardPolicy: None - httpHeaders: - actions: - response: - - name: X-Frame-Options - action: - type: Set - set: - value: SAMEORIGIN - - name: X-Content-Type-Options - action: - type: Set - set: - value: no-sniff - - name: Referrer-Policy - action: - type: Set - set: - value: strict-origin-when-cross-origin - - name: Content-Security-Policy - action: - type: Set - set: - value: object-src 'none'; frame-ancestors 'none' -# Persistent storage for the application logfiles -- apiVersion: v1 - kind: PersistentVolumeClaim - metadata: - name: ${APPLICATION_NAME}-logfiles - labels: - app: ${APPLICATION_NAME} - app.kubernetes.io/component: ${APPLICATION_NAME} - app.kubernetes.io/instance: ${APPLICATION_NAME}-1 - app.kubernetes.io/name: ${APPLICATION_NAME} - app.kubernetes.io/part-of: ${APPLICATION_GROUP} - spec: - accessModes: - - ReadWriteMany - resources: - requests: - storage: ${VOLUME_CAPACITY} - storageClassName: netapp-file-standard - volumeMode: Filesystem -# Deployment -- apiVersion: apps/v1 - kind: Deployment - metadata: - name: ${APPLICATION_NAME} - annotations: - app.openshift.io/route-disabled: "false" - app.openshift.io/vcs-ref: ${SOURCE_REPOSITORY_REF} - app.openshift.io/vcs-uri: ${SOURCE_REPOSITORY_URL} - image.openshift.io/triggers: >- - [{"from":{"kind":"ImageStreamTag","name":"${IMAGESTREAM_NAME}:${IMAGESTREAM_TAG}","namespace":"${IMAGEPULL_NAMESPACE}"},"fieldPath":"spec.template.spec.containers[?(@.name==\"${APPLICATION_NAME}\")].image","pause":"false"}] - labels: - app: ${APPLICATION_NAME} - app.openshift.io/runtime: dotnet - app.kubernetes.io/component: ${APPLICATION_NAME} - app.kubernetes.io/instance: ${APPLICATION_NAME}-1 - app.kubernetes.io/name: ${APPLICATION_NAME} - app.kubernetes.io/part-of: ${APPLICATION_GROUP} - spec: - replicas: 3 - selector: - matchLabels: - app: ${APPLICATION_NAME} - strategy: - type: RollingUpdate - rollingUpdate: - maxSurge: 2 - maxUnavailable: 1 - template: - metadata: - labels: - application: ${APPLICATION_NAME} - app: ${APPLICATION_NAME} - spec: - volumes: - - name: ${APPLICATION_NAME}-logfiles - persistentVolumeClaim: - claimName: ${APPLICATION_NAME}-logfiles - containers: - - name: ${APPLICATION_NAME} - image: ${IMAGEPULL_REGISTRY}/${IMAGEPULL_NAMESPACE}/${IMAGESTREAM_NAME}:${IMAGESTREAM_TAG} - imagePullPolicy: Always - env: - - name: ConnectionStrings__Default - value: >- - Host=$(UNITY_DB_HOST);port=$(UNITY_DB_PORT);Database=$(UNITY_POSTGRES_DB);Username=$(UNITY_POSTGRES_USER);Password=$(UNITY_POSTGRES_PASSWORD) - - name: ConnectionStrings__Tenant - value: >- - Host=$(UNITY_DB_HOST);port=$(UNITY_DB_PORT);Database=$(UNITY_TENANT_DB);Username=$(UNITY_POSTGRES_USER);Password=$(UNITY_POSTGRES_PASSWORD) - - name: Redis__Password - valueFrom: - secretKeyRef: - name: ${Redis__HostName} - key: database-password - envFrom: - - configMapRef: - name: ${APPLICATION_NAME} - - secretRef: - name: ${APPLICATION_NAME} - - secretRef: - name: ${DATABASE_SERVICE_NAME} - - configMapRef: - name: ${STORAGE_OBJECT_NAME} - - secretRef: - name: ${STORAGE_OBJECT_NAME} - resources: - requests: - cpu: ${CPU_REQUEST} - memory: ${MEMORY_REQUEST} - readinessProbe: - httpGet: - path: /healthz/ready - port: 8080 - scheme: HTTP - httpHeaders: - - name: content-type - value: text/plain - - name: readiness - value: healthy - timeoutSeconds: 5 - periodSeconds: 30 - successThreshold: 1 - failureThreshold: 3 - livenessProbe: - httpGet: - path: /healthz/live - port: 8080 - scheme: HTTP - httpHeaders: - - name: content-type - value: text/plain - initialDelaySeconds: 120 - timeoutSeconds: 5 - periodSeconds: 30 - successThreshold: 1 - failureThreshold: 3 - startupProbe: - httpGet: - path: /healthz/startup - port: 8080 - scheme: HTTP - httpHeaders: - - name: content-type - value: text/plain - initialDelaySeconds: 30 - timeoutSeconds: 1 - periodSeconds: 5 - successThreshold: 1 - failureThreshold: 12 - ports: - - containerPort: 443 - protocol: TCP - - containerPort: 80 - protocol: TCP - volumeMounts: - - mountPath: /app/logs - name: ${APPLICATION_NAME}-logfiles - restartPolicy: Always - terminationGracePeriodSeconds: 30 - dnsPolicy: ClusterFirst diff --git a/openshift/unity-image-puller.yaml b/openshift/unity-image-puller.yaml deleted file mode 100644 index 38a31fbea..000000000 --- a/openshift/unity-image-puller.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# oc import-image rhel9/postgresql-15:1-28.1697636666 --from=registry.redhat.io/rhel9/postgresql-15:1-28.1697636666 --confirm -kind: RoleBinding -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: 'system:image-puller' - namespace: ${PROJECT_NAMESPACE}-tools -subjects: - - kind: ServiceAccount - name: default - namespace: ${PROJECT_NAMESPACE}-dev - - kind: ServiceAccount - name: default - namespace: ${PROJECT_NAMESPACE}-test - - kind: ServiceAccount - name: default - namespace: ${PROJECT_NAMESPACE}-prod -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: 'system:image-puller' diff --git a/openshift/unity-imagestream.yaml b/openshift/unity-imagestream.yaml deleted file mode 100644 index e3351196c20a0efc658932decc8555c16a52efb6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3532 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 diff --git a/openshift/unity-networkpolicy.yaml b/openshift/unity-networkpolicy.yaml deleted file mode 100644 index 530bdd06e..000000000 --- a/openshift/unity-networkpolicy.yaml +++ /dev/null @@ -1,80 +0,0 @@ -apiVersion: template.openshift.io/v1 -kind: Template -metadata: - name: unity-networkpolicy - # This template uses a separate parameter .env file to override the default values defined in this section. - # oc process -f .\openshift\unity-network-policy.yaml --param-file=.env | oc create -f - - labels: - template: unity-networkpolicy - annotations: - description: |- - Template for communications rules in OpenShift. -parameters: -# Project namespace parameters -- description: The name of the application grouping. - displayName: Application Group - name: APPLICATION_GROUP - value: unity-grantmanager -- description: The name of the application. - displayName: Application Name - name: APPLICATION_NAME - required: true - value: unity-grantmanager-web -# Template objects to instantiate the project application. -objects: - - kind: NetworkPolicy - apiVersion: networking.k8s.io/v1 - metadata: - name: deny-by-default - labels: - app: ${APPLICATION_NAME} - app.kubernetes.io/component: ${APPLICATION_NAME} - app.kubernetes.io/instance: ${APPLICATION_NAME}-1 - app.kubernetes.io/name: ${APPLICATION_NAME} - app.kubernetes.io/part-of: ${APPLICATION_GROUP} - spec: - # The default posture for a security first namespace is to - # deny all traffic. If not added this rule will be added - # by Platform Services during environment cut-over. - podSelector: {} - ingress: [] - - apiVersion: networking.k8s.io/v1 - kind: NetworkPolicy - metadata: - name: allow-from-openshift-ingress - labels: - app: ${APPLICATION_NAME} - app.kubernetes.io/component: ${APPLICATION_NAME} - app.kubernetes.io/instance: ${APPLICATION_NAME}-1 - app.kubernetes.io/name: ${APPLICATION_NAME} - app.kubernetes.io/part-of: ${APPLICATION_GROUP} - spec: - # This policy allows any pod with a route & service combination - # to accept traffic from the OpenShift router pods. This is - # required for things outside of OpenShift (like the Internet) - # to reach your pods. - ingress: - - from: - - namespaceSelector: - matchLabels: - network.openshift.io/policy-group: ingress - podSelector: {} - policyTypes: - - Ingress - - kind: NetworkPolicy - apiVersion: networking.k8s.io/v1 - metadata: - name: allow-same-namespace - labels: - app: ${APPLICATION_NAME} - app.kubernetes.io/component: ${APPLICATION_NAME} - app.kubernetes.io/instance: ${APPLICATION_NAME}-1 - app.kubernetes.io/name: ${APPLICATION_NAME} - app.kubernetes.io/part-of: ${APPLICATION_GROUP} - spec: - # Allow all pods within the current namespace to communicate - # to one another. - podSelector: - ingress: - - from: - - podSelector: {} diff --git a/openshift/unity-rabbitmq.yaml b/openshift/unity-rabbitmq.yaml deleted file mode 100644 index 7b1da3782a096c7278977688dc9aac4380befa90..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16700 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 Ab^rhX diff --git a/openshift/unity-s3-object-storage.yaml b/openshift/unity-s3-object-storage.yaml deleted file mode 100644 index a3b8ffac9..000000000 --- a/openshift/unity-s3-object-storage.yaml +++ /dev/null @@ -1,94 +0,0 @@ -apiVersion: template.openshift.io/v1 -kind: Template -metadata: - name: unity-s3-object-storage - # This template uses a separate parameter .env file to override the default values defined in this section. - # oc process -f .\openshift\unity-s3-object-storage.yaml --param-file=.env | oc create -f - - labels: - template: unity-s3-object-storage - annotations: - description: |- - Template for S3 connection information in OpenShift. -parameters: -# Project namespace parameters -- description: The name of the application grouping. - displayName: Application Group - name: APPLICATION_GROUP - value: unity-grantmanager -- description: The name of the application. - displayName: Application Name - name: APPLICATION_NAME - required: true - value: unity-grantmanager-web -# Additional parameters for S3 object storage -- description: The name of the application. - displayName: Application Name - name: STORAGE_OBJECT_NAME - required: true - value: s3-object-storage -- name: AccessKeyID - displayName: "Access Key Login ID" - description: "The Access Key for S3 compatible object storage account" - from: '[A-Z0-9]{20}_default' - generate: expression -- name: BucketName - displayName: "Bucket Name" - description: "The object storage bucket name" - required: true - value: "econ-unity-dev" -- name: Endpoint - displayName: "API endpoint for S3 compatible storage account" - description: "Object store URL. eg: https://econ.objectstore.gov.bc.ca" - required: true - value: "https://econ.objectstore.gov.bc.ca" -- name: SecretKey - displayName: "Secret Key" - description: "S3 account Secret Access Key, similar to a password." - from: '[\w]{32}_default' - generate: expression -- name: ApplicationFolder - displayName: ApplicationFolder - description: "The object storage Application Folder name" - required: true - value: "Unity/Application" -- name: AssessmentFolder - displayName: AssessmentFolder - description: "The object storage Assessment Folder name" - required: true - value: "Unity/Adjudication" -# Template objects to instantiate the project. -objects: -# Secrets -- apiVersion: v1 - kind: Secret - metadata: - name: ${STORAGE_OBJECT_NAME} - labels: - app: ${APPLICATION_NAME} - app.kubernetes.io/component: ${APPLICATION_NAME} - app.kubernetes.io/instance: ${APPLICATION_NAME}-1 - app.kubernetes.io/name: ${APPLICATION_NAME} - app.kubernetes.io/part-of: ${APPLICATION_GROUP} - stringData: - S3__AccessKeyId: ${AccessKeyID} - S3__Bucket: ${BucketName} - S3__SecretAccessKey: ${SecretKey} - type: Opaque -# Configmap -- apiVersion: v1 - kind: ConfigMap - metadata: - name: ${STORAGE_OBJECT_NAME} - labels: - app: ${APPLICATION_NAME} - app.kubernetes.io/component: ${APPLICATION_NAME} - app.kubernetes.io/instance: ${APPLICATION_NAME}-1 - app.kubernetes.io/name: ${APPLICATION_NAME} - app.kubernetes.io/part-of: ${APPLICATION_GROUP} - data: - # Configuration values can be set as key-value properties - S3__Endpoint: ${Endpoint} - S3__ApplicationS3Folder: ${ApplicationFolder} - S3__AssessmentS3Folder: ${AssessmentFolder} - S3__DisallowedFileTypes: '[ "exe" , "sh" , "ksh" , "bat" , "cmd" ]' - S3__MaxFileSize: '25' diff --git a/openshift/unity-sysdig-team.yaml b/openshift/unity-sysdig-team.yaml deleted file mode 100644 index 6b99c636c..000000000 --- a/openshift/unity-sysdig-team.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: ops.gov.bc.ca/v1alpha1 -kind: SysdigTeam -metadata: - name: ${PROJECT_NAMESPACE}-sysdigteam - namespace: ${PROJECT_NAMESPACE}-tools -spec: - team: - description: The Sysdig Team for the OpenShift Project Set Unity - users: - - name: first.last@gov.bc.ca - role: ROLE_TEAM_EDIT - - name: first.last@gov.bc.ca - role: ROLE_TEAM_EDIT - - name: first.last@gov.bc.ca - role: ROLE_TEAM_EDIT From f276b66b68abd74c61fc86535c4a223d3aac94fd Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Wed, 25 Feb 2026 17:07:26 -0800 Subject: [PATCH 11/19] AB#32005 Simplifying scoresheet display handling --- .../AssessmentScoresWidgetViewComponent.cs | 38 +++---------------- 1 file changed, 5 insertions(+), 33 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/AssessmentScoresWidgetViewComponent.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/AssessmentScoresWidgetViewComponent.cs index 5ba221959..35236847d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/AssessmentScoresWidgetViewComponent.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/AssessmentScoresWidgetViewComponent.cs @@ -5,7 +5,6 @@ using Volo.Abp.AspNetCore.Mvc.UI.Bundling; using System; using System.Threading.Tasks; -using System.Globalization; using Unity.GrantManager.Assessments; using Unity.Flex.Domain.ScoresheetInstances; using Unity.Flex.Domain.Scoresheets; @@ -132,14 +131,16 @@ private static void ResolveAiAnswer(Dictionary aiAnswers, Q question.Answer = rawAnswer; } } - if (aiAnswerValue.TryGetProperty("rationale", out var rationaleProp) || - aiAnswerValue.TryGetProperty("citation", out rationaleProp)) + if (aiAnswerValue.TryGetProperty("rationale", out var rationaleProp)) { question.AICitation = rationaleProp.ToString(); } if (aiAnswerValue.TryGetProperty("confidence", out var confidenceProp)) { - question.AIConfidence = ParseAiConfidence(confidenceProp); + if (confidenceProp.TryGetInt32(out var confidence)) + { + question.AIConfidence = Math.Clamp(confidence, 0, 100); + } } } else @@ -239,35 +240,6 @@ private static string ConvertNumericAnswerToSelectListValue(string numericAnswer return numericAnswer; } - private static int ParseAiConfidence(JsonElement confidenceProp) - { - int confidence = 0; - - if (confidenceProp.ValueKind == JsonValueKind.Number) - { - if (confidenceProp.TryGetInt32(out var intValue)) - { - confidence = intValue; - } - else if (confidenceProp.TryGetDouble(out var doubleValue)) - { - confidence = (int)Math.Round(doubleValue, MidpointRounding.AwayFromZero); - } - } - else if (confidenceProp.ValueKind == JsonValueKind.String) - { - var raw = confidenceProp.GetString(); - if (!int.TryParse(raw, out confidence) && - double.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsedDouble)) - { - confidence = (int)Math.Round(parsedDouble, MidpointRounding.AwayFromZero); - } - } - - var rounded = (int)Math.Round(confidence / 5.0, MidpointRounding.AwayFromZero) * 5; - return Math.Clamp(rounded, 0, 100); - } - } public class AssessmentScoresWidgetStyleBundleContributor : BundleContributor From ac834c3422168d9beef79bbf1844bde9d5370f4d Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Wed, 25 Feb 2026 19:24:38 -0800 Subject: [PATCH 12/19] AB#31785: No gaps for UnityApplicationId Sequence Number --- .../Intakes/IntakeFormSubmissionManager.cs | 2 +- .../Applications/ISequenceRepository.cs | 4 +- ...dd_UnitySequenceCounters_Table.Designer.cs | 4571 +++++++++++++++++ ...6020846_Add_UnitySequenceCounters_Table.cs | 31 + ...pplicationId_And_Seed_Counters.Designer.cs | 4571 +++++++++++++++++ ...er_UnityApplicationId_And_Seed_Counters.cs | 109 + .../GrantTenantDbContextModelSnapshot.cs | 4 +- .../Repositories/SequenceRepository.cs | 119 +- 8 files changed, 9362 insertions(+), 49 deletions(-) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260226020846_Add_UnitySequenceCounters_Table.Designer.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260226020846_Add_UnitySequenceCounters_Table.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260226021054_Renumber_UnityApplicationId_And_Seed_Counters.Designer.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260226021054_Renumber_UnityApplicationId_And_Seed_Counters.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/IntakeFormSubmissionManager.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/IntakeFormSubmissionManager.cs index 858bf9640..1cdcd6b49 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/IntakeFormSubmissionManager.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/IntakeFormSubmissionManager.cs @@ -54,7 +54,7 @@ public async Task ProcessFormSubmissionAsync(ApplicationForm applicationFo intakeMap.SubmissionId = formSubmission.submission.id; intakeMap.SubmissionDate = formSubmission.submission.updatedAt; intakeMap.ConfirmationId = formSubmission.submission.confirmationId; - using var uow = _unitOfWorkManager.Begin(); + using var uow = _unitOfWorkManager.Begin(isTransactional: true);//transaction needed for sequence number generation (SequenceRepository) to ensure atomicity and consistency var application = await CreateNewApplicationAsync(intakeMap, applicationForm); await _intakeFormSubmissionMapper.SaveChefsFiles(formSubmission, application.Id); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ISequenceRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ISequenceRepository.cs index 13bca3efb..d03af8505 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ISequenceRepository.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ISequenceRepository.cs @@ -7,7 +7,9 @@ public interface ISequenceRepository : IRepository { ///

/// Gets the next sequential number for a given prefix within the current tenant. - /// Uses tenant-specific PostgreSQL sequences to ensure uniqueness. + /// Uses a table-based atomic counter (unity_sequence_counters) that participates + /// in the ambient EF Core transaction, ensuring gapless IDs on rollback. + /// Must be called within an active transaction — throws InvalidOperationException otherwise. /// /// The prefix for the sequence (e.g., "CGG-") /// The next sequential number for this tenant+prefix combination diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260226020846_Add_UnitySequenceCounters_Table.Designer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260226020846_Add_UnitySequenceCounters_Table.Designer.cs new file mode 100644 index 000000000..9bbc4bfc9 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260226020846_Add_UnitySequenceCounters_Table.Designer.cs @@ -0,0 +1,4571 @@ +// +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("20260226020246_Add_UnitySequenceCounters_Table")] + partial class Add_UnitySequenceCounters_Table + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.PostgreSql) + .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScoresheetId"); + + b.ToTable("ScoresheetInstances", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Answer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("QuestionId") + .HasColumnType("uuid"); + + b.Property("ScoresheetInstanceId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("QuestionId"); + + b.HasIndex("ScoresheetInstanceId"); + + b.ToTable("Answers", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("SectionId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("Questions", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Scoresheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Scoresheets", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ScoresheetId"); + + b.ToTable("ScoresheetSections", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.CustomFieldValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CustomFieldId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("WorksheetInstanceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetInstanceId"); + + b.ToTable("CustomFieldValues", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UiAnchor") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetCorrelationId") + .HasColumnType("uuid"); + + b.Property("WorksheetCorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("WorksheetInstances", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetLinks.WorksheetLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UiAnchor") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetId"); + + b.ToTable("WorksheetLinks", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.CustomField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("SectionId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("CustomFields", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.Worksheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Worksheets", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetId"); + + b.ToTable("WorksheetSections", "Flex"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Applicant", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantName") + .IsRequired() + .HasMaxLength(600) + .HasColumnType("character varying(600)"); + + b.Property("ApproxNumberOfEmployees") + .HasColumnType("text"); + + b.Property("BusinessNumber") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FiscalDay") + .HasColumnType("integer"); + + b.Property("FiscalMonth") + .HasColumnType("text"); + + b.Property("IndigenousOrgInd") + .HasColumnType("text"); + + b.Property("IsDuplicated") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MatchPercentage") + .HasColumnType("numeric"); + + b.Property("NonRegOrgName") + .HasColumnType("text"); + + b.Property("NonRegisteredBusinessName") + .HasColumnType("text"); + + b.Property("OrgName") + .HasColumnType("text"); + + b.Property("OrgNumber") + .HasColumnType("text"); + + b.Property("OrgStatus") + .HasColumnType("text"); + + b.Property("OrganizationSize") + .HasColumnType("text"); + + b.Property("OrganizationType") + .HasColumnType("text"); + + b.Property("RedStop") + .HasColumnType("boolean"); + + b.Property("Sector") + .HasColumnType("text"); + + b.Property("SectorSubSectorIndustryDesc") + .HasColumnType("text"); + + b.Property("SiteId") + .HasColumnType("uuid"); + + b.Property("StartedOperatingDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("SubSector") + .HasColumnType("text"); + + b.Property("SupplierId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UnityApplicantId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantName"); + + b.ToTable("Applicants", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAddress", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AddressType") + .HasColumnType("integer"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Postal") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("Street") + .HasColumnType("text"); + + b.Property("Street2") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Unit") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicantAddresses", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAgent", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("BceidBusinessGuid") + .HasColumnType("uuid"); + + b.Property("BceidBusinessName") + .HasColumnType("text"); + + b.Property("BceidUserGuid") + .HasColumnType("uuid"); + + b.Property("BceidUserName") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactOrder") + .HasColumnType("integer"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IdentityEmail") + .HasColumnType("text"); + + b.Property("IdentityName") + .HasColumnType("text"); + + b.Property("IdentityProvider") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsConfirmed") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OidcSubUser") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("Phone2") + .HasColumnType("text"); + + b.Property("Phone2Extension") + .HasColumnType("text"); + + b.Property("PhoneExtension") + .HasColumnType("text"); + + b.Property("RoleForApplicant") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationId") + .IsUnique(); + + b.ToTable("ApplicantAgents", (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("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("DueDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DueDiligenceStatus") + .HasColumnType("text"); + + b.Property("EconomicRegion") + .HasColumnType("text"); + + b.Property("ElectoralDistrict") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FinalDecisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Forestry") + .HasColumnType("text"); + + b.Property("ForestryFocus") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LikelihoodOfFunding") + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("NotificationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Payload") + .HasColumnType("jsonb"); + + b.Property("PercentageTotalProjectBudget") + .HasColumnType("double precision"); + + b.Property("Place") + .HasColumnType("text"); + + b.Property("ProjectEndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ProjectFundingTotal") + .HasColumnType("numeric"); + + b.Property("ProjectName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProjectStartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ProjectSummary") + .HasColumnType("text"); + + b.Property("ProposalDate") + .HasColumnType("timestamp without time zone"); + + b.Property("RecommendedAmount") + .HasColumnType("numeric"); + + b.Property("ReferenceNo") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegionalDistrict") + .HasColumnType("text"); + + b.Property("RequestedAmount") + .HasColumnType("numeric"); + + b.Property("RiskRanking") + .HasColumnType("text"); + + b.Property("SigningAuthorityBusinessPhone") + .HasColumnType("text"); + + b.Property("SigningAuthorityCellPhone") + .HasColumnType("text"); + + b.Property("SigningAuthorityEmail") + .HasColumnType("text"); + + b.Property("SigningAuthorityFullName") + .HasColumnType("text"); + + b.Property("SigningAuthorityTitle") + .HasColumnType("text"); + + b.Property("SubStatus") + .HasColumnType("text"); + + b.Property("SubmissionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TotalProjectBudget") + .HasColumnType("numeric"); + + b.Property("TotalScore") + .HasColumnType("integer"); + + b.Property("UnityApplicationId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationFormId"); + + b.HasIndex("ApplicationStatusId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Applications", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAssignment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AssigneeId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Duty") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("AssigneeId"); + + b.ToTable("ApplicationAssignments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationChefsFileAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AISummary") + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ChefsFileId") + .HasColumnType("text"); + + b.Property("ChefsSubmissionId") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationChefsFileAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationContact", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactEmail") + .HasColumnType("text"); + + b.Property("ContactFullName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactMobilePhone") + .HasColumnType("text"); + + b.Property("ContactTitle") + .HasColumnType("text"); + + b.Property("ContactType") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactWorkPhone") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationContact", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationForm", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountCodingId") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .HasColumnType("text"); + + b.Property("ApplicationFormDescription") + .HasColumnType("text"); + + b.Property("ApplicationFormName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AttemptedConnectionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("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("ParentFormId") + .HasColumnType("uuid"); + + b.Property("Payable") + .HasColumnType("boolean"); + + b.Property("PaymentApprovalThreshold") + .HasColumnType("numeric"); + + b.Property("Prefix") + .HasColumnType("text"); + + b.Property("PreventPayment") + .HasColumnType("boolean"); + + b.Property("RenderFormIoToHtml") + .HasColumnType("boolean"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("SuffixType") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IntakeId"); + + b.HasIndex("ParentFormId"); + + b.ToTable("ApplicationForms", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormSubmission", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormVersionId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ChefsSubmissionGuid") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormVersionId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OidcSub") + .IsRequired() + .HasColumnType("text"); + + b.Property("RenderedHTML") + .HasColumnType("text"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Submission") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormSubmissions", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormVersion", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("AvailableChefsFields") + .HasColumnType("text"); + + b.Property("ChefsApplicationFormGuid") + .HasColumnType("text"); + + b.Property("ChefsFormVersionGuid") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormSchema") + .HasColumnType("jsonb"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SubmissionHeaderMapping") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormVersion", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationLink", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LinkType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("Related"); + + b.Property("LinkedApplicationId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationLinks", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationStatus", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExternalStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("InternalStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("StatusCode") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("StatusCode") + .IsUnique(); + + b.ToTable("ApplicationStatuses", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationTags", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TagId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("TagId"); + + b.ToTable("ApplicationTags", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AssessmentAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AssessmentId"); + + b.ToTable("AssessmentAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Assessments.Assessment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ApprovalRecommended") + .HasColumnType("boolean"); + + b.Property("AssessorId") + .HasColumnType("uuid"); + + b.Property("CleanGrowth") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("EconomicImpact") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FinancialAnalysis") + .HasColumnType("integer"); + + b.Property("InclusiveGrowth") + .HasColumnType("integer"); + + b.Property("IsComplete") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("AssessorId"); + + b.ToTable("Assessments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicationComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("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("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastUpdatedInCAS") + .HasColumnType("timestamp without time zone"); + + b.Property("MailingAddress") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("text"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("SIN") + .HasColumnType("text"); + + b.Property("StandardIndustryClassification") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("Subcategory") + .HasColumnType("text"); + + b.Property("SupplierProtected") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Suppliers", "Payments"); + }); + + modelBuilder.Entity("Unity.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.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.Assessments.Assessment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("Assessments") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("AssessorId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicationComment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.AssessmentComment", b => + { + b.HasOne("Unity.GrantManager.Assessments.Assessment", null) + .WithMany() + .HasForeignKey("AssessmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.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/20260226020846_Add_UnitySequenceCounters_Table.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260226020846_Add_UnitySequenceCounters_Table.cs new file mode 100644 index 000000000..78ac7c5b0 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260226020846_Add_UnitySequenceCounters_Table.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + /// + public partial class Add_UnitySequenceCounters_Table : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // PRIMARY KEY on (tenant_id, prefix) enforces uniqueness and provides the index + // required by the ON CONFLICT clause in the upsert counter query. + migrationBuilder.Sql(@" + CREATE TABLE IF NOT EXISTS ""unity_sequence_counters"" ( + ""tenant_id"" UUID NOT NULL, + ""prefix"" TEXT NOT NULL, + ""current_value"" BIGINT NOT NULL DEFAULT 0, + CONSTRAINT ""PK_unity_sequence_counters"" PRIMARY KEY (""tenant_id"", ""prefix"") + ); + "); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@"DROP TABLE IF EXISTS ""unity_sequence_counters"";"); + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260226021054_Renumber_UnityApplicationId_And_Seed_Counters.Designer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260226021054_Renumber_UnityApplicationId_And_Seed_Counters.Designer.cs new file mode 100644 index 000000000..036802c5f --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260226021054_Renumber_UnityApplicationId_And_Seed_Counters.Designer.cs @@ -0,0 +1,4571 @@ +// +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("20260226020620_Renumber_UnityApplicationId_And_Seed_Counters")] + partial class Renumber_UnityApplicationId_And_Seed_Counters + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.PostgreSql) + .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScoresheetId"); + + b.ToTable("ScoresheetInstances", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Answer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("QuestionId") + .HasColumnType("uuid"); + + b.Property("ScoresheetInstanceId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("QuestionId"); + + b.HasIndex("ScoresheetInstanceId"); + + b.ToTable("Answers", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("SectionId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("Questions", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Scoresheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Scoresheets", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ScoresheetId"); + + b.ToTable("ScoresheetSections", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.CustomFieldValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CustomFieldId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("WorksheetInstanceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetInstanceId"); + + b.ToTable("CustomFieldValues", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UiAnchor") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetCorrelationId") + .HasColumnType("uuid"); + + b.Property("WorksheetCorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("WorksheetInstances", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetLinks.WorksheetLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UiAnchor") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetId"); + + b.ToTable("WorksheetLinks", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.CustomField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("SectionId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("CustomFields", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.Worksheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Worksheets", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetId"); + + b.ToTable("WorksheetSections", "Flex"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Applicant", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantName") + .IsRequired() + .HasMaxLength(600) + .HasColumnType("character varying(600)"); + + b.Property("ApproxNumberOfEmployees") + .HasColumnType("text"); + + b.Property("BusinessNumber") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FiscalDay") + .HasColumnType("integer"); + + b.Property("FiscalMonth") + .HasColumnType("text"); + + b.Property("IndigenousOrgInd") + .HasColumnType("text"); + + b.Property("IsDuplicated") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MatchPercentage") + .HasColumnType("numeric"); + + b.Property("NonRegOrgName") + .HasColumnType("text"); + + b.Property("NonRegisteredBusinessName") + .HasColumnType("text"); + + b.Property("OrgName") + .HasColumnType("text"); + + b.Property("OrgNumber") + .HasColumnType("text"); + + b.Property("OrgStatus") + .HasColumnType("text"); + + b.Property("OrganizationSize") + .HasColumnType("text"); + + b.Property("OrganizationType") + .HasColumnType("text"); + + b.Property("RedStop") + .HasColumnType("boolean"); + + b.Property("Sector") + .HasColumnType("text"); + + b.Property("SectorSubSectorIndustryDesc") + .HasColumnType("text"); + + b.Property("SiteId") + .HasColumnType("uuid"); + + b.Property("StartedOperatingDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("SubSector") + .HasColumnType("text"); + + b.Property("SupplierId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UnityApplicantId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantName"); + + b.ToTable("Applicants", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAddress", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AddressType") + .HasColumnType("integer"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Postal") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("Street") + .HasColumnType("text"); + + b.Property("Street2") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Unit") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicantAddresses", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAgent", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("BceidBusinessGuid") + .HasColumnType("uuid"); + + b.Property("BceidBusinessName") + .HasColumnType("text"); + + b.Property("BceidUserGuid") + .HasColumnType("uuid"); + + b.Property("BceidUserName") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactOrder") + .HasColumnType("integer"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IdentityEmail") + .HasColumnType("text"); + + b.Property("IdentityName") + .HasColumnType("text"); + + b.Property("IdentityProvider") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsConfirmed") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OidcSubUser") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("Phone2") + .HasColumnType("text"); + + b.Property("Phone2Extension") + .HasColumnType("text"); + + b.Property("PhoneExtension") + .HasColumnType("text"); + + b.Property("RoleForApplicant") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationId") + .IsUnique(); + + b.ToTable("ApplicantAgents", (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("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("DueDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DueDiligenceStatus") + .HasColumnType("text"); + + b.Property("EconomicRegion") + .HasColumnType("text"); + + b.Property("ElectoralDistrict") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FinalDecisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Forestry") + .HasColumnType("text"); + + b.Property("ForestryFocus") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LikelihoodOfFunding") + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("NotificationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Payload") + .HasColumnType("jsonb"); + + b.Property("PercentageTotalProjectBudget") + .HasColumnType("double precision"); + + b.Property("Place") + .HasColumnType("text"); + + b.Property("ProjectEndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ProjectFundingTotal") + .HasColumnType("numeric"); + + b.Property("ProjectName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProjectStartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ProjectSummary") + .HasColumnType("text"); + + b.Property("ProposalDate") + .HasColumnType("timestamp without time zone"); + + b.Property("RecommendedAmount") + .HasColumnType("numeric"); + + b.Property("ReferenceNo") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegionalDistrict") + .HasColumnType("text"); + + b.Property("RequestedAmount") + .HasColumnType("numeric"); + + b.Property("RiskRanking") + .HasColumnType("text"); + + b.Property("SigningAuthorityBusinessPhone") + .HasColumnType("text"); + + b.Property("SigningAuthorityCellPhone") + .HasColumnType("text"); + + b.Property("SigningAuthorityEmail") + .HasColumnType("text"); + + b.Property("SigningAuthorityFullName") + .HasColumnType("text"); + + b.Property("SigningAuthorityTitle") + .HasColumnType("text"); + + b.Property("SubStatus") + .HasColumnType("text"); + + b.Property("SubmissionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TotalProjectBudget") + .HasColumnType("numeric"); + + b.Property("TotalScore") + .HasColumnType("integer"); + + b.Property("UnityApplicationId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationFormId"); + + b.HasIndex("ApplicationStatusId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Applications", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAssignment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AssigneeId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Duty") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("AssigneeId"); + + b.ToTable("ApplicationAssignments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationChefsFileAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AISummary") + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ChefsFileId") + .HasColumnType("text"); + + b.Property("ChefsSubmissionId") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationChefsFileAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationContact", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactEmail") + .HasColumnType("text"); + + b.Property("ContactFullName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactMobilePhone") + .HasColumnType("text"); + + b.Property("ContactTitle") + .HasColumnType("text"); + + b.Property("ContactType") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactWorkPhone") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationContact", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationForm", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountCodingId") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .HasColumnType("text"); + + b.Property("ApplicationFormDescription") + .HasColumnType("text"); + + b.Property("ApplicationFormName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AttemptedConnectionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("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("ParentFormId") + .HasColumnType("uuid"); + + b.Property("Payable") + .HasColumnType("boolean"); + + b.Property("PaymentApprovalThreshold") + .HasColumnType("numeric"); + + b.Property("Prefix") + .HasColumnType("text"); + + b.Property("PreventPayment") + .HasColumnType("boolean"); + + b.Property("RenderFormIoToHtml") + .HasColumnType("boolean"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("SuffixType") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IntakeId"); + + b.HasIndex("ParentFormId"); + + b.ToTable("ApplicationForms", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormSubmission", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormVersionId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ChefsSubmissionGuid") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormVersionId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OidcSub") + .IsRequired() + .HasColumnType("text"); + + b.Property("RenderedHTML") + .HasColumnType("text"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Submission") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormSubmissions", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormVersion", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("AvailableChefsFields") + .HasColumnType("text"); + + b.Property("ChefsApplicationFormGuid") + .HasColumnType("text"); + + b.Property("ChefsFormVersionGuid") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormSchema") + .HasColumnType("jsonb"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SubmissionHeaderMapping") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormVersion", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationLink", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LinkType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("Related"); + + b.Property("LinkedApplicationId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationLinks", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationStatus", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExternalStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("InternalStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("StatusCode") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("StatusCode") + .IsUnique(); + + b.ToTable("ApplicationStatuses", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationTags", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TagId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("TagId"); + + b.ToTable("ApplicationTags", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AssessmentAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AssessmentId"); + + b.ToTable("AssessmentAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Assessments.Assessment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ApprovalRecommended") + .HasColumnType("boolean"); + + b.Property("AssessorId") + .HasColumnType("uuid"); + + b.Property("CleanGrowth") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("EconomicImpact") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FinancialAnalysis") + .HasColumnType("integer"); + + b.Property("InclusiveGrowth") + .HasColumnType("integer"); + + b.Property("IsComplete") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("AssessorId"); + + b.ToTable("Assessments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicationComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("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("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastUpdatedInCAS") + .HasColumnType("timestamp without time zone"); + + b.Property("MailingAddress") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("text"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("SIN") + .HasColumnType("text"); + + b.Property("StandardIndustryClassification") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("Subcategory") + .HasColumnType("text"); + + b.Property("SupplierProtected") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Suppliers", "Payments"); + }); + + modelBuilder.Entity("Unity.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.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.Assessments.Assessment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("Assessments") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("AssessorId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicationComment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.AssessmentComment", b => + { + b.HasOne("Unity.GrantManager.Assessments.Assessment", null) + .WithMany() + .HasForeignKey("AssessmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.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/20260226021054_Renumber_UnityApplicationId_And_Seed_Counters.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260226021054_Renumber_UnityApplicationId_And_Seed_Counters.cs new file mode 100644 index 000000000..487d80375 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260226021054_Renumber_UnityApplicationId_And_Seed_Counters.cs @@ -0,0 +1,109 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + /// + public partial class Renumber_UnityApplicationId_And_Seed_Counters : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // A: Renumber existing sequential UnityApplicationIds to fill historical gaps. + // + // Partitioned by (TenantId, Prefix) — tenants sharing a schema get separate, + // independent sequences. Prefix comes from the ApplicationForm definition + // (not inferred from the ID string) to avoid regex metacharacter issues. + // Only SuffixType = 1 (SequentialNumber) forms are touched. + // Stable ordering: original suffix number → CreationTime → Id. + // Malformed IDs (non-numeric suffix) are left untouched. + migrationBuilder.Sql(@" + WITH base AS ( + SELECT + a.""Id"", + COALESCE(a.""TenantId"", '00000000-0000-0000-0000-000000000000'::UUID) AS tenant_id, + af.""Prefix"", + a.""CreationTime"", + SUBSTRING(a.""UnityApplicationId"" FROM CHAR_LENGTH(af.""Prefix"") + 1) AS suffix + FROM ""Applications"" a + JOIN ""ApplicationForms"" af ON a.""ApplicationFormId"" = af.""Id"" + WHERE af.""SuffixType"" = 1 + AND af.""Prefix"" IS NOT NULL + AND af.""Prefix"" <> '' + AND a.""UnityApplicationId"" IS NOT NULL + AND LEFT(a.""UnityApplicationId"", CHAR_LENGTH(af.""Prefix"")) = af.""Prefix"" + ), + valid AS ( + SELECT + ""Id"", + tenant_id, + ""Prefix"", + ""CreationTime"", + suffix::BIGINT AS old_seq + FROM base + WHERE suffix ~ '^[0-9]+$' + ), + ranked AS ( + SELECT + ""Id"", + ""Prefix"", + ROW_NUMBER() OVER ( + PARTITION BY tenant_id, ""Prefix"" + ORDER BY old_seq, ""CreationTime"", ""Id"" + ) AS new_seq + FROM valid + ) + UPDATE ""Applications"" a + SET ""UnityApplicationId"" = + r.""Prefix"" || LPAD(r.new_seq::TEXT, GREATEST(5, LENGTH(r.new_seq::TEXT)), '0') + FROM ranked r + WHERE a.""Id"" = r.""Id""; + "); + + + // B: Seed unity_sequence_counters from the post-renumber maximum. + // + // LEFT JOIN from ApplicationForms so that every (tenant, prefix) combination that + // is configured for sequential numbering gets a counter row — even forms that have + // zero applications yet. Those are seeded with current_value = 0, so the first + // real upsert increments to 1 without a gap. + migrationBuilder.Sql(@" + WITH parsed AS ( + SELECT + COALESCE(a.""TenantId"", af.""TenantId"", '00000000-0000-0000-0000-000000000000'::UUID) AS tenant_id, + af.""Prefix"" AS prefix, + CASE + WHEN a.""UnityApplicationId"" IS NOT NULL + AND LEFT(a.""UnityApplicationId"", CHAR_LENGTH(af.""Prefix"")) = af.""Prefix"" + AND SUBSTRING(a.""UnityApplicationId"" FROM CHAR_LENGTH(af.""Prefix"") + 1) ~ '^[0-9]+$' + THEN CAST(SUBSTRING(a.""UnityApplicationId"" FROM CHAR_LENGTH(af.""Prefix"") + 1) AS BIGINT) + ELSE NULL + END AS seq + FROM ""ApplicationForms"" af + LEFT JOIN ""Applications"" a ON a.""ApplicationFormId"" = af.""Id"" + WHERE af.""SuffixType"" = 1 + AND af.""Prefix"" IS NOT NULL + AND af.""Prefix"" <> '' + ) + INSERT INTO ""unity_sequence_counters"" (""tenant_id"", ""prefix"", ""current_value"") + SELECT tenant_id, prefix, COALESCE(MAX(seq), 0) + FROM parsed + GROUP BY tenant_id, prefix + ON CONFLICT (""tenant_id"", ""prefix"") DO UPDATE + SET ""current_value"" = GREATEST( + ""unity_sequence_counters"".""current_value"", + EXCLUDED.""current_value"" + ); + "); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + // Counter rows are removed; un-renumbering IDs is not supported. + // Full rollback requires restoring from a pre-migration backup. + migrationBuilder.Sql(@"DELETE FROM ""unity_sequence_counters"";"); + } + } +} 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 fd2aa8373..be337f9d1 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 @@ -4280,7 +4280,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationLink", b => { b.HasOne("Unity.GrantManager.Applications.Application", null) - .WithMany() + .WithMany("ApplicationLinks") .HasForeignKey("ApplicationId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -4539,6 +4539,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("ApplicationAssignments"); + b.Navigation("ApplicationLinks"); + b.Navigation("ApplicationTags"); b.Navigation("Assessments"); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/SequenceRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/SequenceRepository.cs index ae6e6d3ea..e6c29e5f4 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/SequenceRepository.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/SequenceRepository.cs @@ -1,83 +1,110 @@ using System; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.Logging; using Npgsql; using Unity.GrantManager.Applications; using Unity.GrantManager.EntityFrameworkCore; using Volo.Abp.Domain.Repositories.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore; -using Volo.Abp.Uow; namespace Unity.GrantManager.Repositories; -public class SequenceRepository(IDbContextProvider dbContextProvider, IUnitOfWorkManager unitOfWorkManager) : EfCoreRepository(dbContextProvider), ISequenceRepository +public class SequenceRepository(IDbContextProvider dbContextProvider) + : EfCoreRepository(dbContextProvider), ISequenceRepository { - public async Task GetNextSequenceNumberAsync(string prefix) { var tenantId = CurrentTenant.Id ?? Guid.Empty; - - try + + var dbContext = await GetDbContextAsync(); + + var currentTransaction = dbContext.Database.CurrentTransaction + ?? throw new InvalidOperationException( + $"GetNextSequenceNumberAsync requires an active ambient transaction. " + + $"TenantId: {tenantId}, Prefix: {prefix}"); + + var npgsqlTransaction = currentTransaction.GetDbTransaction() as NpgsqlTransaction + ?? throw new InvalidOperationException( + "The current database transaction is not an NpgsqlTransaction."); + + var schema = dbContext.Model.GetDefaultSchema(); + if (string.IsNullOrEmpty(schema)) { - // Create a new isolated unit of work to prevent transaction pollution - using var uow = unitOfWorkManager.Begin( - requiresNew: true, - isTransactional: true - ); - - var dbContext = await GetDbContextAsync(); - var connection = dbContext.Database.GetDbConnection(); - - var schema = dbContext.Model.GetDefaultSchema(); - - if (string.IsNullOrEmpty(schema)) - { - // Use 'public' as default for PostgreSQL if no schema is configured - schema = "public"; - } + // Use 'public' as default for PostgreSQL if no schema is configured + schema = "public"; + } + var commandBuilder = new NpgsqlCommandBuilder(); + var safeSchema = commandBuilder.QuoteIdentifier(schema); - var commandBuilder = new NpgsqlCommandBuilder(); - var safeSchema = commandBuilder.QuoteIdentifier(schema); - - // Build SQL command with properly quoted schema identifier to prevent SQL injection - var sqlCommand = string.Format("SELECT {0}.get_next_sequence_number(@tenantId, @prefix);", safeSchema); - - using var command = connection.CreateCommand(); - // Schema is sanitized via QuoteIdentifier, parameters are properly parameterized + // Schema is sanitized via NpgsqlCommandBuilder.QuoteIdentifier; table name is a known + // lowercase constant that requires no quoting. #pragma warning disable S2077 // SQL queries should not be dynamically formatted - command.CommandText = sqlCommand; + const string sql = @" + INSERT INTO {0}.unity_sequence_counters (tenant_id, prefix, current_value) + VALUES (@tenantId, @prefix, 1) + ON CONFLICT (tenant_id, prefix) DO UPDATE + SET current_value = unity_sequence_counters.current_value + 1 + RETURNING current_value;"; #pragma warning restore S2077 + + var sqlWithSchema = string.Format(sql, safeSchema); + + // Use a SAVEPOINT so that a DB error during the upsert rolls back only the counter + // statement, leaving the outer transaction alive. This preserves graceful degradation: + // the caller catches the re-thrown exception and continues with UnityApplicationId = null. + const string savepointName = "unity_seq_counter"; + await npgsqlTransaction.SaveAsync(savepointName); + + try + { + var connection = dbContext.Database.GetDbConnection(); + using var command = connection.CreateCommand(); + command.CommandText = sqlWithSchema; + command.Transaction = npgsqlTransaction; command.Parameters.Add(new NpgsqlParameter("tenantId", tenantId)); command.Parameters.Add(new NpgsqlParameter("prefix", prefix)); - - if (connection.State != System.Data.ConnectionState.Open) - { - await connection.OpenAsync(); - } - + var result = await command.ExecuteScalarAsync(); - var sequenceNumber = (long)(result ?? 1L); - + var sequenceNumber = Convert.ToInt64(result ?? 1L); + + await npgsqlTransaction.ReleaseAsync(savepointName); + Logger.LogInformation( "Successfully generated sequence number {SequenceNumber} for prefix {Prefix} in tenant {TenantId}", sequenceNumber, prefix, tenantId); - - await uow.CompleteAsync(); + return sequenceNumber; } catch (Exception ex) { - Logger.LogError(ex, - "Failed to execute get_next_sequence_number function. " + + try + { + // ROLLBACK TO SAVEPOINT recovers the outer transaction from the aborted state. + // RELEASE cleans up the savepoint so it can be reused if this method is called + // again within the same transaction. + await npgsqlTransaction.RollbackAsync(savepointName); + await npgsqlTransaction.ReleaseAsync(savepointName); + } + catch (Exception rollbackEx) + { + Logger.LogError(rollbackEx, + "Failed to rollback to savepoint '{SavepointName}'. " + + "The outer transaction may be in an invalid state.", + savepointName); + } + + Logger.LogError(ex, + "Failed to execute sequence counter upsert. " + "TenantId: {TenantId}, Prefix: {Prefix}. " + - "This error is isolated and will not affect the main transaction.", + "Outer transaction remains alive via savepoint rollback.", tenantId, prefix); - // throw to be handled by the caller's graceful degradation + // Re-throw so the caller's graceful degradation (null UnityApplicationId) kicks in. throw new InvalidOperationException( - $"Failed to generate sequence number for tenant '{tenantId}' with prefix '{prefix}'. ", + $"Failed to generate sequence number for tenant '{tenantId}' with prefix '{prefix}'.", ex); } } -} \ No newline at end of file +} From b604f82add6672ac48cc04ed9ee9e93bcdcfbdc1 Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Wed, 25 Feb 2026 19:45:40 -0800 Subject: [PATCH 13/19] AB#31785: Fix sonarqube issue --- .../Repositories/SequenceRepository.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/SequenceRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/SequenceRepository.cs index e6c29e5f4..34b740a91 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/SequenceRepository.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/SequenceRepository.cs @@ -39,15 +39,14 @@ public async Task GetNextSequenceNumberAsync(string prefix) var safeSchema = commandBuilder.QuoteIdentifier(schema); // Schema is sanitized via NpgsqlCommandBuilder.QuoteIdentifier; table name is a known - // lowercase constant that requires no quoting. -#pragma warning disable S2077 // SQL queries should not be dynamically formatted + // lowercase constant that requires no quoting. All user-supplied values (tenantId, prefix) + // are passed as parameters, not interpolated into the SQL string. const string sql = @" INSERT INTO {0}.unity_sequence_counters (tenant_id, prefix, current_value) VALUES (@tenantId, @prefix, 1) ON CONFLICT (tenant_id, prefix) DO UPDATE SET current_value = unity_sequence_counters.current_value + 1 RETURNING current_value;"; -#pragma warning restore S2077 var sqlWithSchema = string.Format(sql, safeSchema); @@ -61,7 +60,9 @@ ON CONFLICT (tenant_id, prefix) DO UPDATE { var connection = dbContext.Database.GetDbConnection(); using var command = connection.CreateCommand(); +#pragma warning disable S2077 // Schema identifier is sanitized via NpgsqlCommandBuilder.QuoteIdentifier; all user values are parameterized command.CommandText = sqlWithSchema; +#pragma warning restore S2077 command.Transaction = npgsqlTransaction; command.Parameters.Add(new NpgsqlParameter("tenantId", tenantId)); command.Parameters.Add(new NpgsqlParameter("prefix", prefix)); From cb629b5deeed50ec2abb55a596d589a21a6a5336 Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Wed, 25 Feb 2026 20:29:37 -0800 Subject: [PATCH 14/19] AB#31785: Limit UnityApplicationId Prefix to 100 characters --- .../Applications/ApplicationForm.cs | 1 + .../ApplicationFormConfigWidget/Default.cshtml | 11 ++++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationForm.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationForm.cs index b60d8264f..a7324e867 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationForm.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationForm.cs @@ -33,6 +33,7 @@ public class ApplicationForm : FullAuditedAggregateRoot, IMultiTenant public Guid? ParentFormId { get; set; } public bool RenderFormIoToHtml { get; set; } = false; public bool IsDirectApproval { get; set; } = false; + [MaxLength(100)] public string? Prefix { get; set; } public SuffixConfigType? SuffixType { get; set; } public static List<(SuffixConfigType SuffixType, string DisplayName)> GetAvailableSuffixTypes() diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationFormConfigWidget/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationFormConfigWidget/Default.cshtml index b8b339ebf..a37d8d733 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationFormConfigWidget/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationFormConfigWidget/Default.cshtml @@ -57,11 +57,12 @@
-
From 45bd0828b6d5fc7040937d4a976c46801f0d16f6 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 20 Feb 2026 17:02:23 -0800 Subject: [PATCH 15/19] AB#32007 Add PDF text extraction support in TextExtractionService --- .../AI/TextExtractionService.cs | 51 ++++++++++++++++--- .../Unity.GrantManager.Application.csproj | 3 +- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/TextExtractionService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/TextExtractionService.cs index 28d5af2b4..b73800d7d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/TextExtractionService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/TextExtractionService.cs @@ -3,12 +3,14 @@ using System.IO; using System.Text; using System.Threading.Tasks; +using UglyToad.PdfPig; using Volo.Abp.DependencyInjection; namespace Unity.GrantManager.AI { public class TextExtractionService : ITextExtractionService, ITransientDependency { + private const int MaxExtractedTextLength = 50000; private readonly ILogger _logger; public TextExtractionService(ILogger logger) @@ -43,9 +45,7 @@ public async Task ExtractTextAsync(string fileName, byte[] fileContent, // Handle PDF files if (normalizedContentType.Contains("pdf") || extension == ".pdf") { - // For now, return empty string - can be enhanced with PDF parsing library - _logger.LogDebug("PDF text extraction not yet implemented for {FileName}", fileName); - return string.Empty; + return await Task.FromResult(ExtractTextFromPdfFile(fileName, fileContent)); } // Handle Word documents @@ -97,12 +97,11 @@ private async Task ExtractTextFromTextFileAsync(byte[] fileContent) text = Encoding.ASCII.GetString(fileContent); } - // Limit the extracted text to a reasonable size (e.g., first 50,000 characters) - const int maxLength = 50000; - if (text.Length > maxLength) + // Limit the extracted text to a reasonable size. + if (text.Length > MaxExtractedTextLength) { - text = text.Substring(0, maxLength); - _logger.LogDebug("Truncated text content to {MaxLength} characters", maxLength); + text = text.Substring(0, MaxExtractedTextLength); + _logger.LogDebug("Truncated text content to {MaxLength} characters", MaxExtractedTextLength); } return await Task.FromResult(text); @@ -113,5 +112,41 @@ private async Task ExtractTextFromTextFileAsync(byte[] fileContent) return string.Empty; } } + + private string ExtractTextFromPdfFile(string fileName, byte[] fileContent) + { + try + { + using var stream = new MemoryStream(fileContent, writable: false); + using var document = PdfDocument.Open(stream); + var builder = new StringBuilder(); + + foreach (var page in document.GetPages()) + { + if (builder.Length >= MaxExtractedTextLength) + { + break; + } + + if (!string.IsNullOrWhiteSpace(page.Text)) + { + builder.AppendLine(page.Text); + } + } + + var text = builder.ToString(); + if (text.Length > MaxExtractedTextLength) + { + text = text.Substring(0, MaxExtractedTextLength); + } + + return text; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "PDF text extraction failed for {FileName}", fileName); + return string.Empty; + } + } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Unity.GrantManager.Application.csproj b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Unity.GrantManager.Application.csproj index ecb9a894a..8ec3e53bc 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Unity.GrantManager.Application.csproj +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Unity.GrantManager.Application.csproj @@ -30,8 +30,9 @@ - + + From 8a6bc901129b6f7f947a5d9abe582d205a9eea15 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Thu, 26 Feb 2026 11:03:55 -0800 Subject: [PATCH 16/19] AB#32007 Text extraction post processing --- .../AI/TextExtractionService.cs | 85 ++++++++++++++++++- 1 file changed, 83 insertions(+), 2 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/TextExtractionService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/TextExtractionService.cs index b73800d7d..3c2b3f2b3 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/TextExtractionService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/TextExtractionService.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; using UglyToad.PdfPig; using Volo.Abp.DependencyInjection; @@ -32,6 +33,8 @@ public async Task ExtractTextAsync(string fileName, byte[] fileContent, var normalizedContentType = contentType?.ToLowerInvariant() ?? string.Empty; var extension = Path.GetExtension(fileName)?.ToLowerInvariant() ?? string.Empty; + string rawText; + // Handle text-based files if (normalizedContentType.Contains("text/") || extension == ".txt" || @@ -39,13 +42,15 @@ public async Task ExtractTextAsync(string fileName, byte[] fileContent, extension == ".json" || extension == ".xml") { - return await ExtractTextFromTextFileAsync(fileContent); + rawText = await ExtractTextFromTextFileAsync(fileContent); + return NormalizeAndLimitText(rawText, fileName); } // Handle PDF files if (normalizedContentType.Contains("pdf") || extension == ".pdf") { - return await Task.FromResult(ExtractTextFromPdfFile(fileName, fileContent)); + rawText = await Task.FromResult(ExtractTextFromPdfFile(fileName, fileContent)); + return NormalizeAndLimitText(rawText, fileName); } // Handle Word documents @@ -148,5 +153,81 @@ private string ExtractTextFromPdfFile(string fileName, byte[] fileContent) return string.Empty; } } + + private string NormalizeAndLimitText(string text, string fileName) + { + var normalized = NormalizeExtractedText(text); + normalized = RemoveLeadingFileNameArtifact(normalized, fileName); + + if (normalized.Length > MaxExtractedTextLength) + { + normalized = normalized.Substring(0, MaxExtractedTextLength); + _logger.LogDebug("Truncated extracted content to {MaxLength} characters", MaxExtractedTextLength); + } + + return normalized; + } + + private static string NormalizeExtractedText(string text) + { + if (string.IsNullOrWhiteSpace(text)) + { + return string.Empty; + } + + var normalized = text + .Replace('\0', ' ') + .Replace("\r\n", "\n") + .Replace('\r', '\n'); + + normalized = Regex.Replace(normalized, @"(?<=[a-z])(?=[A-Z])", " "); + normalized = Regex.Replace(normalized, @"(?<=[\.\,\:\;\)])(?=[A-Za-z0-9])", " "); + normalized = Regex.Replace(normalized, @":-", ": - "); + normalized = Regex.Replace(normalized, @"(?<=\S)- (?=[A-Za-z])", " - "); + normalized = Regex.Replace( + normalized, + @"(?<=[a-z])(?=(project|funding|budget|community|summary|notes|details|planning|outcomes|background|services)\b)", + " ", + RegexOptions.IgnoreCase); + normalized = Regex.Replace(normalized, @"[ \t]+", " "); + normalized = Regex.Replace(normalized, @"\n\s*", "\n"); + normalized = Regex.Replace(normalized, @"\n{2,}", "\n"); + + return normalized.Trim(); + } + + private static string RemoveLeadingFileNameArtifact(string text, string fileName) + { + if (string.IsNullOrWhiteSpace(text) || string.IsNullOrWhiteSpace(fileName)) + { + return text; + } + + var rawStem = Path.GetFileNameWithoutExtension(fileName)?.Trim(); + if (string.IsNullOrWhiteSpace(rawStem)) + { + return text; + } + + var decodedStem = Uri.UnescapeDataString(rawStem); + foreach (var candidate in new[] { rawStem, decodedStem }) + { + if (string.IsNullOrWhiteSpace(candidate)) + { + continue; + } + + if (text.StartsWith(candidate, StringComparison.OrdinalIgnoreCase)) + { + var stripped = text.Substring(candidate.Length).TrimStart(' ', '-', ':', '.', '\t'); + if (!string.IsNullOrWhiteSpace(stripped)) + { + return stripped; + } + } + } + + return text; + } } } From 0dac2febd471a8fb9deb7c79d1a39ab3baef6a5d Mon Sep 17 00:00:00 2001 From: David Bright Date: Thu, 26 Feb 2026 14:05:16 -0800 Subject: [PATCH 17/19] Added hooks for DataTables state save/load/loaded events. This is to allow the Applications table to save and restore custom filter values (Search / Quick Date) when saving views, but future tables can hook into as necessary. Fixed the "Reset View" of the DataTable as it had stopped refreshing the data appropriately. Also cleaned up all instances of previously implemented $('#quickDateRange') with UIElements.quickDateRange --- .../wwwroot/themes/ux2/table-utils.js | 22 ++++ .../Pages/GrantApplications/Index.js | 101 +++++++++++++----- 2 files changed, 99 insertions(+), 24 deletions(-) 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 0d3372b1b..2e9125f45 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 @@ -216,6 +216,9 @@ function initializeDataTable(options) { externalSearchId = 'search', disableColumnSelect = false, listColumnDefs, + onStateSaveParams,//External hooks for save/load/loaded + onStateLoadParams, + onStateLoaded, } = options; // Process columns and visibility @@ -314,15 +317,29 @@ function initializeDataTable(options) { processing: true, stateSave: true, stateDuration: 0, + externalSearchInputId: `#${externalSearchId}`, + onStateSaveParams, + onStateLoadParams, + onStateLoaded, stateSaveParams: function (settings, data) { let externalSearch = $(settings.oInit.externalSearchInputId); if (externalSearch.length) data.externalSearch = externalSearch.val(); + + // Call custom stateSave hook if provided + if (typeof settings.oInit.onStateSaveParams === 'function') { + settings.oInit.onStateSaveParams(settings, data); + } }, stateLoadParams: function (settings, data) { if (data.externalSearch) { let externalSearch = $(settings.oInit.externalSearchInputId); if (externalSearch.length) externalSearch.val(data.externalSearch); } + + // Call custom stateLoad hook if provided + if (typeof settings.oInit.onStateLoadParams === 'function') { + settings.oInit.onStateLoadParams(settings, data); + } }, stateLoaded: function (settings, data) { let dtApi = new $.fn.dataTable.Api(settings); @@ -360,6 +377,11 @@ function initializeDataTable(options) { } } } + + // Call custom loaded hook if provided + if (typeof settings.oInit.onStateLoaded === 'function') { + settings.oInit.onStateLoaded(dtApi, data); + } }, }); 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 050bfb23b..056d3ee72 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 @@ -92,10 +92,10 @@ $('#search, .custom-filter-input').val(''); dt.columns().search(''); dt.search(''); - dt.order(initialSortOrder).draw(); + dt.order(initialSortOrder); // Reset date range filters - $('#quickDateRange').val(defaultQuickDateRange); + UIElements.quickDateRange.val(defaultQuickDateRange); toggleCustomDateInputs(defaultQuickDateRange === 'custom'); const range = getDateRange(defaultQuickDateRange); @@ -110,6 +110,9 @@ localStorage.setItem('GrantApplications_QuickRange', defaultQuickDateRange); } + // Reload table data with updated filters + dt.ajax.reload(null, false); + // Close the dropdown dt.buttons('.grp-savedStates') .container() @@ -127,7 +130,7 @@ } ]; -const listColumns = getColumns(); + const listColumns = getColumns(); const defaultVisibleColumns = ['select', 'applicantName', 'category', @@ -150,10 +153,12 @@ const listColumns = getColumns(); }; const UIElements = { + searchField: $('#search'), + quickDateRange: $('#quickDateRange'), inputFilter: $('.date-input-filter'), submittedToInput: $('#submittedToDate'), submittedFromInput: $('#submittedFromDate'), - }; + }; let responseCallback = function (result) { return { @@ -187,7 +192,7 @@ const listColumns = getColumns(); const savedRange = localStorage.getItem('GrantApplications_QuickRange') || defaultQuickDateRange; // Set the dropdown value - $('#quickDateRange').val(savedRange); + UIElements.quickDateRange.val(savedRange); // Show/hide custom date inputs based on saved selection toggleCustomDateInputs(savedRange === 'custom'); @@ -215,8 +220,8 @@ const listColumns = getColumns(); } function bindUIEvents() { - UIElements.inputFilter.on('change', handleInputFilterChange); - $('#quickDateRange').on('change', handleQuickDateRangeChange); + UIElements.inputFilter.on('change', handleInputFilterChange); + UIElements.quickDateRange.on('change', handleQuickDateRangeChange); } function validateDate(dateValue, element) { @@ -224,28 +229,28 @@ const listColumns = getColumns(); const selectedDate = new Date(dateValue); const today = new Date(); today.setHours(0, 0, 0, 0); - + const minDate = element.attr('min') ? new Date(element.attr('min')) : null; const maxDate = element.attr('max') ? new Date(element.attr('max')) : null; - + if (selectedDate > today) { element.addClass('input-validation-error'); abp.notify.error('The date cannot be in the future', 'Invalid Date'); return false; } - + if (minDate && selectedDate < minDate) { element.addClass('input-validation-error'); abp.notify.error('The date cannot be before the minimum allowed date', 'Invalid Date'); return false; } - + if (maxDate && selectedDate > maxDate) { element.addClass('input-validation-error'); abp.notify.error('The date cannot be after the maximum allowed date', 'Invalid Date'); return false; } - + element.removeClass('input-validation-error'); return true; } @@ -263,7 +268,7 @@ const listColumns = getColumns(); case 'today': fromDate = toDate; break; - case 'last7days': + case 'last7days': fromDate = formatDate(new Date(today.setDate(today.getDate() - 7))); break; case 'last30days': @@ -278,7 +283,7 @@ const listColumns = getColumns(); case 'alltime': fromDate = null; return { fromDate: null, toDate: null }; - case 'custom': + case 'custom': default: return null; // Don't modify dates for custom } @@ -315,7 +320,7 @@ const listColumns = getColumns(); //If the values for FromDate and ToDate are being set outside of the //quick drop down handler, custom SHOULD be shown, but set just in case - $('#quickDateRange').val('custom'); + UIElements.quickDateRange.val('custom'); localStorage.setItem('GrantApplications_QuickRange', 'custom'); const dtInstance = $('#GrantApplicationsTable').DataTable(); @@ -365,13 +370,13 @@ const listColumns = getColumns(); dtInstance.ajax.reload(null, true); } } - + function initializeDataTableAndEvents() { dataTable = initializeDataTable({ dt, defaultVisibleColumns, listColumns, - maxRowsPerPage: 10, + maxRowsPerPage: 10, defaultSortColumn: { name: 'submissionDate', dir: 'desc' @@ -382,7 +387,7 @@ const listColumns = getColumns(); submittedFromDate: grantTableFilters.submittedFromDate, submittedToDate: grantTableFilters.submittedToDate }; - }, + }, responseCallback, actionButtons, serverSideEnabled: false, @@ -390,7 +395,31 @@ const listColumns = getColumns(); reorderEnabled: true, languageSetValues, dataTableName: 'GrantApplicationsTable', - dynamicButtonContainerId: 'dynamicButtonContainerId' + dynamicButtonContainerId: 'dynamicButtonContainerId', + onStateSaveParams: function (settings, data) { + data.customFilters = { + searchValue: UIElements.searchField.val() || '', + quickDateRange: UIElements.quickDateRange.val(), + submittedFromDate: UIElements.submittedFromInput.val(), + submittedToDate: UIElements.submittedToInput.val() + }; + }, + onStateLoadParams: function (settings, data) { + if (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); + } + }, + onStateLoaded: function (dtApi, data) { + if (data?.refreshTableWithDates) { + dtApi.ajax.reload(null, false); + } + } }); dataTable.on('search.dt', () => handleSearch()); @@ -430,6 +459,31 @@ const listColumns = getColumns(); $('.grp-savedStates').text('Save View'); $('.grp-savedStates').closest('.btn-group').addClass('cstm-save-view'); + // Helper function to restore custom filters + function restoreCustomFilters(filters) { + UIElements.searchField.val(filters.searchValue || ''); + + UIElements.quickDateRange.val(filters.quickDateRange || defaultQuickDateRange); + toggleCustomDateInputs(filters.quickDateRange === 'custom'); + + UIElements.submittedFromInput.val(filters.submittedFromDate || ''); + UIElements.submittedToInput.val(filters.submittedToDate || ''); + + grantTableFilters.submittedFromDate = filters.submittedFromDate || null; + grantTableFilters.submittedToDate = filters.submittedToDate || null; + + // Update localStorage to stay in sync + if (filters.submittedFromDate && filters.submittedToDate) { + localStorage.setItem('GrantApplications_FromDate', filters.submittedFromDate); + localStorage.setItem('GrantApplications_ToDate', filters.submittedToDate); + } else { + localStorage.removeItem('GrantApplications_FromDate'); + localStorage.removeItem('GrantApplications_ToDate'); + } + localStorage.setItem('GrantApplications_QuickRange', filters.quickDateRange || defaultQuickDateRange); + } + + function selectApplication(type, indexes, action) { if (type === 'row') { let data = dataTable.row(indexes).data(); @@ -439,7 +493,6 @@ const listColumns = getColumns(); function handleSearch() { let filter = $('.dt-search input').val(); - console.info(filter); } function getColumns() { @@ -464,7 +517,7 @@ const listColumns = getColumns(); getOrganizationNumberColumn(columnIndex++), getOrgBookStatusColumn(columnIndex++), getProjectStartDateColumn(columnIndex++), - getProjectEndDateColumn(columnIndex++), + getProjectEndDateColumn(columnIndex++), getProjectedFundingTotalColumn(columnIndex++), getTotalProjectBudgetPercentageColumn(columnIndex++), getTotalPaidAmountColumn(columnIndex++), @@ -534,7 +587,7 @@ const listColumns = getColumns(); data: 'referenceNo', name: 'referenceNo', className: 'data-table-header text-nowrap', - render: function (data, type, row) { + render: function (data, type, row) { return `${data}`; }, index: columnIndex @@ -977,8 +1030,8 @@ const listColumns = getColumns(); render: function (data) { let tagNames = data - .filter(x => x?.tag?.name) - .map(x => x.tag.name); + .filter(x => x?.tag?.name) + .map(x => x.tag.name); return tagNames.join(', ') ?? ''; }, index: columnIndex From 11193f3c90bbc4478c19541b4e612d5a5fa8cf30 Mon Sep 17 00:00:00 2001 From: David Bright Date: Thu, 26 Feb 2026 14:59:24 -0800 Subject: [PATCH 18/19] Centralized multiple calls to setting local date storage under a single method of "setDateRangeLocalStorage" Added copilot's suggestion of switching to strict equality checks for filter comparisons --- .../Pages/GrantApplications/Index.js | 47 +++++++++---------- 1 file changed, 21 insertions(+), 26 deletions(-) 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 056d3ee72..7a4fab9bf 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 @@ -99,15 +99,13 @@ toggleCustomDateInputs(defaultQuickDateRange === 'custom'); const range = getDateRange(defaultQuickDateRange); + setDateRangeLocalStorage(defaultQuickDateRange, range); + if (range) { UIElements.submittedFromInput.val(range.fromDate); UIElements.submittedToInput.val(range.toDate); grantTableFilters.submittedFromDate = range.fromDate; grantTableFilters.submittedToDate = range.toDate; - - localStorage.setItem('GrantApplications_FromDate', range.fromDate); - localStorage.setItem('GrantApplications_ToDate', range.toDate); - localStorage.setItem('GrantApplications_QuickRange', defaultQuickDateRange); } // Reload table data with updated filters @@ -331,6 +329,20 @@ dtInstance.ajax.reload(null, true); } + function setDateRangeLocalStorage(quickDateRange, fromToRange) { + localStorage.setItem('GrantApplications_QuickRange', quickDateRange || defaultQuickDateRange); + if (fromToRange) { + if (fromToRange.fromDate && fromToRange.toDate) { + localStorage.setItem('GrantApplications_FromDate', fromToRange.fromDate); + localStorage.setItem('GrantApplications_ToDate', fromToRange.toDate); + } else { + // For "All time", clear the date filters + localStorage.removeItem('GrantApplications_FromDate'); + localStorage.removeItem('GrantApplications_ToDate'); + } + } + } + function handleQuickDateRangeChange() { const selectedRange = $(this).val(); @@ -347,7 +359,7 @@ // Get the date range for the selected option const range = getDateRange(selectedRange); - + setDateRangeLocalStorage(selectedRange, range); if (range) { // Populate the hidden date fields UIElements.submittedFromInput.val(range.fromDate || ''); @@ -355,16 +367,6 @@ grantTableFilters.submittedFromDate = range.fromDate; grantTableFilters.submittedToDate = range.toDate; - // Save to localStorage - if (range.fromDate && range.toDate) { - localStorage.setItem('GrantApplications_FromDate', range.fromDate); - localStorage.setItem('GrantApplications_ToDate', range.toDate); - } else { - // For "All time", clear the date filters - localStorage.removeItem('GrantApplications_FromDate'); - localStorage.removeItem('GrantApplications_ToDate'); - } - // Reload the table with new filters const dtInstance = $('#GrantApplicationsTable').DataTable(); dtInstance.ajax.reload(null, true); @@ -409,9 +411,9 @@ // 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(); + data.customFilters.quickDateRange !== UIElements.quickDateRange.val() + || data.customFilters.submittedFromDate !== UIElements.submittedFromInput.val() + || data.customFilters.submittedToDate !== UIElements.submittedToInput.val(); restoreCustomFilters(data.customFilters); } }, @@ -473,14 +475,7 @@ grantTableFilters.submittedToDate = filters.submittedToDate || null; // Update localStorage to stay in sync - if (filters.submittedFromDate && filters.submittedToDate) { - localStorage.setItem('GrantApplications_FromDate', filters.submittedFromDate); - localStorage.setItem('GrantApplications_ToDate', filters.submittedToDate); - } else { - localStorage.removeItem('GrantApplications_FromDate'); - localStorage.removeItem('GrantApplications_ToDate'); - } - localStorage.setItem('GrantApplications_QuickRange', filters.quickDateRange || defaultQuickDateRange); + setDateRangeLocalStorage(filters?.quickDateRange, { fromDate: filters.submittedFromDate, toDate: filters.submittedToDate }); } From 4714f490d2ccb7a8c745d735093d3d36a2a58b99 Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Fri, 27 Feb 2026 11:36:09 -0800 Subject: [PATCH 19/19] hotfix/AB#32016-FixBatchPaymentsNightly --- .../PaymentRequests/PaymentRequestDto.cs | 5 +++- .../RabbitMQ/ReconciliationConsumer.cs | 29 +++++++++++-------- .../CasPaymentRequestCoordinator.cs | 2 +- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/PaymentRequestDto.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/PaymentRequestDto.cs index e00b7be5a..b6ef9d46d 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/PaymentRequestDto.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/PaymentRequestDto.cs @@ -4,12 +4,13 @@ using Unity.Payments.PaymentTags; using Unity.Payments.Suppliers; using Volo.Abp.Application.Dtos; +using Volo.Abp.MultiTenancy; namespace Unity.Payments.PaymentRequests { #pragma warning disable CS8618 [Serializable] - public class PaymentRequestDto : AuditedEntityDto + public class PaymentRequestDto : AuditedEntityDto, IMultiTenant { public string InvoiceNumber { get; set; } public decimal Amount { get; set; } @@ -46,6 +47,8 @@ public class PaymentRequestDto : AuditedEntityDto public DateTime? FsbNotificationSentDate { get; set; } public string? FsbApNotified { get; set; } + public Guid? TenantId { get; set; } + public static explicit operator PaymentRequestDto(CreatePaymentRequestDto v) { throw new NotImplementedException(); diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/RabbitMQ/ReconciliationConsumer.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/RabbitMQ/ReconciliationConsumer.cs index 20bd5b882..c462ae9b3 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/RabbitMQ/ReconciliationConsumer.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/RabbitMQ/ReconciliationConsumer.cs @@ -4,31 +4,36 @@ using System; using Unity.Payments.PaymentRequests; using Unity.Payments.Integrations.Cas; +using Volo.Abp.MultiTenancy; namespace Unity.Payments.Integrations.RabbitMQ; public class ReconciliationConsumer( CasPaymentRequestCoordinator casPaymentRequestCoordinator, - InvoiceService invoiceService + InvoiceService invoiceService, + ICurrentTenant currentTenant ) : IQueueConsumer { public async Task ConsumeAsync(ReconcilePaymentMessages reconcilePaymentMessage) { if (reconcilePaymentMessage != null && !reconcilePaymentMessage.InvoiceNumber.IsNullOrEmpty() && reconcilePaymentMessage.TenantId != Guid.Empty) - { - // string invoiceNumber, string supplierNumber, string siteNumber) - // Go to CAS retrieve the status of the payment - CasPaymentSearchResult result = await invoiceService.GetCasPaymentAsync( - reconcilePaymentMessage.TenantId, - reconcilePaymentMessage.InvoiceNumber, - reconcilePaymentMessage.SupplierNumber, - reconcilePaymentMessage.SiteNumber); + { - if (result != null && result.InvoiceStatus != null && result.InvoiceStatus != "") + using (currentTenant.Change(reconcilePaymentMessage.TenantId)) { - await casPaymentRequestCoordinator.UpdatePaymentRequestStatus(reconcilePaymentMessage.TenantId, reconcilePaymentMessage.PaymentRequestId, result); - } + // string invoiceNumber, string supplierNumber, string siteNumber) + // Go to CAS retrieve the status of the payment + CasPaymentSearchResult result = await invoiceService.GetCasPaymentAsync( + reconcilePaymentMessage.TenantId, + reconcilePaymentMessage.InvoiceNumber, + reconcilePaymentMessage.SupplierNumber, + reconcilePaymentMessage.SiteNumber); + if (result != null && result.InvoiceStatus != null && result.InvoiceStatus != "") + { + await casPaymentRequestCoordinator.UpdatePaymentRequestStatus(reconcilePaymentMessage.TenantId, reconcilePaymentMessage.PaymentRequestId, result); + } + } } } diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/CasPaymentRequestCoordinator.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/CasPaymentRequestCoordinator.cs index e908d215f..84b8cc8fa 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/CasPaymentRequestCoordinator.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/CasPaymentRequestCoordinator.cs @@ -92,7 +92,7 @@ public async Task ManuallyAddPaymentRequestsToReconciliationQueue(List