From 7774dfe51afab9b123f00e2da2ac927e762a943d Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Tue, 27 Jan 2026 15:01:31 -0800 Subject: [PATCH 001/191] AB#30936 include dynamic datagrid columns in worksheet - WIP --- .../IWorksheetsMetadataService.cs | 2 +- .../WorksheetFieldSchemaParser.cs | 280 ++++++++++++++++-- .../WorksheetsMetadataService.cs | 11 +- .../Definitions/DataGridDefinition.cs | 3 + .../WorksheetFieldsProvider.cs | 2 +- 5 files changed, 271 insertions(+), 27 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Reporting/Configuration/IWorksheetsMetadataService.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Reporting/Configuration/IWorksheetsMetadataService.cs index 17ba6be14d..e1d3ec7745 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Reporting/Configuration/IWorksheetsMetadataService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Reporting/Configuration/IWorksheetsMetadataService.cs @@ -5,7 +5,7 @@ namespace Unity.Flex.Reporting.Configuration { public interface IWorksheetsMetadataService { - Task GetWorksheetSchemaMetaDataAsync(Guid worksheetId); + Task GetWorksheetSchemaMetaDataAsync(Guid worksheetId, Guid formVersionId); Task GetWorksheetSchemaMetaDataItemAsync(Guid worksheetId, string fieldKey); } } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Reporting/Configuration/WorksheetFieldSchemaParser.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Reporting/Configuration/WorksheetFieldSchemaParser.cs index 8077c8fe63..61ad42dad3 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Reporting/Configuration/WorksheetFieldSchemaParser.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Reporting/Configuration/WorksheetFieldSchemaParser.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; @@ -23,8 +24,13 @@ public static partial class WorksheetFieldSchemaParser /// /// The custom field to parse /// The worksheet containing the field (for name context) + /// Optional form schema JSON string for resolving dynamic DataGrid columns + /// Optional submission header mapping (not used in current implementation) /// List of component metadata items - public static List ParseField(CustomField field, Worksheet worksheet) + public static List ParseField(CustomField field, + Worksheet worksheet, + string? formSchema = null, + string? submissionHeaderMapping = null) { if (field == null) return []; @@ -34,7 +40,7 @@ public static List ParseField(CustomField fie switch (field.Type) { case CustomFieldType.DataGrid: - components.AddRange(ParseDataGridField(field, worksheet)); + components.AddRange(ParseDataGridField(field, worksheet, formSchema, submissionHeaderMapping)); break; case CustomFieldType.CheckboxGroup: @@ -58,8 +64,12 @@ public static List ParseField(CustomField fie /// Parses all fields in a worksheet and returns flattened component metadata. /// /// The worksheet to parse + /// Optional form schema JSON string for resolving dynamic DataGrid columns + /// Optional submission header mapping (not used in current implementation) /// List of all component metadata items - public static List ParseWorksheet(Worksheet worksheet) + public static List ParseWorksheet(Worksheet worksheet, + string? formSchema = null, + string? submissionHeaderMapping = null) { if (worksheet?.Sections == null) return []; @@ -67,17 +77,23 @@ public static List ParseWorksheet(Worksheet w return [..worksheet.Sections .Where(section => section.Fields != null) .SelectMany(section => section.Fields) - .SelectMany(field => ParseField(field, worksheet))]; + .SelectMany(field => ParseField(field, worksheet, formSchema, submissionHeaderMapping))]; } /// /// Parses a DataGrid field and returns metadata for each column defined in the DataGrid definition. - /// If dynamic is true, creates a placeholder column for dynamically determined columns. + /// If dynamic is true, attempts to extract columns from the form schema. If form schema is not available + /// or parsing fails, creates a placeholder column for dynamically determined columns. /// /// The DataGrid field to parse /// The worksheet containing the field + /// Optional form schema JSON string for resolving dynamic DataGrid columns + /// Optional submission header mapping (not used in current implementation) /// List of component metadata items for each DataGrid column - private static List ParseDataGridField(CustomField field, Worksheet worksheet) + private static List ParseDataGridField(CustomField field, + Worksheet worksheet, + string? formSchema = null, + string? submissionHeaderMapping = null) { var components = new List(); @@ -98,25 +114,68 @@ private static List ParseDataGridField(Custom var worksheetName = SanitizeName(worksheet.Name); var dataGridName = SanitizeName(field.Key); - // If dynamic is true, create a placeholder for dynamically determined columns + // Track whether we successfully extracted columns from CHEFS schema + bool extractedFromChefs = false; + + // If dynamic is true, try to extract columns from form schema if (dataGridDefinition.Dynamic) - { - var dynamicComponent = new WorksheetComponentMetaDataItemDto + { + var headerMappingKey = MatchHeaderMapping(field.Name + ".DataGrid", submissionHeaderMapping); + List? dynamicColumns = null; + + if (!string.IsNullOrWhiteSpace(headerMappingKey)) { - Id = $"{field.Id}_dynamic", - Key = "dynamic_columns", - Label = "Dynamic Columns", - Type = "Dynamic", - Path = $"{worksheetName}->{sectionName}->{dataGridName}->dynamic_columns", - TypePath = $"worksheet->section->datagrid->Dynamic", - DataPath = $"({worksheetName}){dataGridName}->dynamic_columns" - }; + dynamicColumns = ExtractDynamicDataGridColumns(headerMappingKey, formSchema); + } - components.Add(dynamicComponent); + if (dynamicColumns != null && dynamicColumns.Count > 0) + { + // We found columns in the form schema, use them + // CHEFS schema includes ALL columns (both static and dynamic), so we mark this as extracted + extractedFromChefs = true; + + foreach (var column in dynamicColumns) + { + // Use the key for the component Key (becomes PropertyName), sanitize for ID + var columnKey = !string.IsNullOrEmpty(column.Key) ? column.Key : column.Name; + var sanitizedKey = SanitizeName(columnKey); + + var component = new WorksheetComponentMetaDataItemDto + { + Id = $"{field.Id}_{sanitizedKey}", + Key = columnKey, + Label = column.Name, + Type = MapDataGridColumnType(column.Type), + Path = $"{worksheetName}->{sectionName}->{dataGridName}->{columnKey}", + TypePath = $"worksheet->section->datagrid->{MapDataGridColumnType(column.Type)}", + DataPath = $"({worksheetName}){dataGridName}->{columnKey}" + }; + + components.Add(component); + } + } + else + { + // Form schema not available or no columns found, create dynamic placeholder + var dynamicComponent = new WorksheetComponentMetaDataItemDto + { + Id = $"{field.Id}_dynamic", + Key = "dynamic_columns", + Label = "Dynamic Columns", + Type = "Dynamic", + Path = $"{worksheetName}->{sectionName}->{dataGridName}->dynamic_columns", + TypePath = $"worksheet->section->datagrid->Dynamic", + DataPath = $"({worksheetName}){dataGridName}->dynamic_columns" + }; + + components.Add(dynamicComponent); + } } - // Process additional defined columns (if any) - if (dataGridDefinition.Columns != null && dataGridDefinition.Columns.Count > 0) + // Process additional defined columns only if we haven't already extracted them from CHEFS + // When dynamic is true and CHEFS extraction succeeded, the CHEFS schema already includes + // all columns (both static and dynamic), so we skip this to avoid duplicates + if (!extractedFromChefs && dataGridDefinition.Columns != null && dataGridDefinition.Columns.Count > 0) { // Create a component for each column in the DataGrid foreach (var column in dataGridDefinition.Columns) @@ -137,7 +196,7 @@ private static List ParseDataGridField(Custom components.Add(component); } } - else if (!dataGridDefinition.Dynamic) + else if (!dataGridDefinition.Dynamic && (dataGridDefinition.Columns == null || dataGridDefinition.Columns.Count == 0)) { // If no columns defined and not dynamic, return the DataGrid itself as a component return [CreateSimpleComponent(field, worksheet)]; @@ -153,6 +212,30 @@ private static List ParseDataGridField(Custom } } + private static string? MatchHeaderMapping(string keyToSearch, string? submissionHeaderMapping) + { + if (string.IsNullOrWhiteSpace(submissionHeaderMapping)) + return null; + + try + { + using var document = JsonDocument.Parse(submissionHeaderMapping); + var root = document.RootElement; + + if (root.TryGetProperty(keyToSearch, out var valueElement)) + { + return valueElement.GetString(); + } + } + catch (JsonException) + { + // Failed to parse submission header mapping + return null; + } + + return null; + } + /// /// Parses a CheckboxGroup field and returns metadata for each checkbox option defined in the CheckboxGroup definition. /// @@ -298,6 +381,161 @@ private static string SanitizeName(string name) return SantizedNameExpression().Replace(name.Trim().Replace(" ", "_"), ""); } + /// + /// Extracts DataGrid column definitions from a form schema JSON string. + /// Searches for a DataGrid component with the specified key in the form schema and returns its columns. + /// + /// The key of the DataGrid field to find + /// The form schema JSON string to search + /// List of DataGrid column definitions, or null if not found or schema is invalid + private static List? ExtractDynamicDataGridColumns(string dataGridKey, string? formSchema) + { + if (string.IsNullOrWhiteSpace(formSchema)) + return null; + + try + { + using var document = JsonDocument.Parse(formSchema); + var root = document.RootElement; + + // CHEFS form schemas have a "components" array at the root + if (root.TryGetProperty("components", out var componentsElement)) + { + return FindDataGridInComponents(componentsElement, dataGridKey); + } + } + catch (JsonException) + { + // Failed to parse form schema + return null; + } + + return null; + } + + /// + /// Recursively searches for a DataGrid component with the specified key in a components array. + /// + /// The JSON element representing a components array + /// The key of the DataGrid to find + /// List of DataGrid column definitions, or null if not found + private static List? FindDataGridInComponents(JsonElement componentsElement, string dataGridKey) + { + if (componentsElement.ValueKind != JsonValueKind.Array) + return null; + + foreach (var component in componentsElement.EnumerateArray()) + { + // Check if this component matches the DataGrid key + if (component.TryGetProperty("key", out var keyElement) && + keyElement.GetString() == dataGridKey) + { + // Check if it's a DataGrid type + if (component.TryGetProperty("type", out var typeElement)) + { + var type = typeElement.GetString(); + if (type == "datagrid" || type == "dataGrid") + { + // Extract columns from the DataGrid + return ExtractColumnsFromDataGrid(component); + } + } + } + + // Recursively search in nested components (for panels, columns, etc.) + if (component.TryGetProperty("components", out var nestedComponents)) + { + var result = FindDataGridInComponents(nestedComponents, dataGridKey); + if (result != null) + return result; + } + + // Also check columns property (for layout components) + if (component.TryGetProperty("columns", out var columnsElement) && + columnsElement.ValueKind == JsonValueKind.Array) + { + foreach (var column in columnsElement.EnumerateArray()) + { + if (column.TryGetProperty("components", out var columnComponents)) + { + var result = FindDataGridInComponents(columnComponents, dataGridKey); + if (result != null) + return result; + } + } + } + } + + return null; + } + + /// + /// Extracts column definitions from a DataGrid component in the form schema. + /// + /// The JSON element representing a DataGrid component + /// List of DataGrid column definitions + private static List? ExtractColumnsFromDataGrid(JsonElement dataGridComponent) + { + var columns = new List(); + + // CHEFS DataGrid components have a "components" property that contains the column definitions + if (dataGridComponent.TryGetProperty("components", out var componentsElement) && + componentsElement.ValueKind == JsonValueKind.Array) + { + foreach (var columnComponent in componentsElement.EnumerateArray()) + { + // Get the key (property name) from the column component + var columnKey = columnComponent.TryGetProperty("key", out var keyElement) + ? keyElement.GetString() + : null; + + // Get the label (display name) from the column component + var columnLabel = columnComponent.TryGetProperty("label", out var labelElement) + ? labelElement.GetString() + : columnKey; + + var columnType = columnComponent.TryGetProperty("type", out var typeElement) + ? MapChefsTypeToDataGridType(typeElement.GetString()) + : "text"; + + if (!string.IsNullOrEmpty(columnKey)) + { + columns.Add(new DataGridDefinitionColumn + { + Key = columnKey, + Name = columnLabel ?? columnKey, + Type = columnType + }); + } + } + } + + return columns.Count > 0 ? columns : null; + } + + /// + /// Maps CHEFS form component types to DataGrid column types. + /// + /// The CHEFS component type + /// Mapped DataGrid column type + private static string MapChefsTypeToDataGridType(string? chefsType) + { + return chefsType?.ToLowerInvariant() switch + { + "textfield" => "text", + "textarea" => "text", + "number" => "numeric", + "currency" => "currency", + "checkbox" => "checkbox", + "day" => "date", + "datetime" => "datetime", + "email" => "text", + "phoneNumber" => "text", + "url" => "text", + _ => "text" // Default to text for unknown types + }; + } + [GeneratedRegex(@"[^a-zA-Z0-9_\-]")] private static partial Regex SantizedNameExpression(); } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Reporting/Configuration/WorksheetsMetadataService.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Reporting/Configuration/WorksheetsMetadataService.cs index d51a1f8630..13c9b2efdc 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Reporting/Configuration/WorksheetsMetadataService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Reporting/Configuration/WorksheetsMetadataService.cs @@ -2,19 +2,22 @@ using System.Linq; using System.Threading.Tasks; using Unity.Flex.Domain.Worksheets; +using Unity.GrantManager.ApplicationForms; using Volo.Abp.DependencyInjection; namespace Unity.Flex.Reporting.Configuration { - public class WorksheetsMetadataService(IWorksheetRepository worksheetRepository) + public class WorksheetsMetadataService(IWorksheetRepository worksheetRepository, + IApplicationFormVersionAppService formVersionAppService) : IWorksheetsMetadataService, ITransientDependency { - public async Task GetWorksheetSchemaMetaDataAsync(Guid worksheetId) + public async Task GetWorksheetSchemaMetaDataAsync(Guid worksheetId, Guid formVersionId) { var worksheet = await worksheetRepository.GetAsync(worksheetId); - + var version = await formVersionAppService.GetAsync(formVersionId); + // Use the utility class to parse all fields in the worksheet - var components = WorksheetFieldSchemaParser.ParseWorksheet(worksheet); + var components = WorksheetFieldSchemaParser.ParseWorksheet(worksheet, version.FormSchema, version.SubmissionHeaderMapping); return new WorksheetComponentMetaDataDto() { diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/Worksheets/Definitions/DataGridDefinition.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/Worksheets/Definitions/DataGridDefinition.cs index 368a06274f..bcb81619d1 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/Worksheets/Definitions/DataGridDefinition.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/Worksheets/Definitions/DataGridDefinition.cs @@ -27,6 +27,9 @@ public class DataGridDefinitionColumn [JsonPropertyName("type")] public string Type { get; set; } = string.Empty; + + [JsonPropertyName("key")] + public string Key { get; set; } = string.Empty; } public enum DataGridDefinitionSummaryOption diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/WorksheetFieldsProvider.cs b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/WorksheetFieldsProvider.cs index 135bb2ae83..a3ebe73d1d 100644 --- a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/WorksheetFieldsProvider.cs +++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/WorksheetFieldsProvider.cs @@ -37,7 +37,7 @@ public async Task GetFieldsMetadataAsync(Guid correlationId foreach (var link in links) { - var metadata = await worksheetsMetadataService.GetWorksheetSchemaMetaDataAsync(link.WorksheetId); + var metadata = await worksheetsMetadataService.GetWorksheetSchemaMetaDataAsync(link.WorksheetId, correlationId); worksheetMetadata.Add(metadata); // Add worksheet information to the metadata map From e9cfa4ad80b1b4d2f5d63ac64000442a857a74d1 Mon Sep 17 00:00:00 2001 From: Velang Date: Thu, 12 Feb 2026 15:54:38 -0800 Subject: [PATCH 002/191] Initial Commit --- .../cypress/e2e/ApplicationsActionBar.cy.ts | 629 ++++++------------ .../cypress/pages/ApplicationDetailsPage.ts | 506 ++++++++++++++ 2 files changed, 693 insertions(+), 442 deletions(-) diff --git a/applications/Unity.AutoUI/cypress/e2e/ApplicationsActionBar.cy.ts b/applications/Unity.AutoUI/cypress/e2e/ApplicationsActionBar.cy.ts index a6dc210c55..97fd8dcf8d 100644 --- a/applications/Unity.AutoUI/cypress/e2e/ApplicationsActionBar.cy.ts +++ b/applications/Unity.AutoUI/cypress/e2e/ApplicationsActionBar.cy.ts @@ -1,426 +1,14 @@ /// import { loginIfNeeded } from "../support/auth"; +import { ApplicationsListPage } from "../pages/ApplicationDetailsPage"; describe("Unity Login and check data from CHEFS", () => { - const STANDARD_TIMEOUT = 20000; + const page = new ApplicationsListPage(); - function switchToDefaultGrantsProgramIfAvailable() { - cy.get("body").then(($body) => { - const hasUserInitials = $body.find(".unity-user-initials").length > 0; - - if (!hasUserInitials) { - cy.log("Skipping tenant switch: no user initials menu found"); - return; - } - - cy.get(".unity-user-initials").click(); - - cy.get("body").then(($body2) => { - const switchLink = $body2 - .find("#user-dropdown a.dropdown-item") - .filter((_, el) => { - return (el.textContent || "").trim() === "Switch Grant Programs"; - }); - - if (switchLink.length === 0) { - cy.log( - 'Skipping tenant switch: "Switch Grant Programs" not present for this user/session', - ); - cy.get("body").click(0, 0); - return; - } - - cy.wrap(switchLink.first()).click(); - - cy.url({ timeout: STANDARD_TIMEOUT }).should( - "include", - "/GrantPrograms", - ); - - cy.get("#search-grant-programs", { timeout: STANDARD_TIMEOUT }) - .should("be.visible") - .clear() - .type("Default Grants Program"); - - // Flatten nested `within` usage to satisfy S2004 (limit nesting depth) - cy.contains( - "#UserGrantProgramsTable tbody tr", - "Default Grants Program", - { timeout: STANDARD_TIMEOUT }, - ) - .should("exist") - .within(() => { - cy.contains("button", "Select").should("be.enabled").click(); - }); - - cy.location("pathname", { timeout: STANDARD_TIMEOUT }).should((p) => { - expect( - p.indexOf("/GrantApplications") >= 0 || p.indexOf("/auth/") >= 0, - ).to.eq(true); - }); - }); - }); - } - - // TEST renders the Submission tab inside an open shadow root (Form.io). - // Enabling this makes cy.get / cy.contains pierce shadow DOM consistently across envs. - before(() => { - Cypress.config("includeShadowDom", true); - loginIfNeeded({ timeout: STANDARD_TIMEOUT }); - }); - - it("Switch to Default Grants Program if available", () => { - switchToDefaultGrantsProgramIfAvailable(); - }); - - it("Tests the existence and functionality of the Submitted Date From and Submitted Date To filters", () => { - const pad2 = (n: number) => String(n).padStart(2, "0"); - - const todayIsoLocal = () => { - const d = new Date(); - return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`; - }; - - const waitForRefresh = () => { - // S3923 fix: remove identical branches; assert spinner is hidden when present. - cy.get('div.spinner-grow[role="status"]', { - timeout: STANDARD_TIMEOUT, - }).then(($s) => { - cy.wrap($s) - .should("have.attr", "style") - .and("contain", "display: none"); - }); - }; - - // --- Submitted Date From --- - cy.get("input#submittedFromDate", { timeout: STANDARD_TIMEOUT }) - .click({ force: true }) - .clear({ force: true }) - .type("2022-01-01", { force: true }) - .trigger("change", { force: true }) - .blur({ force: true }) - .should("have.value", "2022-01-01"); - - waitForRefresh(); - - // --- Submitted Date To --- - const today = todayIsoLocal(); - - cy.get("input#submittedToDate", { timeout: STANDARD_TIMEOUT }) - .click({ force: true }) - .clear({ force: true }) - .type(today, { force: true }) - .trigger("change", { force: true }) - .blur({ force: true }) - .should("have.value", today); - - waitForRefresh(); - }); - - // With no rows selected verify the visibility of Filter, Export, Save View, and Columns. - it("Verify the action buttons are visible with no rows selected", () => {}); - - // With one row selected verify the visibility of Filter, Export, Save View, and Columns. - it("Verify the action buttons are visible with one row selected", () => {}); - - it("Clicks Payment and force-closes the modal", () => { - const BUTTON_TIMEOUT = 60000; - - // Ensure table has rows - cy.get(".dt-scroll-body tbody tr", { timeout: STANDARD_TIMEOUT }).should( - "have.length.greaterThan", - 1, - ); - - // Select two rows using non-link cells - const clickSelectableCell = (rowIdx: number, withCtrl = false) => { - cy.get(".dt-scroll-body tbody tr", { timeout: STANDARD_TIMEOUT }) - .eq(rowIdx) - .find("td") - .not(":has(a)") - .first() - .click({ force: true, ctrlKey: withCtrl }); - }; - clickSelectableCell(0); - clickSelectableCell(1, true); - - // ActionBar - cy.get("#app_custom_buttons", { timeout: STANDARD_TIMEOUT }) - .should("exist") - .scrollIntoView(); - - // Click Payment - cy.get("#applicationPaymentRequest", { timeout: BUTTON_TIMEOUT }) - .should("be.visible") - .and("not.be.disabled") - .click({ force: true }); - - // Wait until modal is shown - cy.get("#payment-modal", { timeout: STANDARD_TIMEOUT }) - .should("be.visible") - .and("have.class", "show"); - - // Attempt graceful closes first - cy.get("body").type("{esc}", { force: true }); // Bootstrap listens to ESC - cy.get(".modal-backdrop", { timeout: STANDARD_TIMEOUT }).then(($bd) => { - if ($bd.length) { - cy.wrap($bd).click("topLeft", { force: true }); - } - }); - - // Try footer Cancel if available (avoid .catch on Cypress chainable) - cy.contains("#payment-modal .modal-footer button", "Cancel", { - timeout: STANDARD_TIMEOUT, - }).then(($btn) => { - if ($btn && $btn.length > 0) { - cy.wrap($btn).scrollIntoView().click({ force: true }); - } else { - cy.log("Cancel button not present, proceeding to hard-close fallback"); - } - }); - - // Use window API (if present), then hard-close fallback - cy.window().then((win: any) => { - try { - if (typeof win.closePaymentModal === "function") { - win.closePaymentModal(); - } - } catch { - /* ignore */ - } - - // HARD CLOSE: forcibly hide modal and remove backdrop - const $ = (win as any).jQuery || (win as any).$; - if ($) { - try { - $("#payment-modal") - .removeClass("show") - .attr("aria-hidden", "true") - .css("display", "none"); - $(".modal-backdrop").remove(); - $("body").removeClass("modal-open").css("overflow", ""); // restore scroll - } catch { - /* ignore */ - } - } - }); - - // Verify modal/backdrop gone (be tolerant: assert non-interference instead of visibility only) - cy.get("#payment-modal", { timeout: STANDARD_TIMEOUT }).should(($m) => { - const isHidden = !$m.is(":visible") || !$m.hasClass("show"); - expect(isHidden, "payment-modal hidden or not shown").to.eq(true); - }); - cy.get(".modal-backdrop", { timeout: STANDARD_TIMEOUT }).should( - "not.exist", - ); - - // Right-side buttons usable - cy.get("#dynamicButtonContainerId", { timeout: STANDARD_TIMEOUT }) - .should("exist") - .scrollIntoView(); - - cy.contains("#dynamicButtonContainerId .dt-buttons button span", "Export", { - timeout: STANDARD_TIMEOUT, - }).should("be.visible"); - cy.contains( - "#dynamicButtonContainerId button.grp-savedStates", - "Save View", - { timeout: STANDARD_TIMEOUT }, - ).should("be.visible"); - cy.contains( - "#dynamicButtonContainerId .dt-buttons button span", - "Columns", - { timeout: STANDARD_TIMEOUT }, - ).should("be.visible"); - }); - - // Walk the Columns menu and toggle each column on, verifying the column is visible. - it("Verify all columns in the menu are visible when and toggled on.", () => { - const clickColumnsItem = (label: string) => { - cy.contains("a.dropdown-item", label, { timeout: STANDARD_TIMEOUT }) - .should("exist") - .scrollIntoView() - .click({ force: true }); - }; - - const getVisibleHeaderTitles = () => { - return cy - .get(".dt-scroll-head span.dt-column-title", { - timeout: STANDARD_TIMEOUT, - }) - .then(($els) => { - const titles = Cypress.$($els) - .toArray() - .map((el) => (el.textContent || "").replace(/\s+/g, " ").trim()) - .filter((t) => t.length > 0); - return titles; - }); - }; - - const assertVisibleHeadersInclude = (expected: string[]) => { - getVisibleHeaderTitles().then((titles) => { - expected.forEach((e) => { - expect(titles, `visible headers should include "${e}"`).to.include(e); - }); - }); - }; - - const scrollX = (x: number) => { - cy.get(".dt-scroll-body", { timeout: STANDARD_TIMEOUT }) - .should("exist") - .scrollTo(x, 0, { duration: 0, ensureScrollable: false }); - }; - - // Close any open dropdowns/modals first - cy.get("body").then(($body) => { - if ($body.find(".dt-button-background").length > 0) { - cy.get(".dt-button-background").click({ force: true }); - } - }); - - // Open the "Save View" dropdown - cy.get("button.grp-savedStates", { timeout: STANDARD_TIMEOUT }) - .should("be.visible") - .and("contain.text", "Save View") - .click(); - - // Click "Reset to Default View" - cy.contains("a.dropdown-item", "Reset to Default View", { - timeout: STANDARD_TIMEOUT, - }) - .should("exist") - .click({ force: true }); - - // Wait for table to rebuild after reset - check for default columns - cy.get(".dt-scroll-head span.dt-column-title", { - timeout: STANDARD_TIMEOUT, - }).should("have.length.gt", 5); - - // Open Columns menu - cy.contains("span", "Columns", { timeout: STANDARD_TIMEOUT }) - .should("be.visible") - .click(); - - // Wait for columns dropdown to be fully populated - cy.get("a.dropdown-item", { timeout: STANDARD_TIMEOUT }).should( - "have.length.gt", - 50, - ); - - clickColumnsItem("% of Total Project Budget"); - clickColumnsItem("Acquisition"); - clickColumnsItem("Applicant Electoral District"); - - clickColumnsItem("Applicant Id"); - clickColumnsItem("Applicant Id"); - - clickColumnsItem("Applicant Name"); - clickColumnsItem("Applicant Name"); - - clickColumnsItem("Approved Amount"); - clickColumnsItem("Approved Amount"); - - clickColumnsItem("Assessment Result"); - - clickColumnsItem("Assignee"); - clickColumnsItem("Assignee"); - - clickColumnsItem("Business Number"); - - clickColumnsItem("Category"); - clickColumnsItem("Category"); - - clickColumnsItem("City"); - - clickColumnsItem("Community"); - clickColumnsItem("Community"); - - clickColumnsItem("Community Population"); - clickColumnsItem("Contact Business Phone"); - clickColumnsItem("Contact Cell Phone"); - clickColumnsItem("Contact Email"); - clickColumnsItem("Contact Full Name"); - clickColumnsItem("Contact Title"); - clickColumnsItem("Decision Date"); - clickColumnsItem("Decline Rationale"); - clickColumnsItem("Due Date"); - clickColumnsItem("Due Diligence Status"); - clickColumnsItem("Economic Region"); - clickColumnsItem("Forestry Focus"); - clickColumnsItem("Forestry or Non-Forestry"); - clickColumnsItem("FYE Day"); - clickColumnsItem("FYE Month"); - clickColumnsItem("Indigenous"); - clickColumnsItem("Likelihood of Funding"); - clickColumnsItem("Non-Registered Organization Name"); - clickColumnsItem("Notes"); - clickColumnsItem("Org Book Status"); - clickColumnsItem("Organization Type"); - clickColumnsItem("Other Sector/Sub/Industry Description"); - clickColumnsItem("Owner"); - clickColumnsItem("Payout"); - clickColumnsItem("Place"); - clickColumnsItem("Project Electoral District"); - clickColumnsItem("Project End Date"); - - clickColumnsItem("Project Name"); - clickColumnsItem("Project Name"); - - clickColumnsItem("Project Start Date"); - clickColumnsItem("Project Summary"); - clickColumnsItem("Projected Funding Total"); - clickColumnsItem("Recommended Amount"); - clickColumnsItem("Red-Stop"); - clickColumnsItem("Regional District"); - clickColumnsItem("Registered Organization Name"); - clickColumnsItem("Registered Organization Number"); - - clickColumnsItem("Requested Amount"); - clickColumnsItem("Requested Amount"); - - clickColumnsItem("Risk Ranking"); - clickColumnsItem("Sector"); - clickColumnsItem("Signing Authority Business Phone"); - clickColumnsItem("Signing Authority Cell Phone"); - clickColumnsItem("Signing Authority Email"); - clickColumnsItem("Signing Authority Full Name"); - clickColumnsItem("Signing Authority Title"); - - clickColumnsItem("Status"); - clickColumnsItem("Status"); - - clickColumnsItem("Sub-Status"); - clickColumnsItem("Sub-Status"); - - clickColumnsItem("Submission #"); - clickColumnsItem("Submission #"); - - clickColumnsItem("Submission Date"); - clickColumnsItem("Submission Date"); - - clickColumnsItem("SubSector"); - - clickColumnsItem("Tags"); - clickColumnsItem("Tags"); - - clickColumnsItem("Total Paid Amount $"); - clickColumnsItem("Total Project Budget"); - clickColumnsItem("Total Score"); - clickColumnsItem("Unity Application Id"); - - // Close the menu and wait until the overlay is gone - cy.get("div.dt-button-background", { timeout: STANDARD_TIMEOUT }) - .should("exist") - .click({ force: true }); - - cy.get("div.dt-button-background", { timeout: STANDARD_TIMEOUT }).should( - "not.exist", - ); - - // Assertions by horizontal scroll segments (human-style scan) - scrollX(0); - assertVisibleHeadersInclude([ + // Column visibility test data - organized by scroll position + const COLUMN_VISIBILITY_DATA = { + scrollPosition0: [ "Applicant Name", "Category", "Submission #", @@ -432,10 +20,8 @@ describe("Unity Login and check data from CHEFS", () => { "Approved Amount", "Project Name", "Applicant Id", - ]); - - scrollX(1500); - assertVisibleHeadersInclude([ + ], + scrollPosition1500: [ "Tags", "Assignee", "SubSector", @@ -443,20 +29,16 @@ describe("Unity Login and check data from CHEFS", () => { "Regional District", "Registered Organization Number", "Org Book Status", - ]); - - scrollX(3000); - assertVisibleHeadersInclude([ + ], + scrollPosition3000: [ "Project Start Date", "Project End Date", "Projected Funding Total", "Total Paid Amount $", "Project Electoral District", "Applicant Electoral District", - ]); - - scrollX(4500); - assertVisibleHeadersInclude([ + ], + scrollPosition4500: [ "Forestry or Non-Forestry", "Forestry Focus", "Acquisition", @@ -464,10 +46,8 @@ describe("Unity Login and check data from CHEFS", () => { "Community Population", "Likelihood of Funding", "Total Score", - ]); - - scrollX(6000); - assertVisibleHeadersInclude([ + ], + scrollPosition6000: [ "Assessment Result", "Recommended Amount", "Due Date", @@ -476,10 +56,8 @@ describe("Unity Login and check data from CHEFS", () => { "Project Summary", "Organization Type", "Business Number", - ]); - - scrollX(7500); - assertVisibleHeadersInclude([ + ], + scrollPosition7500: [ "Due Diligence Status", "Decline Rationale", "Contact Full Name", @@ -487,10 +65,8 @@ describe("Unity Login and check data from CHEFS", () => { "Contact Email", "Contact Business Phone", "Contact Cell Phone", - ]); - - scrollX(9000); - assertVisibleHeadersInclude([ + ], + scrollPosition9000: [ "Signing Authority Full Name", "Signing Authority Title", "Signing Authority Email", @@ -505,7 +81,176 @@ describe("Unity Login and check data from CHEFS", () => { "FYE Month", "Payout", "Unity Application Id", - ]); + ], + }; + + // Columns to toggle during the test - organized for maintainability + const COLUMNS_TO_TOGGLE = { + singleToggle: [ + "% of Total Project Budget", + "Acquisition", + "Applicant Electoral District", + "Assessment Result", + "Business Number", + "City", + "Community Population", + "Contact Business Phone", + "Contact Cell Phone", + "Contact Email", + "Contact Full Name", + "Contact Title", + "Decision Date", + "Decline Rationale", + "Due Date", + "Due Diligence Status", + "Economic Region", + "Forestry Focus", + "Forestry or Non-Forestry", + "FYE Day", + "FYE Month", + "Indigenous", + "Likelihood of Funding", + "Non-Registered Organization Name", + "Notes", + "Org Book Status", + "Organization Type", + "Other Sector/Sub/Industry Description", + "Owner", + "Payout", + "Place", + "Project Electoral District", + "Project End Date", + "Project Start Date", + "Project Summary", + "Projected Funding Total", + "Recommended Amount", + "Red-Stop", + "Regional District", + "Registered Organization Name", + "Registered Organization Number", + "Risk Ranking", + "Sector", + "Signing Authority Business Phone", + "Signing Authority Cell Phone", + "Signing Authority Email", + "Signing Authority Full Name", + "Signing Authority Title", + "SubSector", + "Total Paid Amount $", + "Total Project Budget", + "Total Score", + "Unity Application Id", + ], + doubleToggle: [ + "Applicant Id", + "Applicant Name", + "Approved Amount", + "Assignee", + "Category", + "Community", + "Project Name", + "Requested Amount", + "Status", + "Sub-Status", + "Submission #", + "Submission Date", + "Tags", + ], + }; + + // TEST renders the Submission tab inside an open shadow root (Form.io). + // Enabling this makes cy.get / cy.contains pierce shadow DOM consistently across envs. + before(() => { + Cypress.config("includeShadowDom", true); + loginIfNeeded({ timeout: 20000 }); + }); + + it("Switch to Default Grants Program if available", () => { + page.switchToGrantProgram("Default Grants Program"); + }); + + it("Tests the existence and functionality of the Submitted Date From and Submitted Date To filters", () => { + // Set date filters and verify table refresh + page + .setSubmittedFromDate("2022-01-01") + .waitForTableRefresh() + .setSubmittedToDate(page.getTodayIsoLocal()) + .waitForTableRefresh(); + }); + + // With no rows selected verify the visibility of Filter, Export, Save View, and Columns. + it("Verify the action buttons are visible with no rows selected", () => { + // Placeholder for future implementation + }); + + // With one row selected verify the visibility of Filter, Export, Save View, and Columns. + it("Verify the action buttons are visible with one row selected", () => { + // Placeholder for future implementation + }); + + it("Clicks Payment and force-closes the modal", () => { + // Ensure table has data and select two rows + page + .verifyTableHasData() + .selectMultipleRows([0, 1]) + .verifyActionBarExists() + .clickPaymentButton() + .waitForPaymentModalVisible() + .closePaymentModal() + .verifyPaymentModalClosed(); + + // Verify right-side buttons are still usable + page + .verifyDynamicButtonContainerExists() + .verifyExportButtonVisible() + .verifySaveViewButtonVisible() + .verifyColumnsButtonVisible(); + }); + + // Walk the Columns menu and toggle each column on, verifying the column is visible. + it("Verify all columns in the menu are visible when and toggled on.", () => { + // Reset to default view and open columns menu + page.closeOpenDropdowns().resetToDefaultView().openColumnsMenu(); + + // Toggle all single-toggle columns + page.toggleColumns(COLUMNS_TO_TOGGLE.singleToggle); + + // Toggle all double-toggle columns (toggle twice to ensure visibility) + COLUMNS_TO_TOGGLE.doubleToggle.forEach((column) => { + page.clickColumnsItem(column).clickColumnsItem(column); + }); + + // Close the columns menu + page.closeColumnsMenu(); + + // Verify columns by scrolling through the table + page + .scrollTableHorizontally(0) + .assertVisibleHeadersInclude(COLUMN_VISIBILITY_DATA.scrollPosition0); + + page + .scrollTableHorizontally(1500) + .assertVisibleHeadersInclude(COLUMN_VISIBILITY_DATA.scrollPosition1500); + + page + .scrollTableHorizontally(3000) + .assertVisibleHeadersInclude(COLUMN_VISIBILITY_DATA.scrollPosition3000); + + page + .scrollTableHorizontally(4500) + .assertVisibleHeadersInclude(COLUMN_VISIBILITY_DATA.scrollPosition4500); + + page + .scrollTableHorizontally(6000) + .assertVisibleHeadersInclude(COLUMN_VISIBILITY_DATA.scrollPosition6000); + + page + .scrollTableHorizontally(7500) + .assertVisibleHeadersInclude(COLUMN_VISIBILITY_DATA.scrollPosition7500); + + page + .scrollTableHorizontally(9000) + .assertVisibleHeadersInclude(COLUMN_VISIBILITY_DATA.scrollPosition9000); }); it("Verify Logout", () => { diff --git a/applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts b/applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts index 8415064169..34eb158273 100644 --- a/applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts +++ b/applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts @@ -1,5 +1,511 @@ +/// + import { BasePage } from "./BasePage"; +/** + * ApplicationsListPage - Page Object for the Grant Applications List page + * Handles action bar, filters, table operations, columns menu, and modals + */ +export class ApplicationsListPage extends BasePage { + private readonly STANDARD_TIMEOUT = 20000; + private readonly BUTTON_TIMEOUT = 60000; + + // Date filter selectors + private readonly dateFilters = { + submittedFromDate: "input#submittedFromDate", + submittedToDate: "input#submittedToDate", + spinner: 'div.spinner-grow[role="status"]', + }; + + // Action bar selectors + private readonly actionBar = { + customButtons: "#app_custom_buttons", + dynamicButtonContainer: "#dynamicButtonContainerId", + paymentButton: "#applicationPaymentRequest", + exportButton: "#dynamicButtonContainerId .dt-buttons button span", + saveViewButton: "button.grp-savedStates", + columnsButton: "span", + }; + + // Table selectors + private readonly table = { + scrollBody: ".dt-scroll-body", + tableRows: ".dt-scroll-body tbody tr", + scrollHead: ".dt-scroll-head", + columnTitles: ".dt-scroll-head span.dt-column-title", + }; + + // Columns menu selectors + private readonly columnsMenu = { + dropdownItem: "a.dropdown-item", + buttonBackground: "div.dt-button-background", + }; + + // Payment modal selectors + private readonly paymentModal = { + modal: "#payment-modal", + backdrop: ".modal-backdrop", + cancelButton: "#payment-modal .modal-footer button", + }; + + // Grant program selectors + private readonly grantProgram = { + userInitials: ".unity-user-initials", + userDropdown: "#user-dropdown a.dropdown-item", + searchInput: "#search-grant-programs", + programsTable: "#UserGrantProgramsTable", + programsTableRow: "#UserGrantProgramsTable tbody tr", + }; + + // Save view selectors + private readonly saveView = { + button: "button.grp-savedStates", + resetOption: "a.dropdown-item", + }; + + constructor() { + super(); + } + + // ============ Date Filter Methods ============ + + /** + * Set the Submitted From Date filter + */ + setSubmittedFromDate(date: string): this { + cy.get(this.dateFilters.submittedFromDate, { timeout: this.STANDARD_TIMEOUT }) + .click({ force: true }) + .clear({ force: true }) + .type(date, { force: true }) + .trigger("change", { force: true }) + .blur({ force: true }) + .should("have.value", date); + return this; + } + + /** + * Set the Submitted To Date filter + */ + setSubmittedToDate(date: string): this { + cy.get(this.dateFilters.submittedToDate, { timeout: this.STANDARD_TIMEOUT }) + .click({ force: true }) + .clear({ force: true }) + .type(date, { force: true }) + .trigger("change", { force: true }) + .blur({ force: true }) + .should("have.value", date); + return this; + } + + /** + * Wait for table refresh (spinner to be hidden) + */ + waitForTableRefresh(): this { + cy.get(this.dateFilters.spinner, { timeout: this.STANDARD_TIMEOUT }).then( + ($s: JQuery) => { + cy.wrap($s) + .should("have.attr", "style") + .and("contain", "display: none"); + } + ); + return this; + } + + /** + * Get today's date in ISO local format (YYYY-MM-DD) + */ + getTodayIsoLocal(): string { + const d = new Date(); + const pad2 = (n: number) => String(n).padStart(2, "0"); + return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`; + } + + // ============ Table Methods ============ + + /** + * Verify table has rows + */ + verifyTableHasData(): this { + cy.get(this.table.tableRows, { timeout: this.STANDARD_TIMEOUT }).should( + "have.length.greaterThan", + 1 + ); + return this; + } + + /** + * Select a row by index (clicks on a non-link cell) + */ + selectRowByIndex(rowIndex: number, withCtrl = false): this { + cy.get(this.table.tableRows, { timeout: this.STANDARD_TIMEOUT }) + .eq(rowIndex) + .find("td") + .not(":has(a)") + .first() + .click({ force: true, ctrlKey: withCtrl }); + return this; + } + + /** + * Select multiple rows by indices + */ + selectMultipleRows(indices: number[]): this { + indices.forEach((index, i) => { + this.selectRowByIndex(index, i > 0); + }); + return this; + } + + /** + * Scroll table horizontally to a specific position + */ + scrollTableHorizontally(x: number): this { + cy.get(this.table.scrollBody, { timeout: this.STANDARD_TIMEOUT }) + .should("exist") + .scrollTo(x, 0, { duration: 0, ensureScrollable: false }); + return this; + } + + /** + * Get visible header titles from the table + */ + getVisibleHeaderTitles(): Cypress.Chainable { + return cy + .get(this.table.columnTitles, { timeout: this.STANDARD_TIMEOUT }) + .then(($els: JQuery) => { + const titles: string[] = Cypress.$($els) + .toArray() + .map((el: HTMLElement) => (el.textContent || "").replace(/\s+/g, " ").trim()) + .filter((t: string) => t.length > 0); + return titles; + }); + } + + /** + * Assert that visible headers include expected columns + */ + assertVisibleHeadersInclude(expected: string[]): this { + this.getVisibleHeaderTitles().then((titles: string[]) => { + expected.forEach((e: string) => { + expect(titles, `visible headers should include "${e}"`).to.include(e); + }); + }); + return this; + } + + // ============ Action Bar Methods ============ + + /** + * Scroll to and verify action bar exists + */ + verifyActionBarExists(): this { + cy.get(this.actionBar.customButtons, { timeout: this.STANDARD_TIMEOUT }) + .should("exist") + .scrollIntoView(); + return this; + } + + /** + * Click the Payment button + */ + clickPaymentButton(): this { + cy.get(this.actionBar.paymentButton, { timeout: this.BUTTON_TIMEOUT }) + .should("be.visible") + .and("not.be.disabled") + .click({ force: true }); + return this; + } + + /** + * Verify Export button is visible + */ + verifyExportButtonVisible(): this { + cy.contains(this.actionBar.exportButton, "Export", { + timeout: this.STANDARD_TIMEOUT, + }).should("be.visible"); + return this; + } + + /** + * Verify Save View button is visible + */ + verifySaveViewButtonVisible(): this { + cy.contains( + "#dynamicButtonContainerId button.grp-savedStates", + "Save View", + { timeout: this.STANDARD_TIMEOUT } + ).should("be.visible"); + return this; + } + + /** + * Verify Columns button is visible + */ + verifyColumnsButtonVisible(): this { + cy.contains( + "#dynamicButtonContainerId .dt-buttons button span", + "Columns", + { timeout: this.STANDARD_TIMEOUT } + ).should("be.visible"); + return this; + } + + /** + * Verify dynamic button container exists + */ + verifyDynamicButtonContainerExists(): this { + cy.get(this.actionBar.dynamicButtonContainer, { + timeout: this.STANDARD_TIMEOUT, + }) + .should("exist") + .scrollIntoView(); + return this; + } + + // ============ Payment Modal Methods ============ + + /** + * Wait for payment modal to be visible + */ + waitForPaymentModalVisible(): this { + cy.get(this.paymentModal.modal, { timeout: this.STANDARD_TIMEOUT }) + .should("be.visible") + .and("have.class", "show"); + return this; + } + + /** + * Close payment modal using multiple strategies + */ + closePaymentModal(): this { + // Attempt ESC key + cy.get("body").type("{esc}", { force: true }); + + // Click backdrop if present + cy.get(this.paymentModal.backdrop, { timeout: this.STANDARD_TIMEOUT }).then( + ($bd: JQuery) => { + if ($bd.length) { + cy.wrap($bd).click("topLeft", { force: true }); + } + } + ); + + // Try Cancel button if available + cy.contains(this.paymentModal.cancelButton, "Cancel", { + timeout: this.STANDARD_TIMEOUT, + }).then(($btn: JQuery) => { + if ($btn && $btn.length > 0) { + cy.wrap($btn).scrollIntoView().click({ force: true }); + } else { + cy.log("Cancel button not present, proceeding to hard-close fallback"); + } + }); + + // Hard close fallback using jQuery + cy.window().then((win: Cypress.AUTWindow) => { + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const windowWithModal = win as any; + if (typeof windowWithModal.closePaymentModal === "function") { + windowWithModal.closePaymentModal(); + } + } catch { + /* ignore */ + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const $ = (win as any).jQuery || (win as any).$; + if ($) { + try { + $("#payment-modal") + .removeClass("show") + .attr("aria-hidden", "true") + .css("display", "none"); + $(".modal-backdrop").remove(); + $("body").removeClass("modal-open").css("overflow", ""); + } catch { + /* ignore */ + } + } + }); + return this; + } + + /** + * Verify payment modal is closed + */ + verifyPaymentModalClosed(): this { + cy.get(this.paymentModal.modal, { timeout: this.STANDARD_TIMEOUT }).should( + ($m: JQuery) => { + const isHidden = !$m.is(":visible") || !$m.hasClass("show"); + expect(isHidden, "payment-modal hidden or not shown").to.eq(true); + } + ); + cy.get(this.paymentModal.backdrop, { timeout: this.STANDARD_TIMEOUT }).should( + "not.exist" + ); + return this; + } + + // ============ Columns Menu Methods ============ + + /** + * Close any open dropdowns or modals + */ + closeOpenDropdowns(): this { + cy.get("body").then(($body: JQuery) => { + if ($body.find(this.columnsMenu.buttonBackground).length > 0) { + cy.get(this.columnsMenu.buttonBackground).click({ force: true }); + } + }); + return this; + } + + /** + * Open Save View dropdown and reset to default + */ + resetToDefaultView(): this { + cy.get(this.saveView.button, { timeout: this.STANDARD_TIMEOUT }) + .should("be.visible") + .and("contain.text", "Save View") + .click(); + + cy.contains(this.saveView.resetOption, "Reset to Default View", { + timeout: this.STANDARD_TIMEOUT, + }) + .should("exist") + .click({ force: true }); + + // Wait for table to rebuild + cy.get(this.table.columnTitles, { timeout: this.STANDARD_TIMEOUT }).should( + "have.length.gt", + 5 + ); + return this; + } + + /** + * Open the Columns menu + */ + openColumnsMenu(): this { + cy.contains("span", "Columns", { timeout: this.STANDARD_TIMEOUT }) + .should("be.visible") + .click(); + + // Wait for dropdown to be fully populated + cy.get(this.columnsMenu.dropdownItem, { timeout: this.STANDARD_TIMEOUT }).should( + "have.length.gt", + 50 + ); + return this; + } + + /** + * Click a column item in the Columns menu + */ + clickColumnsItem(label: string): this { + cy.contains(this.columnsMenu.dropdownItem, label, { + timeout: this.STANDARD_TIMEOUT, + }) + .should("exist") + .scrollIntoView() + .click({ force: true }); + return this; + } + + /** + * Toggle multiple columns (click each one) + */ + toggleColumns(columns: string[]): this { + columns.forEach((column) => { + this.clickColumnsItem(column); + }); + return this; + } + + /** + * Close the Columns menu + */ + closeColumnsMenu(): this { + cy.get(this.columnsMenu.buttonBackground, { timeout: this.STANDARD_TIMEOUT }) + .should("exist") + .click({ force: true }); + + cy.get(this.columnsMenu.buttonBackground, { + timeout: this.STANDARD_TIMEOUT, + }).should("not.exist"); + return this; + } + + // ============ Grant Program Methods ============ + + /** + * Switch to a specific grant program if available + */ + switchToGrantProgram(programName: string): this { + cy.get("body").then(($body: JQuery) => { + const hasUserInitials = + $body.find(this.grantProgram.userInitials).length > 0; + + if (!hasUserInitials) { + cy.log("Skipping tenant switch: no user initials menu found"); + return; + } + + cy.get(this.grantProgram.userInitials).click(); + + cy.get("body").then(($body2: JQuery) => { + const switchLink = $body2 + .find(this.grantProgram.userDropdown) + .filter((_: number, el: HTMLElement) => { + return (el.textContent || "").trim() === "Switch Grant Programs"; + }); + + if (switchLink.length === 0) { + cy.log( + 'Skipping tenant switch: "Switch Grant Programs" not present for this user/session' + ); + cy.get("body").click(0, 0); + return; + } + + cy.wrap(switchLink.first()).click(); + + cy.url({ timeout: this.STANDARD_TIMEOUT }).should( + "include", + "/GrantPrograms" + ); + + cy.get(this.grantProgram.searchInput, { timeout: this.STANDARD_TIMEOUT }) + .should("be.visible") + .clear() + .type(programName); + + cy.contains(this.grantProgram.programsTableRow, programName, { + timeout: this.STANDARD_TIMEOUT, + }) + .should("exist") + .within(() => { + cy.contains("button", "Select").should("be.enabled").click(); + }); + + cy.location("pathname", { timeout: this.STANDARD_TIMEOUT }).should( + (p: string) => { + expect( + p.indexOf("/GrantApplications") >= 0 || p.indexOf("/auth/") >= 0 + ).to.eq(true); + } + ); + }); + }); + return this; + } +} + +/** + * ApplicationDetailsPage - Page Object for the Application Details page + * Handles tabs, status actions, and field verification + */ export class ApplicationDetailsPage extends BasePage { // Tab selectors private readonly tabs = { From 79329bb0ffbe18d0c1c60b2666b6a7fcec534ef6 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:06:56 -0800 Subject: [PATCH 003/191] AB#28769 - Refactor ApplicationContactsWidget to support reload --- .../EditContactModal.cshtml | 4 + .../ApplicationContact/EditContactModal.js | 46 ++++++ .../Components/ApplicantInfo/Default.cshtml | 13 +- .../ApplicationContactsWidgetController.cs | 4 +- .../ApplicationContactsWidgetViewComponent.cs | 9 +- .../ApplicationContactsWidgetViewModel.cs | 1 - .../ApplicationContactsWidget/Default.cshtml | 104 ++++++++----- .../ApplicationContactsWidget/Default.js | 143 +++++++++++------- .../Components/SummaryWidget/Default.js | 37 +---- 9 files changed, 208 insertions(+), 153 deletions(-) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationContact/EditContactModal.js diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationContact/EditContactModal.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationContact/EditContactModal.cshtml index 4370847408..4e09aea7c7 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationContact/EditContactModal.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationContact/EditContactModal.cshtml @@ -10,6 +10,10 @@ Layout = null; } +@section scripts { + +} + diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationContact/EditContactModal.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationContact/EditContactModal.js new file mode 100644 index 0000000000..a0bdff0ab1 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationContact/EditContactModal.js @@ -0,0 +1,46 @@ +(function ($) { + abp.modals.editOrDeleteContactModal = function () { + let initModal = function (publicApi, args) { + let modalManager = publicApi; + + $('#DeleteContactButton').click(handleDeleteContact); + + function handleDeleteContact(e) { + e.preventDefault(); + abp.message.confirm('Are you sure to delete this contact?') + .then(processDeleteConfirmation); + } + + function processDeleteConfirmation(confirmed) { + if (confirmed) { + deleteContact(); + } + } + + function deleteContact() { + try { + unity.grantManager.grantApplications.applicationContact + .delete(args.id) + .done(onContactDeleted) + .fail(onDeleteFailure); + } catch (error) { + onDeleteFailure(error); + } + } + + function onContactDeleted() { + modalManager.close(); + PubSub.publish("refresh_application_contacts"); + abp.notify.success('The contact has been deleted.'); + } + + function onDeleteFailure(error) { + abp.notify.error('Contact deletion failed.'); + if (error) { + console.log(error); + } + } + }; + return { initModal: initModal }; + } +})(jQuery); \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.cshtml index 7f30880c23..45dc00f84a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.cshtml @@ -369,18 +369,7 @@ form-id="@Model.ApplicationFormId" show-legend="false" editable-if="IsAdditionalContactEditable"> -
@L["Summary:ContactsTitle"].Value
-
- @await Component.InvokeAsync("ApplicationContactsWidget", new { applicationId = Model.ApplicationId, isReadOnly = !IsAdditionalContactEditable }) -
- - @if (IsAdditionalContactAddable) - { -
- -
- } + @await Component.InvokeAsync("ApplicationContactsWidget", new { applicationId = Model.ApplicationId })
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/ApplicationContactsWidgetController.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/ApplicationContactsWidgetController.cs index 3d1a62cd12..bdad153023 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/ApplicationContactsWidgetController.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/ApplicationContactsWidgetController.cs @@ -15,14 +15,14 @@ public class ApplicationContactsWidgetController : AbpController [HttpGet] [Route("RefreshApplicationContacts")] - public IActionResult ApplicationContacts(Guid applicationId, Boolean isReadOnly = false) + public IActionResult ApplicationContacts(Guid applicationId) { if (!ModelState.IsValid) { logger.LogWarning("Invalid model state for ApplicationContactsWidgetController: RefreshApplicationContacts"); return ViewComponent("ApplicationContactsWidget"); } - return ViewComponent("ApplicationContactsWidget", new { applicationId, isReadOnly }); + return ViewComponent("ApplicationContactsWidget", new { applicationId }); } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/ApplicationContactsWidgetViewComponent.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/ApplicationContactsWidgetViewComponent.cs index 9022e29a1e..d65bba4c28 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/ApplicationContactsWidgetViewComponent.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/ApplicationContactsWidgetViewComponent.cs @@ -13,7 +13,7 @@ namespace Unity.GrantManager.Web.Views.Shared.Components.ApplicationContactsWidg RefreshUrl = "Widgets/ApplicationContacts/RefreshApplicationContacts", ScriptTypes = new[] { typeof(ApplicationContactsWidgetScriptBundleContributor) }, StyleTypes = new[] { typeof(ApplicationContactsWidgetStyleBundleContributor) }, - AutoInitialize = true)] + AutoInitialize = false)] public class ApplicationContactsWidgetViewComponent : AbpViewComponent { private readonly IApplicationContactService _applicationContactService; @@ -23,13 +23,12 @@ public ApplicationContactsWidgetViewComponent(IApplicationContactService applica _applicationContactService = applicationContactService; } - public async Task InvokeAsync(Guid applicationId, Boolean isReadOnly) + public async Task InvokeAsync(Guid applicationId) { List applicationContacts = await _applicationContactService.GetListByApplicationAsync(applicationId); ApplicationContactsWidgetViewModel model = new() { ApplicationContacts = applicationContacts, - ApplicationId = applicationId, - IsReadOnly = isReadOnly + ApplicationId = applicationId }; return View(model); @@ -52,7 +51,7 @@ public override void ConfigureBundle(BundleConfigurationContext context) context.Files .AddIfNotContains("/Views/Shared/Components/ApplicationContactsWidget/Default.js"); context.Files - .AddIfNotContains("/libs/pubsub-js/src/pubsub.js"); + .AddIfNotContains("/Pages/ApplicationContact/EditContactModal.js"); } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/ApplicationContactsWidgetViewModel.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/ApplicationContactsWidgetViewModel.cs index e76480ab1a..13afa46c34 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/ApplicationContactsWidgetViewModel.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/ApplicationContactsWidgetViewModel.cs @@ -14,7 +14,6 @@ public ApplicationContactsWidgetViewModel() public List ApplicationContacts { get; set; } public Guid ApplicationId { get; set; } - public Boolean IsReadOnly { get; set; } public static String ContactTypeValue(String contactType) { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/Default.cshtml index ae8f74799b..cfa1516620 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/Default.cshtml @@ -1,52 +1,74 @@ +@using Microsoft.Extensions.Localization @using Unity.GrantManager.Web.Views.Shared.Components.ApplicationContactsWidget; +@using Unity.GrantManager.Localization; +@using Unity.Modules.Shared +@using Volo.Abp.Authorization.Permissions + +@inject IPermissionChecker PermissionChecker +@inject IStringLocalizer L @model ApplicationContactsWidgetViewModel @{ Layout = null; } -
- @if (Model.ApplicationContacts.Count > 0) { -

Info

-
- } - @foreach (var contact in Model.ApplicationContacts) - { -
-
-
-

@ApplicationContactsWidgetViewModel.ContactTypeValue(contact.ContactType)

-

@contact.ContactFullName, @contact.ContactTitle

- @if (!contact.ContactEmail.IsNullOrEmpty()) - { -
- -
@contact.ContactEmail
-
- } - @if (!contact.ContactMobilePhone.IsNullOrEmpty()) - { -
- -
@contact.ContactMobilePhone
-
- } - @if (!contact.ContactWorkPhone.IsNullOrEmpty()) - { -
- -
@contact.ContactWorkPhone
-
- } -
- @if(!(Model.IsReadOnly)) { -
- +@section scripts { + +} + +
@L["Summary:ContactsTitle"].Value
+ +@if (Model.ApplicationContacts.Count > 0) { +

Info

+
+} + +@foreach (var contact in Model.ApplicationContacts) +{ +
+
+
+

@ApplicationContactsWidgetViewModel.ContactTypeValue(contact.ContactType)

+

@contact.ContactFullName, @contact.ContactTitle

+ @if (!contact.ContactEmail.IsNullOrEmpty()) + { +
+ +
@contact.ContactEmail
+
+ } + @if (!contact.ContactMobilePhone.IsNullOrEmpty()) + { +
+ +
@contact.ContactMobilePhone
+
+ } + @if (!contact.ContactWorkPhone.IsNullOrEmpty()) + { +
+ +
@contact.ContactWorkPhone
}
-
+ @if (await PermissionChecker.IsGrantedAsync(UnitySelector.Applicant.AdditionalContact.Update)) + { +
+ +
+ }
- } -
+
+
+} + +@if (await PermissionChecker.IsGrantedAsync(UnitySelector.Applicant.AdditionalContact.Create)) +{ +
+ +
+} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/Default.js index 5e48b25ebe..18aadf9954 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/Default.js @@ -1,74 +1,103 @@ $(function () { - - let contactModal = new abp.ModalManager({ + let applicantContactsWidgetToken = null; + let _createContactModal = new abp.ModalManager(abp.appPath + 'ApplicationContact/CreateContactModal'); + let _editContactModal = new abp.ModalManager({ viewUrl: abp.appPath + 'ApplicationContact/EditContactModal', - modalClass: "editContactModal" + scriptUrl: '/Pages/ApplicationContact/EditContactModal.js', + modalClass: "editOrDeleteContactModal" }); - abp.modals.editContactModal = function () { - let initModal = function (publicApi, args) { - setupContactModal(args); - }; - return { initModal: initModal }; - } - - $('body').on('click','.contact-edit-btn',function(e){ - e.preventDefault(); - let itemId = $(this).data('id'); - contactModal.open({ - id: itemId - }); + // Handle modal result - refresh the widget after successful contact creation + _createContactModal.onResult(function () { + PubSub.publish("refresh_application_contacts"); + abp.notify.success( + 'The application contact have been successfully added.', + 'Application Contacts' + ); }); - contactModal.onResult(function () { + _editContactModal.onResult(function () { + PubSub.publish("refresh_application_contacts"); abp.notify.success( 'The application contact have been successfully updated.', 'Application Contacts' ); - PubSub.publish("refresh_application_contacts"); }); - let setupContactModal = function (args) { - $('#DeleteContactButton').click(handleDeleteContact); + abp.widgets.ApplicationContactsWidget = function ($wrapper) { - function handleDeleteContact(e) { - e.preventDefault(); - showDeleteConfirmation(); - } - - function showDeleteConfirmation() { - abp.message.confirm('Are you sure to delete this contact?') - .then(processDeleteConfirmation); - } - - function processDeleteConfirmation(confirmed) { - if (confirmed) { - deleteContact(); - } - } - - function deleteContact() { - try { - unity.grantManager.grantApplications.applicationContact - .delete(args.id) - .done(onContactDeleted) - .fail(onDeleteFailure); - } catch (error) { - onDeleteFailure(error); + let _widgetManager = $wrapper.data('abp-widget-manager'); + + let widgetApi = { + applicationId: null, // Cache the applicationId to prevent reading from stale DOM + + getFilters: function () { + const appId = this.applicationId || $wrapper.find('#ApplicationContactsWidget_ApplicationId').val(); + + return { + applicationId: appId + }; + }, + + init: function (filters) { + this.applicationId = $wrapper.find('#ApplicationContactsWidget_ApplicationId').val(); + this.setupEventHandlers(); + }, + + refresh: function () { + const currentFilters = this.getFilters(); + _widgetManager.refresh($wrapper, currentFilters); + }, + + setupEventHandlers: function() { + const self = this; + + // Unsubscribe from previous subscription if it exists + // This prevents duplicate event handlers after widget refresh + if (applicantContactsWidgetToken) { + PubSub.unsubscribe(applicantContactsWidgetToken); + applicantContactsWidgetToken = null; + } + + // Subscribe to the applicant_info_merged event and store the token + applicantContactsWidgetToken = PubSub.subscribe( + 'refresh_application_contacts', + () => { + self.refresh(); + } + ); + + // Handle Add Contact button click + $wrapper.on('click', '#CreateContactButton', function (e) { + e.preventDefault(); + _createContactModal.open({ + applicationId: $('#ApplicationContactsWidget_ApplicationId').val() + }); + }); + + $wrapper.on('click', '.contact-edit-btn', function (e) { + e.preventDefault(); + let itemId = $(this).data('id'); + _editContactModal.open({ + id: itemId + }); + }); } } - - function onContactDeleted() { - PubSub.publish("refresh_application_contacts"); - contactModal.close(); - abp.notify.success('The contact has been deleted.'); - } - - function onDeleteFailure(error) { - abp.notify.error('Contact deletion failed.'); - if (error) { - console.log(error); - } + + return widgetApi; + }; + + // Initialize the ApplicationContactsWidget manager with filter callback + let applicationContactsWidgetManager = new abp.WidgetManager({ + wrapper: '.abp-widget-wrapper[data-widget-name="ApplicationContactsWidget"]', + filterCallback: function () { + return { + 'applicationId': $('#ApplicationContactsWidget_ApplicationId').val() + }; } - } + }); + + // Initialize the widget + applicationContactsWidgetManager.init(); }); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/SummaryWidget/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/SummaryWidget/Default.js index 84d64f5e4b..72d6b2e036 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/SummaryWidget/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/SummaryWidget/Default.js @@ -1,38 +1,5 @@ $(function () { - let applicationId = document.getElementById('SummaryWidgetApplicationId').value; - let isReadOnly = document.getElementById('SummaryWidgetIsReadOnly').value; - let contactModal = new abp.ModalManager(abp.appPath + 'ApplicationContact/CreateContactModal'); - - let applicationContactsWidgetManager = new abp.WidgetManager({ - wrapper: '#applicationContactsWidget', - filterCallback: function () { - return { - 'applicationId': applicationId, - 'isReadOnly': isReadOnly - }; - } - }); - - $('#AddContactButton').click(function (e) { - e.preventDefault(); - contactModal.open({ - applicationId: applicationId - }); - }); - - contactModal.onResult(function () { - abp.notify.success( - 'The application contact have been successfully added.', - 'Application Contacts' - ); - applicationContactsWidgetManager.refresh(); - }); - - PubSub.subscribe( - 'refresh_application_contacts', - (msg, data) => { - applicationContactsWidgetManager.refresh(); - } - ); + // SummaryWidget initialization + // Contact modal and widget management moved to ApplicationContactsWidget component }); From c5f13e47dfdc4e1adb9274b4f377fd822ab0d3d7 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:12:57 -0800 Subject: [PATCH 004/191] AB#28769 - Update ApplicationContactWidget unity tests --- .../Components/ApplicationContactWidgetTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/ApplicationContactWidgetTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/ApplicationContactWidgetTests.cs index abab7db3d5..9b59458b0f 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/ApplicationContactWidgetTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/ApplicationContactWidgetTests.cs @@ -64,7 +64,7 @@ public async Task ApplicationContactWidgetReturnsStatus() }; //Act - var result = await viewComponent.InvokeAsync(applicationId, true) as ViewViewComponentResult; + var result = await viewComponent.InvokeAsync(applicationId) as ViewViewComponentResult; ApplicationContactsWidgetViewModel? resultModel; resultModel = result!.ViewData!.Model! as ApplicationContactsWidgetViewModel; From 295e5b45f487aba8b5e1a57e7b89a9ad7adfcca1 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:32:46 -0800 Subject: [PATCH 005/191] AB#28769 - Add contact refresh code quality improvements --- .../EditContactModal.cshtml | 6 ++--- .../Components/ApplicantInfo/Default.cshtml | 1 - .../ApplicationContactsWidget/Default.cshtml | 13 ++++++++--- .../ApplicationContactsWidget/Default.js | 23 +++++++++---------- 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationContact/EditContactModal.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationContact/EditContactModal.cshtml index 4e09aea7c7..0ca46aa5db 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationContact/EditContactModal.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationContact/EditContactModal.cshtml @@ -10,9 +10,9 @@ Layout = null; } -@section scripts { - -} + +@* NOTE: Dependency /Pages/ApplicationContact/EditContactModal.js is included through ApplicationContactsWidget *@ + diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.cshtml index 45dc00f84a..ad1d64a50c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.cshtml @@ -30,7 +30,6 @@ }); bool IsViewEditable = !updatePermissionResult.AllProhibited; - bool IsAdditionalContactAddable = await PermissionChecker.IsGrantedAsync(UnitySelector.Applicant.AdditionalContact.Create); bool IsAdditionalContactEditable = await PermissionChecker.IsGrantedAsync(UnitySelector.Applicant.AdditionalContact.Update); bool IsAssignApplicant = await PermissionChecker.IsGrantedAsync(GrantApplicationPermissions.Applicants.AssignApplicant); bool IsLookupEnabled = await PermissionChecker.IsGrantedAsync(UnitySelector.Applicant.Summary.Update); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/Default.cshtml index cfa1516620..44c6883268 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/Default.cshtml @@ -16,6 +16,11 @@ } +@{ + bool IsAdditionalContactAddable = await PermissionChecker.IsGrantedAsync(UnitySelector.Applicant.AdditionalContact.Create); + bool IsAdditionalContactEditable = await PermissionChecker.IsGrantedAsync(UnitySelector.Applicant.AdditionalContact.Update); +} +
@L["Summary:ContactsTitle"].Value
@if (Model.ApplicationContacts.Count > 0) { @@ -52,10 +57,12 @@
}
- @if (await PermissionChecker.IsGrantedAsync(UnitySelector.Applicant.AdditionalContact.Update)) + @if (IsAdditionalContactEditable) {
-
} @@ -64,7 +71,7 @@
} -@if (await PermissionChecker.IsGrantedAsync(UnitySelector.Applicant.AdditionalContact.Create)) +@if (IsAdditionalContactAddable) {
{ @@ -67,15 +66,20 @@ } ); + // Prevent duplicate delegated click handlers on re-init by removing any + // existing handlers in this widget's namespace before re-binding. + $wrapper.off('click.ApplicationContactsWidget', '#CreateContactButton'); + $wrapper.off('click.ApplicationContactsWidget', '.contact-edit-btn'); + // Handle Add Contact button click - $wrapper.on('click', '#CreateContactButton', function (e) { + $wrapper.on('click.ApplicationContactsWidget', '#CreateContactButton', function (e) { e.preventDefault(); _createContactModal.open({ - applicationId: $('#ApplicationContactsWidget_ApplicationId').val() + applicationId: self.applicationId || $wrapper.find('#ApplicationContactsWidget_ApplicationId').val() }); }); - $wrapper.on('click', '.contact-edit-btn', function (e) { + $wrapper.on('click.ApplicationContactsWidget', '.contact-edit-btn', function (e) { e.preventDefault(); let itemId = $(this).data('id'); _editContactModal.open({ @@ -90,12 +94,7 @@ // Initialize the ApplicationContactsWidget manager with filter callback let applicationContactsWidgetManager = new abp.WidgetManager({ - wrapper: '.abp-widget-wrapper[data-widget-name="ApplicationContactsWidget"]', - filterCallback: function () { - return { - 'applicationId': $('#ApplicationContactsWidget_ApplicationId').val() - }; - } + wrapper: '.abp-widget-wrapper[data-widget-name="ApplicationContactsWidget"]' }); // Initialize the widget From b545c6cf6319ea6ce91cebce340b141b735a1c5c Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:44:21 -0800 Subject: [PATCH 006/191] AB#28769 - Update applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/Default.cshtml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Components/ApplicationContactsWidget/Default.cshtml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/Default.cshtml index 44c6883268..1f127697b8 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/Default.cshtml @@ -12,10 +12,6 @@ Layout = null; } -@section scripts { - -} - @{ bool IsAdditionalContactAddable = await PermissionChecker.IsGrantedAsync(UnitySelector.Applicant.AdditionalContact.Create); bool IsAdditionalContactEditable = await PermissionChecker.IsGrantedAsync(UnitySelector.Applicant.AdditionalContact.Update); From 82821c7e29db7a0f203f79a9984cbe3cf95a20bf Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:45:17 -0800 Subject: [PATCH 007/191] AB#28769 - Update applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/ApplicationContactsWidgetController.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../ApplicationContactsWidgetController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/ApplicationContactsWidgetController.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/ApplicationContactsWidgetController.cs index bdad153023..bd396f328f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/ApplicationContactsWidgetController.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/ApplicationContactsWidgetController.cs @@ -20,7 +20,7 @@ public IActionResult ApplicationContacts(Guid applicationId) if (!ModelState.IsValid) { logger.LogWarning("Invalid model state for ApplicationContactsWidgetController: RefreshApplicationContacts"); - return ViewComponent("ApplicationContactsWidget"); + return ViewComponent("ApplicationContactsWidget", new { applicationId }); } return ViewComponent("ApplicationContactsWidget", new { applicationId }); } From 1d21a198d44458941829803c4225bfd90bcccc82 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:45:47 -0800 Subject: [PATCH 008/191] AB#28769 - Update applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/Default.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Shared/Components/ApplicationContactsWidget/Default.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/Default.js index 53e5a8d801..f1858be262 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/Default.js @@ -3,7 +3,7 @@ let _createContactModal = new abp.ModalManager(abp.appPath + 'ApplicationContact/CreateContactModal'); let _editContactModal = new abp.ModalManager({ viewUrl: abp.appPath + 'ApplicationContact/EditContactModal', - scriptUrl: '/Pages/ApplicationContact/EditContactModal.js', + scriptUrl: abp.appPath + 'Pages/ApplicationContact/EditContactModal.js', modalClass: "editOrDeleteContactModal" }); From f6fe19491e9deef4595d56cec8f90a81e2e908d2 Mon Sep 17 00:00:00 2001 From: Velang Date: Tue, 17 Feb 2026 15:40:21 -0800 Subject: [PATCH 009/191] Initial Commit fixing the issue with case insentivity --- .../cypress/e2e/ApplicationsActionBar.cy.ts | 4 +- .../cypress/pages/ApplicationDetailsPage.ts | 500 ------------------ .../Unity.AutoUI/cypress/support/auth.ts | 44 +- 3 files changed, 24 insertions(+), 524 deletions(-) diff --git a/applications/Unity.AutoUI/cypress/e2e/ApplicationsActionBar.cy.ts b/applications/Unity.AutoUI/cypress/e2e/ApplicationsActionBar.cy.ts index 8081e917c4..8a5e222e70 100644 --- a/applications/Unity.AutoUI/cypress/e2e/ApplicationsActionBar.cy.ts +++ b/applications/Unity.AutoUI/cypress/e2e/ApplicationsActionBar.cy.ts @@ -1,7 +1,7 @@ /// import { loginIfNeeded } from "../support/auth"; -import { ApplicationsListPage } from "../pages/ApplicationDetailsPage"; +import { ApplicationsListPage } from "../pages/ApplicationsListPage"; describe("Unity Login and check data from CHEFS", () => { const page = new ApplicationsListPage(); @@ -196,7 +196,7 @@ describe("Unity Login and check data from CHEFS", () => { .verifyTableHasData() .selectMultipleRows([0, 1]) .verifyActionBarExists() - .clickPaymentButton() + .clickPaymentButtonWithWait() .waitForPaymentModalVisible() .closePaymentModal() .verifyPaymentModalClosed(); diff --git a/applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts b/applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts index 34eb158273..6194b02f1f 100644 --- a/applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts +++ b/applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts @@ -2,506 +2,6 @@ import { BasePage } from "./BasePage"; -/** - * ApplicationsListPage - Page Object for the Grant Applications List page - * Handles action bar, filters, table operations, columns menu, and modals - */ -export class ApplicationsListPage extends BasePage { - private readonly STANDARD_TIMEOUT = 20000; - private readonly BUTTON_TIMEOUT = 60000; - - // Date filter selectors - private readonly dateFilters = { - submittedFromDate: "input#submittedFromDate", - submittedToDate: "input#submittedToDate", - spinner: 'div.spinner-grow[role="status"]', - }; - - // Action bar selectors - private readonly actionBar = { - customButtons: "#app_custom_buttons", - dynamicButtonContainer: "#dynamicButtonContainerId", - paymentButton: "#applicationPaymentRequest", - exportButton: "#dynamicButtonContainerId .dt-buttons button span", - saveViewButton: "button.grp-savedStates", - columnsButton: "span", - }; - - // Table selectors - private readonly table = { - scrollBody: ".dt-scroll-body", - tableRows: ".dt-scroll-body tbody tr", - scrollHead: ".dt-scroll-head", - columnTitles: ".dt-scroll-head span.dt-column-title", - }; - - // Columns menu selectors - private readonly columnsMenu = { - dropdownItem: "a.dropdown-item", - buttonBackground: "div.dt-button-background", - }; - - // Payment modal selectors - private readonly paymentModal = { - modal: "#payment-modal", - backdrop: ".modal-backdrop", - cancelButton: "#payment-modal .modal-footer button", - }; - - // Grant program selectors - private readonly grantProgram = { - userInitials: ".unity-user-initials", - userDropdown: "#user-dropdown a.dropdown-item", - searchInput: "#search-grant-programs", - programsTable: "#UserGrantProgramsTable", - programsTableRow: "#UserGrantProgramsTable tbody tr", - }; - - // Save view selectors - private readonly saveView = { - button: "button.grp-savedStates", - resetOption: "a.dropdown-item", - }; - - constructor() { - super(); - } - - // ============ Date Filter Methods ============ - - /** - * Set the Submitted From Date filter - */ - setSubmittedFromDate(date: string): this { - cy.get(this.dateFilters.submittedFromDate, { timeout: this.STANDARD_TIMEOUT }) - .click({ force: true }) - .clear({ force: true }) - .type(date, { force: true }) - .trigger("change", { force: true }) - .blur({ force: true }) - .should("have.value", date); - return this; - } - - /** - * Set the Submitted To Date filter - */ - setSubmittedToDate(date: string): this { - cy.get(this.dateFilters.submittedToDate, { timeout: this.STANDARD_TIMEOUT }) - .click({ force: true }) - .clear({ force: true }) - .type(date, { force: true }) - .trigger("change", { force: true }) - .blur({ force: true }) - .should("have.value", date); - return this; - } - - /** - * Wait for table refresh (spinner to be hidden) - */ - waitForTableRefresh(): this { - cy.get(this.dateFilters.spinner, { timeout: this.STANDARD_TIMEOUT }).then( - ($s: JQuery) => { - cy.wrap($s) - .should("have.attr", "style") - .and("contain", "display: none"); - } - ); - return this; - } - - /** - * Get today's date in ISO local format (YYYY-MM-DD) - */ - getTodayIsoLocal(): string { - const d = new Date(); - const pad2 = (n: number) => String(n).padStart(2, "0"); - return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`; - } - - // ============ Table Methods ============ - - /** - * Verify table has rows - */ - verifyTableHasData(): this { - cy.get(this.table.tableRows, { timeout: this.STANDARD_TIMEOUT }).should( - "have.length.greaterThan", - 1 - ); - return this; - } - - /** - * Select a row by index (clicks on a non-link cell) - */ - selectRowByIndex(rowIndex: number, withCtrl = false): this { - cy.get(this.table.tableRows, { timeout: this.STANDARD_TIMEOUT }) - .eq(rowIndex) - .find("td") - .not(":has(a)") - .first() - .click({ force: true, ctrlKey: withCtrl }); - return this; - } - - /** - * Select multiple rows by indices - */ - selectMultipleRows(indices: number[]): this { - indices.forEach((index, i) => { - this.selectRowByIndex(index, i > 0); - }); - return this; - } - - /** - * Scroll table horizontally to a specific position - */ - scrollTableHorizontally(x: number): this { - cy.get(this.table.scrollBody, { timeout: this.STANDARD_TIMEOUT }) - .should("exist") - .scrollTo(x, 0, { duration: 0, ensureScrollable: false }); - return this; - } - - /** - * Get visible header titles from the table - */ - getVisibleHeaderTitles(): Cypress.Chainable { - return cy - .get(this.table.columnTitles, { timeout: this.STANDARD_TIMEOUT }) - .then(($els: JQuery) => { - const titles: string[] = Cypress.$($els) - .toArray() - .map((el: HTMLElement) => (el.textContent || "").replace(/\s+/g, " ").trim()) - .filter((t: string) => t.length > 0); - return titles; - }); - } - - /** - * Assert that visible headers include expected columns - */ - assertVisibleHeadersInclude(expected: string[]): this { - this.getVisibleHeaderTitles().then((titles: string[]) => { - expected.forEach((e: string) => { - expect(titles, `visible headers should include "${e}"`).to.include(e); - }); - }); - return this; - } - - // ============ Action Bar Methods ============ - - /** - * Scroll to and verify action bar exists - */ - verifyActionBarExists(): this { - cy.get(this.actionBar.customButtons, { timeout: this.STANDARD_TIMEOUT }) - .should("exist") - .scrollIntoView(); - return this; - } - - /** - * Click the Payment button - */ - clickPaymentButton(): this { - cy.get(this.actionBar.paymentButton, { timeout: this.BUTTON_TIMEOUT }) - .should("be.visible") - .and("not.be.disabled") - .click({ force: true }); - return this; - } - - /** - * Verify Export button is visible - */ - verifyExportButtonVisible(): this { - cy.contains(this.actionBar.exportButton, "Export", { - timeout: this.STANDARD_TIMEOUT, - }).should("be.visible"); - return this; - } - - /** - * Verify Save View button is visible - */ - verifySaveViewButtonVisible(): this { - cy.contains( - "#dynamicButtonContainerId button.grp-savedStates", - "Save View", - { timeout: this.STANDARD_TIMEOUT } - ).should("be.visible"); - return this; - } - - /** - * Verify Columns button is visible - */ - verifyColumnsButtonVisible(): this { - cy.contains( - "#dynamicButtonContainerId .dt-buttons button span", - "Columns", - { timeout: this.STANDARD_TIMEOUT } - ).should("be.visible"); - return this; - } - - /** - * Verify dynamic button container exists - */ - verifyDynamicButtonContainerExists(): this { - cy.get(this.actionBar.dynamicButtonContainer, { - timeout: this.STANDARD_TIMEOUT, - }) - .should("exist") - .scrollIntoView(); - return this; - } - - // ============ Payment Modal Methods ============ - - /** - * Wait for payment modal to be visible - */ - waitForPaymentModalVisible(): this { - cy.get(this.paymentModal.modal, { timeout: this.STANDARD_TIMEOUT }) - .should("be.visible") - .and("have.class", "show"); - return this; - } - - /** - * Close payment modal using multiple strategies - */ - closePaymentModal(): this { - // Attempt ESC key - cy.get("body").type("{esc}", { force: true }); - - // Click backdrop if present - cy.get(this.paymentModal.backdrop, { timeout: this.STANDARD_TIMEOUT }).then( - ($bd: JQuery) => { - if ($bd.length) { - cy.wrap($bd).click("topLeft", { force: true }); - } - } - ); - - // Try Cancel button if available - cy.contains(this.paymentModal.cancelButton, "Cancel", { - timeout: this.STANDARD_TIMEOUT, - }).then(($btn: JQuery) => { - if ($btn && $btn.length > 0) { - cy.wrap($btn).scrollIntoView().click({ force: true }); - } else { - cy.log("Cancel button not present, proceeding to hard-close fallback"); - } - }); - - // Hard close fallback using jQuery - cy.window().then((win: Cypress.AUTWindow) => { - try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const windowWithModal = win as any; - if (typeof windowWithModal.closePaymentModal === "function") { - windowWithModal.closePaymentModal(); - } - } catch { - /* ignore */ - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const $ = (win as any).jQuery || (win as any).$; - if ($) { - try { - $("#payment-modal") - .removeClass("show") - .attr("aria-hidden", "true") - .css("display", "none"); - $(".modal-backdrop").remove(); - $("body").removeClass("modal-open").css("overflow", ""); - } catch { - /* ignore */ - } - } - }); - return this; - } - - /** - * Verify payment modal is closed - */ - verifyPaymentModalClosed(): this { - cy.get(this.paymentModal.modal, { timeout: this.STANDARD_TIMEOUT }).should( - ($m: JQuery) => { - const isHidden = !$m.is(":visible") || !$m.hasClass("show"); - expect(isHidden, "payment-modal hidden or not shown").to.eq(true); - } - ); - cy.get(this.paymentModal.backdrop, { timeout: this.STANDARD_TIMEOUT }).should( - "not.exist" - ); - return this; - } - - // ============ Columns Menu Methods ============ - - /** - * Close any open dropdowns or modals - */ - closeOpenDropdowns(): this { - cy.get("body").then(($body: JQuery) => { - if ($body.find(this.columnsMenu.buttonBackground).length > 0) { - cy.get(this.columnsMenu.buttonBackground).click({ force: true }); - } - }); - return this; - } - - /** - * Open Save View dropdown and reset to default - */ - resetToDefaultView(): this { - cy.get(this.saveView.button, { timeout: this.STANDARD_TIMEOUT }) - .should("be.visible") - .and("contain.text", "Save View") - .click(); - - cy.contains(this.saveView.resetOption, "Reset to Default View", { - timeout: this.STANDARD_TIMEOUT, - }) - .should("exist") - .click({ force: true }); - - // Wait for table to rebuild - cy.get(this.table.columnTitles, { timeout: this.STANDARD_TIMEOUT }).should( - "have.length.gt", - 5 - ); - return this; - } - - /** - * Open the Columns menu - */ - openColumnsMenu(): this { - cy.contains("span", "Columns", { timeout: this.STANDARD_TIMEOUT }) - .should("be.visible") - .click(); - - // Wait for dropdown to be fully populated - cy.get(this.columnsMenu.dropdownItem, { timeout: this.STANDARD_TIMEOUT }).should( - "have.length.gt", - 50 - ); - return this; - } - - /** - * Click a column item in the Columns menu - */ - clickColumnsItem(label: string): this { - cy.contains(this.columnsMenu.dropdownItem, label, { - timeout: this.STANDARD_TIMEOUT, - }) - .should("exist") - .scrollIntoView() - .click({ force: true }); - return this; - } - - /** - * Toggle multiple columns (click each one) - */ - toggleColumns(columns: string[]): this { - columns.forEach((column) => { - this.clickColumnsItem(column); - }); - return this; - } - - /** - * Close the Columns menu - */ - closeColumnsMenu(): this { - cy.get(this.columnsMenu.buttonBackground, { timeout: this.STANDARD_TIMEOUT }) - .should("exist") - .click({ force: true }); - - cy.get(this.columnsMenu.buttonBackground, { - timeout: this.STANDARD_TIMEOUT, - }).should("not.exist"); - return this; - } - - // ============ Grant Program Methods ============ - - /** - * Switch to a specific grant program if available - */ - switchToGrantProgram(programName: string): this { - cy.get("body").then(($body: JQuery) => { - const hasUserInitials = - $body.find(this.grantProgram.userInitials).length > 0; - - if (!hasUserInitials) { - cy.log("Skipping tenant switch: no user initials menu found"); - return; - } - - cy.get(this.grantProgram.userInitials).click(); - - cy.get("body").then(($body2: JQuery) => { - const switchLink = $body2 - .find(this.grantProgram.userDropdown) - .filter((_: number, el: HTMLElement) => { - return (el.textContent || "").trim() === "Switch Grant Programs"; - }); - - if (switchLink.length === 0) { - cy.log( - 'Skipping tenant switch: "Switch Grant Programs" not present for this user/session' - ); - cy.get("body").click(0, 0); - return; - } - - cy.wrap(switchLink.first()).click(); - - cy.url({ timeout: this.STANDARD_TIMEOUT }).should( - "include", - "/GrantPrograms" - ); - - cy.get(this.grantProgram.searchInput, { timeout: this.STANDARD_TIMEOUT }) - .should("be.visible") - .clear() - .type(programName); - - cy.contains(this.grantProgram.programsTableRow, programName, { - timeout: this.STANDARD_TIMEOUT, - }) - .should("exist") - .within(() => { - cy.contains("button", "Select").should("be.enabled").click(); - }); - - cy.location("pathname", { timeout: this.STANDARD_TIMEOUT }).should( - (p: string) => { - expect( - p.indexOf("/GrantApplications") >= 0 || p.indexOf("/auth/") >= 0 - ).to.eq(true); - } - ); - }); - }); - return this; - } -} - /** * ApplicationDetailsPage - Page Object for the Application Details page * Handles tabs, status actions, and field verification diff --git a/applications/Unity.AutoUI/cypress/support/auth.ts b/applications/Unity.AutoUI/cypress/support/auth.ts index 764222b8fe..bd83e88c50 100644 --- a/applications/Unity.AutoUI/cypress/support/auth.ts +++ b/applications/Unity.AutoUI/cypress/support/auth.ts @@ -176,31 +176,31 @@ function performLogin(options: LoginOptions = {}): void { */ export function loginIfNeeded( options: LoginOptions = {}, -): Cypress.Chainable { +): void { const username = options.username || (Cypress.env("test1username") as string); const baseUrl = options.baseUrl || (Cypress.env("webapp.url") as string); const sessionId = `unity-${baseUrl}-${username}`; - return cy - .session( - sessionId, - () => { - performLogin(options); - }, - { - validate() { - // Lightweight validation: check auth cookies exist without visiting - return cy.getCookie(".AspNetCore.Cookies").then((cookie) => { - if (!cookie) { - throw new Error("Session expired - auth cookie missing"); - } - }); - }, - cacheAcrossSpecs: true, + cy.session( + sessionId, + () => { + performLogin(options); + }, + { + validate() { + // Lightweight validation: check auth cookies exist without visiting + cy.getCookie(".AspNetCore.Cookies").then((cookie) => { + if (!cookie) { + throw new Error("Session expired - auth cookie missing"); + } + }); }, - ) - .then(() => { - cy.visit(baseUrl); - ensureGrantApplicationsPage(options.timeout || 20000); - }); + cacheAcrossSpecs: true, + }, + ); + + cy.then(() => { + cy.visit(baseUrl); + ensureGrantApplicationsPage(options.timeout || 20000); + }); } From 8882dc5d778bcd9b74ed93c3daf1b749881aff25 Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Tue, 17 Feb 2026 15:41:09 -0800 Subject: [PATCH 010/191] AB#31848: Compute Table Height Offset for Applicant Profile Details --- .../Pages/Applicants/Details.css | 2 +- .../Pages/Applicants/Details.js | 15 +++++++++++++++ .../Components/ApplicantSubmissions/Default.css | 6 +++--- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.css index 0fb63a2056..8d77b2fed5 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.css +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.css @@ -21,7 +21,7 @@ /* Left panel tabs scrolling */ #detailsTab .tab-content { - overflow-y: scroll; + overflow-y: auto; overflow-x: hidden; height: calc(100vh - 220px); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.js index b8b668979a..6b95640637 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.js @@ -31,6 +31,7 @@ function initializeApplicantDetailsPage() { setTimeout(function () { $('#main-loading').fadeOut(300, function () { $('.fade-in-load').addClass('visible'); + applyTabHeightOffset(); }); }, 500); @@ -82,6 +83,18 @@ function adjustVisibleTablesInContainer(containerId) { }); } +function applyTabHeightOffset() { + const detailsTab = document.getElementById('detailsTab'); + if (!detailsTab) return; + const tabNav = detailsTab.querySelector('ul.nav-tabs, ul.nav'); + const tabContent = detailsTab.querySelector('.tab-content'); + if (!tabNav || !tabContent) return; + const baseOffset = 175; + const totalOffset = baseOffset + tabNav.clientHeight; + tabContent.style.height = `calc(100vh - ${totalOffset}px)`; + tabContent.style.overflowY = 'auto'; +} + function initializeResizableDivider() { const divider = document.getElementById('main-divider'); const leftPanel = document.getElementById('main-left'); @@ -114,6 +127,7 @@ function initializeResizableDivider() { // Resize DataTables during panel resize debouncedResizeAwareDataTables(); + applyTabHeightOffset(); localStorage.setItem(storageKey, leftPercentage.toString()); } }; @@ -150,6 +164,7 @@ function initializeResizableDivider() { }); window.addEventListener('resize', restoreDividerPosition); + window.addEventListener('resize', applyTabHeightOffset); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantSubmissions/Default.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantSubmissions/Default.css index 295de3ccd2..7a017fac31 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantSubmissions/Default.css +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantSubmissions/Default.css @@ -108,9 +108,9 @@ word-wrap: break-word; } -/* Override tab-content scrolling for Submissions tab - no scrolling */ -#detailsTab .tab-content:has(#SubmissionsWidget) { - overflow: hidden !important; +/* Submissions tab pane - handle overflow internally via dt-scroll-body */ +#nav-submissions { + overflow: hidden; } /* Make Submissions widget fill available space */ From d2af5276854bdf8ed59d42ec7ed8e041d72c5bab Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Tue, 17 Feb 2026 15:42:16 -0800 Subject: [PATCH 011/191] AB#31896 - generic contacts service start, and applicant profile read --- .../ApplicantProfileDto.cs | 4 +- .../ApplicantProfileRequest.cs | 2 +- .../IApplicantProfileAppService.cs | 3 +- .../IApplicantProfileContactService.cs | 29 + .../IApplicantProfileDataProvider.cs | 4 +- .../ProfileData/ApplicantAddressInfoDto.cs | 2 +- .../ProfileData/ApplicantContactInfoDto.cs | 13 + .../ProfileData/ApplicantOrgInfoDto.cs | 2 +- .../ProfileData/ApplicantPaymentInfoDto.cs | 2 +- .../ProfileData/ApplicantProfileDataDto.cs | 15 + .../ProfileData/ApplicantSubmissionInfoDto.cs | 2 +- .../ProfileData/ContactInfoItemDto.cs | 21 + .../ProfileData/ApplicantContactInfoDto.cs | 7 - .../ProfileData/ApplicantProfileDataDto.cs | 7 - .../Contacts/ContactDto.cs | 39 ++ .../Contacts/CreateContactLinkDto.cs | 42 ++ .../Contacts/IContactAppService.cs | 40 ++ ...rantManagerPermissionDefinitionProvider.cs | 5 + .../AddressInfoDataProvider.cs | 4 +- .../ApplicantProfileAppService.cs | 4 +- .../ApplicantProfileContactService.cs | 88 +++ .../ApplicantProfile/ApplicantProfileKeys.cs | 2 +- .../ContactInfoDataProvider.cs | 43 ++ .../ApplicantProfile/OrgInfoDataProvider.cs | 4 +- .../PaymentInfoDataProvider.cs | 4 +- .../SubmissionInfoDataProvider.cs | 4 +- .../ContactInfoDataProvider.cs | 17 - .../ApplicantTenantMapReconciliationWorker.cs | 1 + .../Contacts/ContactAppService.cs | 135 ++++ .../Permissions/GrantManagerPermissions.cs | 12 + .../Repositories/ContactLinkRepository.cs | 21 + .../Repositories/ContactRepository.cs | 21 + .../Controllers/ApplicantProfileController.cs | 18 +- .../GrantManagerWebModule.cs | 1 + .../ApplicantProfileDataSchemaFilter.cs | 50 ++ .../ApplicantProfileAppServiceTests.cs | 12 +- .../ApplicantProfileDataProviderTests.cs | 25 +- .../Contacts/ContactAppServiceTests.cs | 604 ++++++++++++++++++ .../Contacts/ContactInfoDataProviderTests.cs | 180 ++++++ .../Contacts/ContactInfoServiceTests.cs | 379 +++++++++++ .../TestAsyncEnumerableQueryable.cs | 61 ++ 41 files changed, 1864 insertions(+), 65 deletions(-) rename applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/{Applicants => ApplicantProfile}/ApplicantProfileDto.cs (76%) rename applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/{Applicants => ApplicantProfile}/ApplicantProfileRequest.cs (89%) rename applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/{Applicants => ApplicantProfile}/IApplicantProfileAppService.cs (82%) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileContactService.cs rename applications/Unity.GrantManager/src/{Unity.GrantManager.Application/Applicants => Unity.GrantManager.Application.Contracts}/ApplicantProfile/IApplicantProfileDataProvider.cs (90%) rename applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/{Applicants => ApplicantProfile}/ProfileData/ApplicantAddressInfoDto.cs (70%) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantContactInfoDto.cs rename applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/{Applicants => ApplicantProfile}/ProfileData/ApplicantOrgInfoDto.cs (69%) rename applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/{Applicants => ApplicantProfile}/ProfileData/ApplicantPaymentInfoDto.cs (70%) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantProfileDataDto.cs rename applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/{Applicants => ApplicantProfile}/ProfileData/ApplicantSubmissionInfoDto.cs (71%) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ContactInfoItemDto.cs delete mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ProfileData/ApplicantContactInfoDto.cs delete mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ProfileData/ApplicantProfileDataDto.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Contacts/ContactDto.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Contacts/CreateContactLinkDto.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Contacts/IContactAppService.cs rename applications/Unity.GrantManager/src/Unity.GrantManager.Application/{Applicants => }/ApplicantProfile/AddressInfoDataProvider.cs (82%) rename applications/Unity.GrantManager/src/Unity.GrantManager.Application/{Applicants => ApplicantProfile}/ApplicantProfileAppService.cs (98%) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs rename applications/Unity.GrantManager/src/Unity.GrantManager.Application/{Applicants => }/ApplicantProfile/ApplicantProfileKeys.cs (85%) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ContactInfoDataProvider.cs rename applications/Unity.GrantManager/src/Unity.GrantManager.Application/{Applicants => }/ApplicantProfile/OrgInfoDataProvider.cs (82%) rename applications/Unity.GrantManager/src/Unity.GrantManager.Application/{Applicants => }/ApplicantProfile/PaymentInfoDataProvider.cs (82%) rename applications/Unity.GrantManager/src/Unity.GrantManager.Application/{Applicants => }/ApplicantProfile/SubmissionInfoDataProvider.cs (83%) delete mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantProfile/ContactInfoDataProvider.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/Contacts/ContactAppService.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ContactLinkRepository.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ContactRepository.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Swagger/ApplicantProfileDataSchemaFilter.cs create mode 100644 applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactAppServiceTests.cs create mode 100644 applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoDataProviderTests.cs create mode 100644 applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoServiceTests.cs create mode 100644 applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/TestHelpers/TestAsyncEnumerableQueryable.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ApplicantProfileDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ApplicantProfileDto.cs similarity index 76% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ApplicantProfileDto.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ApplicantProfileDto.cs index 9ec81114fb..c36d91fe4b 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ApplicantProfileDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ApplicantProfileDto.cs @@ -1,7 +1,7 @@ using System; -using Unity.GrantManager.Applicants.ProfileData; +using Unity.GrantManager.ApplicantProfile.ProfileData; -namespace Unity.GrantManager.Applicants +namespace Unity.GrantManager.ApplicantProfile { public class ApplicantProfileDto { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ApplicantProfileRequest.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ApplicantProfileRequest.cs similarity index 89% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ApplicantProfileRequest.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ApplicantProfileRequest.cs index 9f65d31cdb..5b6ef5b92c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ApplicantProfileRequest.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ApplicantProfileRequest.cs @@ -1,6 +1,6 @@ using System; -namespace Unity.GrantManager.Applicants +namespace Unity.GrantManager.ApplicantProfile { public class ApplicantProfileRequest { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/IApplicantProfileAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileAppService.cs similarity index 82% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/IApplicantProfileAppService.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileAppService.cs index 968cef47f5..feb3645823 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/IApplicantProfileAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileAppService.cs @@ -1,7 +1,8 @@ using System.Collections.Generic; using System.Threading.Tasks; +using Unity.GrantManager.Applicants; -namespace Unity.GrantManager.Applicants +namespace Unity.GrantManager.ApplicantProfile { public interface IApplicantProfileAppService { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileContactService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileContactService.cs new file mode 100644 index 0000000000..671c497363 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileContactService.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Unity.GrantManager.ApplicantProfile.ProfileData; + +namespace Unity.GrantManager.ApplicantProfile; + +/// +/// Provides applicant-profile-specific contact retrieval operations. +/// This service aggregates contacts from two sources: profile-linked contacts +/// and application-level contacts matched by OIDC subject. +/// +public interface IApplicantProfileContactService +{ + /// + /// Retrieves contacts linked to the specified applicant profile. + /// + /// The unique identifier of the applicant profile. + /// A list of with IsEditable set to true. + Task> GetProfileContactsAsync(Guid profileId); + + /// + /// Retrieves application contacts associated with submissions matching the given OIDC subject. + /// The subject is normalized by stripping the domain portion (after @) and converting to upper case. + /// + /// The OIDC subject identifier (e.g. "user@idir"). + /// A list of with IsEditable set to false. + Task> GetApplicationContactsBySubjectAsync(string subject); +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantProfile/IApplicantProfileDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileDataProvider.cs similarity index 90% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantProfile/IApplicantProfileDataProvider.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileDataProvider.cs index 6a1b6b6914..1dd8404874 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantProfile/IApplicantProfileDataProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileDataProvider.cs @@ -1,7 +1,7 @@ using System.Threading.Tasks; -using Unity.GrantManager.Applicants.ProfileData; +using Unity.GrantManager.ApplicantProfile.ProfileData; -namespace Unity.GrantManager.Applicants.ApplicantProfile +namespace Unity.GrantManager.ApplicantProfile { /// /// Defines a contract for components that can provide applicant profile data diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ProfileData/ApplicantAddressInfoDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantAddressInfoDto.cs similarity index 70% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ProfileData/ApplicantAddressInfoDto.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantAddressInfoDto.cs index fde1734a05..a532be4069 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ProfileData/ApplicantAddressInfoDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantAddressInfoDto.cs @@ -1,4 +1,4 @@ -namespace Unity.GrantManager.Applicants.ProfileData +namespace Unity.GrantManager.ApplicantProfile.ProfileData { public class ApplicantAddressInfoDto : ApplicantProfileDataDto { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantContactInfoDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantContactInfoDto.cs new file mode 100644 index 0000000000..1da48226c8 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantContactInfoDto.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Unity.GrantManager.ApplicantProfile.ProfileData +{ + public class ApplicantContactInfoDto : ApplicantProfileDataDto + { + public override string DataType => "CONTACTINFO"; + + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public List Contacts { get; set; } = []; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ProfileData/ApplicantOrgInfoDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantOrgInfoDto.cs similarity index 69% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ProfileData/ApplicantOrgInfoDto.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantOrgInfoDto.cs index c14ac04137..4a99135f3d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ProfileData/ApplicantOrgInfoDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantOrgInfoDto.cs @@ -1,4 +1,4 @@ -namespace Unity.GrantManager.Applicants.ProfileData +namespace Unity.GrantManager.ApplicantProfile.ProfileData { public class ApplicantOrgInfoDto : ApplicantProfileDataDto { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ProfileData/ApplicantPaymentInfoDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantPaymentInfoDto.cs similarity index 70% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ProfileData/ApplicantPaymentInfoDto.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantPaymentInfoDto.cs index a6f7b77c3a..c17c89eebe 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ProfileData/ApplicantPaymentInfoDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantPaymentInfoDto.cs @@ -1,4 +1,4 @@ -namespace Unity.GrantManager.Applicants.ProfileData +namespace Unity.GrantManager.ApplicantProfile.ProfileData { public class ApplicantPaymentInfoDto : ApplicantProfileDataDto { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantProfileDataDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantProfileDataDto.cs new file mode 100644 index 0000000000..65da1a0fac --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantProfileDataDto.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Unity.GrantManager.ApplicantProfile.ProfileData +{ + [JsonPolymorphic(TypeDiscriminatorPropertyName = "dataType")] + [JsonDerivedType(typeof(ApplicantContactInfoDto), "CONTACTINFO")] + [JsonDerivedType(typeof(ApplicantOrgInfoDto), "ORGINFO")] + [JsonDerivedType(typeof(ApplicantAddressInfoDto), "ADDRESSINFO")] + [JsonDerivedType(typeof(ApplicantSubmissionInfoDto), "SUBMISSIONINFO")] + [JsonDerivedType(typeof(ApplicantPaymentInfoDto), "PAYMENTINFO")] + public class ApplicantProfileDataDto + { + public virtual string DataType { get; } = ""; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ProfileData/ApplicantSubmissionInfoDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantSubmissionInfoDto.cs similarity index 71% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ProfileData/ApplicantSubmissionInfoDto.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantSubmissionInfoDto.cs index 4c0a0ba60b..5c3618c30c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ProfileData/ApplicantSubmissionInfoDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantSubmissionInfoDto.cs @@ -1,4 +1,4 @@ -namespace Unity.GrantManager.Applicants.ProfileData +namespace Unity.GrantManager.ApplicantProfile.ProfileData { public class ApplicantSubmissionInfoDto : ApplicantProfileDataDto { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ContactInfoItemDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ContactInfoItemDto.cs new file mode 100644 index 0000000000..2be1b4ed0c --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ContactInfoItemDto.cs @@ -0,0 +1,21 @@ +using System; + +namespace Unity.GrantManager.ApplicantProfile.ProfileData +{ + public class ContactInfoItemDto + { + public Guid ContactId { get; set; } + public string Name { get; set; } = string.Empty; + public string? Title { get; set; } + public string? Email { get; set; } + public string? HomePhoneNumber { get; set; } + public string? MobilePhoneNumber { get; set; } + public string? WorkPhoneNumber { get; set; } + public string? WorkPhoneExtension { get; set; } + public string? ContactType { get; set; } + public string? Role { get; set; } + public bool IsPrimary { get; set; } + public bool IsEditable { get; set; } + public Guid? ApplicationId { get; set; } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ProfileData/ApplicantContactInfoDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ProfileData/ApplicantContactInfoDto.cs deleted file mode 100644 index 74c15630b9..0000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ProfileData/ApplicantContactInfoDto.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Unity.GrantManager.Applicants.ProfileData -{ - public class ApplicantContactInfoDto : ApplicantProfileDataDto - { - public override string DataType => "CONTACTINFO"; - } -} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ProfileData/ApplicantProfileDataDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ProfileData/ApplicantProfileDataDto.cs deleted file mode 100644 index 3c717b28b8..0000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ProfileData/ApplicantProfileDataDto.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Unity.GrantManager.Applicants.ProfileData -{ - public class ApplicantProfileDataDto - { - public virtual string DataType { get; } = ""; - } -} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Contacts/ContactDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Contacts/ContactDto.cs new file mode 100644 index 0000000000..263a547570 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Contacts/ContactDto.cs @@ -0,0 +1,39 @@ +using System; + +namespace Unity.GrantManager.Contacts; + +/// +/// Represents a contact linked to an entity, returned by the generic contacts service. +/// +public class ContactDto +{ + /// The unique identifier of the contact. + public Guid ContactId { get; set; } + + /// The full name of the contact. + public string Name { get; set; } = string.Empty; + + /// The job title of the contact. + public string? Title { get; set; } + + /// The email address of the contact. + public string? Email { get; set; } + + /// The home phone number of the contact. + public string? HomePhoneNumber { get; set; } + + /// The mobile phone number of the contact. + public string? MobilePhoneNumber { get; set; } + + /// The work phone number of the contact. + public string? WorkPhoneNumber { get; set; } + + /// The work phone extension of the contact. + public string? WorkPhoneExtension { get; set; } + + /// The role of the contact within the linked entity context. + public string? Role { get; set; } + + /// Whether this contact is the primary contact for the linked entity. + public bool IsPrimary { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Contacts/CreateContactLinkDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Contacts/CreateContactLinkDto.cs new file mode 100644 index 0000000000..58e49b7ed4 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Contacts/CreateContactLinkDto.cs @@ -0,0 +1,42 @@ +using System; + +namespace Unity.GrantManager.Contacts; + +/// +/// Input DTO for creating a new contact and linking it to a related entity. +/// +public class CreateContactLinkDto +{ + /// The full name of the contact. + public string Name { get; set; } = string.Empty; + + /// The job title of the contact. + public string? Title { get; set; } + + /// The email address of the contact. + public string? Email { get; set; } + + /// The home phone number of the contact. + public string? HomePhoneNumber { get; set; } + + /// The mobile phone number of the contact. + public string? MobilePhoneNumber { get; set; } + + /// The work phone number of the contact. + public string? WorkPhoneNumber { get; set; } + + /// The work phone extension of the contact. + public string? WorkPhoneExtension { get; set; } + + /// The role of the contact within the linked entity context. + public string? Role { get; set; } + + /// Whether this contact should be set as the primary contact. Only one primary is allowed per entity type and entity ID. + public bool IsPrimary { get; set; } + + /// The type of the entity to link the contact to (e.g. "ApplicantProfile"). + public string RelatedEntityType { get; set; } = string.Empty; + + /// The unique identifier of the related entity. + public Guid RelatedEntityId { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Contacts/IContactAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Contacts/IContactAppService.cs new file mode 100644 index 0000000000..4a07057c5d --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Contacts/IContactAppService.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Unity.GrantManager.Contacts; + +/// +/// Generic contact management service. Provides operations for creating, retrieving, +/// and managing contacts linked to any entity type via . +/// +public interface IContactAppService +{ + /// + /// Retrieves all active contacts linked to the specified entity. + /// + /// The type of the related entity (e.g. "ApplicantProfile"). + /// The unique identifier of the related entity. + /// A list of for the matching entity. + Task> GetContactsByEntityAsync(string entityType, Guid entityId); + + /// + /// Creates a new contact and links it to the specified entity. + /// If is true, any existing primary + /// contact for the same entity type and ID will be cleared first. + /// + /// The contact and link details. + /// The created . + Task CreateContactAsync(CreateContactLinkDto input); + + /// + /// Sets the specified contact as the primary contact for the given entity. + /// Only one primary contact is allowed per entity type and entity ID; + /// any existing primary will be cleared before setting the new one. + /// + /// The type of the related entity. + /// The unique identifier of the related entity. + /// The unique identifier of the contact to set as primary. + /// Thrown when no active contact link is found for the given parameters. + Task SetPrimaryContactAsync(string entityType, Guid entityId, Guid contactId); +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantManagerPermissionDefinitionProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantManagerPermissionDefinitionProvider.cs index 06eefb1396..52d3b5c62c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantManagerPermissionDefinitionProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantManagerPermissionDefinitionProvider.cs @@ -19,6 +19,11 @@ public override void Define(IPermissionDefinitionContext context) grantManagerPermissionsGroup.AddPermission(GrantManagerPermissions.Intakes.Default, L("Permission:GrantManagerManagement.Intakes.Default")); grantManagerPermissionsGroup.AddPermission(GrantManagerPermissions.ApplicationForms.Default, L("Permission:GrantManagerManagement.ApplicationForms.Default")); + + var contactPermissions = grantManagerPermissionsGroup.AddPermission(GrantManagerPermissions.Contacts.Default, L("Permission:GrantManagerManagement.Contacts.Default")); + contactPermissions.AddChild(GrantManagerPermissions.Contacts.Create, L("Permission:GrantManagerManagement.Contacts.Create")); + contactPermissions.AddChild(GrantManagerPermissions.Contacts.Read, L("Permission:GrantManagerManagement.Contacts.Read")); + contactPermissions.AddChild(GrantManagerPermissions.Contacts.Update, L("Permission:GrantManagerManagement.Contacts.Update")); } private static LocalizableString L(string name) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantProfile/AddressInfoDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/AddressInfoDataProvider.cs similarity index 82% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantProfile/AddressInfoDataProvider.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/AddressInfoDataProvider.cs index 693a289943..f77a53000c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantProfile/AddressInfoDataProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/AddressInfoDataProvider.cs @@ -1,8 +1,8 @@ using System.Threading.Tasks; -using Unity.GrantManager.Applicants.ProfileData; +using Unity.GrantManager.ApplicantProfile.ProfileData; using Volo.Abp.DependencyInjection; -namespace Unity.GrantManager.Applicants.ApplicantProfile +namespace Unity.GrantManager.ApplicantProfile { [ExposeServices(typeof(IApplicantProfileDataProvider))] public class AddressInfoDataProvider : IApplicantProfileDataProvider, ITransientDependency diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantProfileAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileAppService.cs similarity index 98% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantProfileAppService.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileAppService.cs index 057b27d905..df8617813d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantProfileAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileAppService.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Unity.GrantManager.Applicants.ApplicantProfile; +using Unity.GrantManager.Applicants; using Unity.GrantManager.Applications; using Volo.Abp; using Volo.Abp.Application.Services; @@ -12,7 +12,7 @@ using Volo.Abp.MultiTenancy; using Volo.Abp.TenantManagement; -namespace Unity.GrantManager.Applicants +namespace Unity.GrantManager.ApplicantProfile { [RemoteService(false)] public class ApplicantProfileAppService( diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs new file mode 100644 index 0000000000..476940ed3a --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Unity.GrantManager.ApplicantProfile.ProfileData; +using Unity.GrantManager.Applications; +using Unity.GrantManager.Contacts; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; + +namespace Unity.GrantManager.ApplicantProfile; + +/// +/// Applicant-profile-specific contact service. Retrieves contacts linked to applicant profiles +/// and application-level contacts matched by OIDC subject. This service operates independently +/// from the generic and queries repositories directly. +/// +public class ApplicantProfileContactService( + IContactRepository contactRepository, + IContactLinkRepository contactLinkRepository, + IRepository applicationFormSubmissionRepository, + IRepository applicationContactRepository) + : IApplicantProfileContactService, ITransientDependency +{ + private const string ApplicantProfileEntityType = "ApplicantProfile"; + + /// + public async Task> GetProfileContactsAsync(Guid profileId) + { + var contactLinksQuery = await contactLinkRepository.GetQueryableAsync(); + var contactsQuery = await contactRepository.GetQueryableAsync(); + + return await ( + from link in contactLinksQuery + join contact in contactsQuery on link.ContactId equals contact.Id + where link.RelatedEntityType == ApplicantProfileEntityType + && link.RelatedEntityId == profileId + && link.IsActive + select new ContactInfoItemDto + { + ContactId = contact.Id, + Name = contact.Name, + Title = contact.Title, + Email = contact.Email, + HomePhoneNumber = contact.HomePhoneNumber, + MobilePhoneNumber = contact.MobilePhoneNumber, + WorkPhoneNumber = contact.WorkPhoneNumber, + WorkPhoneExtension = contact.WorkPhoneExtension, + ContactType = link.RelatedEntityType, + Role = link.Role, + IsPrimary = link.IsPrimary, + IsEditable = true, + ApplicationId = null + }).ToListAsync(); + } + + /// + public async Task> GetApplicationContactsBySubjectAsync(string subject) + { + var normalizedSubject = subject.Contains('@') + ? subject[..subject.IndexOf('@')].ToUpperInvariant() + : subject.ToUpperInvariant(); + + var submissionsQuery = await applicationFormSubmissionRepository.GetQueryableAsync(); + var applicationContactsQuery = await applicationContactRepository.GetQueryableAsync(); + + var applicationContacts = await ( + from submission in submissionsQuery + join appContact in applicationContactsQuery on submission.ApplicationId equals appContact.ApplicationId + where submission.OidcSub == normalizedSubject + select new ContactInfoItemDto + { + ContactId = appContact.Id, + Name = appContact.ContactFullName, + Title = appContact.ContactTitle, + Email = appContact.ContactEmail, + MobilePhoneNumber = appContact.ContactMobilePhone, + WorkPhoneNumber = appContact.ContactWorkPhone, + ContactType = appContact.ContactType, + IsPrimary = false, + IsEditable = false, + ApplicationId = appContact.ApplicationId + }).ToListAsync(); + + return applicationContacts; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantProfile/ApplicantProfileKeys.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileKeys.cs similarity index 85% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantProfile/ApplicantProfileKeys.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileKeys.cs index 4b232c4537..70bdfaaaa1 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantProfile/ApplicantProfileKeys.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileKeys.cs @@ -1,4 +1,4 @@ -namespace Unity.GrantManager.Applicants.ApplicantProfile +namespace Unity.GrantManager.ApplicantProfile { public static class ApplicantProfileKeys { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ContactInfoDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ContactInfoDataProvider.cs new file mode 100644 index 0000000000..13bd414eeb --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ContactInfoDataProvider.cs @@ -0,0 +1,43 @@ +using System.Threading.Tasks; +using Unity.GrantManager.ApplicantProfile.ProfileData; +using Volo.Abp.DependencyInjection; +using Volo.Abp.MultiTenancy; + +namespace Unity.GrantManager.ApplicantProfile +{ + /// + /// Provides contact information for the applicant profile by aggregating + /// profile-linked contacts and application-level contacts. + /// + [ExposeServices(typeof(IApplicantProfileDataProvider))] + public class ContactInfoDataProvider( + ICurrentTenant currentTenant, + IApplicantProfileContactService applicantProfileContactService) + : IApplicantProfileDataProvider, ITransientDependency + { + /// + public string Key => ApplicantProfileKeys.ContactInfo; + + /// + public async Task GetDataAsync(ApplicantProfileInfoRequest request) + { + var dto = new ApplicantContactInfoDto + { + Contacts = [] + }; + + var tenantId = request.TenantId; + + using (currentTenant.Change(tenantId)) + { + var profileContacts = await applicantProfileContactService.GetProfileContactsAsync(request.ProfileId); + dto.Contacts.AddRange(profileContacts); + + var applicationContacts = await applicantProfileContactService.GetApplicationContactsBySubjectAsync(request.Subject); + dto.Contacts.AddRange(applicationContacts); + } + + return dto; + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantProfile/OrgInfoDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/OrgInfoDataProvider.cs similarity index 82% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantProfile/OrgInfoDataProvider.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/OrgInfoDataProvider.cs index 6d7f3c7cc1..2d1ced24c2 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantProfile/OrgInfoDataProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/OrgInfoDataProvider.cs @@ -1,8 +1,8 @@ using System.Threading.Tasks; -using Unity.GrantManager.Applicants.ProfileData; +using Unity.GrantManager.ApplicantProfile.ProfileData; using Volo.Abp.DependencyInjection; -namespace Unity.GrantManager.Applicants.ApplicantProfile +namespace Unity.GrantManager.ApplicantProfile { [ExposeServices(typeof(IApplicantProfileDataProvider))] public class OrgInfoDataProvider : IApplicantProfileDataProvider, ITransientDependency diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantProfile/PaymentInfoDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/PaymentInfoDataProvider.cs similarity index 82% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantProfile/PaymentInfoDataProvider.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/PaymentInfoDataProvider.cs index 5684f158e1..8e1ddde042 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantProfile/PaymentInfoDataProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/PaymentInfoDataProvider.cs @@ -1,8 +1,8 @@ using System.Threading.Tasks; -using Unity.GrantManager.Applicants.ProfileData; +using Unity.GrantManager.ApplicantProfile.ProfileData; using Volo.Abp.DependencyInjection; -namespace Unity.GrantManager.Applicants.ApplicantProfile +namespace Unity.GrantManager.ApplicantProfile { [ExposeServices(typeof(IApplicantProfileDataProvider))] public class PaymentInfoDataProvider : IApplicantProfileDataProvider, ITransientDependency diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantProfile/SubmissionInfoDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/SubmissionInfoDataProvider.cs similarity index 83% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantProfile/SubmissionInfoDataProvider.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/SubmissionInfoDataProvider.cs index 7af7e641fc..9bd91fcbd4 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantProfile/SubmissionInfoDataProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/SubmissionInfoDataProvider.cs @@ -1,8 +1,8 @@ using System.Threading.Tasks; -using Unity.GrantManager.Applicants.ProfileData; +using Unity.GrantManager.ApplicantProfile.ProfileData; using Volo.Abp.DependencyInjection; -namespace Unity.GrantManager.Applicants.ApplicantProfile +namespace Unity.GrantManager.ApplicantProfile { [ExposeServices(typeof(IApplicantProfileDataProvider))] public class SubmissionInfoDataProvider : IApplicantProfileDataProvider, ITransientDependency diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantProfile/ContactInfoDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantProfile/ContactInfoDataProvider.cs deleted file mode 100644 index 71539ca65c..0000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantProfile/ContactInfoDataProvider.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Threading.Tasks; -using Unity.GrantManager.Applicants.ProfileData; -using Volo.Abp.DependencyInjection; - -namespace Unity.GrantManager.Applicants.ApplicantProfile -{ - [ExposeServices(typeof(IApplicantProfileDataProvider))] - public class ContactInfoDataProvider : IApplicantProfileDataProvider, ITransientDependency - { - public string Key => ApplicantProfileKeys.ContactInfo; - - public Task GetDataAsync(ApplicantProfileInfoRequest request) - { - return Task.FromResult(new ApplicantContactInfoDto()); - } - } -} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/BackgroundWorkers/ApplicantTenantMapReconciliationWorker.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/BackgroundWorkers/ApplicantTenantMapReconciliationWorker.cs index b93b81032c..61d07b1ff4 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/BackgroundWorkers/ApplicantTenantMapReconciliationWorker.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/BackgroundWorkers/ApplicantTenantMapReconciliationWorker.cs @@ -2,6 +2,7 @@ using Quartz; using System; using System.Threading.Tasks; +using Unity.GrantManager.ApplicantProfile; using Unity.GrantManager.Settings; using Unity.Modules.Shared.Utils; using Volo.Abp.BackgroundWorkers.Quartz; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Contacts/ContactAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Contacts/ContactAppService.cs new file mode 100644 index 0000000000..8a70946c5a --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Contacts/ContactAppService.cs @@ -0,0 +1,135 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.DependencyInjection; + +namespace Unity.GrantManager.Contacts; + +/// +/// Generic contact management service. Manages contacts and their links to arbitrary entity types. +/// Currently marked as [RemoteService(false)] not exposed as an HTTP endpoint. +/// Authorization roles to be configured before enabling remote access. +/// + +[Authorize] +[RemoteService(false)] +[ExposeServices(typeof(ContactAppService), typeof(IContactAppService))] +public class ContactAppService( + IContactRepository contactRepository, + IContactLinkRepository contactLinkRepository) + : GrantManagerAppService, IContactAppService +{ + /// + public async Task> GetContactsByEntityAsync(string entityType, Guid entityId) + { + var contactLinksQuery = await contactLinkRepository.GetQueryableAsync(); + var contactsQuery = await contactRepository.GetQueryableAsync(); + + return await ( + from link in contactLinksQuery + join contact in contactsQuery on link.ContactId equals contact.Id + where link.RelatedEntityType == entityType + && link.RelatedEntityId == entityId + && link.IsActive + select new ContactDto + { + ContactId = contact.Id, + Name = contact.Name, + Title = contact.Title, + Email = contact.Email, + HomePhoneNumber = contact.HomePhoneNumber, + MobilePhoneNumber = contact.MobilePhoneNumber, + WorkPhoneNumber = contact.WorkPhoneNumber, + WorkPhoneExtension = contact.WorkPhoneExtension, + Role = link.Role, + IsPrimary = link.IsPrimary + }).ToListAsync(); + } + + /// + public async Task CreateContactAsync(CreateContactLinkDto input) + { + var contact = await contactRepository.InsertAsync(new Contact + { + Name = input.Name, + Title = input.Title, + Email = input.Email, + HomePhoneNumber = input.HomePhoneNumber, + MobilePhoneNumber = input.MobilePhoneNumber, + WorkPhoneNumber = input.WorkPhoneNumber, + WorkPhoneExtension = input.WorkPhoneExtension + }, autoSave: true); + + if (input.IsPrimary) + { + await ClearPrimaryAsync(input.RelatedEntityType, input.RelatedEntityId); + } + + await contactLinkRepository.InsertAsync(new ContactLink + { + ContactId = contact.Id, + RelatedEntityType = input.RelatedEntityType, + RelatedEntityId = input.RelatedEntityId, + Role = input.Role, + IsPrimary = input.IsPrimary, + IsActive = true + }, autoSave: true); + + return new ContactDto + { + ContactId = contact.Id, + Name = contact.Name, + Title = contact.Title, + Email = contact.Email, + HomePhoneNumber = contact.HomePhoneNumber, + MobilePhoneNumber = contact.MobilePhoneNumber, + WorkPhoneNumber = contact.WorkPhoneNumber, + WorkPhoneExtension = contact.WorkPhoneExtension, + Role = input.Role, + IsPrimary = input.IsPrimary + }; + } + + /// + public async Task SetPrimaryContactAsync(string entityType, Guid entityId, Guid contactId) + { + await ClearPrimaryAsync(entityType, entityId); + + var contactLinksQuery = await contactLinkRepository.GetQueryableAsync(); + var link = await contactLinksQuery + .Where(l => l.RelatedEntityType == entityType + && l.RelatedEntityId == entityId + && l.ContactId == contactId + && l.IsActive) + .FirstOrDefaultAsync() ?? throw new BusinessException("Contacts:ContactLinkNotFound") + .WithData("contactId", contactId) + .WithData("entityType", entityType) + .WithData("entityId", entityId); + link.IsPrimary = true; + await contactLinkRepository.UpdateAsync(link, autoSave: true); + } + + /// + /// Clears the primary flag on all active contact links for the specified entity. + /// + private async Task ClearPrimaryAsync(string entityType, Guid entityId) + { + var contactLinksQuery = await contactLinkRepository.GetQueryableAsync(); + var currentPrimaryLinks = await contactLinksQuery + .Where(l => l.RelatedEntityType == entityType + && l.RelatedEntityId == entityId + && l.IsPrimary + && l.IsActive) + .ToListAsync(); + + foreach (var existing in currentPrimaryLinks) + { + existing.IsPrimary = false; + await contactLinkRepository.UpdateAsync(existing, autoSave: true); + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/GrantManagerPermissions.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/GrantManagerPermissions.cs index 991948f975..169659b1d9 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/GrantManagerPermissions.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/GrantManagerPermissions.cs @@ -29,5 +29,17 @@ public static class Endpoints public const string Default = GroupName + ".Endpoints"; public const string ManageEndpoints = Default + ".ManageEndpoints"; } + + /// + /// Permission constants for the generic contacts service. + /// These are pre-wired for future HTTP endpoint exposure. + /// + public static class Contacts + { + public const string Default = GroupName + ".Contacts"; + public const string Create = Default + ".Create"; + public const string Read = Default + ".Read"; + public const string Update = Default + ".Update"; + } } #pragma warning restore S3218 // Inner class members should not shadow outer class "static" or type members \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ContactLinkRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ContactLinkRepository.cs new file mode 100644 index 0000000000..9319b7e257 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ContactLinkRepository.cs @@ -0,0 +1,21 @@ +using System; +using Unity.GrantManager.Contacts; +using Unity.GrantManager.EntityFrameworkCore; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +namespace Unity.GrantManager.Repositories +{ + [Dependency(ReplaceServices = true)] + [ExposeServices(typeof(IContactLinkRepository))] +#pragma warning disable CS8613 // Nullability of reference types in return type doesn't match implicitly implemented member. + // This pattern is an implementation ontop of ABP framework, will not change this + public class ContactLinkRepository : EfCoreRepository, IContactLinkRepository +#pragma warning restore CS8613 // Nullability of reference types in return type doesn't match implicitly implemented member. + { + public ContactLinkRepository(IDbContextProvider dbContextProvider) : base(dbContextProvider) + { + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ContactRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ContactRepository.cs new file mode 100644 index 0000000000..0d7222fc01 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ContactRepository.cs @@ -0,0 +1,21 @@ +using System; +using Unity.GrantManager.Contacts; +using Unity.GrantManager.EntityFrameworkCore; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +namespace Unity.GrantManager.Repositories +{ + [Dependency(ReplaceServices = true)] + [ExposeServices(typeof(IContactRepository))] +#pragma warning disable CS8613 // Nullability of reference types in return type doesn't match implicitly implemented member. + // This pattern is an implementation ontop of ABP framework, will not change this + public class ContactRepository : EfCoreRepository, IContactRepository +#pragma warning restore CS8613 // Nullability of reference types in return type doesn't match implicitly implemented member. + { + public ContactRepository(IDbContextProvider dbContextProvider) : base(dbContextProvider) + { + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/ApplicantProfileController.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/ApplicantProfileController.cs index bff4d0f6bf..becf33e31c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/ApplicantProfileController.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/ApplicantProfileController.cs @@ -1,6 +1,8 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; using System.Threading.Tasks; -using Unity.GrantManager.Applicants; +using Unity.GrantManager.ApplicantProfile; +using Unity.GrantManager.ApplicantProfile.ProfileData; using Unity.GrantManager.Controllers.Authentication; using Volo.Abp.AspNetCore.Mvc; @@ -12,8 +14,20 @@ namespace Unity.GrantManager.Controllers public class ApplicantProfileController(IApplicantProfileAppService applicantProfileAppService) : AbpControllerBase { + /// + /// Retrieves applicant profile data based on the specified key. + /// The response data property is polymorphic and varies by key: + /// + /// CONTACTINFO — returns + /// ORGINFO — returns + /// ADDRESSINFO — returns + /// SUBMISSIONINFO — returns + /// PAYMENTINFO — returns + /// + /// [HttpGet] [Route("profile")] + [ProducesResponseType(typeof(ApplicantProfileDto), StatusCodes.Status200OK)] public async Task GetApplicantProfileAsync([FromQuery] ApplicantProfileInfoRequest applicantProfileRequest) { var profile = await applicantProfileAppService.GetApplicantProfileAsync(applicantProfileRequest); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebModule.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebModule.cs index 4a345330cc..caf91fc168 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebModule.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebModule.cs @@ -460,6 +460,7 @@ private static void ConfigureSwaggerServices(IServiceCollection services) Type = SecuritySchemeType.ApiKey, Scheme = "ApiKeyScheme" }); + options.SchemaFilter(); } ); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Swagger/ApplicantProfileDataSchemaFilter.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Swagger/ApplicantProfileDataSchemaFilter.cs new file mode 100644 index 0000000000..a4640af5db --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Swagger/ApplicantProfileDataSchemaFilter.cs @@ -0,0 +1,50 @@ +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; +using System.Collections.Generic; +using Unity.GrantManager.ApplicantProfile.ProfileData; + +namespace Unity.GrantManager.Swagger +{ + public class ApplicantProfileDataSchemaFilter : ISchemaFilter + { + public void Apply(OpenApiSchema schema, SchemaFilterContext context) + { + if (context.Type != typeof(ApplicantProfileDataDto)) + return; + + var subTypes = new Dictionary + { + ["CONTACTINFO"] = typeof(ApplicantContactInfoDto), + ["ORGINFO"] = typeof(ApplicantOrgInfoDto), + ["ADDRESSINFO"] = typeof(ApplicantAddressInfoDto), + ["SUBMISSIONINFO"] = typeof(ApplicantSubmissionInfoDto), + ["PAYMENTINFO"] = typeof(ApplicantPaymentInfoDto) + }; + + var oneOfSchemas = new List(); + foreach (var (discriminatorValue, subType) in subTypes) + { + var subSchema = context.SchemaGenerator.GenerateSchema(subType, context.SchemaRepository); + oneOfSchemas.Add(subSchema); + } + + schema.OneOf = oneOfSchemas; + schema.Discriminator = new OpenApiDiscriminator + { + PropertyName = "dataType", + Mapping = new Dictionary() + }; + + foreach (var (discriminatorValue, subType) in subTypes) + { + var schemaId = context.SchemaRepository.Schemas.ContainsKey(subType.FullName!) + ? subType.FullName! + : subType.Name; + schema.Discriminator.Mapping[discriminatorValue] = $"#/components/schemas/{schemaId}"; + } + + schema.Description = "Polymorphic data payload. The shape depends on the 'dataType' discriminator (key parameter)."; + } + } +} diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/ApplicantProfileAppServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/ApplicantProfileAppServiceTests.cs index 29cde43446..81f5180892 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/ApplicantProfileAppServiceTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/ApplicantProfileAppServiceTests.cs @@ -1,8 +1,8 @@ using Shouldly; using System; using System.Threading.Tasks; -using Unity.GrantManager.Applicants.ApplicantProfile; -using Unity.GrantManager.Applicants.ProfileData; +using Unity.GrantManager.ApplicantProfile; +using Unity.GrantManager.ApplicantProfile.ProfileData; using Xunit; using Xunit.Abstractions; @@ -21,7 +21,7 @@ public ApplicantProfileAppServiceTests(ITestOutputHelper outputHelper) : base(ou { ProfileId = Guid.NewGuid(), Subject = "testuser@idir", - TenantId = Guid.NewGuid(), + TenantId = Guid.Empty, Key = key }; @@ -37,7 +37,7 @@ public async Task GetApplicantProfileAsync_WithValidKey_ShouldReturnData(string var request = CreateRequest(key); // Act - var result = await _service.GetApplicantProfileAsync(request); + var result = await WithUnitOfWorkAsync(() => _service.GetApplicantProfileAsync(request)); // Assert result.ShouldNotBeNull(); @@ -60,7 +60,7 @@ public async Task GetApplicantProfileAsync_WithValidKey_ShouldReturnCorrectDataT var request = CreateRequest(key); // Act - var result = await _service.GetApplicantProfileAsync(request); + var result = await WithUnitOfWorkAsync(() => _service.GetApplicantProfileAsync(request)); // Assert result.Data.ShouldNotBeNull(); @@ -89,7 +89,7 @@ public async Task GetApplicantProfileAsync_KeyLookupIsCaseInsensitive() var request = CreateRequest("contactinfo"); // Act - var result = await _service.GetApplicantProfileAsync(request); + var result = await WithUnitOfWorkAsync(() => _service.GetApplicantProfileAsync(request)); // Assert result.Data.ShouldNotBeNull(); diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/ApplicantProfileDataProviderTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/ApplicantProfileDataProviderTests.cs index c9fa64a0f8..74ccc3f9ad 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/ApplicantProfileDataProviderTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/ApplicantProfileDataProviderTests.cs @@ -1,9 +1,12 @@ +using NSubstitute; using Shouldly; using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Unity.GrantManager.Applicants.ApplicantProfile; -using Unity.GrantManager.Applicants.ProfileData; +using Unity.GrantManager.ApplicantProfile; +using Unity.GrantManager.ApplicantProfile.ProfileData; +using Volo.Abp.MultiTenancy; using Xunit; namespace Unity.GrantManager.Applicants @@ -18,17 +21,29 @@ public class ApplicantProfileDataProviderTests Key = key }; + private static ContactInfoDataProvider CreateContactInfoDataProvider() + { + var currentTenant = Substitute.For(); + currentTenant.Change(Arg.Any()).Returns(Substitute.For()); + var applicantProfileContactService = Substitute.For(); + applicantProfileContactService.GetProfileContactsAsync(Arg.Any()) + .Returns(Task.FromResult(new List())); + applicantProfileContactService.GetApplicationContactsBySubjectAsync(Arg.Any()) + .Returns(Task.FromResult(new List())); + return new ContactInfoDataProvider(currentTenant, applicantProfileContactService); + } + [Fact] public void ContactInfoDataProvider_Key_ShouldMatchExpected() { - var provider = new ContactInfoDataProvider(); + var provider = CreateContactInfoDataProvider(); provider.Key.ShouldBe(ApplicantProfileKeys.ContactInfo); } [Fact] public async Task ContactInfoDataProvider_GetDataAsync_ShouldReturnContactInfoDto() { - var provider = new ContactInfoDataProvider(); + var provider = CreateContactInfoDataProvider(); var result = await provider.GetDataAsync(CreateRequest(ApplicantProfileKeys.ContactInfo)); result.ShouldNotBeNull(); result.ShouldBeOfType(); @@ -103,7 +118,7 @@ public void AllProviders_ShouldHaveUniqueKeys() { IApplicantProfileDataProvider[] providers = [ - new ContactInfoDataProvider(), + CreateContactInfoDataProvider(), new OrgInfoDataProvider(), new AddressInfoDataProvider(), new SubmissionInfoDataProvider(), diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactAppServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactAppServiceTests.cs new file mode 100644 index 0000000000..e44be9fb43 --- /dev/null +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactAppServiceTests.cs @@ -0,0 +1,604 @@ +using NSubstitute; +using Shouldly; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Unity.GrantManager.TestHelpers; +using Volo.Abp; +using Volo.Abp.Domain.Entities; +using Xunit; + +namespace Unity.GrantManager.Contacts +{ + public class ContactAppServiceTests + { + private readonly IContactRepository _contactRepository; + private readonly IContactLinkRepository _contactLinkRepository; + private readonly ContactAppService _service; + + public ContactAppServiceTests() + { + _contactRepository = Substitute.For(); + _contactLinkRepository = Substitute.For(); + + _service = new ContactAppService( + _contactRepository, + _contactLinkRepository); + } + + private static T WithId(T entity, Guid id) where T : Entity + { + EntityHelper.TrySetId(entity, () => id); + return entity; + } + + #region GetContactsByEntityAsync + + [Fact] + public async Task GetContactsByEntityAsync_WithMatchingLinks_ShouldReturnAllFields() + { + // Arrange + var entityId = Guid.NewGuid(); + var contactId = Guid.NewGuid(); + + var contacts = new[] + { + WithId(new Contact + { + Name = "John Doe", + Title = "Manager", + Email = "john@example.com", + HomePhoneNumber = "111-1111", + MobilePhoneNumber = "222-2222", + WorkPhoneNumber = "333-3333", + WorkPhoneExtension = "101" + }, contactId) + }.AsAsyncQueryable(); + + var contactLinks = new[] + { + new ContactLink + { + ContactId = contactId, + RelatedEntityType = "TestEntity", + RelatedEntityId = entityId, + Role = "Primary Contact", + IsPrimary = true, + IsActive = true + } + }.AsAsyncQueryable(); + + _contactRepository.GetQueryableAsync().Returns(contacts); + _contactLinkRepository.GetQueryableAsync().Returns(contactLinks); + + // Act + var result = await _service.GetContactsByEntityAsync("TestEntity", entityId); + + // Assert + result.Count.ShouldBe(1); + var contact = result[0]; + contact.ContactId.ShouldBe(contactId); + contact.Name.ShouldBe("John Doe"); + contact.Title.ShouldBe("Manager"); + contact.Email.ShouldBe("john@example.com"); + contact.HomePhoneNumber.ShouldBe("111-1111"); + contact.MobilePhoneNumber.ShouldBe("222-2222"); + contact.WorkPhoneNumber.ShouldBe("333-3333"); + contact.WorkPhoneExtension.ShouldBe("101"); + contact.Role.ShouldBe("Primary Contact"); + contact.IsPrimary.ShouldBeTrue(); + } + + [Fact] + public async Task GetContactsByEntityAsync_WithMultipleContacts_ShouldReturnAll() + { + // Arrange + var entityId = Guid.NewGuid(); + var contactId1 = Guid.NewGuid(); + var contactId2 = Guid.NewGuid(); + + var contacts = new[] + { + WithId(new Contact { Name = "Contact One" }, contactId1), + WithId(new Contact { Name = "Contact Two" }, contactId2) + }.AsAsyncQueryable(); + + var contactLinks = new[] + { + new ContactLink + { + ContactId = contactId1, + RelatedEntityType = "TestEntity", + RelatedEntityId = entityId, + IsPrimary = true, + IsActive = true + }, + new ContactLink + { + ContactId = contactId2, + RelatedEntityType = "TestEntity", + RelatedEntityId = entityId, + IsPrimary = false, + IsActive = true + } + }.AsAsyncQueryable(); + + _contactRepository.GetQueryableAsync().Returns(contacts); + _contactLinkRepository.GetQueryableAsync().Returns(contactLinks); + + // Act + var result = await _service.GetContactsByEntityAsync("TestEntity", entityId); + + // Assert + result.Count.ShouldBe(2); + result.ShouldContain(c => c.Name == "Contact One" && c.IsPrimary); + result.ShouldContain(c => c.Name == "Contact Two" && !c.IsPrimary); + } + + [Fact] + public async Task GetContactsByEntityAsync_ShouldExcludeInactiveLinks() + { + // Arrange + var entityId = Guid.NewGuid(); + var contactId = Guid.NewGuid(); + + var contacts = new[] + { + WithId(new Contact { Name = "Inactive Contact" }, contactId) + }.AsAsyncQueryable(); + + var contactLinks = new[] + { + new ContactLink + { + ContactId = contactId, + RelatedEntityType = "TestEntity", + RelatedEntityId = entityId, + IsActive = false + } + }.AsAsyncQueryable(); + + _contactRepository.GetQueryableAsync().Returns(contacts); + _contactLinkRepository.GetQueryableAsync().Returns(contactLinks); + + // Act + var result = await _service.GetContactsByEntityAsync("TestEntity", entityId); + + // Assert + result.ShouldBeEmpty(); + } + + [Fact] + public async Task GetContactsByEntityAsync_ShouldExcludeDifferentEntityType() + { + // Arrange + var entityId = Guid.NewGuid(); + var contactId = Guid.NewGuid(); + + var contacts = new[] + { + WithId(new Contact { Name = "Wrong Type" }, contactId) + }.AsAsyncQueryable(); + + var contactLinks = new[] + { + new ContactLink + { + ContactId = contactId, + RelatedEntityType = "OtherType", + RelatedEntityId = entityId, + IsActive = true + } + }.AsAsyncQueryable(); + + _contactRepository.GetQueryableAsync().Returns(contacts); + _contactLinkRepository.GetQueryableAsync().Returns(contactLinks); + + // Act + var result = await _service.GetContactsByEntityAsync("TestEntity", entityId); + + // Assert + result.ShouldBeEmpty(); + } + + [Fact] + public async Task GetContactsByEntityAsync_ShouldExcludeDifferentEntityId() + { + // Arrange + var entityId = Guid.NewGuid(); + var otherEntityId = Guid.NewGuid(); + var contactId = Guid.NewGuid(); + + var contacts = new[] + { + WithId(new Contact { Name = "Other Entity" }, contactId) + }.AsAsyncQueryable(); + + var contactLinks = new[] + { + new ContactLink + { + ContactId = contactId, + RelatedEntityType = "TestEntity", + RelatedEntityId = otherEntityId, + IsActive = true + } + }.AsAsyncQueryable(); + + _contactRepository.GetQueryableAsync().Returns(contacts); + _contactLinkRepository.GetQueryableAsync().Returns(contactLinks); + + // Act + var result = await _service.GetContactsByEntityAsync("TestEntity", entityId); + + // Assert + result.ShouldBeEmpty(); + } + + [Fact] + public async Task GetContactsByEntityAsync_WithNoLinks_ShouldReturnEmpty() + { + // Arrange + _contactRepository.GetQueryableAsync().Returns(Array.Empty().AsAsyncQueryable()); + _contactLinkRepository.GetQueryableAsync().Returns(Array.Empty().AsAsyncQueryable()); + + // Act + var result = await _service.GetContactsByEntityAsync("TestEntity", Guid.NewGuid()); + + // Assert + result.ShouldBeEmpty(); + } + + #endregion + + #region CreateContactAsync + + [Fact] + public async Task CreateContactAsync_ShouldCreateContactAndLink() + { + // Arrange + var contactId = Guid.NewGuid(); + var entityId = Guid.NewGuid(); + + _contactRepository.InsertAsync(Arg.Any(), true, Arg.Any()) + .Returns(ci => + { + var c = ci.Arg(); + EntityHelper.TrySetId(c, () => contactId); + return c; + }); + + _contactLinkRepository.GetQueryableAsync() + .Returns(Array.Empty().AsAsyncQueryable()); + + var input = new CreateContactLinkDto + { + Name = "New Contact", + Title = "Analyst", + Email = "new@example.com", + HomePhoneNumber = "111-1111", + MobilePhoneNumber = "222-2222", + WorkPhoneNumber = "333-3333", + WorkPhoneExtension = "101", + Role = "Reviewer", + IsPrimary = false, + RelatedEntityType = "TestEntity", + RelatedEntityId = entityId + }; + + // Act + var result = await _service.CreateContactAsync(input); + + // Assert + result.ContactId.ShouldBe(contactId); + result.Name.ShouldBe("New Contact"); + result.Title.ShouldBe("Analyst"); + result.Email.ShouldBe("new@example.com"); + result.HomePhoneNumber.ShouldBe("111-1111"); + result.MobilePhoneNumber.ShouldBe("222-2222"); + result.WorkPhoneNumber.ShouldBe("333-3333"); + result.WorkPhoneExtension.ShouldBe("101"); + result.Role.ShouldBe("Reviewer"); + result.IsPrimary.ShouldBeFalse(); + + await _contactRepository.Received(1).InsertAsync( + Arg.Is(c => + c.Name == "New Contact" + && c.Title == "Analyst" + && c.Email == "new@example.com" + && c.HomePhoneNumber == "111-1111" + && c.MobilePhoneNumber == "222-2222" + && c.WorkPhoneNumber == "333-3333" + && c.WorkPhoneExtension == "101"), + true, + Arg.Any()); + + await _contactLinkRepository.Received(1).InsertAsync( + Arg.Is(l => + l.ContactId == contactId + && l.RelatedEntityType == "TestEntity" + && l.RelatedEntityId == entityId + && l.Role == "Reviewer" + && !l.IsPrimary + && l.IsActive), + true, + Arg.Any()); + } + + [Fact] + public async Task CreateContactAsync_NonPrimary_ShouldNotClearExistingPrimary() + { + // Arrange + var contactId = Guid.NewGuid(); + var entityId = Guid.NewGuid(); + + _contactRepository.InsertAsync(Arg.Any(), true, Arg.Any()) + .Returns(ci => + { + var c = ci.Arg(); + EntityHelper.TrySetId(c, () => contactId); + return c; + }); + + var input = new CreateContactLinkDto + { + Name = "Non-Primary Contact", + IsPrimary = false, + RelatedEntityType = "TestEntity", + RelatedEntityId = entityId + }; + + // Act + await _service.CreateContactAsync(input); + + // Assert GetQueryableAsync should not be called (ClearPrimaryAsync not invoked) + await _contactLinkRepository.DidNotReceive().GetQueryableAsync(); + } + + [Fact] + public async Task CreateContactAsync_WhenPrimary_ShouldClearExistingPrimary() + { + // Arrange + var contactId = Guid.NewGuid(); + var entityId = Guid.NewGuid(); + var existingLinkId = Guid.NewGuid(); + + var existingLink = new ContactLink + { + ContactId = Guid.NewGuid(), + RelatedEntityType = "TestEntity", + RelatedEntityId = entityId, + IsPrimary = true, + IsActive = true + }; + EntityHelper.TrySetId(existingLink, () => existingLinkId); + + _contactLinkRepository.GetQueryableAsync() + .Returns( + new[] { existingLink }.AsAsyncQueryable(), + Array.Empty().AsAsyncQueryable()); + + _contactRepository.InsertAsync(Arg.Any(), true, Arg.Any()) + .Returns(ci => + { + var c = ci.Arg(); + EntityHelper.TrySetId(c, () => contactId); + return c; + }); + + var input = new CreateContactLinkDto + { + Name = "Primary Contact", + IsPrimary = true, + RelatedEntityType = "TestEntity", + RelatedEntityId = entityId + }; + + // Act + var result = await _service.CreateContactAsync(input); + + // Assert + result.IsPrimary.ShouldBeTrue(); + await _contactLinkRepository.Received(1).UpdateAsync( + Arg.Is(l => l.Id == existingLinkId && !l.IsPrimary), + true, + Arg.Any()); + } + + #endregion + + #region SetPrimaryContactAsync + + [Fact] + public async Task SetPrimaryContactAsync_ShouldClearExistingAndSetNew() + { + // Arrange + var entityId = Guid.NewGuid(); + var contactId = Guid.NewGuid(); + var existingPrimaryLinkId = Guid.NewGuid(); + var targetLinkId = Guid.NewGuid(); + + var existingPrimaryLink = new ContactLink + { + ContactId = Guid.NewGuid(), + RelatedEntityType = "TestEntity", + RelatedEntityId = entityId, + IsPrimary = true, + IsActive = true + }; + EntityHelper.TrySetId(existingPrimaryLink, () => existingPrimaryLinkId); + + var targetLink = new ContactLink + { + ContactId = contactId, + RelatedEntityType = "TestEntity", + RelatedEntityId = entityId, + IsPrimary = false, + IsActive = true + }; + EntityHelper.TrySetId(targetLink, () => targetLinkId); + + _contactLinkRepository.GetQueryableAsync() + .Returns( + new[] { existingPrimaryLink }.AsAsyncQueryable(), + new[] { targetLink }.AsAsyncQueryable()); + + // Act + await _service.SetPrimaryContactAsync("TestEntity", entityId, contactId); + + // Assert + await _contactLinkRepository.Received(1).UpdateAsync( + Arg.Is(l => l.Id == existingPrimaryLinkId && !l.IsPrimary), + true, + Arg.Any()); + await _contactLinkRepository.Received(1).UpdateAsync( + Arg.Is(l => l.Id == targetLinkId && l.IsPrimary), + true, + Arg.Any()); + } + + [Fact] + public async Task SetPrimaryContactAsync_WithNoExistingPrimary_ShouldSetNew() + { + // Arrange + var entityId = Guid.NewGuid(); + var contactId = Guid.NewGuid(); + var targetLinkId = Guid.NewGuid(); + + var targetLink = new ContactLink + { + ContactId = contactId, + RelatedEntityType = "TestEntity", + RelatedEntityId = entityId, + IsPrimary = false, + IsActive = true + }; + EntityHelper.TrySetId(targetLink, () => targetLinkId); + + _contactLinkRepository.GetQueryableAsync() + .Returns( + Array.Empty().AsAsyncQueryable(), + new[] { targetLink }.AsAsyncQueryable()); + + // Act + await _service.SetPrimaryContactAsync("TestEntity", entityId, contactId); + + // Assert only the target link should be updated (set to primary) + await _contactLinkRepository.Received(1).UpdateAsync( + Arg.Is(l => l.Id == targetLinkId && l.IsPrimary), + true, + Arg.Any()); + } + + [Fact] + public async Task SetPrimaryContactAsync_WithMultipleExistingPrimaries_ShouldClearAll() + { + // Arrange + var entityId = Guid.NewGuid(); + var contactId = Guid.NewGuid(); + var primaryLinkId1 = Guid.NewGuid(); + var primaryLinkId2 = Guid.NewGuid(); + var targetLinkId = Guid.NewGuid(); + + var primaryLink1 = new ContactLink + { + ContactId = Guid.NewGuid(), + RelatedEntityType = "TestEntity", + RelatedEntityId = entityId, + IsPrimary = true, + IsActive = true + }; + EntityHelper.TrySetId(primaryLink1, () => primaryLinkId1); + + var primaryLink2 = new ContactLink + { + ContactId = Guid.NewGuid(), + RelatedEntityType = "TestEntity", + RelatedEntityId = entityId, + IsPrimary = true, + IsActive = true + }; + EntityHelper.TrySetId(primaryLink2, () => primaryLinkId2); + + var targetLink = new ContactLink + { + ContactId = contactId, + RelatedEntityType = "TestEntity", + RelatedEntityId = entityId, + IsPrimary = false, + IsActive = true + }; + EntityHelper.TrySetId(targetLink, () => targetLinkId); + + _contactLinkRepository.GetQueryableAsync() + .Returns( + new[] { primaryLink1, primaryLink2 }.AsAsyncQueryable(), + new[] { targetLink }.AsAsyncQueryable()); + + // Act + await _service.SetPrimaryContactAsync("TestEntity", entityId, contactId); + + // Assert both existing primaries cleared + await _contactLinkRepository.Received(1).UpdateAsync( + Arg.Is(l => l.Id == primaryLinkId1 && !l.IsPrimary), + true, + Arg.Any()); + await _contactLinkRepository.Received(1).UpdateAsync( + Arg.Is(l => l.Id == primaryLinkId2 && !l.IsPrimary), + true, + Arg.Any()); + // Target set as primary + await _contactLinkRepository.Received(1).UpdateAsync( + Arg.Is(l => l.Id == targetLinkId && l.IsPrimary), + true, + Arg.Any()); + } + + [Fact] + public async Task SetPrimaryContactAsync_ShouldNotMatchInactiveLink() + { + // Arrange + var entityId = Guid.NewGuid(); + var contactId = Guid.NewGuid(); + + var inactiveLink = new ContactLink + { + ContactId = contactId, + RelatedEntityType = "TestEntity", + RelatedEntityId = entityId, + IsPrimary = false, + IsActive = false + }; + + _contactLinkRepository.GetQueryableAsync() + .Returns( + Array.Empty().AsAsyncQueryable(), + new[] { inactiveLink }.AsAsyncQueryable()); + + // Act & Assert + await Should.ThrowAsync( + () => _service.SetPrimaryContactAsync("TestEntity", entityId, contactId)); + } + + [Fact] + public async Task SetPrimaryContactAsync_WhenContactLinkNotFound_ShouldThrow() + { + // Arrange + var entityId = Guid.NewGuid(); + var contactId = Guid.NewGuid(); + + _contactLinkRepository.GetQueryableAsync() + .Returns( + Array.Empty().AsAsyncQueryable(), + Array.Empty().AsAsyncQueryable()); + + // Act & Assert + var ex = await Should.ThrowAsync( + () => _service.SetPrimaryContactAsync("TestEntity", entityId, contactId)); + ex.Code.ShouldBe("Contacts:ContactLinkNotFound"); + } + + #endregion + } +} diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoDataProviderTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoDataProviderTests.cs new file mode 100644 index 0000000000..976ad574c5 --- /dev/null +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoDataProviderTests.cs @@ -0,0 +1,180 @@ +using NSubstitute; +using Shouldly; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Unity.GrantManager.ApplicantProfile; +using Unity.GrantManager.ApplicantProfile.ProfileData; +using Volo.Abp.MultiTenancy; +using Xunit; + +namespace Unity.GrantManager.Contacts +{ + public class ContactInfoDataProviderTests + { + private readonly ICurrentTenant _currentTenant; + private readonly IApplicantProfileContactService _applicantProfileContactService; + private readonly ContactInfoDataProvider _provider; + + public ContactInfoDataProviderTests() + { + _currentTenant = Substitute.For(); + _currentTenant.Change(Arg.Any()).Returns(Substitute.For()); + _applicantProfileContactService = Substitute.For(); + _provider = new ContactInfoDataProvider(_currentTenant, _applicantProfileContactService); + } + + private static ApplicantProfileInfoRequest CreateRequest() => new() + { + ProfileId = Guid.NewGuid(), + Subject = "testuser@idir", + TenantId = Guid.NewGuid(), + Key = ApplicantProfileKeys.ContactInfo + }; + + [Fact] + public async Task GetDataAsync_ShouldChangeTenant() + { + // Arrange + var request = CreateRequest(); + _applicantProfileContactService.GetProfileContactsAsync(Arg.Any()) + .Returns(new List()); + _applicantProfileContactService.GetApplicationContactsBySubjectAsync(Arg.Any()) + .Returns(new List()); + + // Act + await _provider.GetDataAsync(request); + + // Assert + _currentTenant.Received(1).Change(request.TenantId); + } + + [Fact] + public async Task GetDataAsync_ShouldCallGetProfileContactsWithProfileId() + { + // Arrange + var request = CreateRequest(); + _applicantProfileContactService.GetProfileContactsAsync(Arg.Any()) + .Returns(new List()); + _applicantProfileContactService.GetApplicationContactsBySubjectAsync(Arg.Any()) + .Returns(new List()); + + // Act + await _provider.GetDataAsync(request); + + // Assert + await _applicantProfileContactService.Received(1).GetProfileContactsAsync(request.ProfileId); + } + + [Fact] + public async Task GetDataAsync_ShouldCallGetApplicationContactsWithSubject() + { + // Arrange + var request = CreateRequest(); + _applicantProfileContactService.GetProfileContactsAsync(Arg.Any()) + .Returns(new List()); + _applicantProfileContactService.GetApplicationContactsBySubjectAsync(Arg.Any()) + .Returns(new List()); + + // Act + await _provider.GetDataAsync(request); + + // Assert + await _applicantProfileContactService.Received(1).GetApplicationContactsBySubjectAsync(request.Subject); + } + + [Fact] + public async Task GetDataAsync_ShouldCombineBothContactSets() + { + // Arrange + var request = CreateRequest(); + var profileContacts = new List + { + new() { ContactId = Guid.NewGuid(), Name = "Profile Contact 1", IsEditable = true }, + new() { ContactId = Guid.NewGuid(), Name = "Profile Contact 2", IsEditable = true } + }; + var appContacts = new List + { + new() { ContactId = Guid.NewGuid(), Name = "App Contact 1", IsEditable = false } + }; + _applicantProfileContactService.GetProfileContactsAsync(request.ProfileId).Returns(profileContacts); + _applicantProfileContactService.GetApplicationContactsBySubjectAsync(request.Subject).Returns(appContacts); + + // Act + var result = await _provider.GetDataAsync(request); + + // Assert + var dto = result.ShouldBeOfType(); + dto.Contacts.Count.ShouldBe(3); + dto.Contacts.Count(c => c.IsEditable).ShouldBe(2); + dto.Contacts.Count(c => !c.IsEditable).ShouldBe(1); + } + + [Fact] + public async Task GetDataAsync_WithNoContacts_ShouldReturnEmptyList() + { + // Arrange + var request = CreateRequest(); + _applicantProfileContactService.GetProfileContactsAsync(Arg.Any()) + .Returns(new List()); + _applicantProfileContactService.GetApplicationContactsBySubjectAsync(Arg.Any()) + .Returns(new List()); + + // Act + var result = await _provider.GetDataAsync(request); + + // Assert + var dto = result.ShouldBeOfType(); + dto.Contacts.ShouldBeEmpty(); + } + + [Fact] + public async Task GetDataAsync_ProfileContactsShouldAppearBeforeApplicationContacts() + { + // Arrange + var request = CreateRequest(); + var profileContact = new ContactInfoItemDto + { + ContactId = Guid.NewGuid(), + Name = "Profile First", + IsEditable = true + }; + var appContact = new ContactInfoItemDto + { + ContactId = Guid.NewGuid(), + Name = "App Second", + IsEditable = false + }; + _applicantProfileContactService.GetProfileContactsAsync(request.ProfileId) + .Returns(new List { profileContact }); + _applicantProfileContactService.GetApplicationContactsBySubjectAsync(request.Subject) + .Returns(new List { appContact }); + + // Act + var result = await _provider.GetDataAsync(request); + + // Assert + var dto = result.ShouldBeOfType(); + dto.Contacts[0].Name.ShouldBe("Profile First"); + dto.Contacts[1].Name.ShouldBe("App Second"); + } + + [Fact] + public async Task GetDataAsync_ShouldReturnCorrectDataType() + { + // Arrange + var request = CreateRequest(); + _applicantProfileContactService.GetProfileContactsAsync(Arg.Any()) + .Returns(new List()); + _applicantProfileContactService.GetApplicationContactsBySubjectAsync(Arg.Any()) + .Returns(new List()); + + // Act + var result = await _provider.GetDataAsync(request); + + // Assert + result.DataType.ShouldBe("CONTACTINFO"); + } + } +} diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoServiceTests.cs new file mode 100644 index 0000000000..8241c1a3b7 --- /dev/null +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoServiceTests.cs @@ -0,0 +1,379 @@ +using NSubstitute; +using Shouldly; +using System; +using System.Threading.Tasks; +using Unity.GrantManager.ApplicantProfile; +using Unity.GrantManager.ApplicantProfile.ProfileData; +using Unity.GrantManager.Applications; +using Unity.GrantManager.TestHelpers; +using Volo.Abp.Domain.Entities; +using Volo.Abp.Domain.Repositories; +using Xunit; + +namespace Unity.GrantManager.Contacts +{ + public class ApplicantProfileContactServiceTests + { + private readonly IContactRepository _contactRepository; + private readonly IContactLinkRepository _contactLinkRepository; + private readonly IRepository _submissionRepository; + private readonly IRepository _applicationContactRepository; + private readonly ApplicantProfileContactService _service; + + public ApplicantProfileContactServiceTests() + { + _contactRepository = Substitute.For(); + _contactLinkRepository = Substitute.For(); + _submissionRepository = Substitute.For>(); + _applicationContactRepository = Substitute.For>(); + + _service = new ApplicantProfileContactService( + _contactRepository, + _contactLinkRepository, + _submissionRepository, + _applicationContactRepository); + } + + private static T WithId(T entity, Guid id) where T : Entity + { + EntityHelper.TrySetId(entity, () => id); + return entity; + } + + [Fact] + public async Task GetProfileContactsAsync_WithMatchingLinks_ShouldReturnContacts() + { + // Arrange + var profileId = Guid.NewGuid(); + var contactId = Guid.NewGuid(); + + var contacts = new[] + { + WithId(new Contact + { + Name = "John Doe", + Title = "Manager", + Email = "john@example.com", + HomePhoneNumber = "111-1111", + MobilePhoneNumber = "222-2222", + WorkPhoneNumber = "333-3333", + WorkPhoneExtension = "101" + }, contactId) + }.AsAsyncQueryable(); + + var contactLinks = new[] + { + new ContactLink + { + ContactId = contactId, + RelatedEntityType = "ApplicantProfile", + RelatedEntityId = profileId, + Role = "Primary Contact", + IsPrimary = true, + IsActive = true + } + }.AsAsyncQueryable(); + + _contactRepository.GetQueryableAsync().Returns(contacts); + _contactLinkRepository.GetQueryableAsync().Returns(contactLinks); + + // Act + var result = await _service.GetProfileContactsAsync(profileId); + + // Assert + result.Count.ShouldBe(1); + var contact = result[0]; + contact.ContactId.ShouldBe(contactId); + contact.Name.ShouldBe("John Doe"); + contact.Title.ShouldBe("Manager"); + contact.Email.ShouldBe("john@example.com"); + contact.HomePhoneNumber.ShouldBe("111-1111"); + contact.MobilePhoneNumber.ShouldBe("222-2222"); + contact.WorkPhoneNumber.ShouldBe("333-3333"); + contact.WorkPhoneExtension.ShouldBe("101"); + contact.Role.ShouldBe("Primary Contact"); + contact.IsPrimary.ShouldBeTrue(); + contact.IsEditable.ShouldBeTrue(); + contact.ApplicationId.ShouldBeNull(); + } + + [Fact] + public async Task GetProfileContactsAsync_WithNoLinks_ShouldReturnEmpty() + { + // Arrange + _contactRepository.GetQueryableAsync().Returns(Array.Empty().AsAsyncQueryable()); + _contactLinkRepository.GetQueryableAsync().Returns(Array.Empty().AsAsyncQueryable()); + + // Act + var result = await _service.GetProfileContactsAsync(Guid.NewGuid()); + + // Assert + result.ShouldBeEmpty(); + } + + [Fact] + public async Task GetApplicationContactsBySubjectAsync_WithMatchingSubmission_ShouldReturnContacts() + { + // Arrange + var applicationId = Guid.NewGuid(); + var appContactId = Guid.NewGuid(); + + var submissions = new[] + { + new ApplicationFormSubmission + { + OidcSub = "TESTUSER", + ApplicationId = applicationId, + ApplicantId = Guid.NewGuid(), + ApplicationFormId = Guid.NewGuid() + } + }.AsAsyncQueryable(); + + var applicationContacts = new[] + { + WithId(new ApplicationContact + { + ApplicationId = applicationId, + ContactFullName = "Jane Smith", + ContactTitle = "Director", + ContactEmail = "jane@example.com", + ContactMobilePhone = "444-4444", + ContactWorkPhone = "555-5555", + ContactType = "Signing Authority" + }, appContactId) + }.AsAsyncQueryable(); + + _submissionRepository.GetQueryableAsync().Returns(submissions); + _applicationContactRepository.GetQueryableAsync().Returns(applicationContacts); + + // Act + var result = await _service.GetApplicationContactsBySubjectAsync("testuser@idir"); + + // Assert + result.Count.ShouldBe(1); + var contact = result[0]; + contact.ContactId.ShouldBe(appContactId); + contact.Name.ShouldBe("Jane Smith"); + contact.Title.ShouldBe("Director"); + contact.Email.ShouldBe("jane@example.com"); + contact.MobilePhoneNumber.ShouldBe("444-4444"); + contact.WorkPhoneNumber.ShouldBe("555-5555"); + contact.ContactType.ShouldBe("Signing Authority"); + contact.IsPrimary.ShouldBeFalse(); + contact.IsEditable.ShouldBeFalse(); + contact.ApplicationId.ShouldBe(applicationId); + } + + [Fact] + public async Task GetApplicationContactsBySubjectAsync_ShouldMatchCaseInsensitively() + { + // Arrange + var applicationId = Guid.NewGuid(); + + var submissions = new[] + { + new ApplicationFormSubmission + { + OidcSub = "TESTUSER", + ApplicationId = applicationId, + ApplicantId = Guid.NewGuid(), + ApplicationFormId = Guid.NewGuid() + } + }.AsAsyncQueryable(); + + var applicationContacts = new[] + { + WithId(new ApplicationContact + { + ApplicationId = applicationId, + ContactFullName = "Case Test" + }, Guid.NewGuid()) + }.AsAsyncQueryable(); + + _submissionRepository.GetQueryableAsync().Returns(submissions); + _applicationContactRepository.GetQueryableAsync().Returns(applicationContacts); + + // Act + var result = await _service.GetApplicationContactsBySubjectAsync("testuser@IDIR"); + + // Assert + result.Count.ShouldBe(1); + } + + [Fact] + public async Task GetApplicationContactsBySubjectAsync_ShouldStripDomainFromSubject() + { + // Arrange + var applicationId = Guid.NewGuid(); + + var submissions = new[] + { + new ApplicationFormSubmission + { + OidcSub = "MYUSER", + ApplicationId = applicationId, + ApplicantId = Guid.NewGuid(), + ApplicationFormId = Guid.NewGuid() + } + }.AsAsyncQueryable(); + + var applicationContacts = new[] + { + WithId(new ApplicationContact + { + ApplicationId = applicationId, + ContactFullName = "Domain Strip Test" + }, Guid.NewGuid()) + }.AsAsyncQueryable(); + + _submissionRepository.GetQueryableAsync().Returns(submissions); + _applicationContactRepository.GetQueryableAsync().Returns(applicationContacts); + + // Act + var result = await _service.GetApplicationContactsBySubjectAsync("myuser@differentdomain"); + + // Assert + result.Count.ShouldBe(1); + result[0].Name.ShouldBe("Domain Strip Test"); + } + + [Fact] + public async Task GetApplicationContactsBySubjectAsync_WithSubjectWithoutAtSign_ShouldStillMatch() + { + // Arrange + var applicationId = Guid.NewGuid(); + + var submissions = new[] + { + new ApplicationFormSubmission + { + OidcSub = "PLAINUSER", + ApplicationId = applicationId, + ApplicantId = Guid.NewGuid(), + ApplicationFormId = Guid.NewGuid() + } + }.AsAsyncQueryable(); + + var applicationContacts = new[] + { + WithId(new ApplicationContact + { + ApplicationId = applicationId, + ContactFullName = "Plain User Contact" + }, Guid.NewGuid()) + }.AsAsyncQueryable(); + + _submissionRepository.GetQueryableAsync().Returns(submissions); + _applicationContactRepository.GetQueryableAsync().Returns(applicationContacts); + + // Act + var result = await _service.GetApplicationContactsBySubjectAsync("plainuser"); + + // Assert + result.Count.ShouldBe(1); + } + + [Fact] + public async Task GetApplicationContactsBySubjectAsync_WithNonMatchingSubject_ShouldReturnEmpty() + { + // Arrange + var applicationId = Guid.NewGuid(); + + var submissions = new[] + { + new ApplicationFormSubmission + { + OidcSub = "OTHERUSER", + ApplicationId = applicationId, + ApplicantId = Guid.NewGuid(), + ApplicationFormId = Guid.NewGuid() + } + }.AsAsyncQueryable(); + + var applicationContacts = new[] + { + WithId(new ApplicationContact + { + ApplicationId = applicationId, + ContactFullName = "Should Not Match" + }, Guid.NewGuid()) + }.AsAsyncQueryable(); + + _submissionRepository.GetQueryableAsync().Returns(submissions); + _applicationContactRepository.GetQueryableAsync().Returns(applicationContacts); + + // Act + var result = await _service.GetApplicationContactsBySubjectAsync("differentuser@idir"); + + // Assert + result.ShouldBeEmpty(); + } + + [Fact] + public async Task GetApplicationContactsBySubjectAsync_WithNoSubmissions_ShouldReturnEmpty() + { + // Arrange + _submissionRepository.GetQueryableAsync() + .Returns(Array.Empty().AsAsyncQueryable()); + _applicationContactRepository.GetQueryableAsync() + .Returns(Array.Empty().AsAsyncQueryable()); + + // Act + var result = await _service.GetApplicationContactsBySubjectAsync("testuser@idir"); + + // Assert + result.ShouldBeEmpty(); + } + + [Fact] + public async Task GetApplicationContactsBySubjectAsync_WithMultipleSubmissions_ShouldReturnAllContacts() + { + // Arrange + var appId1 = Guid.NewGuid(); + var appId2 = Guid.NewGuid(); + + var submissions = new[] + { + new ApplicationFormSubmission + { + OidcSub = "TESTUSER", + ApplicationId = appId1, + ApplicantId = Guid.NewGuid(), + ApplicationFormId = Guid.NewGuid() + }, + new ApplicationFormSubmission + { + OidcSub = "TESTUSER", + ApplicationId = appId2, + ApplicantId = Guid.NewGuid(), + ApplicationFormId = Guid.NewGuid() + } + }.AsAsyncQueryable(); + + var applicationContacts = new[] + { + WithId(new ApplicationContact + { + ApplicationId = appId1, + ContactFullName = "Contact App 1" + }, Guid.NewGuid()), + WithId(new ApplicationContact + { + ApplicationId = appId2, + ContactFullName = "Contact App 2" + }, Guid.NewGuid()) + }.AsAsyncQueryable(); + + _submissionRepository.GetQueryableAsync().Returns(submissions); + _applicationContactRepository.GetQueryableAsync().Returns(applicationContacts); + + // Act + var result = await _service.GetApplicationContactsBySubjectAsync("testuser@idir"); + + // Assert + result.Count.ShouldBe(2); + result.ShouldAllBe(c => !c.IsEditable); + result.ShouldAllBe(c => !c.IsPrimary); + } + } +} diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/TestHelpers/TestAsyncEnumerableQueryable.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/TestHelpers/TestAsyncEnumerableQueryable.cs new file mode 100644 index 0000000000..e0a27bbcb8 --- /dev/null +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/TestHelpers/TestAsyncEnumerableQueryable.cs @@ -0,0 +1,61 @@ +using Microsoft.EntityFrameworkCore.Query; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; + +namespace Unity.GrantManager.TestHelpers +{ + internal class TestAsyncEnumerableQueryable : EnumerableQuery, IAsyncEnumerable, IQueryable + { + public TestAsyncEnumerableQueryable(IEnumerable enumerable) : base(enumerable) { } + public TestAsyncEnumerableQueryable(Expression expression) : base(expression) { } + + public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + => new TestAsyncEnumerator(this.AsEnumerable().GetEnumerator()); + + IQueryProvider IQueryable.Provider => new TestAsyncQueryProvider(this); + } + + internal class TestAsyncEnumerator(IEnumerator inner) : IAsyncEnumerator + { + public T Current => inner.Current; + public ValueTask MoveNextAsync() => new(inner.MoveNext()); + public ValueTask DisposeAsync() + { + inner.Dispose(); + return default; + } + } + + internal class TestAsyncQueryProvider(IQueryProvider inner) : IQueryProvider, IAsyncQueryProvider + { + public IQueryable CreateQuery(Expression expression) + => new TestAsyncEnumerableQueryable(expression); + + public IQueryable CreateQuery(Expression expression) + => new TestAsyncEnumerableQueryable(expression); + + public object? Execute(Expression expression) + => inner.Execute(expression); + + public TResult Execute(Expression expression) + => inner.Execute(expression); + + public TResult ExecuteAsync(Expression expression, CancellationToken cancellationToken = default) + { + var resultType = typeof(TResult).GetGenericArguments()[0]; + var result = inner.Execute(expression); + return (TResult)typeof(Task).GetMethod(nameof(Task.FromResult))! + .MakeGenericMethod(resultType) + .Invoke(null, [result])!; + } + } + + internal static class TestQueryableExtensions + { + public static IQueryable AsAsyncQueryable(this IEnumerable source) + => new TestAsyncEnumerableQueryable(source); + } +} From c8cca0daecd2b0897e2ded982345c9a5c99f0727 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Tue, 17 Feb 2026 15:44:28 -0800 Subject: [PATCH 012/191] AB#31284 - Allow copying of disabled input fields --- .../Unity.Theme.UX2/wwwroot/themes/ux2/layout.css | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/layout.css b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/layout.css index 1de7a1885e..e35ed9757b 100644 --- a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/layout.css +++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/layout.css @@ -518,20 +518,28 @@ input.form-control-currency { input.form-control:disabled, textarea.form-control:disabled, .form-select:disabled, input.form-control-currency:disabled { color: var(--bc-colors-grey-text-200); - pointer-events: none; background-color: var(--bc-colors-grey-hover) !important; opacity: var(--bs-btn-disabled-opacity); background-blend-mode: difference; border: var(--bs-border-width) solid var(--bs-border-color); + + pointer-events: auto; + user-select: text; + -webkit-user-select: text; + cursor: text; } input.form-control-currency:disabled { color: var(--bc-colors-grey-text-200); - pointer-events: none; background-color: var(--bc-colors-grey-hover) !important; opacity: var(--bs-btn-disabled-opacity); background-blend-mode: difference; border: var(--bs-border-width) solid var(--bs-border-color); + + pointer-events: auto; + user-select: text; + -webkit-user-select: text; + cursor: text; } textarea.form-control:disabled { From e79c86ddcabc2d684fcf7e1797bf93998e09a4f4 Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Tue, 17 Feb 2026 16:28:23 -0800 Subject: [PATCH 013/191] AB#31896 update role map on getcontacts profile --- .../ApplicantProfile/ApplicantProfileContactService.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs index 476940ed3a..a1de6bd35f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs @@ -77,7 +77,8 @@ join appContact in applicationContactsQuery on submission.ApplicationId equals a Email = appContact.ContactEmail, MobilePhoneNumber = appContact.ContactMobilePhone, WorkPhoneNumber = appContact.ContactWorkPhone, - ContactType = appContact.ContactType, + Role = appContact.ContactType, + ContactType = "Application", IsPrimary = false, IsEditable = false, ApplicationId = appContact.ApplicationId From 6bca7fe1d8b84452273411c9442f3daf48929790 Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Tue, 17 Feb 2026 16:50:24 -0800 Subject: [PATCH 014/191] AB#31896 update the lookup for profile contacts --- .../ApplicantProfile/ApplicantProfileContactService.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs index a1de6bd35f..c9c5752cd6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs @@ -1,11 +1,12 @@ +using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; using Unity.GrantManager.ApplicantProfile.ProfileData; using Unity.GrantManager.Applications; using Unity.GrantManager.Contacts; +using Unity.GrantManager.GrantApplications; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories; @@ -77,7 +78,7 @@ join appContact in applicationContactsQuery on submission.ApplicationId equals a Email = appContact.ContactEmail, MobilePhoneNumber = appContact.ContactMobilePhone, WorkPhoneNumber = appContact.ContactWorkPhone, - Role = appContact.ContactType, + Role = ApplicationContactOptionList.ContactTypeList[appContact.ContactType], ContactType = "Application", IsPrimary = false, IsEditable = false, From 0a82052431fb707399a3ced6fa568031f86eab07 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Wed, 18 Feb 2026 08:55:55 -0800 Subject: [PATCH 015/191] AB#31384 - Bugfix - Total Paid Amount - Update payment status checks to use status constant --- .../PaymentInfo/PaymentInfoViewComponent.cs | 3 ++- .../GrantApplications/GrantApplicationAppService.cs | 13 +++++++------ .../ApplicantSubmissionsViewComponent.cs | 5 +++-- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/PaymentInfoViewComponent.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/PaymentInfoViewComponent.cs index c3b195e304..0633472271 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/PaymentInfoViewComponent.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/PaymentInfoViewComponent.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Unity.GrantManager.Applications; using Unity.GrantManager.GrantApplications; +using Unity.Payments.Codes; using Unity.Payments.Enums; using Unity.Payments.PaymentRequests; using Volo.Abp.AspNetCore.Mvc; @@ -101,7 +102,7 @@ private static (decimal paidAmount, decimal pendingAmount) CalculatePaymentAmoun var paidAmount = requestsList .Where(e => !string.IsNullOrWhiteSpace(e.PaymentStatus) - && e.PaymentStatus.Trim().Equals("Fully Paid", StringComparison.OrdinalIgnoreCase)) + && e.PaymentStatus.Trim().Equals(CasPaymentRequestStatus.FullyPaid, StringComparison.OrdinalIgnoreCase)) .Sum(e => e.Amount); var pendingAmount = requestsList diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs index 66ddcf3713..d590a54823 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs @@ -23,7 +23,7 @@ using Unity.GrantManager.Payments; using Unity.Modules.Shared; using Unity.Modules.Shared.Correlation; -using Unity.Payments.Enums; +using Unity.Payments.Codes; using Unity.Payments.PaymentRequests; using Volo.Abp; using Volo.Abp.Application.Dtos; @@ -52,7 +52,7 @@ public class GrantApplicationAppService( { public async Task> GetListAsync(GrantApplicationListInputDto input) { - // 1️⃣ Fetch applications with filters + paging in DB + // 1️ Fetch applications with filters + paging in DB var applications = await applicationRepository.WithFullDetailsAsync( input.SkipCount, input.MaxResultCount, @@ -72,13 +72,14 @@ public async Task> GetListAsync(GrantApplica paymentRequests = await paymentRequestService.GetListByApplicationIdsAsync(applicationIds); } - // 2️⃣ Pre-aggregate payment amounts for O(1) lookup + // 2️ Pre-aggregate payment amounts for O(1) lookup var paymentRequestsByApplication = paymentRequests - .Where(pr => pr.Status == PaymentRequestStatus.Submitted) + .Where(pr => !string.IsNullOrWhiteSpace(pr.PaymentStatus) + && pr.PaymentStatus.Trim().Equals(CasPaymentRequestStatus.FullyPaid, StringComparison.OrdinalIgnoreCase)) .GroupBy(pr => pr.CorrelationId) .ToDictionary(g => g.Key, g => g.Sum(pr => pr.Amount)); - // 3️⃣ Map applications to DTOs + // 3️ Map applications to DTOs var appDtos = applications.Select(app => { var appDto = ObjectMapper.Map(app); @@ -113,7 +114,7 @@ public async Task> GetListAsync(GrantApplica }).ToList(); - // 4️⃣ Get total count using same filters + // 4️ Get total count using same filters var totalCount = await applicationRepository.GetCountAsync( input.SubmittedFromDate, input.SubmittedToDate diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantSubmissions/ApplicantSubmissionsViewComponent.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantSubmissions/ApplicantSubmissionsViewComponent.cs index fe8ef8e380..355613cac4 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantSubmissions/ApplicantSubmissionsViewComponent.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantSubmissions/ApplicantSubmissionsViewComponent.cs @@ -6,7 +6,7 @@ using Unity.GrantManager.Applications; using Unity.GrantManager.GrantApplications; using Unity.GrantManager.Payments; -using Unity.Payments.Enums; +using Unity.Payments.Codes; using Unity.Payments.PaymentRequests; using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.AspNetCore.Mvc.UI.Bundling; @@ -62,7 +62,8 @@ public async Task InvokeAsync(Guid applicantId) { var paymentRequests = await _paymentRequestService.GetListByApplicationIdsAsync(applicationIds); paymentRequestsByApplication = paymentRequests - .Where(pr => pr.Status == PaymentRequestStatus.Submitted) + .Where(pr => !string.IsNullOrWhiteSpace(pr.PaymentStatus) + && pr.PaymentStatus.Trim().Equals(CasPaymentRequestStatus.FullyPaid, StringComparison.OrdinalIgnoreCase)) .GroupBy(pr => pr.CorrelationId) .ToDictionary(g => g.Key, g => g.Sum(pr => pr.Amount)); } From 4341723eb61f89b86f8658bfbd272713c87cd313 Mon Sep 17 00:00:00 2001 From: Velang Date: Wed, 18 Feb 2026 09:35:43 -0800 Subject: [PATCH 016/191] adding new file --- .../cypress/pages/ApplicationsListPage.ts | 509 ++++++++++++++++++ 1 file changed, 509 insertions(+) create mode 100644 applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts diff --git a/applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts b/applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts new file mode 100644 index 0000000000..ae424c36f3 --- /dev/null +++ b/applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts @@ -0,0 +1,509 @@ +/// + +import { ApplicationsPage } from "./ListPages"; + +/** + * ApplicationsListPage - Extended Page Object for the Grant Applications List page + * Extends ApplicationsPage with additional functionality for: + * - Date filters + * - Columns menu operations + * - Payment modal handling + * - Table horizontal scrolling and column visibility + */ +export class ApplicationsListPage extends ApplicationsPage { + private readonly STANDARD_TIMEOUT = 20000; + private readonly BUTTON_TIMEOUT = 60000; + + // Date filter selectors + private readonly dateFilters = { + submittedFromDate: "input#submittedFromDate", + submittedToDate: "input#submittedToDate", + spinner: 'div.spinner-grow[role="status"]', + }; + + // Extended action bar selectors (beyond ApplicationsPage) + private readonly extendedActionBar = { + customButtons: "#app_custom_buttons", + dynamicButtonContainer: "#dynamicButtonContainerId", + exportButton: "#dynamicButtonContainerId .dt-buttons button span", + saveViewButton: "button.grp-savedStates", + }; + + // Table scrolling selectors + private readonly scrollTable = { + scrollBody: ".dt-scroll-body", + tableRows: ".dt-scroll-body tbody tr", + scrollHead: ".dt-scroll-head", + columnTitles: ".dt-scroll-head span.dt-column-title", + }; + + // Columns menu selectors + private readonly columnsMenu = { + dropdownItem: "a.dropdown-item", + buttonBackground: "div.dt-button-background", + }; + + // Payment modal selectors + private readonly paymentModal = { + modal: "#payment-modal", + backdrop: ".modal-backdrop", + cancelButton: "#payment-modal .modal-footer button", + }; + + // Grant program selectors + private readonly grantProgram = { + userInitials: ".unity-user-initials", + userDropdown: "#user-dropdown a.dropdown-item", + searchInput: "#search-grant-programs", + programsTableRow: "#UserGrantProgramsTable tbody tr", + }; + + // Save view selectors + private readonly saveView = { + button: "button.grp-savedStates", + resetOption: "a.dropdown-item", + }; + + constructor() { + super(); + } + + // ============ Date Filter Methods ============ + + /** + * Set the Submitted From Date filter + */ + setSubmittedFromDate(date: string): this { + cy.get(this.dateFilters.submittedFromDate, { timeout: this.STANDARD_TIMEOUT }) + .click({ force: true }) + .clear({ force: true }) + .type(date, { force: true }) + .trigger("change", { force: true }) + .blur({ force: true }) + .should("have.value", date); + return this; + } + + /** + * Set the Submitted To Date filter + */ + setSubmittedToDate(date: string): this { + cy.get(this.dateFilters.submittedToDate, { timeout: this.STANDARD_TIMEOUT }) + .click({ force: true }) + .clear({ force: true }) + .type(date, { force: true }) + .trigger("change", { force: true }) + .blur({ force: true }) + .should("have.value", date); + return this; + } + + /** + * Wait for table refresh (spinner to be hidden) + */ + waitForTableRefresh(): this { + cy.get(this.dateFilters.spinner, { timeout: this.STANDARD_TIMEOUT }).then( + ($s: JQuery) => { + cy.wrap($s) + .should("have.attr", "style") + .and("contain", "display: none"); + } + ); + return this; + } + + /** + * Get today's date in ISO local format (YYYY-MM-DD) + */ + getTodayIsoLocal(): string { + const d = new Date(); + const pad2 = (n: number) => String(n).padStart(2, "0"); + return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`; + } + + // ============ Extended Table Methods ============ + + /** + * Verify table has rows (using scroll body selector) + */ + verifyTableHasData(): this { + cy.get(this.scrollTable.tableRows, { timeout: this.STANDARD_TIMEOUT }).should( + "have.length.greaterThan", + 1 + ); + return this; + } + + /** + * Select a row by index (clicks on a non-link cell) + */ + selectRowByIndex(rowIndex: number, withCtrl = false): this { + cy.get(this.scrollTable.tableRows, { timeout: this.STANDARD_TIMEOUT }) + .eq(rowIndex) + .find("td") + .not(":has(a)") + .first() + .click({ force: true, ctrlKey: withCtrl }); + return this; + } + + /** + * Select multiple rows by indices + */ + selectMultipleRows(indices: number[]): this { + indices.forEach((index, i) => { + this.selectRowByIndex(index, i > 0); + }); + return this; + } + + /** + * Scroll table horizontally to a specific position + */ + scrollTableHorizontally(x: number): this { + cy.get(this.scrollTable.scrollBody, { timeout: this.STANDARD_TIMEOUT }) + .should("exist") + .scrollTo(x, 0, { duration: 0, ensureScrollable: false }); + return this; + } + + /** + * Get visible header titles from the table + */ + getVisibleHeaderTitles(): Cypress.Chainable { + return cy + .get(this.scrollTable.columnTitles, { timeout: this.STANDARD_TIMEOUT }) + .then(($els: JQuery) => { + const titles: string[] = Cypress.$($els) + .toArray() + .map((el: HTMLElement) => (el.textContent || "").replace(/\s+/g, " ").trim()) + .filter((t: string) => t.length > 0); + return titles; + }); + } + + /** + * Assert that visible headers include expected columns (case-insensitive) + */ + assertVisibleHeadersInclude(expected: string[]): this { + this.getVisibleHeaderTitles().then((titles: string[]) => { + const titlesLower = titles.map((t: string) => t.toLowerCase()); + expected.forEach((e: string) => { + expect( + titlesLower, + `visible headers should include "${e}"` + ).to.include(e.toLowerCase()); + }); + }); + return this; + } + + // ============ Extended Action Bar Methods ============ + + /** + * Scroll to and verify action bar exists + */ + verifyActionBarExists(): this { + cy.get(this.extendedActionBar.customButtons, { timeout: this.STANDARD_TIMEOUT }) + .should("exist") + .scrollIntoView(); + return this; + } + + /** + * Click the Payment button (extended with visibility checks) + */ + clickPaymentButtonWithWait(): this { + cy.get("#applicationPaymentRequest", { timeout: this.BUTTON_TIMEOUT }) + .should("be.visible") + .and("not.be.disabled") + .click({ force: true }); + return this; + } + + /** + * Verify Export button is visible + */ + verifyExportButtonVisible(): this { + cy.contains(this.extendedActionBar.exportButton, "Export", { + timeout: this.STANDARD_TIMEOUT, + }).should("be.visible"); + return this; + } + + /** + * Verify Save View button is visible + */ + verifySaveViewButtonVisible(): this { + cy.contains( + "#dynamicButtonContainerId button.grp-savedStates", + "Save View", + { timeout: this.STANDARD_TIMEOUT } + ).should("be.visible"); + return this; + } + + /** + * Verify Columns button is visible + */ + verifyColumnsButtonVisible(): this { + cy.contains( + "#dynamicButtonContainerId .dt-buttons button span", + "Columns", + { timeout: this.STANDARD_TIMEOUT } + ).should("be.visible"); + return this; + } + + /** + * Verify dynamic button container exists + */ + verifyDynamicButtonContainerExists(): this { + cy.get(this.extendedActionBar.dynamicButtonContainer, { + timeout: this.STANDARD_TIMEOUT, + }) + .should("exist") + .scrollIntoView(); + return this; + } + + // ============ Payment Modal Methods ============ + + /** + * Wait for payment modal to be visible + */ + waitForPaymentModalVisible(): this { + cy.get(this.paymentModal.modal, { timeout: this.STANDARD_TIMEOUT }) + .should("be.visible") + .and("have.class", "show"); + return this; + } + + /** + * Close payment modal using multiple strategies + */ + closePaymentModal(): this { + // Attempt ESC key + cy.get("body").type("{esc}", { force: true }); + + // Click backdrop if present (check existence first to avoid timeout) + cy.get("body").then(($body: JQuery) => { + if ($body.find(this.paymentModal.backdrop).length > 0) { + cy.get(this.paymentModal.backdrop).click("topLeft", { force: true }); + } + }); + + // Try Cancel button if available (check existence first to avoid timeout) + cy.get("body").then(($body: JQuery) => { + const $cancelBtn = $body.find(this.paymentModal.cancelButton).filter( + (_: number, el: HTMLElement) => (el.textContent || "").includes("Cancel") + ); + if ($cancelBtn.length > 0) { + cy.wrap($cancelBtn.first()).scrollIntoView().click({ force: true }); + } else { + cy.log("Cancel button not present, proceeding to hard-close fallback"); + } + }); + + // Hard close fallback using jQuery + cy.window().then((win: Cypress.AUTWindow) => { + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const windowWithModal = win as any; + if (typeof windowWithModal.closePaymentModal === "function") { + windowWithModal.closePaymentModal(); + } + } catch { + /* ignore */ + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const $ = (win as any).jQuery || (win as any).$; + if ($) { + try { + $("#payment-modal") + .removeClass("show") + .attr("aria-hidden", "true") + .css("display", "none"); + $(".modal-backdrop").remove(); + $("body").removeClass("modal-open").css("overflow", ""); + } catch { + /* ignore */ + } + } + }); + return this; + } + + /** + * Verify payment modal is closed + */ + verifyPaymentModalClosed(): this { + cy.get(this.paymentModal.modal, { timeout: this.STANDARD_TIMEOUT }).should( + ($m: JQuery) => { + const isHidden = !$m.is(":visible") || !$m.hasClass("show"); + expect(isHidden, "payment-modal hidden or not shown").to.eq(true); + } + ); + cy.get(this.paymentModal.backdrop, { timeout: this.STANDARD_TIMEOUT }).should( + "not.exist" + ); + return this; + } + + // ============ Columns Menu Methods ============ + + /** + * Close any open dropdowns or modals + */ + closeOpenDropdowns(): this { + cy.get("body").then(($body: JQuery) => { + if ($body.find(this.columnsMenu.buttonBackground).length > 0) { + cy.get(this.columnsMenu.buttonBackground).click({ force: true }); + } + }); + return this; + } + + /** + * Open Save View dropdown and reset to default + */ + resetToDefaultView(): this { + cy.get(this.saveView.button, { timeout: this.STANDARD_TIMEOUT }) + .should("be.visible") + .and("contain.text", "Save View") + .click(); + + cy.contains(this.saveView.resetOption, "Reset to Default View", { + timeout: this.STANDARD_TIMEOUT, + }) + .should("exist") + .click({ force: true }); + + // Wait for table to rebuild + cy.get(this.scrollTable.columnTitles, { timeout: this.STANDARD_TIMEOUT }).should( + "have.length.gt", + 5 + ); + return this; + } + + /** + * Open the Columns menu + */ + openColumnsMenu(): this { + cy.contains("span", "Columns", { timeout: this.STANDARD_TIMEOUT }) + .should("be.visible") + .click(); + + // Wait for dropdown to be fully populated + cy.get(this.columnsMenu.dropdownItem, { timeout: this.STANDARD_TIMEOUT }).should( + "have.length.gt", + 50 + ); + return this; + } + + /** + * Click a column item in the Columns menu (case-insensitive) + */ + clickColumnsItem(label: string): this { + cy.contains(this.columnsMenu.dropdownItem, label, { + timeout: this.STANDARD_TIMEOUT, + matchCase: false, + }) + .should("exist") + .scrollIntoView() + .click({ force: true }); + return this; + } + + /** + * Toggle multiple columns (click each one) + */ + toggleColumns(columns: string[]): this { + columns.forEach((column) => { + this.clickColumnsItem(column); + }); + return this; + } + + /** + * Close the Columns menu + */ + closeColumnsMenu(): this { + cy.get(this.columnsMenu.buttonBackground, { timeout: this.STANDARD_TIMEOUT }) + .should("exist") + .click({ force: true }); + + cy.get(this.columnsMenu.buttonBackground, { + timeout: this.STANDARD_TIMEOUT, + }).should("not.exist"); + return this; + } + + // ============ Grant Program Methods ============ + + /** + * Switch to a specific grant program if available + * Note: Consider using NavigationPage.switchToTenantIfAvailable() for consistency + */ + switchToGrantProgram(programName: string): this { + cy.get("body").then(($body: JQuery) => { + const hasUserInitials = + $body.find(this.grantProgram.userInitials).length > 0; + + if (!hasUserInitials) { + cy.log("Skipping tenant switch: no user initials menu found"); + return; + } + + cy.get(this.grantProgram.userInitials).click(); + + cy.get("body").then(($body2: JQuery) => { + const switchLink = $body2 + .find(this.grantProgram.userDropdown) + .filter((_: number, el: HTMLElement) => { + return (el.textContent || "").trim() === "Switch Grant Programs"; + }); + + if (switchLink.length === 0) { + cy.log( + 'Skipping tenant switch: "Switch Grant Programs" not present for this user/session' + ); + cy.get("body").click(0, 0); + return; + } + + cy.wrap(switchLink.first()).click(); + + cy.url({ timeout: this.STANDARD_TIMEOUT }).should( + "include", + "/GrantPrograms" + ); + + cy.get(this.grantProgram.searchInput, { timeout: this.STANDARD_TIMEOUT }) + .should("be.visible") + .clear() + .type(programName); + + cy.contains(this.grantProgram.programsTableRow, programName, { + timeout: this.STANDARD_TIMEOUT, + }) + .should("exist") + .within(() => { + cy.contains("button", "Select").should("be.enabled").click(); + }); + + cy.location("pathname", { timeout: this.STANDARD_TIMEOUT }).should( + (p: string) => { + expect( + p.indexOf("/GrantApplications") >= 0 || p.indexOf("/auth/") >= 0 + ).to.eq(true); + } + ); + }); + }); + return this; + } +} From 90f5b01fa2588c68fd2836b75e35d32f4d10ac31 Mon Sep 17 00:00:00 2001 From: Velang Date: Wed, 18 Feb 2026 13:32:59 -0800 Subject: [PATCH 017/191] Fixing a modal issue --- .../Unity.AutoUI/cypress/e2e/basicEmail.cy.ts | 18 ++++++++++++++++++ applications/Unity.AutoUI/tsconfig.json | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts b/applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts index 152081bae6..0c08e11ce6 100644 --- a/applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts +++ b/applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts @@ -269,6 +269,24 @@ describe("Send an email", () => { }); it("Open Emails tab", () => { + // Dismiss any swal2 modal that may be covering the tab + cy.get("body").then(($body) => { + if ($body.find(".swal2-container").length > 0) { + cy.get(".swal2-container").then(($swal) => { + if ($swal.find(".swal2-close").length > 0) { + cy.get(".swal2-close").click({ force: true }); + } else if ($swal.find(".swal2-confirm").length > 0) { + cy.get(".swal2-confirm").click({ force: true }); + } else { + cy.get("body").type("{esc}", { force: true }); + } + }); + cy.get(".swal2-container", { timeout: STANDARD_TIMEOUT }).should( + "not.exist", + ); + } + }); + cy.get("#emails-tab", { timeout: STANDARD_TIMEOUT }) .should("exist") .should("be.visible") diff --git a/applications/Unity.AutoUI/tsconfig.json b/applications/Unity.AutoUI/tsconfig.json index 6e47c796f6..89a0c8e415 100644 --- a/applications/Unity.AutoUI/tsconfig.json +++ b/applications/Unity.AutoUI/tsconfig.json @@ -106,5 +106,5 @@ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, // Included or excluded files or folders... https://www.typescriptlang.org/tsconfig - "include": ["cypress/support/**/*.ts","cypress/e2e/**/*.ts"] + "include": ["cypress/support/**/*.ts", "cypress/e2e/**/*.ts", "cypress/pages/**/*.ts"] } From 88788297187ae813eab348422d4ba9a9477a33bc Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Wed, 18 Feb 2026 15:23:09 -0800 Subject: [PATCH 018/191] AB#31896 SonarQube cleanup --- .../Controllers/ApplicantProfileController.cs | 1 - .../Swagger/ApplicantProfileDataSchemaFilter.cs | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/ApplicantProfileController.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/ApplicantProfileController.cs index becf33e31c..127a2861ba 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/ApplicantProfileController.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/ApplicantProfileController.cs @@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Mvc; using System.Threading.Tasks; using Unity.GrantManager.ApplicantProfile; -using Unity.GrantManager.ApplicantProfile.ProfileData; using Unity.GrantManager.Controllers.Authentication; using Volo.Abp.AspNetCore.Mvc; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Swagger/ApplicantProfileDataSchemaFilter.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Swagger/ApplicantProfileDataSchemaFilter.cs index a4640af5db..46a7fedc90 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Swagger/ApplicantProfileDataSchemaFilter.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Swagger/ApplicantProfileDataSchemaFilter.cs @@ -1,4 +1,3 @@ -using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; using System.Collections.Generic; @@ -23,7 +22,7 @@ public void Apply(OpenApiSchema schema, SchemaFilterContext context) }; var oneOfSchemas = new List(); - foreach (var (discriminatorValue, subType) in subTypes) + foreach (var (_, subType) in subTypes) { var subSchema = context.SchemaGenerator.GenerateSchema(subType, context.SchemaRepository); oneOfSchemas.Add(subSchema); From 51d7ee556e941eb45e81acd970de45e6771fc8c0 Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Wed, 18 Feb 2026 18:30:57 -0800 Subject: [PATCH 019/191] AB#31413: Layout Adjustments on Applicant Profile - Applicant Info --- .../Components/ApplicantAddresses/Default.css | 1 - .../ApplicantOrganizationInfo/Default.cshtml | 33 ++++++++++--------- .../ApplicantOrganizationInfo/Default.css | 1 - 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAddresses/Default.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAddresses/Default.css index 7ae7cc4f79..d6f31385ec 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAddresses/Default.css +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAddresses/Default.css @@ -24,7 +24,6 @@ } .applicant-organization-info { - background-color: #f8f9fa; border-radius: 8px; padding: 1rem; margin-bottom: 1rem; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantOrganizationInfo/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantOrganizationInfo/Default.cshtml index 0c7366d67e..552b702320 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantOrganizationInfo/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantOrganizationInfo/Default.cshtml @@ -40,26 +40,19 @@ - + - + - +
- -
-
- -
-
-
@@ -74,7 +67,7 @@ - + - - + @@ -128,7 +127,11 @@ - +
+
+ +
+
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantOrganizationInfo/Default.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantOrganizationInfo/Default.css index a1c02309a3..7f9d5e60a2 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantOrganizationInfo/Default.css +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantOrganizationInfo/Default.css @@ -31,7 +31,6 @@ } .applicant-organization-info { - background-color: #f8f9fa; border-radius: 8px; padding: 1rem; margin-bottom: 1rem; From 8a7d12f92e86ce8720a833cb7b9042c694fe7912 Mon Sep 17 00:00:00 2001 From: David Bright Date: Thu, 19 Feb 2026 10:06:22 -0800 Subject: [PATCH 020/191] Improve race condition handling in application links modal Enhances server-side validation by refreshing linked applications from the database before updates, preventing conflicts from stale client data. Improves client-side feedback and error handling, ensuring users are notified of conflicts or errors and the UI stays in sync with the latest data. --- .../ApplicationLinksModal.cshtml.cs | 169 +++++++++++++----- .../ApplicationLinksWidget/Default.js | 27 ++- 2 files changed, 150 insertions(+), 46 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinksModal.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinksModal.cshtml.cs index 715b1af77f..ece2fb7c72 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinksModal.cshtml.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinksModal.cshtml.cs @@ -119,7 +119,33 @@ public async Task OnPostAsync() { List? selectedLinksWithTypes = JsonConvert.DeserializeObject>(LinksWithTypes); List? grantApplications = JsonConvert.DeserializeObject>(GrantApplicationsList!); - List? linkedApplications = JsonConvert.DeserializeObject>(LinkedApplicationsList!); + List? linkedApplications = JsonConvert.DeserializeObject>(LinkedApplicationsList!) ?? []; + + // Refresh from database instead of deserializing stale client data coming in to catch race conditions added. + var allLinks = await _applicationLinksService.GetListByApplicationAsync(CurrentApplicationId ?? Guid.Empty); + // Filter out the reverse links + var databaseLinkedApplications = allLinks.Where(item => item.ApplicationId != CurrentApplicationId).ToList(); + + // We only care if the data in the database is different to do the validation. + var listsAreEqual = new HashSet(linkedApplications, new ApplicationLinksInfoDtoComparer()).SetEquals(databaseLinkedApplications); + if (!listsAreEqual) + { + var linkValidationResult = await ValidateOnPostLinks( + selectedLinksWithTypes ?? [], + grantApplications ?? [], + databaseLinkedApplications); + + if (linkValidationResult.HasErrors) + { + return new JsonResult(new + { + success = false, + //Updates have occured while this window has been opened + message = string.Join(", ", linkValidationResult.ErrorMessages.Select(kvp => $"[{kvp.Key}]: {kvp.Value}")) + }); + } + } + if (selectedLinksWithTypes != null && grantApplications != null && linkedApplications != null) { @@ -127,49 +153,13 @@ public async Task OnPostAsync() foreach (var linkWithType in selectedLinksWithTypes) { var existingLink = linkedApplications.Find(app => app.ReferenceNumber == linkWithType.ReferenceNumber); - if (existingLink == null) { - // Add new link - var targetApplication = grantApplications.Find(app => app.ReferenceNo == linkWithType.ReferenceNumber); - if (targetApplication != null) - { - var linkedApplicationId = targetApplication.Id; - - // For CurrentApplication -> LinkedApplication - await _applicationLinksService.CreateAsync(new ApplicationLinksDto - { - ApplicationId = CurrentApplicationId ?? Guid.Empty, - LinkedApplicationId = linkedApplicationId, - LinkType = linkWithType.LinkType - }); - - // For LinkedApplication -> CurrentApplication (reverse link with appropriate type) - var reverseLinkType = GetReverseLinkType(linkWithType.LinkType); - await _applicationLinksService.CreateAsync(new ApplicationLinksDto - { - ApplicationId = linkedApplicationId, - LinkedApplicationId = CurrentApplicationId ?? Guid.Empty, - LinkType = reverseLinkType - }); - } + await AddLink(linkWithType, grantApplications); } else { - // Check if the link type has changed - if (existingLink.LinkType != linkWithType.LinkType) - { - // Update the existing link's type - await _applicationLinksService.UpdateLinkTypeAsync(existingLink.Id, linkWithType.LinkType); - - // Also update the reverse link - var reverseLink = await _applicationLinksService.GetLinkedApplicationAsync(CurrentApplicationId ?? Guid.Empty, existingLink.ApplicationId); - var reverseLinkType = GetReverseLinkType(linkWithType.LinkType); - await _applicationLinksService.UpdateLinkTypeAsync(reverseLink.Id, reverseLinkType); - - Logger.LogInformation("Updated link type for {ReferenceNumber} from {OldType} to {NewType}", - linkWithType.ReferenceNumber, existingLink.LinkType, linkWithType.LinkType); - } + await UpdateLink(linkWithType, existingLink); } } @@ -192,10 +182,109 @@ await _applicationLinksService.CreateAsync(new ApplicationLinksDto { Logger.LogError(ex, message: "Error updating application links"); } - return new JsonResult(new { success = true }); } + /// + /// Comparer to check for Application, LinkType and ProjectName when comparing data thats currently stored in the running + /// window versus what is stored in the database. Used to assist with race conditions prior to submitting from the modal. + /// + private sealed class ApplicationLinksInfoDtoComparer : IEqualityComparer + { + public bool Equals(ApplicationLinksInfoDto? x, ApplicationLinksInfoDto? y) + { + if (ReferenceEquals(x, y)) return true; + if (x is null || y is null) return false; + return x.ApplicationId == y.ApplicationId && x.LinkType == y.LinkType && x.ProjectName == y.ProjectName; + } + + public int GetHashCode(ApplicationLinksInfoDto obj) => obj.ApplicationId.GetHashCode(); + } + + /// + /// If there is an inequality between what is in the application modal for links and the database, re-run the + /// validation checks to compare what is stored in the database rather than the local user window + /// + /// Link change the user is requesting + /// List of applications to retrieve their reference numbers for generating links + /// Existing links to compare against for validation + /// + /// List of ApplicationLinkValidationResult + private async Task ValidateOnPostLinks( + List newLinks, + List currentApplications, + List existingLinks) + { + var validateAllLinks = new List(); + + validateAllLinks.AddRange([.. newLinks.Select(link => + new ApplicationLinkValidationRequest + { + TargetApplicationId = currentApplications!.Single(app => app.ReferenceNo == link.ReferenceNumber).Id, + ReferenceNumber = link.ReferenceNumber, + LinkType = link.LinkType + })]); + + validateAllLinks.AddRange([.. existingLinks.Select(app => + new ApplicationLinkValidationRequest + { + TargetApplicationId = app.ApplicationId, + ReferenceNumber = app.ReferenceNumber, + LinkType = app.LinkType + } + )]); + + return await _applicationLinksService.ValidateApplicationLinksAsync(CurrentApplicationId ?? Guid.Empty, validateAllLinks); + } + + + private async Task AddLink(LinkWithType linkWithType, List grantApplications) + { + // Add new link + var targetApplication = grantApplications.Find(app => app.ReferenceNo == linkWithType.ReferenceNumber); + if (targetApplication != null) + { + var linkedApplicationId = targetApplication.Id; + + // For CurrentApplication -> LinkedApplication + await _applicationLinksService.CreateAsync(new ApplicationLinksDto + { + ApplicationId = CurrentApplicationId ?? Guid.Empty, + LinkedApplicationId = linkedApplicationId, + LinkType = linkWithType.LinkType + }); + + // For LinkedApplication -> CurrentApplication (reverse link with appropriate type) + var reverseLinkType = GetReverseLinkType(linkWithType.LinkType); + await _applicationLinksService.CreateAsync(new ApplicationLinksDto + { + ApplicationId = linkedApplicationId, + LinkedApplicationId = CurrentApplicationId ?? Guid.Empty, + LinkType = reverseLinkType + }); + } + } + + + private async Task UpdateLink(LinkWithType linkWithType, ApplicationLinksInfoDto existingLink) + { + // Check if the link type has changed + if (existingLink.LinkType != linkWithType.LinkType) + { + // Update the existing link's type + await _applicationLinksService.UpdateLinkTypeAsync(existingLink.Id, linkWithType.LinkType); + + // Also update the reverse link + var reverseLink = await _applicationLinksService.GetLinkedApplicationAsync(CurrentApplicationId ?? Guid.Empty, existingLink.ApplicationId); + var reverseLinkType = GetReverseLinkType(linkWithType.LinkType); + await _applicationLinksService.UpdateLinkTypeAsync(reverseLink.Id, reverseLinkType); + + Logger.LogInformation("Updated link type for {ReferenceNumber} from {OldType} to {NewType}", + linkWithType.ReferenceNumber, existingLink.LinkType, linkWithType.LinkType); + } + } + + private static ApplicationLinkType GetReverseLinkType(ApplicationLinkType linkType) { return linkType switch diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationLinksWidget/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationLinksWidget/Default.js index 4e29d4aeae..d183030c94 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationLinksWidget/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationLinksWidget/Default.js @@ -136,6 +136,7 @@ $(function () { }) .catch(function (error) { abp.notify.error('Error deleting application link.'); + dataTable.ajax.reload(); }); } } @@ -163,6 +164,9 @@ $(function () { 'The application links have been successfully updated.', 'Application Links' ); + }); + + applicationLinksModal.onClose(function () { dataTable.ajax.reload(); }); @@ -921,17 +925,28 @@ $(function () { data: formData, processData: false, contentType: false, - success: () => { + success: (response) => { applicationLinksModal.close(); - abp.notify.success( - 'The application links have been successfully updated.', - 'Application Links' - ); + if (response.success) + { + abp.notify.success('The application links have been successfully updated.','Application Links'); + } + else + { // Display the error message from the server + abp.notify.error(response.message || 'Failed to update application links.','Application Links'); + } dataTable.ajax.reload(); }, error: (xhr, status, error) => { console.error('Error updating application links:', status, error); - abp.notify.error('Error updating application links: ' + error); + let errorMessage = 'Error updating application links.'; + + // Try to extract error message from response + if (xhr.responseJSON && xhr.responseJSON.message) { + errorMessage = xhr.responseJSON.message; + } + + abp.notify.error(errorMessage); } }); } From 9c2724db2ae93eb5e32684c0dec64378f171b43e Mon Sep 17 00:00:00 2001 From: David Bright Date: Thu, 19 Feb 2026 10:27:12 -0800 Subject: [PATCH 021/191] Typo --- .../Pages/ApplicationLinks/ApplicationLinksModal.cshtml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinksModal.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinksModal.cshtml.cs index ece2fb7c72..ee360d4aaf 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinksModal.cshtml.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinksModal.cshtml.cs @@ -140,7 +140,7 @@ public async Task OnPostAsync() return new JsonResult(new { success = false, - //Updates have occured while this window has been opened + //Updates have occurred while this window has been opened message = string.Join(", ", linkValidationResult.ErrorMessages.Select(kvp => $"[{kvp.Key}]: {kvp.Value}")) }); } From 98933c4b9386b5d7c83eee5686012a13fbed1464 Mon Sep 17 00:00:00 2001 From: David Bright Date: Thu, 19 Feb 2026 10:57:25 -0800 Subject: [PATCH 022/191] Making sonarqube happy --- .../Views/Shared/Components/ApplicationLinksWidget/Default.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationLinksWidget/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationLinksWidget/Default.js index d183030c94..dcaeb4b1a4 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationLinksWidget/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationLinksWidget/Default.js @@ -942,7 +942,7 @@ $(function () { let errorMessage = 'Error updating application links.'; // Try to extract error message from response - if (xhr.responseJSON && xhr.responseJSON.message) { + if (xhr.responseJSON?.message) { errorMessage = xhr.responseJSON.message; } From 900a0d95d22c1ebc2832b94789bb58f24488ca81 Mon Sep 17 00:00:00 2001 From: David Bright Date: Thu, 19 Feb 2026 11:20:50 -0800 Subject: [PATCH 023/191] Fixing a logic condition of assigning the refreshed database data post-verification --- .../Pages/ApplicationLinks/ApplicationLinksModal.cshtml.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinksModal.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinksModal.cshtml.cs index ee360d4aaf..4a4656c0e6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinksModal.cshtml.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinksModal.cshtml.cs @@ -142,8 +142,10 @@ public async Task OnPostAsync() success = false, //Updates have occurred while this window has been opened message = string.Join(", ", linkValidationResult.ErrorMessages.Select(kvp => $"[{kvp.Key}]: {kvp.Value}")) - }); + }); } + // Replace the links with what is currently in the database to ensure we are working with the most up to date data + linkedApplications = databaseLinkedApplications; } From 9332a667120c7cf23eef65c8fde759b407898512 Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Thu, 19 Feb 2026 12:39:40 -0800 Subject: [PATCH 024/191] AB#31896 - fix unit tests --- .../Contacts/ContactInfoServiceTests.cs | 20 ++++++++++------ .../TestAsyncEnumerableQueryable.cs | 23 +++++++++++++++---- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoServiceTests.cs index 8241c1a3b7..48fadd7b97 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoServiceTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoServiceTests.cs @@ -139,7 +139,7 @@ public async Task GetApplicationContactsBySubjectAsync_WithMatchingSubmission_Sh ContactEmail = "jane@example.com", ContactMobilePhone = "444-4444", ContactWorkPhone = "555-5555", - ContactType = "Signing Authority" + ContactType = "ADDITIONAL_SIGNING_AUTHORITY" }, appContactId) }.AsAsyncQueryable(); @@ -158,7 +158,8 @@ public async Task GetApplicationContactsBySubjectAsync_WithMatchingSubmission_Sh contact.Email.ShouldBe("jane@example.com"); contact.MobilePhoneNumber.ShouldBe("444-4444"); contact.WorkPhoneNumber.ShouldBe("555-5555"); - contact.ContactType.ShouldBe("Signing Authority"); + contact.ContactType.ShouldBe("Application"); + contact.Role.ShouldBe("Additional Signing Authority"); contact.IsPrimary.ShouldBeFalse(); contact.IsEditable.ShouldBeFalse(); contact.ApplicationId.ShouldBe(applicationId); @@ -186,7 +187,8 @@ public async Task GetApplicationContactsBySubjectAsync_ShouldMatchCaseInsensitiv WithId(new ApplicationContact { ApplicationId = applicationId, - ContactFullName = "Case Test" + ContactFullName = "Case Test", + ContactType = "ADDITIONAL_CONTACT" }, Guid.NewGuid()) }.AsAsyncQueryable(); @@ -222,7 +224,8 @@ public async Task GetApplicationContactsBySubjectAsync_ShouldStripDomainFromSubj WithId(new ApplicationContact { ApplicationId = applicationId, - ContactFullName = "Domain Strip Test" + ContactFullName = "Domain Strip Test", + ContactType = "ADDITIONAL_CONTACT" }, Guid.NewGuid()) }.AsAsyncQueryable(); @@ -259,7 +262,8 @@ public async Task GetApplicationContactsBySubjectAsync_WithSubjectWithoutAtSign_ WithId(new ApplicationContact { ApplicationId = applicationId, - ContactFullName = "Plain User Contact" + ContactFullName = "Plain User Contact", + ContactType = "ADDITIONAL_CONTACT" }, Guid.NewGuid()) }.AsAsyncQueryable(); @@ -355,12 +359,14 @@ public async Task GetApplicationContactsBySubjectAsync_WithMultipleSubmissions_S WithId(new ApplicationContact { ApplicationId = appId1, - ContactFullName = "Contact App 1" + ContactFullName = "Contact App 1", + ContactType = "ADDITIONAL_CONTACT" }, Guid.NewGuid()), WithId(new ApplicationContact { ApplicationId = appId2, - ContactFullName = "Contact App 2" + ContactFullName = "Contact App 2", + ContactType = "ADDITIONAL_SIGNING_AUTHORITY" }, Guid.NewGuid()) }.AsAsyncQueryable(); diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/TestHelpers/TestAsyncEnumerableQueryable.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/TestHelpers/TestAsyncEnumerableQueryable.cs index e0a27bbcb8..26b6bcd977 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/TestHelpers/TestAsyncEnumerableQueryable.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/TestHelpers/TestAsyncEnumerableQueryable.cs @@ -45,11 +45,24 @@ public TResult Execute(Expression expression) public TResult ExecuteAsync(Expression expression, CancellationToken cancellationToken = default) { - var resultType = typeof(TResult).GetGenericArguments()[0]; - var result = inner.Execute(expression); - return (TResult)typeof(Task).GetMethod(nameof(Task.FromResult))! - .MakeGenericMethod(resultType) - .Invoke(null, [result])!; + // TResult is typically Task for async operations + var resultType = typeof(TResult); + + // Get the actual result synchronously + var syncResult = inner.Execute(expression); + + // If TResult is Task, extract T and wrap the result + if (resultType.IsGenericType && resultType.GetGenericTypeDefinition() == typeof(Task<>)) + { + var taskResultType = resultType.GetGenericArguments()[0]; + var taskFromResult = typeof(Task) + .GetMethod(nameof(Task.FromResult))! + .MakeGenericMethod(taskResultType); + return (TResult)taskFromResult.Invoke(null, new[] { syncResult })!; + } + + // For non-generic Task or other types, just return as-is + return (TResult)(object)Task.CompletedTask; } } From 8fd71f042ad2d1037175b9ff3f77f145952f5756 Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Thu, 19 Feb 2026 12:57:59 -0800 Subject: [PATCH 025/191] AB#31896 - codeQL suggestions and cleanup --- .../ProfileData/ApplicantContactInfoDto.cs | 3 +-- .../GrantManagerPermissionDefinitionProvider.cs | 5 ----- .../ApplicantProfile/ApplicantProfileContactService.cs | 8 +++++++- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantContactInfoDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantContactInfoDto.cs index 1da48226c8..f93d6dbf30 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantContactInfoDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantContactInfoDto.cs @@ -6,8 +6,7 @@ namespace Unity.GrantManager.ApplicantProfile.ProfileData public class ApplicantContactInfoDto : ApplicantProfileDataDto { public override string DataType => "CONTACTINFO"; - - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public List Contacts { get; set; } = []; } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantManagerPermissionDefinitionProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantManagerPermissionDefinitionProvider.cs index 52d3b5c62c..06eefb1396 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantManagerPermissionDefinitionProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantManagerPermissionDefinitionProvider.cs @@ -19,11 +19,6 @@ public override void Define(IPermissionDefinitionContext context) grantManagerPermissionsGroup.AddPermission(GrantManagerPermissions.Intakes.Default, L("Permission:GrantManagerManagement.Intakes.Default")); grantManagerPermissionsGroup.AddPermission(GrantManagerPermissions.ApplicationForms.Default, L("Permission:GrantManagerManagement.ApplicationForms.Default")); - - var contactPermissions = grantManagerPermissionsGroup.AddPermission(GrantManagerPermissions.Contacts.Default, L("Permission:GrantManagerManagement.Contacts.Default")); - contactPermissions.AddChild(GrantManagerPermissions.Contacts.Create, L("Permission:GrantManagerManagement.Contacts.Create")); - contactPermissions.AddChild(GrantManagerPermissions.Contacts.Read, L("Permission:GrantManagerManagement.Contacts.Read")); - contactPermissions.AddChild(GrantManagerPermissions.Contacts.Update, L("Permission:GrantManagerManagement.Contacts.Update")); } private static LocalizableString L(string name) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs index c9c5752cd6..46ef6e66f9 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs @@ -78,7 +78,7 @@ join appContact in applicationContactsQuery on submission.ApplicationId equals a Email = appContact.ContactEmail, MobilePhoneNumber = appContact.ContactMobilePhone, WorkPhoneNumber = appContact.ContactWorkPhone, - Role = ApplicationContactOptionList.ContactTypeList[appContact.ContactType], + Role = GetMatchingRole(appContact.ContactType), ContactType = "Application", IsPrimary = false, IsEditable = false, @@ -87,4 +87,10 @@ join appContact in applicationContactsQuery on submission.ApplicationId equals a return applicationContacts; } + + private static string GetMatchingRole(string contactType) + { + return ApplicationContactOptionList.ContactTypeList.TryGetValue(contactType, out string? value) + ? value : contactType; + } } From d8a84fdeeddd7207c33e3e292437c0b8bda6af1d Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Thu, 19 Feb 2026 14:41:23 -0800 Subject: [PATCH 026/191] AB#31896 sonarQube cleanup --- .../ApplicantProfile/ProfileData/ApplicantContactInfoDto.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantContactInfoDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantContactInfoDto.cs index f93d6dbf30..716f78928c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantContactInfoDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantContactInfoDto.cs @@ -1,4 +1,3 @@ -using Newtonsoft.Json; using System.Collections.Generic; namespace Unity.GrantManager.ApplicantProfile.ProfileData From efa2706406c692cc939612a1be2685fb0caad88b Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Thu, 19 Feb 2026 15:21:43 -0800 Subject: [PATCH 027/191] AB#31896 remove unnecessary #pragma disables --- .../Repositories/ContactLinkRepository.cs | 7 +------ .../Repositories/ContactRepository.cs | 7 +------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ContactLinkRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ContactLinkRepository.cs index 9319b7e257..f8f7f5b952 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ContactLinkRepository.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ContactLinkRepository.cs @@ -9,13 +9,8 @@ namespace Unity.GrantManager.Repositories { [Dependency(ReplaceServices = true)] [ExposeServices(typeof(IContactLinkRepository))] -#pragma warning disable CS8613 // Nullability of reference types in return type doesn't match implicitly implemented member. // This pattern is an implementation ontop of ABP framework, will not change this - public class ContactLinkRepository : EfCoreRepository, IContactLinkRepository -#pragma warning restore CS8613 // Nullability of reference types in return type doesn't match implicitly implemented member. + public class ContactLinkRepository(IDbContextProvider dbContextProvider) : EfCoreRepository(dbContextProvider), IContactLinkRepository { - public ContactLinkRepository(IDbContextProvider dbContextProvider) : base(dbContextProvider) - { - } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ContactRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ContactRepository.cs index 0d7222fc01..e83324eaed 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ContactRepository.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ContactRepository.cs @@ -9,13 +9,8 @@ namespace Unity.GrantManager.Repositories { [Dependency(ReplaceServices = true)] [ExposeServices(typeof(IContactRepository))] -#pragma warning disable CS8613 // Nullability of reference types in return type doesn't match implicitly implemented member. // This pattern is an implementation ontop of ABP framework, will not change this - public class ContactRepository : EfCoreRepository, IContactRepository -#pragma warning restore CS8613 // Nullability of reference types in return type doesn't match implicitly implemented member. + public class ContactRepository(IDbContextProvider dbContextProvider) : EfCoreRepository(dbContextProvider), IContactRepository { - public ContactRepository(IDbContextProvider dbContextProvider) : base(dbContextProvider) - { - } } } From 48bac3bd5a78665ecbd4d27f62ec12141cf9eb96 Mon Sep 17 00:00:00 2001 From: David Bright Date: Thu, 19 Feb 2026 15:54:35 -0800 Subject: [PATCH 028/191] Added the quick date range select drop down next to search with predefined values. The previous "FromDate" and "ToDate" are hidden unless the quick date range selection is set to "Custom" Quick date range calculates the predefined values and sets "FromDate" and "ToDate" to utilize existing fuctions/logic Clearing all filters DOES NOT reset the quick date range filter (change in acceptance) Reviewing initial performance improvements --- .../Pages/GrantApplications/Index.js | 165 ++++++++++++++---- .../Components/ActionBar/Default.cshtml | 58 +++--- .../Shared/Components/ActionBar/Default.css | 6 + 3 files changed, 176 insertions(+), 53 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 3ebe15233d..266a43543f 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 @@ -7,6 +7,8 @@ const formatter = createNumberFormatter(); const l = abp.localization.getResource('GrantManager'); + const defaultQuickDateRange = 'last6months'; + let dt = $('#GrantApplicationsTable'); let dataTable; @@ -92,6 +94,22 @@ dt.search(''); dt.order(initialSortOrder).draw(); + // Reset date range filters + $('#quickDateRange').val(defaultQuickDateRange); + toggleCustomDateInputs(defaultQuickDateRange === 'custom'); + + const range = getDateRange(defaultQuickDateRange); + 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); + } + // Close the dropdown dt.buttons('.grp-savedStates') .container() @@ -164,50 +182,41 @@ const listColumns = getColumns(); } function initializeSubmittedFilterDates() { - const fromDate = localStorage.getItem('GrantApplications_FromDate'); const toDate = localStorage.getItem('GrantApplications_ToDate'); + const savedRange = localStorage.getItem('GrantApplications_QuickRange') || defaultQuickDateRange; - // Check if localStorage has values and use them + // Set the dropdown value + $('#quickDateRange').val(savedRange); + + // Show/hide custom date inputs based on saved selection + toggleCustomDateInputs(savedRange === 'custom'); + + // If we have saved dates, use them if (fromDate && toDate) { UIElements.submittedFromInput.val(fromDate); UIElements.submittedToInput.val(toDate); grantTableFilters.submittedFromDate = fromDate; grantTableFilters.submittedToDate = toDate; - return; + } else { + const range = getDateRange(defaultQuickDateRange); + if (range && range.fromDate && range.toDate) { + UIElements.submittedFromInput.val(range.fromDate); + UIElements.submittedToInput.val(range.toDate); + grantTableFilters.submittedFromDate = range.fromDate; + grantTableFilters.submittedToDate = range.toDate; + } } - let dtToday = new Date(); - let month = dtToday.getMonth() + 1; - let day = dtToday.getDate(); - let year = dtToday.getFullYear(); - if (month < 10) - month = '0' + month.toString(); - if (day < 10) - day = '0' + day.toString(); - let todayDate = year + '-' + month + '-' + day; - - let dtSixMonthsAgo = new Date(); - dtSixMonthsAgo.setMonth(dtSixMonthsAgo.getMonth() - 6); - let minMonth = dtSixMonthsAgo.getMonth() + 1; - let minDay = dtSixMonthsAgo.getDate(); - let minYear = dtSixMonthsAgo.getFullYear(); - if (minMonth < 10) - minMonth = '0' + minMonth.toString(); - if (minDay < 10) - minDay = '0' + minDay.toString(); - let suggestedMinDate = minYear + '-' + minMonth + '-' + minDay; - - UIElements.submittedToInput.attr({ 'max': todayDate }); - UIElements.submittedToInput.val(todayDate); - UIElements.submittedFromInput.attr({ 'max': todayDate }); - UIElements.submittedFromInput.val(suggestedMinDate); - grantTableFilters.submittedFromDate = suggestedMinDate; - grantTableFilters.submittedToDate = todayDate; + // Set max date to today for both inputs + const today = formatDate(new Date()); + UIElements.submittedToInput.attr({ 'max': today }); + UIElements.submittedFromInput.attr({ 'max': today }); } function bindUIEvents() { - UIElements.inputFilter.on('change', handleInputFilterChange); + UIElements.inputFilter.on('change', handleInputFilterChange); + $('#quickDateRange').on('change', handleQuickDateRangeChange); } function validateDate(dateValue, element) { @@ -243,6 +252,55 @@ const listColumns = getColumns(); return true; } + // Returns a formated { fromDate, toDate } for the filter fields. + // Null if 'custom' or no input provided (assumes custom is default break) + function getDateRange(rangeType) { + let today = new Date(); + const toDate = formatDate(new Date()); + let fromDate; + + switch (rangeType) { + case 'today': + fromDate = toDate; + break; + case 'last7days': + fromDate = formatDate(new Date(today.setDate(today.getDate() - 7))); + break; + case 'last30days': + fromDate = formatDate(new Date(today.setDate(today.getDate() - 30))); + break; + case 'last3months': + fromDate = formatDate(new Date(today.setMonth(today.getMonth() - 3))); + break; + case 'last6months': + fromDate = formatDate(new Date(today.setMonth(today.getMonth() - 6))); + break; + case 'alltime': + fromDate = null; + return { fromDate: null, toDate: null }; + case 'custom': + default: + return null; // Don't modify dates for custom + } + + return { fromDate, toDate }; + } + function formatDate(date) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + } + + function toggleCustomDateInputs(show) { + if (show) { + $('#customDateInputs').show(); + } else { + $('#customDateInputs').hide(); + } + } + + // ===================== // Input filter change handler // ===================== @@ -255,6 +313,11 @@ const listColumns = getColumns(); grantTableFilters.submittedFromDate = UIElements.submittedFromInput.val(); grantTableFilters.submittedToDate = UIElements.submittedToInput.val(); + //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'); + localStorage.setItem('GrantApplications_QuickRange', 'custom'); + const dtInstance = $('#GrantApplicationsTable').DataTable(); localStorage.setItem("GrantApplications_FromDate", grantTableFilters.submittedFromDate); @@ -262,6 +325,46 @@ const listColumns = getColumns(); dtInstance.ajax.reload(null, true); } + + function handleQuickDateRangeChange() { + const selectedRange = $(this).val(); + + localStorage.setItem('GrantApplications_QuickRange', selectedRange); + + if (selectedRange === 'custom') { + // Show the custom date inputs and don't modify their values + toggleCustomDateInputs(true); + return; + } + + // Hide custom date inputs for preset ranges + toggleCustomDateInputs(false); + + // Get the date range for the selected option + const range = getDateRange(selectedRange); + + if (range) { + // Populate the hidden date fields + UIElements.submittedFromInput.val(range.fromDate || ''); + UIElements.submittedToInput.val(range.toDate || ''); + 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); + } + } function initializeDataTableAndEvents() { dataTable = initializeDataTable({ diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.cshtml index eaf1c5bb6f..bb425251da 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.cshtml @@ -17,29 +17,43 @@
- - + + +
+ -
- - -
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.css index 81685f3695..cca218855d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.css +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.css @@ -37,3 +37,9 @@ margin-bottom: -10px; padding-bottom: 0px !important; } + +.custom-date-range-container-div { + display: inline-block; + padding: 0px; + margin: 0px; +} From 3a9799a97283935d6878528079393f51bac32d64 Mon Sep 17 00:00:00 2001 From: David Bright Date: Thu, 19 Feb 2026 16:08:16 -0800 Subject: [PATCH 029/191] Fixed an option chaining flagged by Sonarqube --- .../src/Unity.GrantManager.Web/Pages/GrantApplications/Index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 266a43543f..050bfb23b8 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 @@ -200,7 +200,7 @@ const listColumns = getColumns(); grantTableFilters.submittedToDate = toDate; } else { const range = getDateRange(defaultQuickDateRange); - if (range && range.fromDate && range.toDate) { + if (range?.fromDate && range?.toDate) { UIElements.submittedFromInput.val(range.fromDate); UIElements.submittedToInput.val(range.toDate); grantTableFilters.submittedFromDate = range.fromDate; From 27733348b71562aaf01de1b5c4941b2fe4d87dcf Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Fri, 20 Feb 2026 13:11:08 -0800 Subject: [PATCH 030/191] AB#25283 submission data provider for profiles --- .../ProfileData/ApplicantSubmissionInfoDto.cs | 5 + .../ProfileData/SubmissionInfoItemDto.cs | 15 +++ .../SubmissionInfoDataProvider.cs | 114 +++++++++++++++++- .../ApplicantProfileDataProviderTests.cs | 22 +++- 4 files changed, 150 insertions(+), 6 deletions(-) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/SubmissionInfoItemDto.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantSubmissionInfoDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantSubmissionInfoDto.cs index 5c3618c30c..9c1fc36c73 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantSubmissionInfoDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantSubmissionInfoDto.cs @@ -1,7 +1,12 @@ +using System.Collections.Generic; + namespace Unity.GrantManager.ApplicantProfile.ProfileData { public class ApplicantSubmissionInfoDto : ApplicantProfileDataDto { public override string DataType => "SUBMISSIONINFO"; + + public List Submissions { get; set; } = []; + public string LinkSource { get; set; } = string.Empty; } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/SubmissionInfoItemDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/SubmissionInfoItemDto.cs new file mode 100644 index 0000000000..bc1b54c02f --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/SubmissionInfoItemDto.cs @@ -0,0 +1,15 @@ +using System; + +namespace Unity.GrantManager.ApplicantProfile.ProfileData +{ + public class SubmissionInfoItemDto + { + public Guid Id { get; set; } + public string LinkId { get; set; } = string.Empty; + public DateTime ReceivedTime { get; set; } + public DateTime SubmissionTime { get; set; } + public string ReferenceNo { get; set; } = string.Empty; + public string ProjectName { get; set; } = string.Empty; + public string Status { get; set; } = string.Empty; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/SubmissionInfoDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/SubmissionInfoDataProvider.cs index 9bd91fcbd4..298f53c7f6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/SubmissionInfoDataProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/SubmissionInfoDataProvider.cs @@ -1,17 +1,125 @@ +using System; +using System.Linq; +using System.Text.Json; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using Unity.GrantManager.ApplicantProfile.ProfileData; +using Unity.GrantManager.Applications; +using Unity.GrantManager.Integrations; using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.MultiTenancy; namespace Unity.GrantManager.ApplicantProfile { [ExposeServices(typeof(IApplicantProfileDataProvider))] - public class SubmissionInfoDataProvider : IApplicantProfileDataProvider, ITransientDependency + public class SubmissionInfoDataProvider( + ICurrentTenant currentTenant, + IRepository applicationFormSubmissionRepository, + IRepository applicationRepository, + IRepository applicationStatusRepository, + IEndpointManagementAppService endpointManagementAppService, + ILogger logger) + : IApplicantProfileDataProvider, ITransientDependency { public string Key => ApplicantProfileKeys.SubmissionInfo; - public Task GetDataAsync(ApplicantProfileInfoRequest request) + public async Task GetDataAsync(ApplicantProfileInfoRequest request) { - return Task.FromResult(new ApplicantSubmissionInfoDto()); + var dto = new ApplicantSubmissionInfoDto + { + Submissions = [] + }; + + var normalizedSubject = request.Subject.Contains('@') + ? request.Subject[..request.Subject.IndexOf('@')].ToUpperInvariant() + : request.Subject.ToUpperInvariant(); + + dto.LinkSource = await ResolveFormViewUrlAsync(); + + using (currentTenant.Change(request.TenantId)) + { + var submissionsQuery = await applicationFormSubmissionRepository.GetQueryableAsync(); + var applicationsQuery = await applicationRepository.GetQueryableAsync(); + var statusesQuery = await applicationStatusRepository.GetQueryableAsync(); + + var results = await ( + from submission in submissionsQuery + join application in applicationsQuery on submission.ApplicationId equals application.Id + join status in statusesQuery on application.ApplicationStatusId equals status.Id + where submission.OidcSub == normalizedSubject + select new + { + submission.Id, + LinkId = submission.ChefsSubmissionGuid, + submission.CreationTime, + submission.Submission, + application.ReferenceNo, + application.ProjectName, + Status = status.ExternalStatus + }).ToListAsync(); + + dto.Submissions.AddRange(results.Select(s => new SubmissionInfoItemDto + { + Id = s.Id, + LinkId = s.LinkId, + ReceivedTime = s.CreationTime, + SubmissionTime = ResolveSubmissionTime(s.Submission, s.CreationTime), + ReferenceNo = s.ReferenceNo, + ProjectName = s.ProjectName, + Status = s.Status + })); + } + + return dto; + } + + /// + /// Derives the CHEFS form view URL from the INTAKE_API_BASE dynamic URL setting. + /// e.g. https://chefs-dev.apps.silver.devops.gov.bc.ca/app/api/v1 + /// -> https://chefs-dev.apps.silver.devops.gov.bc.ca/app/form/view?s= + /// + private async Task ResolveFormViewUrlAsync() + { + try + { + var chefsApiBaseUrl = await endpointManagementAppService.GetChefsApiBaseUrlAsync(); + var trimmed = chefsApiBaseUrl.TrimEnd('/'); + const string apiSegment = "/api/v1"; + if (trimmed.EndsWith(apiSegment, StringComparison.OrdinalIgnoreCase)) + { + trimmed = trimmed[..^apiSegment.Length]; + } + return $"{trimmed}/form/view?s="; + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to resolve CHEFS form view URL from INTAKE_API_BASE setting."); + return string.Empty; + } + } + + private DateTime ResolveSubmissionTime(string submissionJson, DateTime fallback) + { + try + { + if (!string.IsNullOrEmpty(submissionJson)) + { + using var doc = JsonDocument.Parse(submissionJson); + if (doc.RootElement.TryGetProperty("createdAt", out var createdAt) && + createdAt.TryGetDateTime(out var dateTime)) + { + return dateTime; + } + } + } + catch (JsonException ex) + { + logger.LogWarning(ex, "Failed to parse submission JSON for submission time. Falling back to received time."); + } + + return fallback; } } } diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/ApplicantProfileDataProviderTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/ApplicantProfileDataProviderTests.cs index 74ccc3f9ad..3ff284cb43 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/ApplicantProfileDataProviderTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/ApplicantProfileDataProviderTests.cs @@ -6,6 +6,9 @@ using System.Threading.Tasks; using Unity.GrantManager.ApplicantProfile; using Unity.GrantManager.ApplicantProfile.ProfileData; +using Unity.GrantManager.Applications; +using Unity.GrantManager.Integrations; +using Volo.Abp.Domain.Repositories; using Volo.Abp.MultiTenancy; using Xunit; @@ -33,6 +36,19 @@ private static ContactInfoDataProvider CreateContactInfoDataProvider() return new ContactInfoDataProvider(currentTenant, applicantProfileContactService); } + private static SubmissionInfoDataProvider CreateSubmissionInfoDataProvider() + { + var currentTenant = Substitute.For(); + currentTenant.Change(Arg.Any()).Returns(Substitute.For()); + var submissionRepo = Substitute.For>(); + var applicationRepo = Substitute.For>(); + var statusRepo = Substitute.For>(); + var endpointManagementAppService = Substitute.For(); + endpointManagementAppService.GetChefsApiBaseUrlAsync().Returns(Task.FromResult(string.Empty)); + var logger = Substitute.For>(); + return new SubmissionInfoDataProvider(currentTenant, submissionRepo, applicationRepo, statusRepo, endpointManagementAppService, logger); + } + [Fact] public void ContactInfoDataProvider_Key_ShouldMatchExpected() { @@ -84,14 +100,14 @@ public async Task AddressInfoDataProvider_GetDataAsync_ShouldReturnAddressInfoDt [Fact] public void SubmissionInfoDataProvider_Key_ShouldMatchExpected() { - var provider = new SubmissionInfoDataProvider(); + var provider = CreateSubmissionInfoDataProvider(); provider.Key.ShouldBe(ApplicantProfileKeys.SubmissionInfo); } [Fact] public async Task SubmissionInfoDataProvider_GetDataAsync_ShouldReturnSubmissionInfoDto() { - var provider = new SubmissionInfoDataProvider(); + var provider = CreateSubmissionInfoDataProvider(); var result = await provider.GetDataAsync(CreateRequest(ApplicantProfileKeys.SubmissionInfo)); result.ShouldNotBeNull(); result.ShouldBeOfType(); @@ -121,7 +137,7 @@ public void AllProviders_ShouldHaveUniqueKeys() CreateContactInfoDataProvider(), new OrgInfoDataProvider(), new AddressInfoDataProvider(), - new SubmissionInfoDataProvider(), + CreateSubmissionInfoDataProvider(), new PaymentInfoDataProvider() ]; From f316ee3a3bcae83eb306e85f1299dab8aed9d69c Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Fri, 20 Feb 2026 15:03:38 -0800 Subject: [PATCH 031/191] AB#25283 fix unit test --- .../Applicants/ApplicantProfileDataProviderTests.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/ApplicantProfileDataProviderTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/ApplicantProfileDataProviderTests.cs index 3ff284cb43..51b1c94f24 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/ApplicantProfileDataProviderTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/ApplicantProfileDataProviderTests.cs @@ -8,6 +8,7 @@ using Unity.GrantManager.ApplicantProfile.ProfileData; using Unity.GrantManager.Applications; using Unity.GrantManager.Integrations; +using Unity.GrantManager.TestHelpers; using Volo.Abp.Domain.Repositories; using Volo.Abp.MultiTenancy; using Xunit; @@ -41,8 +42,11 @@ private static SubmissionInfoDataProvider CreateSubmissionInfoDataProvider() var currentTenant = Substitute.For(); currentTenant.Change(Arg.Any()).Returns(Substitute.For()); var submissionRepo = Substitute.For>(); + submissionRepo.GetQueryableAsync().Returns(Task.FromResult(Enumerable.Empty().AsAsyncQueryable())); var applicationRepo = Substitute.For>(); + applicationRepo.GetQueryableAsync().Returns(Task.FromResult(Enumerable.Empty().AsAsyncQueryable())); var statusRepo = Substitute.For>(); + statusRepo.GetQueryableAsync().Returns(Task.FromResult(Enumerable.Empty().AsAsyncQueryable())); var endpointManagementAppService = Substitute.For(); endpointManagementAppService.GetChefsApiBaseUrlAsync().Returns(Task.FromResult(string.Empty)); var logger = Substitute.For>(); From f43730d98ee4fcb066b5c0dece7bf6e21d5b5d9a Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Fri, 20 Feb 2026 15:10:25 -0800 Subject: [PATCH 032/191] AB#25283 missing xml docs --- .../ApplicantProfile/SubmissionInfoDataProvider.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/SubmissionInfoDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/SubmissionInfoDataProvider.cs index 298f53c7f6..abd04e2f96 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/SubmissionInfoDataProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/SubmissionInfoDataProvider.cs @@ -13,6 +13,12 @@ namespace Unity.GrantManager.ApplicantProfile { + /// + /// Provides submission information for the applicant profile by querying + /// application form submissions linked to the applicant's OIDC subject. + /// Resolves actual submission timestamps from CHEFS JSON data and derives + /// the form view URL from the INTAKE_API_BASE dynamic URL setting. + /// [ExposeServices(typeof(IApplicantProfileDataProvider))] public class SubmissionInfoDataProvider( ICurrentTenant currentTenant, @@ -23,8 +29,10 @@ public class SubmissionInfoDataProvider( ILogger logger) : IApplicantProfileDataProvider, ITransientDependency { + /// public string Key => ApplicantProfileKeys.SubmissionInfo; + /// public async Task GetDataAsync(ApplicantProfileInfoRequest request) { var dto = new ApplicantSubmissionInfoDto From 10f8f60e9825f13c3e828ac7d09a22ae14c0cb96 Mon Sep 17 00:00:00 2001 From: Andre Goncalves <98196495+AndreGAot@users.noreply.github.com> Date: Fri, 20 Feb 2026 15:23:47 -0800 Subject: [PATCH 033/191] Update applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/SubmissionInfoDataProvider.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../ApplicantProfile/SubmissionInfoDataProvider.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/SubmissionInfoDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/SubmissionInfoDataProvider.cs index abd04e2f96..de1b1a29a7 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/SubmissionInfoDataProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/SubmissionInfoDataProvider.cs @@ -40,9 +40,10 @@ public async Task GetDataAsync(ApplicantProfileInfoRequ Submissions = [] }; - var normalizedSubject = request.Subject.Contains('@') - ? request.Subject[..request.Subject.IndexOf('@')].ToUpperInvariant() - : request.Subject.ToUpperInvariant(); + var subject = request.Subject ?? string.Empty; + var normalizedSubject = subject.Contains('@') + ? subject[..subject.IndexOf('@')].ToUpperInvariant() + : subject.ToUpperInvariant(); dto.LinkSource = await ResolveFormViewUrlAsync(); From 0cc221b1f1d6d43c45f19868fafe4a1aef712224 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 20 Feb 2026 16:22:05 -0800 Subject: [PATCH 034/191] AB#32001 Improve AI payload logging controls and output formatting --- .../AI/OpenAIService.cs | 166 +++++++++++++++++- 1 file changed, 161 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 1374af052b..742a9abe05 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs @@ -7,6 +7,7 @@ using System.Net.Http; using System.Text; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using Volo.Abp.DependencyInjection; @@ -21,7 +22,10 @@ public class OpenAIService : IAIService, ITransientDependency private string? ApiKey => _configuration["AI:OpenAI:ApiKey"]; private string? ApiUrl => _configuration["AI:OpenAI:ApiUrl"] ?? "https://api.openai.com/v1/chat/completions"; + private bool LogPayloads => _configuration.GetValue("AI:Logging:LogPayloads") ?? false; private readonly string NoKeyError = "OpenAI API key is not configured"; + private const string AiPromptLogRelativePath = "logs/ai-prompts.log"; + private static int _aiPromptLogInitialized; public OpenAIService(HttpClient httpClient, IConfiguration configuration, ILogger logger, ITextExtractionService textExtractionService) { @@ -50,7 +54,7 @@ public async Task GenerateSummaryAsync(string content, string? prompt = return "AI analysis not available - service not configured."; } - _logger.LogDebug("Calling OpenAI with prompt: {Prompt}", content); + _logger.LogDebug("Calling OpenAI chat completions. PromptLength: {PromptLength}, MaxTokens: {MaxTokens}", content?.Length ?? 0, maxTokens); try { @@ -76,7 +80,10 @@ public async Task GenerateSummaryAsync(string content, string? prompt = var response = await _httpClient.PostAsync(ApiUrl, httpContent); var responseContent = await response.Content.ReadAsStringAsync(); - _logger.LogDebug("Response: {Response}", responseContent); + _logger.LogDebug( + "OpenAI chat completions response received. StatusCode: {StatusCode}, ResponseLength: {ResponseLength}", + response.StatusCode, + responseContent?.Length ?? 0); if (!response.IsSuccessStatusCode) { @@ -125,7 +132,10 @@ public async Task GenerateAttachmentSummaryAsync(string fileName, byte[] 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."; } - return await GenerateSummaryAsync(contentToAnalyze, prompt, 150); + LogPromptInput("AttachmentSummary", prompt, contentToAnalyze); + var modelOutput = await GenerateSummaryAsync(contentToAnalyze, prompt, 150); + LogPromptOutput("AttachmentSummary", modelOutput); + return modelOutput; } catch (Exception ex) { @@ -202,7 +212,9 @@ public async Task AnalyzeApplicationAsync(string applicationContent, Lis Respond only with valid JSON in the exact format requested."; + LogPromptInput("ApplicationAnalysis", systemPrompt, analysisContent); var rawAnalysis = await GenerateSummaryAsync(analysisContent, systemPrompt, 1000); + LogPromptOutput("ApplicationAnalysis", rawAnalysis); // Post-process the AI response to add unique IDs to errors and warnings return AddIdsToAnalysisItems(rawAnalysis); @@ -320,7 +332,10 @@ Base your answers on the application content and attachment summaries provided. Be thorough, objective, and fair in your assessment. Base your answers strictly on the provided application content. Respond only with valid JSON in the exact format requested."; - return await GenerateSummaryAsync(analysisContent, systemPrompt, 2000); + LogPromptInput("ScoresheetAll", systemPrompt, analysisContent); + var modelOutput = await GenerateSummaryAsync(analysisContent, systemPrompt, 2000); + LogPromptOutput("ScoresheetAll", modelOutput); + return modelOutput; } catch (Exception ex) { @@ -394,7 +409,10 @@ Always provide citations that reference specific parts of the application conten 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."; - return await GenerateSummaryAsync(analysisContent, systemPrompt, 2000); + LogPromptInput("ScoresheetSection", systemPrompt, analysisContent); + var modelOutput = await GenerateSummaryAsync(analysisContent, systemPrompt, 2000); + LogPromptOutput("ScoresheetSection", modelOutput); + return modelOutput; } catch (Exception ex) { @@ -402,5 +420,143 @@ Always provide citations that reference specific parts of the application conten return "{}"; } } + + private void LogPromptInput(string promptType, string? systemPrompt, string userPrompt) + { + if (!LogPayloads) + { + return; + } + + var formattedInput = FormatPromptInputForLog(systemPrompt, userPrompt); + _logger.LogDebug("AI {PromptType} input payload: {PromptInput}", promptType, formattedInput); + WriteAiPromptLog(promptType, "INPUT", formattedInput); + } + + private void LogPromptOutput(string promptType, string output) + { + if (!LogPayloads) + { + return; + } + + var formattedOutput = FormatPromptOutputForLog(output); + _logger.LogDebug("AI {PromptType} model output payload: {ModelOutput}", promptType, formattedOutput); + WriteAiPromptLog(promptType, "OUTPUT", formattedOutput); + } + + private void WriteAiPromptLog(string promptType, string payloadType, string payload) + { + if (!LogPayloads) + { + return; + } + + try + { + var now = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss zzz"); + var logPath = Path.Combine(AppContext.BaseDirectory, AiPromptLogRelativePath); + EnsureAiPromptLogInitialized(logPath); + + var entry = $"{now} [{promptType}] {payloadType}\n{payload}\n\n"; + File.AppendAllText(logPath, entry); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to write AI prompt log file."); + } + } + + private static void EnsureAiPromptLogInitialized(string logPath) + { + var directory = Path.GetDirectoryName(logPath); + if (!string.IsNullOrWhiteSpace(directory)) + { + Directory.CreateDirectory(directory); + } + + if (Interlocked.Exchange(ref _aiPromptLogInitialized, 1) == 0) + { + File.WriteAllText(logPath, string.Empty); + } + } + + private static string FormatPromptInputForLog(string? systemPrompt, string userPrompt) + { + var normalizedSystemPrompt = string.IsNullOrWhiteSpace(systemPrompt) ? string.Empty : systemPrompt.Trim(); + var normalizedUserPrompt = string.IsNullOrWhiteSpace(userPrompt) ? string.Empty : userPrompt.Trim(); + return $"SYSTEM_PROMPT\n{normalizedSystemPrompt}\n\nUSER_PROMPT\n{normalizedUserPrompt}"; + } + + private static string FormatPromptOutputForLog(string output) + { + if (string.IsNullOrWhiteSpace(output)) + { + return string.Empty; + } + + if (TryParseJsonObjectFromResponse(output, out var jsonObject)) + { + return JsonSerializer.Serialize(jsonObject, new JsonSerializerOptions { WriteIndented = true }); + } + + return output.Trim(); + } + + private static bool TryParseJsonObjectFromResponse(string response, out JsonElement objectElement) + { + objectElement = default; + var cleaned = CleanJsonResponse(response); + if (string.IsNullOrWhiteSpace(cleaned)) + { + return false; + } + + try + { + using var doc = JsonDocument.Parse(cleaned); + if (doc.RootElement.ValueKind != JsonValueKind.Object) + { + return false; + } + + objectElement = doc.RootElement.Clone(); + return true; + } + catch (JsonException) + { + return false; + } + } + + private static string CleanJsonResponse(string response) + { + if (string.IsNullOrWhiteSpace(response)) + { + return string.Empty; + } + + var cleaned = response.Trim(); + + if (cleaned.StartsWith("```json", StringComparison.OrdinalIgnoreCase) || cleaned.StartsWith("```")) + { + var startIndex = cleaned.IndexOf('\n'); + if (startIndex >= 0) + { + cleaned = cleaned[(startIndex + 1)..]; + } + } + + if (cleaned.EndsWith("```", StringComparison.Ordinal)) + { + var lastIndex = cleaned.LastIndexOf("```", StringComparison.Ordinal); + if (lastIndex > 0) + { + cleaned = cleaned[..lastIndex]; + } + } + + return cleaned.Trim(); + } } } From 77ca0f3a8780c558ca633fc2f8294a323398fdc6 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 20 Feb 2026 17:53:09 -0800 Subject: [PATCH 035/191] AB#32001 Update gitignore for local dev artifacts --- .gitignore | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 841973049b..5ccccd8b9e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ *.rsuser *.userosscache *.sln.docstates +*.code-workspace # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs @@ -23,6 +24,7 @@ bld/ [Oo]bj/ [Ll]og/ [Ll]ogs/ +*.log # Visual Studio cache/options directory .vs/ @@ -116,4 +118,7 @@ appsettings.json /applications/Orchestrator *.env -/applications/Unity.GrantManager/src/Unity.GrantManager.Web/package-lock.json \ No newline at end of file +/applications/Unity.GrantManager/src/Unity.GrantManager.Web/package-lock.json + +# Local dev artifacts +/AGENTS.md From de0914357887f5a871e34ddd6cf191e497d48c89 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Mon, 23 Feb 2026 11:01:26 -0800 Subject: [PATCH 036/191] AB#31384 - Add aggregated payment summary DTO and batch queries --- .../ApplicationPaymentSummaryDto.cs | 11 +++ .../IPaymentRequestAppService.cs | 2 + .../IPaymentRequestRepository.cs | 3 +- .../Services/IPaymentRequestQueryManager.cs | 4 + .../Services/PaymentRequestQueryManager.cs | 81 ++++++++++++++++++- .../Repositories/PaymentRequestRepository.cs | 66 +++++++++++---- .../PaymentRequestAppService.cs | 17 +++- .../PaymentInfo/PaymentInfoViewComponent.cs | 68 +--------------- .../PaymentInfoViewComponentTests.cs | 3 +- .../IApplicationLinksService.cs | 3 + .../ApplicationLinksAppService.cs | 24 ++++++ .../GrantApplicationAppService.cs | 17 ++-- .../ApplicantSubmissionsViewComponent.cs | 16 ++-- 13 files changed, 204 insertions(+), 111 deletions(-) create mode 100644 applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/ApplicationPaymentSummaryDto.cs diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/ApplicationPaymentSummaryDto.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/ApplicationPaymentSummaryDto.cs new file mode 100644 index 0000000000..0d6706572c --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/ApplicationPaymentSummaryDto.cs @@ -0,0 +1,11 @@ +using System; + +namespace Unity.Payments.PaymentRequests; + +[Serializable] +public class ApplicationPaymentSummaryDto +{ + public Guid ApplicationId { get; set; } + public decimal TotalPaid { get; set; } + public decimal TotalPending { get; set; } +} diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/IPaymentRequestAppService.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/IPaymentRequestAppService.cs index be35d094cf..ae1fc15a41 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/IPaymentRequestAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/IPaymentRequestAppService.cs @@ -21,5 +21,7 @@ public interface IPaymentRequestAppService : IApplicationService Task GetUserPaymentThresholdAsync(); Task ManuallyAddPaymentRequestsToReconciliationQueue(List paymentRequestIds); Task> GetPaymentPendingListByCorrelationIdAsync(Guid applicationId); + Task GetApplicationPaymentSummaryAsync(Guid applicationId); + Task> GetApplicationPaymentSummariesAsync(List applicationIds); } } diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/PaymentRequests/IPaymentRequestRepository.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/PaymentRequests/IPaymentRequestRepository.cs index 5ae55debbc..9a27897a1f 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/PaymentRequests/IPaymentRequestRepository.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/PaymentRequests/IPaymentRequestRepository.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Unity.Payments.PaymentRequests; using Volo.Abp.Domain.Repositories; namespace Unity.Payments.Domain.PaymentRequests @@ -14,6 +15,6 @@ public interface IPaymentRequestRepository : IRepository Task GetPaymentRequestByInvoiceNumber(string invoiceNumber); Task> GetPaymentRequestsByFailedsStatusAsync(); Task> GetPaymentPendingListByCorrelationIdAsync(Guid correlationId); - + Task> GetPaymentSummariesByCorrelationIdsAsync(List correlationIds); } } diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/IPaymentRequestQueryManager.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/IPaymentRequestQueryManager.cs index 222dc9f83d..d7c2c36cde 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/IPaymentRequestQueryManager.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/IPaymentRequestQueryManager.cs @@ -35,5 +35,9 @@ public interface IPaymentRequestQueryManager // Pending Payments Task> GetPaymentPendingListByCorrelationIdAsync(Guid applicationId); + + // Payment Summaries (paid + pending aggregation) + Task GetApplicationPaymentSummaryAsync(Guid applicationId, List childApplicationIds); + Task> GetApplicationPaymentSummariesAsync(List applicationIds, Dictionary> childApplicationIdsByParent); } } diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/PaymentRequestQueryManager.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/PaymentRequestQueryManager.cs index f5fe8e9728..b7b76ddcb2 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/PaymentRequestQueryManager.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/PaymentRequestQueryManager.cs @@ -88,8 +88,10 @@ public async Task CreatePaymentRequestDtoAsync(Guid paymentRe public async Task> GetListByApplicationIdsAsync(List applicationIds) { var paymentsQueryable = await paymentRequestRepository.GetQueryableAsync(); - var payments = await paymentsQueryable.Include(pr => pr.Site).ToListAsync(); - var filteredPayments = payments.Where(pr => applicationIds.Contains(pr.CorrelationId)).ToList(); + var filteredPayments = await paymentsQueryable + .Include(pr => pr.Site) + .Where(pr => applicationIds.Contains(pr.CorrelationId)) + .ToListAsync(); return objectMapper.Map, List>(filteredPayments); } @@ -97,8 +99,10 @@ public async Task> GetListByApplicationIdsAsync(List> GetListByApplicationIdAsync(Guid applicationId) { var paymentsQueryable = await paymentRequestRepository.GetQueryableAsync(); - var payments = await paymentsQueryable.Include(pr => pr.Site).ToListAsync(); - var filteredPayments = payments.Where(e => e.CorrelationId == applicationId).ToList(); + var filteredPayments = await paymentsQueryable + .Include(pr => pr.Site) + .Where(e => e.CorrelationId == applicationId) + .ToListAsync(); return objectMapper.Map, List>(filteredPayments); } @@ -218,5 +222,74 @@ public async Task> GetPaymentPendingListByCorrelationIdA var payments = await paymentRequestRepository.GetPaymentPendingListByCorrelationIdAsync(applicationId); return objectMapper.Map, List>(payments); } + + public async Task GetApplicationPaymentSummaryAsync(Guid applicationId, List childApplicationIds) + { + var allCorrelationIds = new List { applicationId }; + allCorrelationIds.AddRange(childApplicationIds); + + var summaries = await paymentRequestRepository.GetPaymentSummariesByCorrelationIdsAsync(allCorrelationIds); + + return new ApplicationPaymentSummaryDto + { + ApplicationId = applicationId, + TotalPaid = summaries.Sum(s => s.TotalPaid), + TotalPending = summaries.Sum(s => s.TotalPending) + }; + } + + public async Task> GetApplicationPaymentSummariesAsync( + List applicationIds, + Dictionary> childApplicationIdsByParent) + { + // Collect all unique correlation IDs (parents + all children) for a single DB query + var allCorrelationIds = new HashSet(applicationIds); + foreach (var childIds in childApplicationIdsByParent.Values) + { + foreach (var childId in childIds) + { + allCorrelationIds.Add(childId); + } + } + + var summaries = await paymentRequestRepository.GetPaymentSummariesByCorrelationIdsAsync(allCorrelationIds.ToList()); + var summaryLookup = summaries.ToDictionary(s => s.ApplicationId); + + var result = new Dictionary(); + foreach (var applicationId in applicationIds) + { + decimal totalPaid = 0; + decimal totalPending = 0; + + // Add the parent application's own amounts + if (summaryLookup.TryGetValue(applicationId, out var parentSummary)) + { + totalPaid += parentSummary.TotalPaid; + totalPending += parentSummary.TotalPending; + } + + // Add child application amounts + if (childApplicationIdsByParent.TryGetValue(applicationId, out var childIds)) + { + foreach (var childId in childIds) + { + if (summaryLookup.TryGetValue(childId, out var childSummary)) + { + totalPaid += childSummary.TotalPaid; + totalPending += childSummary.TotalPending; + } + } + } + + result[applicationId] = new ApplicationPaymentSummaryDto + { + ApplicationId = applicationId, + TotalPaid = totalPaid, + TotalPending = totalPending + }; + } + + return result; + } } } diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/EntityFrameworkCore/Repositories/PaymentRequestRepository.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/EntityFrameworkCore/Repositories/PaymentRequestRepository.cs index acb7a4a8b2..01307ab2f6 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/EntityFrameworkCore/Repositories/PaymentRequestRepository.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/EntityFrameworkCore/Repositories/PaymentRequestRepository.cs @@ -1,4 +1,5 @@ -using System; +using Microsoft.EntityFrameworkCore; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -6,6 +7,7 @@ using Unity.Payments.Domain.PaymentRequests; using Unity.Payments.EntityFrameworkCore; using Unity.Payments.Enums; +using Unity.Payments.PaymentRequests; using Volo.Abp.Domain.Repositories.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore; @@ -16,9 +18,6 @@ public class PaymentRequestRepository : EfCoreRepository ReCheckStatusList { get; set; } = new List(); private List FailedStatusList { get; set; } = new List(); - - - public PaymentRequestRepository(IDbContextProvider dbContextProvider) : base(dbContextProvider) { ReCheckStatusList.Add(CasPaymentRequestStatus.ServiceUnavailable); @@ -32,25 +31,27 @@ public PaymentRequestRepository(IDbContextProvider dbContextP public async Task GetCountByCorrelationId(Guid correlationId) { var dbSet = await GetDbSetAsync(); - return dbSet.Count(s => s.CorrelationId == correlationId); + return await dbSet.CountAsync(s => s.CorrelationId == correlationId); } public async Task GetPaymentRequestCountBySiteId(Guid siteId) { var dbSet = await GetDbSetAsync(); - return dbSet.Where(s => s.SiteId == siteId).Count(); - } + return await dbSet.Where(s => s.SiteId == siteId) + .CountAsync(); + } public async Task GetPaymentRequestByInvoiceNumber(string invoiceNumber) { var dbSet = await GetDbSetAsync(); - return dbSet.Where(s => s.InvoiceNumber == invoiceNumber).FirstOrDefault(); + return await dbSet.Where(s => s.InvoiceNumber == invoiceNumber) + .FirstOrDefaultAsync(); } public async Task GetTotalPaymentRequestAmountByCorrelationIdAsync(Guid correlationId) { var dbSet = await GetDbSetAsync(); - decimal applicationPaymentRequestsTotal = dbSet + decimal applicationPaymentRequestsTotal = await dbSet .Where(p => p.CorrelationId.Equals(correlationId)) .Where(p => p.Status != PaymentRequestStatus.L1Declined && p.Status != PaymentRequestStatus.L2Declined @@ -59,7 +60,7 @@ public async Task GetTotalPaymentRequestAmountByCorrelationIdAsync(Guid && p.InvoiceStatus != CasPaymentRequestStatus.ErrorFromCas) .GroupBy(p => p.CorrelationId) .Select(p => p.Sum(q => q.Amount)) - .FirstOrDefault(); + .FirstOrDefaultAsync(); return applicationPaymentRequestsTotal; } @@ -67,15 +68,17 @@ public async Task GetTotalPaymentRequestAmountByCorrelationIdAsync(Guid public async Task> GetPaymentRequestsBySentToCasStatusAsync() { var dbSet = await GetDbSetAsync(); - return dbSet.Where(p => p.InvoiceStatus != null && ReCheckStatusList.Contains(p.InvoiceStatus)).IncludeDetails().ToList(); + return await dbSet.Where(p => p.InvoiceStatus != null && ReCheckStatusList.Contains(p.InvoiceStatus)) + .IncludeDetails() + .ToListAsync(); } public async Task> GetPaymentRequestsByFailedsStatusAsync() { var dbSet = await GetDbSetAsync(); - return dbSet.Where(p => p.InvoiceStatus != null - && FailedStatusList.Contains(p.InvoiceStatus) - && p.LastModificationTime >= DateTime.Now.AddDays(-2)).IncludeDetails().ToList(); + return await dbSet.Where(p => p.InvoiceStatus != null + && FailedStatusList.Contains(p.InvoiceStatus) + && p.LastModificationTime >= DateTime.Now.AddDays(-2)).IncludeDetails().ToListAsync(); } public override async Task> WithDetailsAsync() @@ -87,8 +90,39 @@ public override async Task> WithDetailsAsync() public async Task> GetPaymentPendingListByCorrelationIdAsync(Guid correlationId) { var dbSet = await GetDbSetAsync(); - return dbSet.Where(p => p.CorrelationId.Equals(correlationId)) - .Where(p => p.Status == PaymentRequestStatus.L1Pending || p.Status == PaymentRequestStatus.L2Pending).IncludeDetails().ToList(); + return await dbSet.Where(p => p.CorrelationId.Equals(correlationId)) + .Where(p => p.Status == PaymentRequestStatus.L1Pending || p.Status == PaymentRequestStatus.L2Pending) + .IncludeDetails() + .ToListAsync(); + } + + public async Task> GetPaymentSummariesByCorrelationIdsAsync(List correlationIds) + { + var dbSet = await GetDbSetAsync(); + + var results = await dbSet + .Where(p => correlationIds.Contains(p.CorrelationId)) + .GroupBy(p => p.CorrelationId) + .Select(g => new ApplicationPaymentSummaryDto + { + ApplicationId = g.Key, + TotalPaid = g + .Where(p => p.PaymentStatus != null + && p.PaymentStatus == CasPaymentRequestStatus.FullyPaid) + .Sum(p => p.Amount), + TotalPending = g + .Where(p => p.Status == PaymentRequestStatus.L1Pending + || p.Status == PaymentRequestStatus.L2Pending + || p.Status == PaymentRequestStatus.L3Pending + || (p.Status == PaymentRequestStatus.Submitted + && (p.PaymentStatus == null || p.PaymentStatus == string.Empty) + && (p.InvoiceStatus == null || p.InvoiceStatus == string.Empty + || !p.InvoiceStatus.Contains(CasPaymentRequestStatus.ErrorFromCas)))) + .Sum(p => p.Amount) + }) + .ToListAsync(); + + return results; } } } diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/PaymentRequestAppService.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/PaymentRequestAppService.cs index 81c63c3b8f..3ecdf011a2 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/PaymentRequestAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/PaymentRequestAppService.cs @@ -17,6 +17,7 @@ using Volo.Abp.Authorization.Permissions; using Volo.Abp.Users; using Unity.Payments.PaymentRequests.Notifications; +using Unity.GrantManager.GrantApplications; namespace Unity.Payments.PaymentRequests { @@ -29,7 +30,8 @@ public class PaymentRequestAppService( IPaymentsManager paymentsManager, FsbPaymentNotifier fsbPaymentNotifier, IPaymentRequestQueryManager paymentRequestQueryManager, - IPaymentRequestConfigurationManager paymentRequestConfigurationManager) : PaymentsAppService, IPaymentRequestAppService + IPaymentRequestConfigurationManager paymentRequestConfigurationManager, + IApplicationLinksService applicationLinksService) : PaymentsAppService, IPaymentRequestAppService { public async Task GetDefaultAccountCodingId() @@ -328,5 +330,18 @@ public async Task> GetPaymentPendingListByCorrelationIdA { return await paymentRequestQueryManager.GetPaymentPendingListByCorrelationIdAsync(applicationId); } + + public async Task GetApplicationPaymentSummaryAsync(Guid applicationId) + { + var childLinks = await applicationLinksService.GetChildApplications(applicationId); + var childApplicationIds = childLinks.Select(l => l.LinkedApplicationId).ToList(); + return await paymentRequestQueryManager.GetApplicationPaymentSummaryAsync(applicationId, childApplicationIds); + } + + public async Task> GetApplicationPaymentSummariesAsync(List applicationIds) + { + var childApplicationIdsByParent = await applicationLinksService.GetChildApplicationIdsByParentIdsAsync(applicationIds); + return await paymentRequestQueryManager.GetApplicationPaymentSummariesAsync(applicationIds, childApplicationIdsByParent); + } } } diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/PaymentInfoViewComponent.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/PaymentInfoViewComponent.cs index 0633472271..b422c97832 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/PaymentInfoViewComponent.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/PaymentInfoViewComponent.cs @@ -1,12 +1,8 @@ using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; -using Unity.GrantManager.Applications; using Unity.GrantManager.GrantApplications; -using Unity.Payments.Codes; -using Unity.Payments.Enums; using Unity.Payments.PaymentRequests; using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.AspNetCore.Mvc.UI.Bundling; @@ -25,17 +21,14 @@ public class PaymentInfoViewComponent : AbpViewComponent private readonly IGrantApplicationAppService _grantApplicationAppService; private readonly IPaymentRequestAppService _paymentRequestService; private readonly IFeatureChecker _featureChecker; - private readonly IApplicationLinksService _applicationLinksService; public PaymentInfoViewComponent(IGrantApplicationAppService grantApplicationAppService, IPaymentRequestAppService paymentRequestService, - IFeatureChecker featureChecker, - IApplicationLinksService applicationLinksService) + IFeatureChecker featureChecker) { _grantApplicationAppService = grantApplicationAppService; _paymentRequestService = paymentRequestService; _featureChecker = featureChecker; - _applicationLinksService = applicationLinksService; } public async Task InvokeAsync(Guid applicationId, Guid applicationFormVersionId) @@ -53,41 +46,9 @@ public async Task InvokeAsync(Guid applicationId, Guid app ApplicantId = application.Applicant.Id }; - var paymentRequests = await _paymentRequestService.GetListByApplicationIdAsync(applicationId); - - // Calculate Total Paid and Total Pending Amounts for current application - var (paidAmount, pendingAmount) = CalculatePaymentAmounts(paymentRequests); - model.TotalPaid = paidAmount; - model.TotalPendingAmounts = pendingAmount; - - // Add Total Paid and Total Pending Amounts from child applications - var applicationLinks = await _applicationLinksService.GetListByApplicationAsync(applicationId); - var childApplications = applicationLinks - .Where(link => link.LinkType == ApplicationLinkType.Child - && link.ApplicationId != applicationId) // Exclude self-references - .ToList(); - - // Batch fetch payment requests for all child applications to avoid N+1 queries - var childApplicationIds = childApplications.Select(ca => ca.ApplicationId).ToList(); - if (childApplicationIds.Count != 0) - { - var childPaymentRequests = await _paymentRequestService.GetListByApplicationIdsAsync(childApplicationIds); - var paymentRequestsByAppId = childPaymentRequests - .GroupBy(pr => pr.CorrelationId) - .ToDictionary(g => g.Key, g => g.ToList()); - - foreach (var childApp in childApplications) - { - if (paymentRequestsByAppId.TryGetValue(childApp.ApplicationId, out var requests)) - { - // Add child's Total Paid and Total Pending Amounts - var (childPaidAmount, childPendingAmount) = CalculatePaymentAmounts(requests); - model.TotalPaid += childPaidAmount; - model.TotalPendingAmounts += childPendingAmount; - } - } - } - + var summary = await _paymentRequestService.GetApplicationPaymentSummaryAsync(applicationId); + model.TotalPaid = summary.TotalPaid; + model.TotalPendingAmounts = summary.TotalPending; model.RemainingAmount = application.ApprovedAmount - model.TotalPaid; return View(model); @@ -96,27 +57,6 @@ public async Task InvokeAsync(Guid applicationId, Guid app return View(new PaymentInfoViewModel()); } - private static (decimal paidAmount, decimal pendingAmount) CalculatePaymentAmounts(List paymentRequests) - { - var requestsList = paymentRequests; - - var paidAmount = requestsList - .Where(e => !string.IsNullOrWhiteSpace(e.PaymentStatus) - && e.PaymentStatus.Trim().Equals(CasPaymentRequestStatus.FullyPaid, StringComparison.OrdinalIgnoreCase)) - .Sum(e => e.Amount); - - var pendingAmount = requestsList - .Where(e => e.Status == PaymentRequestStatus.L1Pending - || e.Status == PaymentRequestStatus.L2Pending - || e.Status == PaymentRequestStatus.L3Pending - || (e.Status == PaymentRequestStatus.Submitted - && string.IsNullOrWhiteSpace(e.PaymentStatus) - && (string.IsNullOrWhiteSpace(e.InvoiceStatus) || !e.InvoiceStatus.Trim().Contains("Error", StringComparison.OrdinalIgnoreCase)))) - .Sum(e => e.Amount); - - return (paidAmount, pendingAmount); - } - public class PaymentInfoStyleBundleContributor : BundleContributor { public override void ConfigureBundle(BundleConfigurationContext context) diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/ViewComponents/PaymentInfoViewComponentTests.cs b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/ViewComponents/PaymentInfoViewComponentTests.cs index 0cd3660d8e..58d8df519a 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/ViewComponents/PaymentInfoViewComponentTests.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/ViewComponents/PaymentInfoViewComponentTests.cs @@ -665,8 +665,7 @@ private PaymentInfoViewComponent CreateViewComponent( var viewComponent = new PaymentInfoViewComponent( appService, paymentRequestService, - featureChecker, - applicationLinksService) + featureChecker) { ViewComponentContext = viewComponentContext, LazyServiceProvider = _lazyServiceProvider diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationLinksService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationLinksService.cs index 833fa42327..c8df0b91a0 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationLinksService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationLinksService.cs @@ -11,6 +11,9 @@ public interface IApplicationLinksService : ICrudAppService< Guid> { Task> GetListByApplicationAsync(Guid applicationId); + Task> GetApplicationLinksByType(Guid applicationId, ApplicationLinkType linkType); + Task> GetChildApplications(Guid applicationId); + Task>> GetChildApplicationIdsByParentIdsAsync(List parentApplicationIds); Task GetLinkedApplicationAsync(Guid currentApplicationId, Guid linkedApplicationId); Task GetCurrentApplicationInfoAsync(Guid applicationId); Task DeleteWithPairAsync(Guid applicationLinkId); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationLinksAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationLinksAppService.cs index 3ce28a569e..fd765b4b0f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationLinksAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationLinksAppService.cs @@ -96,6 +96,30 @@ join applicant in applicantsQuery on application.ApplicantId equals applicant.Id return resultList; } + public async Task> GetApplicationLinksByType(Guid applicationId, ApplicationLinkType linkType) + { + var applicationLinksQuery = await ApplicationLinksRepository + .GetListAsync(al => al.ApplicationId == applicationId && al.LinkType == linkType); + + return ObjectMapper.Map, List>(applicationLinksQuery); + } + + public async Task> GetChildApplications(Guid applicationId) + { + return await GetApplicationLinksByType(applicationId, ApplicationLinkType.Child); + } + + public async Task>> GetChildApplicationIdsByParentIdsAsync(List parentApplicationIds) + { + var links = await ApplicationLinksRepository + .GetListAsync(al => parentApplicationIds.Contains(al.ApplicationId) + && al.LinkType == ApplicationLinkType.Child); + + return links + .GroupBy(l => l.ApplicationId) + .ToDictionary(g => g.Key, g => g.Select(l => l.LinkedApplicationId).ToList()); + } + public async Task GetLinkedApplicationAsync(Guid currentApplicationId, Guid linkedApplicationId) { var applicationLinksQuery = await ApplicationLinksRepository.GetQueryableAsync(); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs index d590a54823..5b6df4c684 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs @@ -23,7 +23,6 @@ using Unity.GrantManager.Payments; using Unity.Modules.Shared; using Unity.Modules.Shared.Correlation; -using Unity.Payments.Codes; using Unity.Payments.PaymentRequests; using Volo.Abp; using Volo.Abp.Application.Dtos; @@ -65,20 +64,13 @@ public async Task> GetListAsync(GrantApplica bool paymentsFeatureEnabled = await FeatureChecker.IsEnabledAsync(PaymentConsts.UnityPaymentsFeature); - List paymentRequests = []; + Dictionary paymentSummaries = []; if (paymentsFeatureEnabled && applicationIds.Count > 0) { - paymentRequests = await paymentRequestService.GetListByApplicationIdsAsync(applicationIds); + paymentSummaries = await paymentRequestService.GetApplicationPaymentSummariesAsync(applicationIds); } - // 2️ Pre-aggregate payment amounts for O(1) lookup - var paymentRequestsByApplication = paymentRequests - .Where(pr => !string.IsNullOrWhiteSpace(pr.PaymentStatus) - && pr.PaymentStatus.Trim().Equals(CasPaymentRequestStatus.FullyPaid, StringComparison.OrdinalIgnoreCase)) - .GroupBy(pr => pr.CorrelationId) - .ToDictionary(g => g.Key, g => g.Sum(pr => pr.Amount)); - // 3️ Map applications to DTOs var appDtos = applications.Select(app => { @@ -102,12 +94,13 @@ public async Task> GetListAsync(GrantApplica appDto.ContactCellPhone = app.ApplicantAgent?.Phone2; appDto.ApplicationLinks = ObjectMapper.Map, List>(app.ApplicationLinks?.ToList() ?? []); - if (paymentsFeatureEnabled && paymentRequestsByApplication.Count > 0) + if (paymentsFeatureEnabled && paymentSummaries.Count > 0) { + paymentSummaries.TryGetValue(app.Id, out var summary); appDto.PaymentInfo = new PaymentInfoDto { ApprovedAmount = app.ApprovedAmount, - TotalPaid = paymentRequestsByApplication.GetValueOrDefault(app.Id) + TotalPaid = summary?.TotalPaid ?? 0 }; } return appDto; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantSubmissions/ApplicantSubmissionsViewComponent.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantSubmissions/ApplicantSubmissionsViewComponent.cs index 355613cac4..59cd647b90 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantSubmissions/ApplicantSubmissionsViewComponent.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantSubmissions/ApplicantSubmissionsViewComponent.cs @@ -6,7 +6,6 @@ using Unity.GrantManager.Applications; using Unity.GrantManager.GrantApplications; using Unity.GrantManager.Payments; -using Unity.Payments.Codes; using Unity.Payments.PaymentRequests; using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.AspNetCore.Mvc.UI.Bundling; @@ -57,15 +56,10 @@ public async Task InvokeAsync(Guid applicantId) var applicationIds = applications.Select(app => app.Id).ToList(); var paymentsFeatureEnabled = await _featureChecker.IsEnabledAsync(PaymentConsts.UnityPaymentsFeature); - Dictionary paymentRequestsByApplication = []; + Dictionary paymentSummaries = []; if (paymentsFeatureEnabled && applicationIds.Count > 0) { - var paymentRequests = await _paymentRequestService.GetListByApplicationIdsAsync(applicationIds); - paymentRequestsByApplication = paymentRequests - .Where(pr => !string.IsNullOrWhiteSpace(pr.PaymentStatus) - && pr.PaymentStatus.Trim().Equals(CasPaymentRequestStatus.FullyPaid, StringComparison.OrdinalIgnoreCase)) - .GroupBy(pr => pr.CorrelationId) - .ToDictionary(g => g.Key, g => g.Sum(pr => pr.Amount)); + paymentSummaries = await _paymentRequestService.GetApplicationPaymentSummariesAsync(applicationIds); } // Map to DTOs (similar to GrantApplicationAppService.GetListAsync) @@ -132,13 +126,13 @@ public async Task InvokeAsync(Guid applicantId) } dto.Assignees = assigneeDtos; - if (paymentsFeatureEnabled && paymentRequestsByApplication.Count > 0) + if (paymentsFeatureEnabled && paymentSummaries.Count > 0) { - paymentRequestsByApplication.TryGetValue(app.Id, out var totalPaid); + paymentSummaries.TryGetValue(app.Id, out var summary); dto.PaymentInfo = new PaymentInfoDto { ApprovedAmount = app.ApprovedAmount, - TotalPaid = totalPaid + TotalPaid = summary?.TotalPaid ?? 0 }; } From 15ae1611f883713ae33588a905f7e86d14a63905 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Mon, 23 Feb 2026 11:40:28 -0800 Subject: [PATCH 037/191] AB#31384 - Add aggregated payment summary tests and test modifications --- ...equestQueryManager_PaymentSummary_Tests.cs | 413 ++++++++++++++ .../PaymentInfoViewComponentTests.cs | 510 +++--------------- .../PaymentsTestBase.cs | 4 +- 3 files changed, 505 insertions(+), 422 deletions(-) create mode 100644 applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequestQueryManager_PaymentSummary_Tests.cs diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequestQueryManager_PaymentSummary_Tests.cs b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequestQueryManager_PaymentSummary_Tests.cs new file mode 100644 index 0000000000..0d29fbd87e --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequestQueryManager_PaymentSummary_Tests.cs @@ -0,0 +1,413 @@ +using NSubstitute; +using Shouldly; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Threading.Tasks; +using Unity.Payments.Domain.PaymentRequests; +using Unity.Payments.Domain.Services; +using Unity.Payments.Domain.Suppliers; +using Unity.Payments.PaymentRequests; +using Volo.Abp.Users; +using Xunit; + +namespace Unity.Payments.Domain.PaymentRequests; + +[Category("Domain")] +public class PaymentRequestQueryManager_PaymentSummary_Tests +{ + #region GetApplicationPaymentSummaryAsync (Single Application) + + [Fact] + public async Task Should_Return_Summary_For_Single_Application_With_NoChildren() + { + // Arrange + var appId = Guid.NewGuid(); + var repo = Substitute.For(); + repo.GetPaymentSummariesByCorrelationIdsAsync(Arg.Any>()) + .Returns(new List + { + new() { ApplicationId = appId, TotalPaid = 1500m, TotalPending = 2000m } + }); + + var manager = CreateManager(repo); + + // Act + var result = await manager.GetApplicationPaymentSummaryAsync(appId, []); + + // Assert + result.ShouldNotBeNull(); + result.ApplicationId.ShouldBe(appId); + result.TotalPaid.ShouldBe(1500m); + result.TotalPending.ShouldBe(2000m); + + // Verify repo was called with only the parent ID + await repo.Received(1).GetPaymentSummariesByCorrelationIdsAsync( + Arg.Is>(ids => ids.Count == 1 && ids.Contains(appId))); + } + + [Fact] + public async Task Should_Aggregate_Summary_From_Parent_And_Children() + { + // Arrange + var parentId = Guid.NewGuid(); + var child1Id = Guid.NewGuid(); + var child2Id = Guid.NewGuid(); + + var repo = Substitute.For(); + repo.GetPaymentSummariesByCorrelationIdsAsync(Arg.Any>()) + .Returns(new List + { + new() { ApplicationId = parentId, TotalPaid = 1000m, TotalPending = 500m }, + new() { ApplicationId = child1Id, TotalPaid = 300m, TotalPending = 200m }, + new() { ApplicationId = child2Id, TotalPaid = 700m, TotalPending = 100m } + }); + + var manager = CreateManager(repo); + + // Act + var result = await manager.GetApplicationPaymentSummaryAsync(parentId, [child1Id, child2Id]); + + // Assert + result.ShouldNotBeNull(); + result.ApplicationId.ShouldBe(parentId); + result.TotalPaid.ShouldBe(2000m); // 1000 + 300 + 700 + result.TotalPending.ShouldBe(800m); // 500 + 200 + 100 + + // Verify all IDs were sent to the repository + await repo.Received(1).GetPaymentSummariesByCorrelationIdsAsync( + Arg.Is>(ids => ids.Count == 3 + && ids.Contains(parentId) && ids.Contains(child1Id) && ids.Contains(child2Id))); + } + + [Fact] + public async Task Should_Return_Zeros_When_No_Payment_Data_Exists() + { + // Arrange + var appId = Guid.NewGuid(); + var repo = Substitute.For(); + repo.GetPaymentSummariesByCorrelationIdsAsync(Arg.Any>()) + .Returns(new List()); + + var manager = CreateManager(repo); + + // Act + var result = await manager.GetApplicationPaymentSummaryAsync(appId, []); + + // Assert + result.ShouldNotBeNull(); + result.ApplicationId.ShouldBe(appId); + result.TotalPaid.ShouldBe(0m); + result.TotalPending.ShouldBe(0m); + } + + [Fact] + public async Task Should_Return_Zeros_When_Children_Have_No_Payments() + { + // Arrange + var parentId = Guid.NewGuid(); + var childId = Guid.NewGuid(); + + var repo = Substitute.For(); + repo.GetPaymentSummariesByCorrelationIdsAsync(Arg.Any>()) + .Returns(new List + { + new() { ApplicationId = parentId, TotalPaid = 500m, TotalPending = 0m } + // childId has no payments - not returned by repository + }); + + var manager = CreateManager(repo); + + // Act + var result = await manager.GetApplicationPaymentSummaryAsync(parentId, [childId]); + + // Assert + result.TotalPaid.ShouldBe(500m); // Only parent amount + result.TotalPending.ShouldBe(0m); + } + + [Fact] + public async Task Should_Handle_Single_Child_Application() + { + // Arrange + var parentId = Guid.NewGuid(); + var childId = Guid.NewGuid(); + + var repo = Substitute.For(); + repo.GetPaymentSummariesByCorrelationIdsAsync(Arg.Any>()) + .Returns(new List + { + new() { ApplicationId = parentId, TotalPaid = 1000m, TotalPending = 0m }, + new() { ApplicationId = childId, TotalPaid = 500m, TotalPending = 300m } + }); + + var manager = CreateManager(repo); + + // Act + var result = await manager.GetApplicationPaymentSummaryAsync(parentId, [childId]); + + // Assert + result.ApplicationId.ShouldBe(parentId); + result.TotalPaid.ShouldBe(1500m); // 1000 + 500 + result.TotalPending.ShouldBe(300m); // 0 + 300 + } + + #endregion + + #region GetApplicationPaymentSummariesAsync (Batch) + + [Fact] + public async Task Should_Return_Batch_Summaries_For_Multiple_Applications_Without_Children() + { + // Arrange + var app1Id = Guid.NewGuid(); + var app2Id = Guid.NewGuid(); + var app3Id = Guid.NewGuid(); + + var repo = Substitute.For(); + repo.GetPaymentSummariesByCorrelationIdsAsync(Arg.Any>()) + .Returns(new List + { + new() { ApplicationId = app1Id, TotalPaid = 1000m, TotalPending = 200m }, + new() { ApplicationId = app2Id, TotalPaid = 500m, TotalPending = 100m }, + new() { ApplicationId = app3Id, TotalPaid = 0m, TotalPending = 3000m } + }); + + var manager = CreateManager(repo); + + // Act + var result = await manager.GetApplicationPaymentSummariesAsync( + [app1Id, app2Id, app3Id], + new Dictionary>()); + + // Assert + result.Count.ShouldBe(3); + result[app1Id].TotalPaid.ShouldBe(1000m); + result[app1Id].TotalPending.ShouldBe(200m); + result[app2Id].TotalPaid.ShouldBe(500m); + result[app2Id].TotalPending.ShouldBe(100m); + result[app3Id].TotalPaid.ShouldBe(0m); + result[app3Id].TotalPending.ShouldBe(3000m); + } + + [Fact] + public async Task Should_Aggregate_Child_Amounts_In_Batch_Summaries() + { + // Arrange + var parentAId = Guid.NewGuid(); + var parentBId = Guid.NewGuid(); + var childA1Id = Guid.NewGuid(); + var childA2Id = Guid.NewGuid(); + var childB1Id = Guid.NewGuid(); + + var childMap = new Dictionary> + { + { parentAId, [childA1Id, childA2Id] }, + { parentBId, [childB1Id] } + }; + + var repo = Substitute.For(); + repo.GetPaymentSummariesByCorrelationIdsAsync(Arg.Any>()) + .Returns(new List + { + new() { ApplicationId = parentAId, TotalPaid = 1000m, TotalPending = 100m }, + new() { ApplicationId = childA1Id, TotalPaid = 200m, TotalPending = 50m }, + new() { ApplicationId = childA2Id, TotalPaid = 300m, TotalPending = 75m }, + new() { ApplicationId = parentBId, TotalPaid = 500m, TotalPending = 0m }, + new() { ApplicationId = childB1Id, TotalPaid = 400m, TotalPending = 200m } + }); + + var manager = CreateManager(repo); + + // Act + var result = await manager.GetApplicationPaymentSummariesAsync( + [parentAId, parentBId], childMap); + + // Assert + result.Count.ShouldBe(2); + + // Parent A: 1000+200+300 paid, 100+50+75 pending + result[parentAId].TotalPaid.ShouldBe(1500m); + result[parentAId].TotalPending.ShouldBe(225m); + result[parentAId].ApplicationId.ShouldBe(parentAId); + + // Parent B: 500+400 paid, 0+200 pending + result[parentBId].TotalPaid.ShouldBe(900m); + result[parentBId].TotalPending.ShouldBe(200m); + result[parentBId].ApplicationId.ShouldBe(parentBId); + } + + [Fact] + public async Task Should_Handle_Application_With_No_Matching_Summary_In_Batch() + { + // Arrange + var app1Id = Guid.NewGuid(); + var app2Id = Guid.NewGuid(); + + var repo = Substitute.For(); + repo.GetPaymentSummariesByCorrelationIdsAsync(Arg.Any>()) + .Returns(new List + { + // Only app1 has payment data, app2 doesn't + new() { ApplicationId = app1Id, TotalPaid = 1000m, TotalPending = 500m } + }); + + var manager = CreateManager(repo); + + // Act + var result = await manager.GetApplicationPaymentSummariesAsync( + [app1Id, app2Id], + new Dictionary>()); + + // Assert + result.Count.ShouldBe(2); + result[app1Id].TotalPaid.ShouldBe(1000m); + result[app1Id].TotalPending.ShouldBe(500m); + + // app2 gets zero amounts since no data was returned + result[app2Id].TotalPaid.ShouldBe(0m); + result[app2Id].TotalPending.ShouldBe(0m); + result[app2Id].ApplicationId.ShouldBe(app2Id); + } + + [Fact] + public async Task Should_Deduplicate_CorrelationIds_In_Batch_Repository_Call() + { + // Arrange - A child is shared between two parents (edge case) + var parentAId = Guid.NewGuid(); + var parentBId = Guid.NewGuid(); + var sharedChildId = Guid.NewGuid(); + + var childMap = new Dictionary> + { + { parentAId, [sharedChildId] }, + { parentBId, [sharedChildId] } + }; + + var repo = Substitute.For(); + repo.GetPaymentSummariesByCorrelationIdsAsync(Arg.Any>()) + .Returns(new List + { + new() { ApplicationId = parentAId, TotalPaid = 100m, TotalPending = 0m }, + new() { ApplicationId = parentBId, TotalPaid = 200m, TotalPending = 0m }, + new() { ApplicationId = sharedChildId, TotalPaid = 50m, TotalPending = 25m } + }); + + var manager = CreateManager(repo); + + // Act + var result = await manager.GetApplicationPaymentSummariesAsync( + [parentAId, parentBId], childMap); + + // Assert + // Verify repository was called with deduplicated IDs (3 unique, not 4) + await repo.Received(1).GetPaymentSummariesByCorrelationIdsAsync( + Arg.Is>(ids => ids.Distinct().Count() == 3)); + + // Both parents should include the shared child's amounts + result[parentAId].TotalPaid.ShouldBe(150m); // 100 + 50 + result[parentAId].TotalPending.ShouldBe(25m); // 0 + 25 + result[parentBId].TotalPaid.ShouldBe(250m); // 200 + 50 + result[parentBId].TotalPending.ShouldBe(25m); // 0 + 25 + } + + [Fact] + public async Task Should_Make_Single_Repository_Call_For_Batch() + { + // Arrange + var app1Id = Guid.NewGuid(); + var app2Id = Guid.NewGuid(); + var child1Id = Guid.NewGuid(); + + var childMap = new Dictionary> + { + { app1Id, [child1Id] } + }; + + var repo = Substitute.For(); + repo.GetPaymentSummariesByCorrelationIdsAsync(Arg.Any>()) + .Returns(new List()); + + var manager = CreateManager(repo); + + // Act + await manager.GetApplicationPaymentSummariesAsync([app1Id, app2Id], childMap); + + // Assert - should only call repository once (batch optimization) + await repo.Received(1).GetPaymentSummariesByCorrelationIdsAsync(Arg.Any>()); + } + + [Fact] + public async Task Should_Return_Empty_Dictionary_For_Empty_Application_List() + { + // Arrange + var repo = Substitute.For(); + repo.GetPaymentSummariesByCorrelationIdsAsync(Arg.Any>()) + .Returns(new List()); + + var manager = CreateManager(repo); + + // Act + var result = await manager.GetApplicationPaymentSummariesAsync( + [], new Dictionary>()); + + // Assert + result.ShouldNotBeNull(); + result.ShouldBeEmpty(); + } + + [Fact] + public async Task Should_Handle_Parent_Without_Children_In_Mixed_Batch() + { + // Arrange - app1 has children, app2 does not + var app1Id = Guid.NewGuid(); + var app2Id = Guid.NewGuid(); + var childId = Guid.NewGuid(); + + var childMap = new Dictionary> + { + { app1Id, [childId] } + // app2 has no entry in childMap + }; + + var repo = Substitute.For(); + repo.GetPaymentSummariesByCorrelationIdsAsync(Arg.Any>()) + .Returns(new List + { + new() { ApplicationId = app1Id, TotalPaid = 1000m, TotalPending = 0m }, + new() { ApplicationId = childId, TotalPaid = 500m, TotalPending = 100m }, + new() { ApplicationId = app2Id, TotalPaid = 300m, TotalPending = 50m } + }); + + var manager = CreateManager(repo); + + // Act + var result = await manager.GetApplicationPaymentSummariesAsync( + [app1Id, app2Id], childMap); + + // Assert + result[app1Id].TotalPaid.ShouldBe(1500m); // 1000 + 500 + result[app1Id].TotalPending.ShouldBe(100m); // 0 + 100 + + result[app2Id].TotalPaid.ShouldBe(300m); // Only own amount + result[app2Id].TotalPending.ShouldBe(50m); // Only own amount + } + + #endregion + + #region Helpers + + private static PaymentRequestQueryManager CreateManager(IPaymentRequestRepository repo) + { + return new PaymentRequestQueryManager( + repo, + Substitute.For(), + Substitute.For(), + null!, // CasPaymentRequestCoordinator - not used by summary methods + null! // IObjectMapper - not used by summary methods + ); + } + + #endregion +} diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/ViewComponents/PaymentInfoViewComponentTests.cs b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/ViewComponents/PaymentInfoViewComponentTests.cs index 58d8df519a..18fba64ff6 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/ViewComponents/PaymentInfoViewComponentTests.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/ViewComponents/PaymentInfoViewComponentTests.cs @@ -4,12 +4,8 @@ using NSubstitute; using Shouldly; using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; -using Unity.GrantManager.Applications; using Unity.GrantManager.GrantApplications; -using Unity.Payments.Enums; using Unity.Payments.PaymentRequests; using Unity.Payments.Web.Views.Shared.Components.PaymentInfo; using Volo.Abp.DependencyInjection; @@ -28,7 +24,7 @@ public PaymentInfoViewComponentTests() } [Fact] - public async Task PaymentInfo_Should_Calculate_TotalPaid_And_TotalPending_For_Current_Application() + public async Task PaymentInfo_Should_Display_TotalPaid_And_TotalPending_From_Summary() { // Arrange var applicationId = Guid.NewGuid(); @@ -41,29 +37,25 @@ public async Task PaymentInfo_Should_Calculate_TotalPaid_And_TotalPending_For_Cu RequestedAmount = 10000, RecommendedAmount = 8000, ApprovedAmount = 7000, - Applicant = new GrantManager.GrantApplications.GrantApplicationApplicantDto { Id = applicantId } + Applicant = new GrantApplicationApplicantDto { Id = applicantId } }; - var paymentRequests = new List + var summary = new ApplicationPaymentSummaryDto { - new() { Amount = 1000, Status = PaymentRequestStatus.Submitted, PaymentStatus = "Fully Paid" }, // Paid - new() { Amount = 500, Status = PaymentRequestStatus.Submitted, PaymentStatus = "Fully Paid" }, // Paid - new() { Amount = 2000, Status = PaymentRequestStatus.L1Pending }, // Pending - new() { Amount = 1500, Status = PaymentRequestStatus.L1Declined }, // Not counted - new() { Amount = 800, Status = PaymentRequestStatus.Paid }, // Not counted (no PaymentStatus = "Fully Paid") + ApplicationId = applicationId, + TotalPaid = 1500m, + TotalPending = 2000m }; var appService = Substitute.For(); var paymentRequestService = Substitute.For(); var featureChecker = Substitute.For(); - var applicationLinksService = Substitute.For(); appService.GetAsync(applicationId).Returns(applicationDto); - paymentRequestService.GetListByApplicationIdAsync(applicationId).Returns(paymentRequests); + paymentRequestService.GetApplicationPaymentSummaryAsync(applicationId).Returns(summary); featureChecker.IsEnabledAsync("Unity.Payments").Returns(true); - applicationLinksService.GetListByApplicationAsync(applicationId).Returns([]); - var viewComponent = CreateViewComponent(appService, paymentRequestService, featureChecker, applicationLinksService); + var viewComponent = CreateViewComponent(appService, paymentRequestService, featureChecker); // Act var result = await viewComponent.InvokeAsync(applicationId, applicationFormVersionId) as ViewViewComponentResult; @@ -71,338 +63,86 @@ public async Task PaymentInfo_Should_Calculate_TotalPaid_And_TotalPending_For_Cu // Assert model.ShouldNotBeNull(); - model.TotalPaid.ShouldBe(1500m); // 1000 + 500 (PaymentStatus = "Fully Paid") - model.TotalPendingAmounts.ShouldBe(2000m); // 2000 (L1Pending only) + model.TotalPaid.ShouldBe(1500m); + model.TotalPendingAmounts.ShouldBe(2000m); model.RemainingAmount.ShouldBe(5500m); // 7000 - 1500 } [Fact] - public async Task PaymentInfo_Should_Aggregate_TotalPaid_From_Child_Applications() + public async Task PaymentInfo_Should_Calculate_RemainingAmount_From_ApprovedAmount_Minus_TotalPaid() { // Arrange - var parentAppId = Guid.NewGuid(); - var childApp1Id = Guid.NewGuid(); - var childApp2Id = Guid.NewGuid(); - var applicationFormVersionId = Guid.NewGuid(); - var applicantId = Guid.NewGuid(); - - var parentApplicationDto = new GrantApplicationDto - { - Id = parentAppId, - ApprovedAmount = 10000, - Applicant = new GrantManager.GrantApplications.GrantApplicationApplicantDto { Id = applicantId } - }; - - var parentPayments = new List - { - new() { Amount = 1000, Status = PaymentRequestStatus.Submitted, PaymentStatus = "Fully Paid" } - }; - - var childApp1Payments = new List - { - new() { CorrelationId = childApp1Id, Amount = 500, Status = PaymentRequestStatus.Submitted, PaymentStatus = "Fully Paid" } - }; - - var childApp2Payments = new List - { - new() { CorrelationId = childApp2Id, Amount = 800, Status = PaymentRequestStatus.Submitted, PaymentStatus = "Fully Paid" } - }; - - var childLinks = new List - { - new() { ApplicationId = childApp1Id, LinkType = ApplicationLinkType.Child }, - new() { ApplicationId = childApp2Id, LinkType = ApplicationLinkType.Child } - }; - - var allChildPayments = childApp1Payments.Concat(childApp2Payments).ToList(); - - var appService = Substitute.For(); - var paymentRequestService = Substitute.For(); - var featureChecker = Substitute.For(); - var applicationLinksService = Substitute.For(); - - appService.GetAsync(parentAppId).Returns(parentApplicationDto); - paymentRequestService.GetListByApplicationIdAsync(parentAppId).Returns(parentPayments); - applicationLinksService.GetListByApplicationAsync(parentAppId).Returns(childLinks); - paymentRequestService.GetListByApplicationIdsAsync(Arg.Any>()).Returns(allChildPayments); - featureChecker.IsEnabledAsync("Unity.Payments").Returns(true); - - var viewComponent = CreateViewComponent(appService, paymentRequestService, featureChecker, applicationLinksService); - - // Act - var result = await viewComponent.InvokeAsync(parentAppId, applicationFormVersionId) as ViewViewComponentResult; - var model = result!.ViewData!.Model as PaymentInfoViewModel; - - // Assert - model.ShouldNotBeNull(); - model.TotalPaid.ShouldBe(2300m); // 1000 (parent) + 500 (child1) + 800 (child2) - } - - [Fact] - public async Task PaymentInfo_Should_Aggregate_TotalPendingAmounts_From_Child_Applications() - { - // Arrange - var parentAppId = Guid.NewGuid(); - var childApp1Id = Guid.NewGuid(); - var childApp2Id = Guid.NewGuid(); - var applicationFormVersionId = Guid.NewGuid(); - var applicantId = Guid.NewGuid(); - - var parentApplicationDto = new GrantApplicationDto - { - Id = parentAppId, - ApprovedAmount = 10000, - Applicant = new GrantManager.GrantApplications.GrantApplicationApplicantDto { Id = applicantId } - }; - - var parentPayments = new List - { - new() { Amount = 2000, Status = PaymentRequestStatus.L1Pending } - }; - - var childApp1Payments = new List - { - new() { CorrelationId = childApp1Id, Amount = 1000, Status = PaymentRequestStatus.L1Pending } - }; - - var childApp2Payments = new List - { - new() { CorrelationId = childApp2Id, Amount = 500, Status = PaymentRequestStatus.L1Pending } - }; - - var childLinks = new List - { - new() { ApplicationId = childApp1Id, LinkType = ApplicationLinkType.Child }, - new() { ApplicationId = childApp2Id, LinkType = ApplicationLinkType.Child } - }; - - var allChildPayments = childApp1Payments.Concat(childApp2Payments).ToList(); - - var appService = Substitute.For(); - var paymentRequestService = Substitute.For(); - var featureChecker = Substitute.For(); - var applicationLinksService = Substitute.For(); - - appService.GetAsync(parentAppId).Returns(parentApplicationDto); - paymentRequestService.GetListByApplicationIdAsync(parentAppId).Returns(parentPayments); - applicationLinksService.GetListByApplicationAsync(parentAppId).Returns(childLinks); - paymentRequestService.GetListByApplicationIdsAsync(Arg.Any>()).Returns(allChildPayments); - featureChecker.IsEnabledAsync("Unity.Payments").Returns(true); - - var viewComponent = CreateViewComponent(appService, paymentRequestService, featureChecker, applicationLinksService); - - // Act - var result = await viewComponent.InvokeAsync(parentAppId, applicationFormVersionId) as ViewViewComponentResult; - var model = result!.ViewData!.Model as PaymentInfoViewModel; - - // Assert - model.ShouldNotBeNull(); - model.TotalPendingAmounts.ShouldBe(3500m); // 2000 (parent) + 1000 (child1) + 500 (child2) - } - - [Fact] - public async Task PaymentInfo_Should_Filter_Only_Child_LinkType() - { - // Arrange - var parentAppId = Guid.NewGuid(); - var childAppId = Guid.NewGuid(); - var relatedAppId = Guid.NewGuid(); - var parentLinkAppId = Guid.NewGuid(); - var applicationFormVersionId = Guid.NewGuid(); - var applicantId = Guid.NewGuid(); - - var parentApplicationDto = new GrantApplicationDto - { - Id = parentAppId, - ApprovedAmount = 10000, - Applicant = new GrantManager.GrantApplications.GrantApplicationApplicantDto { Id = applicantId } - }; - - var parentPayments = new List - { - new() { Amount = 1000, Status = PaymentRequestStatus.Submitted, PaymentStatus = "Fully Paid" } - }; - - var childPayments = new List - { - new() { CorrelationId = childAppId, Amount = 500, Status = PaymentRequestStatus.Submitted, PaymentStatus = "Fully Paid" } - }; - - var links = new List - { - new() { ApplicationId = childAppId, LinkType = ApplicationLinkType.Child }, // Should be included - new() { ApplicationId = relatedAppId, LinkType = ApplicationLinkType.Related }, // Should be excluded - new() { ApplicationId = parentLinkAppId, LinkType = ApplicationLinkType.Parent } // Should be excluded - }; - - var appService = Substitute.For(); - var paymentRequestService = Substitute.For(); - var featureChecker = Substitute.For(); - var applicationLinksService = Substitute.For(); - - appService.GetAsync(parentAppId).Returns(parentApplicationDto); - paymentRequestService.GetListByApplicationIdAsync(parentAppId).Returns(parentPayments); - applicationLinksService.GetListByApplicationAsync(parentAppId).Returns(links); - paymentRequestService.GetListByApplicationIdsAsync(Arg.Any>()).Returns(childPayments); - featureChecker.IsEnabledAsync("Unity.Payments").Returns(true); - - var viewComponent = CreateViewComponent(appService, paymentRequestService, featureChecker, applicationLinksService); - - // Act - var result = await viewComponent.InvokeAsync(parentAppId, applicationFormVersionId) as ViewViewComponentResult; - var model = result!.ViewData!.Model as PaymentInfoViewModel; - - // Assert - model.ShouldNotBeNull(); - model.TotalPaid.ShouldBe(1500m); // 1000 (parent) + 500 (only child, not related or parent links) - } - - [Fact] - public async Task PaymentInfo_Should_Exclude_SelfReferences() - { - // Arrange - var appId = Guid.NewGuid(); - var childAppId = Guid.NewGuid(); + var applicationId = Guid.NewGuid(); var applicationFormVersionId = Guid.NewGuid(); var applicantId = Guid.NewGuid(); var applicationDto = new GrantApplicationDto { - Id = appId, + Id = applicationId, ApprovedAmount = 10000, - Applicant = new GrantManager.GrantApplications.GrantApplicationApplicantDto { Id = applicantId } - }; - - var appPayments = new List - { - new() { Amount = 1000, Status = PaymentRequestStatus.Submitted, PaymentStatus = "Fully Paid" } + Applicant = new GrantApplicationApplicantDto { Id = applicantId } }; - var childPayments = new List + var summary = new ApplicationPaymentSummaryDto { - new() { CorrelationId = childAppId, Amount = 500, Status = PaymentRequestStatus.Submitted, PaymentStatus = "Fully Paid" } - }; - - var links = new List - { - new() { ApplicationId = appId, LinkType = ApplicationLinkType.Child }, // Self-reference - should be excluded - new() { ApplicationId = childAppId, LinkType = ApplicationLinkType.Child } // Real child - should be included + ApplicationId = applicationId, + TotalPaid = 3500m, + TotalPending = 1000m }; var appService = Substitute.For(); var paymentRequestService = Substitute.For(); var featureChecker = Substitute.For(); - var applicationLinksService = Substitute.For(); - - appService.GetAsync(appId).Returns(applicationDto); - paymentRequestService.GetListByApplicationIdAsync(appId).Returns(appPayments); - applicationLinksService.GetListByApplicationAsync(appId).Returns(links); - paymentRequestService.GetListByApplicationIdsAsync(Arg.Any>()).Returns(childPayments); - featureChecker.IsEnabledAsync("Unity.Payments").Returns(true); - - var viewComponent = CreateViewComponent(appService, paymentRequestService, featureChecker, applicationLinksService); - - // Act - var result = await viewComponent.InvokeAsync(appId, applicationFormVersionId) as ViewViewComponentResult; - var model = result!.ViewData!.Model as PaymentInfoViewModel; - - // Assert - model.ShouldNotBeNull(); - model.TotalPaid.ShouldBe(1500m); // 1000 (parent) + 500 (child only, self-reference excluded) - // Verify that GetListByApplicationIdsAsync was called with only the child app, not the self-reference - await paymentRequestService.Received(1).GetListByApplicationIdsAsync( - Arg.Is>(list => list.Count == 1 && list.Contains(childAppId) && !list.Contains(appId)) - ); - } - - [Fact] - public async Task PaymentInfo_Should_Handle_NoChildApplications() - { - // Arrange - var appId = Guid.NewGuid(); - var applicationFormVersionId = Guid.NewGuid(); - var applicantId = Guid.NewGuid(); - - var applicationDto = new GrantApplicationDto - { - Id = appId, - RequestedAmount = 10000, - RecommendedAmount = 8000, - ApprovedAmount = 7000, - Applicant = new GrantManager.GrantApplications.GrantApplicationApplicantDto { Id = applicantId } - }; - - var parentPayments = new List - { - new() { Amount = 1000, Status = PaymentRequestStatus.Submitted, PaymentStatus = "Fully Paid" }, - new() { Amount = 2000, Status = PaymentRequestStatus.L1Pending } - }; - - var appService = Substitute.For(); - var paymentRequestService = Substitute.For(); - var featureChecker = Substitute.For(); - var applicationLinksService = Substitute.For(); - - appService.GetAsync(appId).Returns(applicationDto); - paymentRequestService.GetListByApplicationIdAsync(appId).Returns(parentPayments); - applicationLinksService.GetListByApplicationAsync(appId).Returns(new List()); + appService.GetAsync(applicationId).Returns(applicationDto); + paymentRequestService.GetApplicationPaymentSummaryAsync(applicationId).Returns(summary); featureChecker.IsEnabledAsync("Unity.Payments").Returns(true); - var viewComponent = CreateViewComponent(appService, paymentRequestService, featureChecker, applicationLinksService); + var viewComponent = CreateViewComponent(appService, paymentRequestService, featureChecker); // Act - var result = await viewComponent.InvokeAsync(appId, applicationFormVersionId) as ViewViewComponentResult; + var result = await viewComponent.InvokeAsync(applicationId, applicationFormVersionId) as ViewViewComponentResult; var model = result!.ViewData!.Model as PaymentInfoViewModel; // Assert model.ShouldNotBeNull(); - model.TotalPaid.ShouldBe(1000m); // Only parent payments (PaymentStatus = "Fully Paid") - model.TotalPendingAmounts.ShouldBe(2000m); // 2000 (L1Pending only) - model.RemainingAmount.ShouldBe(6000m); // 7000 - 1000 - - // Verify that GetListByApplicationIdsAsync was NOT called since there are no children - await paymentRequestService.DidNotReceive().GetListByApplicationIdsAsync(Arg.Any>()); + model.RemainingAmount.ShouldBe(6500m); // 10000 - 3500 } [Fact] - public async Task PaymentInfo_Should_Handle_ChildApplications_WithNoPaymentRequests() + public async Task PaymentInfo_Should_Include_Child_Application_Amounts_Via_Summary() { + // The ViewComponent now delegates child aggregation to the service layer. + // This test verifies it correctly uses the pre-aggregated summary. // Arrange var parentAppId = Guid.NewGuid(); - var childAppId = Guid.NewGuid(); var applicationFormVersionId = Guid.NewGuid(); var applicantId = Guid.NewGuid(); - var parentApplicationDto = new GrantApplicationDto + var applicationDto = new GrantApplicationDto { Id = parentAppId, ApprovedAmount = 10000, - Applicant = new GrantManager.GrantApplications.GrantApplicationApplicantDto { Id = applicantId } + Applicant = new GrantApplicationApplicantDto { Id = applicantId } }; - var parentPayments = new List + // Summary includes parent + child amounts (pre-aggregated by service) + var summary = new ApplicationPaymentSummaryDto { - new() { Amount = 1000, Status = PaymentRequestStatus.Submitted, PaymentStatus = "Fully Paid" } - }; - - var childLinks = new List - { - new() { ApplicationId = childAppId, LinkType = ApplicationLinkType.Child } + ApplicationId = parentAppId, + TotalPaid = 2300m, // e.g., 1000 (parent) + 500 (child1) + 800 (child2) + TotalPending = 3500m // e.g., 2000 (parent) + 1000 (child1) + 500 (child2) }; var appService = Substitute.For(); var paymentRequestService = Substitute.For(); var featureChecker = Substitute.For(); - var applicationLinksService = Substitute.For(); - appService.GetAsync(parentAppId).Returns(parentApplicationDto); - paymentRequestService.GetListByApplicationIdAsync(parentAppId).Returns(parentPayments); - applicationLinksService.GetListByApplicationAsync(parentAppId).Returns(childLinks); - paymentRequestService.GetListByApplicationIdsAsync(Arg.Any>()).Returns([]); // Empty list + appService.GetAsync(parentAppId).Returns(applicationDto); + paymentRequestService.GetApplicationPaymentSummaryAsync(parentAppId).Returns(summary); featureChecker.IsEnabledAsync("Unity.Payments").Returns(true); - var viewComponent = CreateViewComponent(appService, paymentRequestService, featureChecker, applicationLinksService); + var viewComponent = CreateViewComponent(appService, paymentRequestService, featureChecker); // Act var result = await viewComponent.InvokeAsync(parentAppId, applicationFormVersionId) as ViewViewComponentResult; @@ -410,12 +150,13 @@ public async Task PaymentInfo_Should_Handle_ChildApplications_WithNoPaymentReque // Assert model.ShouldNotBeNull(); - model.TotalPaid.ShouldBe(1000m); // Only parent payments (child has none) - model.TotalPendingAmounts.ShouldBe(0m); // No pending payments + model.TotalPaid.ShouldBe(2300m); + model.TotalPendingAmounts.ShouldBe(3500m); + model.RemainingAmount.ShouldBe(7700m); // 10000 - 2300 } [Fact] - public async Task PaymentInfo_Should_Exclude_Declined_Statuses_From_Pending() + public async Task PaymentInfo_Should_Handle_Zero_Payments() { // Arrange var appId = Guid.NewGuid(); @@ -425,30 +166,26 @@ public async Task PaymentInfo_Should_Exclude_Declined_Statuses_From_Pending() var applicationDto = new GrantApplicationDto { Id = appId, - ApprovedAmount = 10000, - Applicant = new GrantManager.GrantApplications.GrantApplicationApplicantDto { Id = applicantId } + ApprovedAmount = 5000, + Applicant = new GrantApplicationApplicantDto { Id = applicantId } }; - var paymentRequests = new List + var summary = new ApplicationPaymentSummaryDto { - new() { Amount = 1000, Status = PaymentRequestStatus.L1Pending }, // Pending - new() { Amount = 500, Status = PaymentRequestStatus.Paid, PaymentStatus = "Fully Paid" }, // Paid - new() { Amount = 2000, Status = PaymentRequestStatus.L1Declined }, // Not pending - new() { Amount = 1500, Status = PaymentRequestStatus.L2Declined }, // Not pending - new() { Amount = 1200, Status = PaymentRequestStatus.L3Declined } // Not pending + ApplicationId = appId, + TotalPaid = 0m, + TotalPending = 0m }; var appService = Substitute.For(); var paymentRequestService = Substitute.For(); var featureChecker = Substitute.For(); - var applicationLinksService = Substitute.For(); appService.GetAsync(appId).Returns(applicationDto); - paymentRequestService.GetListByApplicationIdAsync(appId).Returns(paymentRequests); - applicationLinksService.GetListByApplicationAsync(appId).Returns([]); + paymentRequestService.GetApplicationPaymentSummaryAsync(appId).Returns(summary); featureChecker.IsEnabledAsync("Unity.Payments").Returns(true); - var viewComponent = CreateViewComponent(appService, paymentRequestService, featureChecker, applicationLinksService); + var viewComponent = CreateViewComponent(appService, paymentRequestService, featureChecker); // Act var result = await viewComponent.InvokeAsync(appId, applicationFormVersionId) as ViewViewComponentResult; @@ -456,12 +193,13 @@ public async Task PaymentInfo_Should_Exclude_Declined_Statuses_From_Pending() // Assert model.ShouldNotBeNull(); - model.TotalPendingAmounts.ShouldBe(1000m); // Only L1Pending status - model.TotalPaid.ShouldBe(500m); // PaymentStatus = "Fully Paid" + model.TotalPaid.ShouldBe(0m); + model.TotalPendingAmounts.ShouldBe(0m); + model.RemainingAmount.ShouldBe(5000m); // 5000 - 0 } [Fact] - public async Task PaymentInfo_Should_Handle_PaymentStatus_FullyPaid_CaseInsensitive_WithSpaces() + public async Task PaymentInfo_Should_Map_RequestedAmount_And_RecommendedAmount() { // Arrange var appId = Guid.NewGuid(); @@ -471,30 +209,28 @@ public async Task PaymentInfo_Should_Handle_PaymentStatus_FullyPaid_CaseInsensit var applicationDto = new GrantApplicationDto { Id = appId, + RequestedAmount = 15000, + RecommendedAmount = 12000, ApprovedAmount = 10000, - Applicant = new GrantManager.GrantApplications.GrantApplicationApplicantDto { Id = applicantId } + Applicant = new GrantApplicationApplicantDto { Id = applicantId } }; - var paymentRequests = new List + var summary = new ApplicationPaymentSummaryDto { - new() { Amount = 1000, Status = PaymentRequestStatus.Submitted, PaymentStatus = "Fully Paid" }, // Exact match - new() { Amount = 500, Status = PaymentRequestStatus.Submitted, PaymentStatus = "FULLY PAID" }, // Upper case - new() { Amount = 800, Status = PaymentRequestStatus.Submitted, PaymentStatus = "fully paid" }, // Lower case - new() { Amount = 300, Status = PaymentRequestStatus.Submitted, PaymentStatus = " Fully Paid " }, // With spaces - new() { Amount = 200, Status = PaymentRequestStatus.Submitted, PaymentStatus = "Paid" }, // Not fully paid + ApplicationId = appId, + TotalPaid = 0m, + TotalPending = 0m }; var appService = Substitute.For(); var paymentRequestService = Substitute.For(); var featureChecker = Substitute.For(); - var applicationLinksService = Substitute.For(); appService.GetAsync(appId).Returns(applicationDto); - paymentRequestService.GetListByApplicationIdAsync(appId).Returns(paymentRequests); - applicationLinksService.GetListByApplicationAsync(appId).Returns([]); + paymentRequestService.GetApplicationPaymentSummaryAsync(appId).Returns(summary); featureChecker.IsEnabledAsync("Unity.Payments").Returns(true); - var viewComponent = CreateViewComponent(appService, paymentRequestService, featureChecker, applicationLinksService); + var viewComponent = CreateViewComponent(appService, paymentRequestService, featureChecker); // Act var result = await viewComponent.InvokeAsync(appId, applicationFormVersionId) as ViewViewComponentResult; @@ -502,44 +238,28 @@ public async Task PaymentInfo_Should_Handle_PaymentStatus_FullyPaid_CaseInsensit // Assert model.ShouldNotBeNull(); - model.TotalPaid.ShouldBe(2600m); // 1000 + 500 + 800 + 300 (all "Fully Paid" variations, excluding 200) + model.RequestedAmount.ShouldBe(15000); + model.RecommendedAmount.ShouldBe(12000); + model.ApprovedAmount.ShouldBe(10000); + model.ApplicationId.ShouldBe(appId); + model.ApplicationFormVersionId.ShouldBe(applicationFormVersionId); + model.ApplicantId.ShouldBe(applicantId); } [Fact] - public async Task PaymentInfo_Should_Include_Submitted_WithNullPaymentStatus_InPending() + public async Task PaymentInfo_Should_Return_Empty_View_When_Feature_Disabled() { // Arrange var appId = Guid.NewGuid(); var applicationFormVersionId = Guid.NewGuid(); - var applicantId = Guid.NewGuid(); - - var applicationDto = new GrantApplicationDto - { - Id = appId, - ApprovedAmount = 10000, - Applicant = new GrantManager.GrantApplications.GrantApplicationApplicantDto { Id = applicantId } - }; - - var paymentRequests = new List - { - new() { Amount = 1000, Status = PaymentRequestStatus.Submitted, PaymentStatus = null, InvoiceStatus = null }, // Pending - new() { Amount = 500, Status = PaymentRequestStatus.Submitted, PaymentStatus = null, InvoiceStatus = "SentToCas" }, // Pending - new() { Amount = 800, Status = PaymentRequestStatus.Submitted, PaymentStatus = null, InvoiceStatus = "Validated" }, // Pending - new() { Amount = 300, Status = PaymentRequestStatus.Submitted, PaymentStatus = null, InvoiceStatus = " " }, // Pending (whitespace) - new() { Amount = 200, Status = PaymentRequestStatus.Submitted, PaymentStatus = "Fully Paid", InvoiceStatus = "Validated" }, // Not pending (paid) - }; var appService = Substitute.For(); var paymentRequestService = Substitute.For(); var featureChecker = Substitute.For(); - var applicationLinksService = Substitute.For(); - appService.GetAsync(appId).Returns(applicationDto); - paymentRequestService.GetListByApplicationIdAsync(appId).Returns(paymentRequests); - applicationLinksService.GetListByApplicationAsync(appId).Returns([]); - featureChecker.IsEnabledAsync("Unity.Payments").Returns(true); + featureChecker.IsEnabledAsync("Unity.Payments").Returns(false); - var viewComponent = CreateViewComponent(appService, paymentRequestService, featureChecker, applicationLinksService); + var viewComponent = CreateViewComponent(appService, paymentRequestService, featureChecker); // Act var result = await viewComponent.InvokeAsync(appId, applicationFormVersionId) as ViewViewComponentResult; @@ -547,58 +267,17 @@ public async Task PaymentInfo_Should_Include_Submitted_WithNullPaymentStatus_InP // Assert model.ShouldNotBeNull(); - model.TotalPendingAmounts.ShouldBe(2600m); // 1000 + 500 + 800 + 300 (all with null PaymentStatus) - model.TotalPaid.ShouldBe(200m); // Only the one with PaymentStatus = "Fully Paid" - } - - [Fact] - public async Task PaymentInfo_Should_Exclude_Submitted_WithError_InvoiceStatus_FromPending() - { - // Arrange - var appId = Guid.NewGuid(); - var applicationFormVersionId = Guid.NewGuid(); - var applicantId = Guid.NewGuid(); + model.TotalPaid.ShouldBeNull(); + model.TotalPendingAmounts.ShouldBeNull(); + model.RemainingAmount.ShouldBeNull(); - var applicationDto = new GrantApplicationDto - { - Id = appId, - ApprovedAmount = 10000, - Applicant = new GrantManager.GrantApplications.GrantApplicationApplicantDto { Id = applicantId } - }; - - var paymentRequests = new List - { - new() { Amount = 1000, Status = PaymentRequestStatus.Submitted, PaymentStatus = null, InvoiceStatus = "ErrorFromCas" }, // Not pending (error) - new() { Amount = 500, Status = PaymentRequestStatus.Submitted, PaymentStatus = null, InvoiceStatus = " Error " }, // Not pending (error with spaces) - new() { Amount = 800, Status = PaymentRequestStatus.Submitted, PaymentStatus = null, InvoiceStatus = "ServiceUnavailable" }, // Pending (will be retried) - new() { Amount = 300, Status = PaymentRequestStatus.Submitted, PaymentStatus = null, InvoiceStatus = "SentToCas" }, // Pending (no error) - new() { Amount = 200, Status = PaymentRequestStatus.L1Pending }, // Pending (L1) - }; - - var appService = Substitute.For(); - var paymentRequestService = Substitute.For(); - var featureChecker = Substitute.For(); - var applicationLinksService = Substitute.For(); - - appService.GetAsync(appId).Returns(applicationDto); - paymentRequestService.GetListByApplicationIdAsync(appId).Returns(paymentRequests); - applicationLinksService.GetListByApplicationAsync(appId).Returns([]); - featureChecker.IsEnabledAsync("Unity.Payments").Returns(true); - - var viewComponent = CreateViewComponent(appService, paymentRequestService, featureChecker, applicationLinksService); - - // Act - var result = await viewComponent.InvokeAsync(appId, applicationFormVersionId) as ViewViewComponentResult; - var model = result!.ViewData!.Model as PaymentInfoViewModel; - - // Assert - model.ShouldNotBeNull(); - model.TotalPendingAmounts.ShouldBe(1300m); // 800 (ServiceUnavailable - will retry) + 300 (SentToCas) + 200 (L1Pending) - model.TotalPaid.ShouldBe(0m); // None with PaymentStatus = "Fully Paid" + // Verify no service calls were made + await appService.DidNotReceive().GetAsync(Arg.Any()); + await paymentRequestService.DidNotReceive().GetApplicationPaymentSummaryAsync(Arg.Any()); } [Fact] - public async Task PaymentInfo_Should_Include_All_Pending_Levels_InPending() + public async Task PaymentInfo_Should_Call_GetApplicationPaymentSummaryAsync_With_ApplicationId() { // Arrange var appId = Guid.NewGuid(); @@ -608,49 +287,38 @@ public async Task PaymentInfo_Should_Include_All_Pending_Levels_InPending() var applicationDto = new GrantApplicationDto { Id = appId, - ApprovedAmount = 20000, - Applicant = new GrantManager.GrantApplications.GrantApplicationApplicantDto { Id = applicantId } + ApprovedAmount = 5000, + Applicant = new GrantApplicationApplicantDto { Id = applicantId } }; - var paymentRequests = new List + var summary = new ApplicationPaymentSummaryDto { - new() { Amount = 1000, Status = PaymentRequestStatus.L1Pending }, // Pending - new() { Amount = 2000, Status = PaymentRequestStatus.L2Pending }, // Pending - new() { Amount = 3000, Status = PaymentRequestStatus.L3Pending }, // Pending - new() { Amount = 500, Status = PaymentRequestStatus.Submitted, PaymentStatus = null, InvoiceStatus = "SentToCas" }, // Pending - new() { Amount = 4000, Status = PaymentRequestStatus.Submitted, PaymentStatus = "Fully Paid" }, // Paid - new() { Amount = 100, Status = PaymentRequestStatus.L1Declined }, // Not pending - new() { Amount = 200, Status = PaymentRequestStatus.L2Declined }, // Not pending - new() { Amount = 300, Status = PaymentRequestStatus.L3Declined }, // Not pending + ApplicationId = appId, + TotalPaid = 100m, + TotalPending = 200m }; var appService = Substitute.For(); var paymentRequestService = Substitute.For(); var featureChecker = Substitute.For(); - var applicationLinksService = Substitute.For(); appService.GetAsync(appId).Returns(applicationDto); - paymentRequestService.GetListByApplicationIdAsync(appId).Returns(paymentRequests); - applicationLinksService.GetListByApplicationAsync(appId).Returns([]); + paymentRequestService.GetApplicationPaymentSummaryAsync(appId).Returns(summary); featureChecker.IsEnabledAsync("Unity.Payments").Returns(true); - var viewComponent = CreateViewComponent(appService, paymentRequestService, featureChecker, applicationLinksService); + var viewComponent = CreateViewComponent(appService, paymentRequestService, featureChecker); // Act - var result = await viewComponent.InvokeAsync(appId, applicationFormVersionId) as ViewViewComponentResult; - var model = result!.ViewData!.Model as PaymentInfoViewModel; + await viewComponent.InvokeAsync(appId, applicationFormVersionId); - // Assert - model.ShouldNotBeNull(); - model.TotalPendingAmounts.ShouldBe(6500m); // 1000 (L1) + 2000 (L2) + 3000 (L3) + 500 (Submitted with null PaymentStatus) - model.TotalPaid.ShouldBe(4000m); // Only PaymentStatus = "Fully Paid" + // Assert - Verify the correct service method was called with the right ID + await paymentRequestService.Received(1).GetApplicationPaymentSummaryAsync(appId); } private PaymentInfoViewComponent CreateViewComponent( IGrantApplicationAppService appService, IPaymentRequestAppService paymentRequestService, - IFeatureChecker featureChecker, - IApplicationLinksService applicationLinksService) + IFeatureChecker featureChecker) { var viewContext = new ViewContext { diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.TestBase/PaymentsTestBase.cs b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.TestBase/PaymentsTestBase.cs index a64ffdf37b..ce9e36c469 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.TestBase/PaymentsTestBase.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.TestBase/PaymentsTestBase.cs @@ -12,6 +12,7 @@ using Volo.Abp.SettingManagement; using Volo.Abp.TenantManagement; using Unity.GrantManager.Applications; +using Unity.GrantManager.GrantApplications; using Volo.Abp.Identity; using Unity.Notifications.EmailGroups; @@ -72,9 +73,10 @@ protected override void AfterAddApplication(IServiceCollection services) featureMock.IsEnabledAsync(Arg.Any()).Returns(true); services.AddSingleton(featureMock); - // Mock the repositories to avoid database access + // Mock the repositories and services to avoid database access services.AddSingleton(Substitute.For()); services.AddSingleton(Substitute.For()); + services.AddSingleton(Substitute.For()); var externalUserLookupMock = Substitute.For(); services.AddSingleton(externalUserLookupMock); From 13f58bdb9c67ffccc39590209da2bb99f88db344 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Mon, 23 Feb 2026 11:40:38 -0800 Subject: [PATCH 038/191] AB#31384 - Fix file encoding issue raising SQ warning --- .../Contacts/ContactAppServiceTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactAppServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactAppServiceTests.cs index e44be9fb43..ba28c58009 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactAppServiceTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactAppServiceTests.cs @@ -1,4 +1,4 @@ -using NSubstitute; +using NSubstitute; using Shouldly; using System; using System.Linq; @@ -352,7 +352,7 @@ public async Task CreateContactAsync_NonPrimary_ShouldNotClearExistingPrimary() // Act await _service.CreateContactAsync(input); - // Assert GetQueryableAsync should not be called (ClearPrimaryAsync not invoked) + // Assert - GetQueryableAsync should not be called (ClearPrimaryAsync not invoked) await _contactLinkRepository.DidNotReceive().GetQueryableAsync(); } @@ -484,7 +484,7 @@ public async Task SetPrimaryContactAsync_WithNoExistingPrimary_ShouldSetNew() // Act await _service.SetPrimaryContactAsync("TestEntity", entityId, contactId); - // Assert only the target link should be updated (set to primary) + // Assert — only the target link should be updated (set to primary) await _contactLinkRepository.Received(1).UpdateAsync( Arg.Is(l => l.Id == targetLinkId && l.IsPrimary), true, @@ -539,7 +539,7 @@ public async Task SetPrimaryContactAsync_WithMultipleExistingPrimaries_ShouldCle // Act await _service.SetPrimaryContactAsync("TestEntity", entityId, contactId); - // Assert both existing primaries cleared + // Assert — both existing primaries cleared await _contactLinkRepository.Received(1).UpdateAsync( Arg.Is(l => l.Id == primaryLinkId1 && !l.IsPrimary), true, From 535dab33b84f55ca555ad49411d9c5e2b96f9aac Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Mon, 23 Feb 2026 11:52:20 -0800 Subject: [PATCH 039/191] AB#31384 - Fix circular dependency on IApplicationLinksService --- .../PaymentRequests/PaymentRequestAppService.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/PaymentRequestAppService.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/PaymentRequestAppService.cs index 3ecdf011a2..b73281088c 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/PaymentRequestAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/PaymentRequestAppService.cs @@ -31,7 +31,7 @@ public class PaymentRequestAppService( FsbPaymentNotifier fsbPaymentNotifier, IPaymentRequestQueryManager paymentRequestQueryManager, IPaymentRequestConfigurationManager paymentRequestConfigurationManager, - IApplicationLinksService applicationLinksService) : PaymentsAppService, IPaymentRequestAppService + Lazy applicationLinksService) : PaymentsAppService, IPaymentRequestAppService { public async Task GetDefaultAccountCodingId() @@ -333,14 +333,14 @@ public async Task> GetPaymentPendingListByCorrelationIdA public async Task GetApplicationPaymentSummaryAsync(Guid applicationId) { - var childLinks = await applicationLinksService.GetChildApplications(applicationId); + var childLinks = await applicationLinksService.Value.GetChildApplications(applicationId); var childApplicationIds = childLinks.Select(l => l.LinkedApplicationId).ToList(); return await paymentRequestQueryManager.GetApplicationPaymentSummaryAsync(applicationId, childApplicationIds); } public async Task> GetApplicationPaymentSummariesAsync(List applicationIds) { - var childApplicationIdsByParent = await applicationLinksService.GetChildApplicationIdsByParentIdsAsync(applicationIds); + var childApplicationIdsByParent = await applicationLinksService.Value.GetChildApplicationIdsByParentIdsAsync(applicationIds); return await paymentRequestQueryManager.GetApplicationPaymentSummariesAsync(applicationIds, childApplicationIdsByParent); } } From 9a9d32ad1ad5ed3a5780dae92b4361e3a9e30c20 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Mon, 23 Feb 2026 12:20:09 -0800 Subject: [PATCH 040/191] AB#31384 - Payment Request query code quality improvements --- .../Repositories/PaymentRequestRepository.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/EntityFrameworkCore/Repositories/PaymentRequestRepository.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/EntityFrameworkCore/Repositories/PaymentRequestRepository.cs index 01307ab2f6..d26865a5ef 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/EntityFrameworkCore/Repositories/PaymentRequestRepository.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/EntityFrameworkCore/Repositories/PaymentRequestRepository.cs @@ -108,15 +108,15 @@ public async Task> GetPaymentSummariesByCorre ApplicationId = g.Key, TotalPaid = g .Where(p => p.PaymentStatus != null - && p.PaymentStatus == CasPaymentRequestStatus.FullyPaid) + && p.PaymentStatus.Trim() == CasPaymentRequestStatus.FullyPaid) .Sum(p => p.Amount), TotalPending = g .Where(p => p.Status == PaymentRequestStatus.L1Pending || p.Status == PaymentRequestStatus.L2Pending || p.Status == PaymentRequestStatus.L3Pending || (p.Status == PaymentRequestStatus.Submitted - && (p.PaymentStatus == null || p.PaymentStatus == string.Empty) - && (p.InvoiceStatus == null || p.InvoiceStatus == string.Empty + && string.IsNullOrEmpty(p.PaymentStatus) + && (string.IsNullOrEmpty(p.InvoiceStatus) || !p.InvoiceStatus.Contains(CasPaymentRequestStatus.ErrorFromCas)))) .Sum(p => p.Amount) }) From 19bb97f2935c3b411132d575303035e0e8a28b15 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Mon, 23 Feb 2026 12:45:58 -0800 Subject: [PATCH 041/191] AB#31384 - Add Payment Request Repository Tests --- .../Repositories/PaymentRequestRepository.cs | 5 +- ...tRequestRepository_PaymentSummary_Tests.cs | 411 ++++++++++++++++++ 2 files changed, 415 insertions(+), 1 deletion(-) create mode 100644 applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequestRepository_PaymentSummary_Tests.cs diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/EntityFrameworkCore/Repositories/PaymentRequestRepository.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/EntityFrameworkCore/Repositories/PaymentRequestRepository.cs index d26865a5ef..54bb015048 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/EntityFrameworkCore/Repositories/PaymentRequestRepository.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/EntityFrameworkCore/Repositories/PaymentRequestRepository.cs @@ -96,6 +96,9 @@ public async Task> GetPaymentPendingListByCorrelationIdAsyn .ToListAsync(); } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", + "CA1862:Use the 'StringComparison' method overloads to perform case-insensitive string comparisons", + Justification = "EF Core does not support StringComparison - https://github.com/dotnet/efcore/issues/1222")] public async Task> GetPaymentSummariesByCorrelationIdsAsync(List correlationIds) { var dbSet = await GetDbSetAsync(); @@ -108,7 +111,7 @@ public async Task> GetPaymentSummariesByCorre ApplicationId = g.Key, TotalPaid = g .Where(p => p.PaymentStatus != null - && p.PaymentStatus.Trim() == CasPaymentRequestStatus.FullyPaid) + && p.PaymentStatus.Trim().ToUpper() == CasPaymentRequestStatus.FullyPaid.ToUpper()) .Sum(p => p.Amount), TotalPending = g .Where(p => p.Status == PaymentRequestStatus.L1Pending diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequestRepository_PaymentSummary_Tests.cs b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequestRepository_PaymentSummary_Tests.cs new file mode 100644 index 0000000000..0b8df1af66 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequestRepository_PaymentSummary_Tests.cs @@ -0,0 +1,411 @@ +using Shouldly; +using System; +using System.ComponentModel; +using System.Threading.Tasks; +using Unity.Modules.Shared.Correlation; +using Unity.Payments.Domain.Suppliers; +using Unity.Payments.Enums; +using Unity.Payments.PaymentRequests; +using Volo.Abp.Uow; +using Xunit; + +namespace Unity.Payments.Domain.PaymentRequests; + +[Category("Integration")] +public class PaymentRequestRepository_PaymentSummary_Tests : PaymentsApplicationTestBase +{ + private readonly IPaymentRequestRepository _paymentRequestRepository; + private readonly ISupplierRepository _supplierRepository; + private readonly IUnitOfWorkManager _unitOfWorkManager; + + public PaymentRequestRepository_PaymentSummary_Tests() + { + _paymentRequestRepository = GetRequiredService(); + _supplierRepository = GetRequiredService(); + _unitOfWorkManager = GetRequiredService(); + } + + #region PaymentStatus Case-Insensitive Matching + + [Fact] + [Trait("Category", "Integration")] + public async Task Should_Count_FullyPaid_With_Exact_Case() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 1000m, + PaymentRequestStatus.Submitted, paymentStatus: "Fully Paid"); + + // Act + var results = await _paymentRequestRepository + .GetPaymentSummariesByCorrelationIdsAsync([correlationId]); + + // Assert + results.Count.ShouldBe(1); + results[0].ApplicationId.ShouldBe(correlationId); + results[0].TotalPaid.ShouldBe(1000m); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task Should_Count_FullyPaid_With_UpperCase() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 500m, + PaymentRequestStatus.Submitted, paymentStatus: "FULLY PAID"); + + // Act + var results = await _paymentRequestRepository + .GetPaymentSummariesByCorrelationIdsAsync([correlationId]); + + // Assert + results.Count.ShouldBe(1); + results[0].TotalPaid.ShouldBe(500m); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task Should_Count_FullyPaid_With_LowerCase() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 800m, + PaymentRequestStatus.Submitted, paymentStatus: "fully paid"); + + // Act + var results = await _paymentRequestRepository + .GetPaymentSummariesByCorrelationIdsAsync([correlationId]); + + // Assert + results.Count.ShouldBe(1); + results[0].TotalPaid.ShouldBe(800m); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task Should_Count_FullyPaid_With_LeadingAndTrailingSpaces() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 300m, + PaymentRequestStatus.Submitted, paymentStatus: " Fully Paid "); + + // Act + var results = await _paymentRequestRepository + .GetPaymentSummariesByCorrelationIdsAsync([correlationId]); + + // Assert + results.Count.ShouldBe(1); + results[0].TotalPaid.ShouldBe(300m); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task Should_Aggregate_FullyPaid_Across_All_Case_Variations() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 1000m, + PaymentRequestStatus.Submitted, paymentStatus: "Fully Paid"); + await InsertPaymentRequestAsync(siteId, correlationId, 500m, + PaymentRequestStatus.Submitted, paymentStatus: "FULLY PAID"); + await InsertPaymentRequestAsync(siteId, correlationId, 800m, + PaymentRequestStatus.Submitted, paymentStatus: "fully paid"); + await InsertPaymentRequestAsync(siteId, correlationId, 300m, + PaymentRequestStatus.Submitted, paymentStatus: " Fully Paid "); + + // Act + var results = await _paymentRequestRepository + .GetPaymentSummariesByCorrelationIdsAsync([correlationId]); + + // Assert + results.Count.ShouldBe(1); + results[0].TotalPaid.ShouldBe(2600m); // 1000 + 500 + 800 + 300 + } + + [Fact] + [Trait("Category", "Integration")] + public async Task Should_Not_Count_PartialMatch_PaymentStatus_As_Paid() + { + // Arrange - "Paid" alone should not match "Fully Paid" + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 200m, + PaymentRequestStatus.Submitted, paymentStatus: "Paid"); + + // Act + var results = await _paymentRequestRepository + .GetPaymentSummariesByCorrelationIdsAsync([correlationId]); + + // Assert + results.Count.ShouldBe(1); + results[0].TotalPaid.ShouldBe(0m); // "Paid" != "Fully Paid" + } + + #endregion + + #region Pending Status Calculation + + [Fact] + [Trait("Category", "Integration")] + public async Task Should_Sum_All_Pending_Levels() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 1000m, + PaymentRequestStatus.L1Pending); + await InsertPaymentRequestAsync(siteId, correlationId, 2000m, + PaymentRequestStatus.L2Pending); + await InsertPaymentRequestAsync(siteId, correlationId, 3000m, + PaymentRequestStatus.L3Pending); + + // Act + var results = await _paymentRequestRepository + .GetPaymentSummariesByCorrelationIdsAsync([correlationId]); + + // Assert + results.Count.ShouldBe(1); + results[0].TotalPending.ShouldBe(6000m); // 1000 + 2000 + 3000 + } + + [Fact] + [Trait("Category", "Integration")] + public async Task Should_Include_Submitted_WithNullPaymentStatus_InPending() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 500m, + PaymentRequestStatus.Submitted, paymentStatus: null, invoiceStatus: null); + await InsertPaymentRequestAsync(siteId, correlationId, 300m, + PaymentRequestStatus.Submitted, paymentStatus: null, invoiceStatus: "SentToCas"); + + // Act + var results = await _paymentRequestRepository + .GetPaymentSummariesByCorrelationIdsAsync([correlationId]); + + // Assert + results.Count.ShouldBe(1); + results[0].TotalPending.ShouldBe(800m); // 500 + 300 + } + + [Fact] + [Trait("Category", "Integration")] + public async Task Should_Exclude_Submitted_WithErrorFromCas_FromPending() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + // This one has ErrorFromCas - should NOT be pending + await InsertPaymentRequestAsync(siteId, correlationId, 1000m, + PaymentRequestStatus.Submitted, paymentStatus: null, invoiceStatus: "Error"); + // This one has no error - SHOULD be pending + await InsertPaymentRequestAsync(siteId, correlationId, 200m, + PaymentRequestStatus.Submitted, paymentStatus: null, invoiceStatus: "SentToCas"); + + // Act + var results = await _paymentRequestRepository + .GetPaymentSummariesByCorrelationIdsAsync([correlationId]); + + // Assert + results.Count.ShouldBe(1); + results[0].TotalPending.ShouldBe(200m); // Only the non-error one + } + + [Fact] + [Trait("Category", "Integration")] + public async Task Should_Exclude_Declined_Statuses_From_Both_Paid_And_Pending() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 1000m, + PaymentRequestStatus.L1Declined); + await InsertPaymentRequestAsync(siteId, correlationId, 2000m, + PaymentRequestStatus.L2Declined); + await InsertPaymentRequestAsync(siteId, correlationId, 3000m, + PaymentRequestStatus.L3Declined); + + // Act + var results = await _paymentRequestRepository + .GetPaymentSummariesByCorrelationIdsAsync([correlationId]); + + // Assert + results.Count.ShouldBe(1); + results[0].TotalPaid.ShouldBe(0m); + results[0].TotalPending.ShouldBe(0m); + } + + #endregion + + #region Mixed Paid and Pending + + [Fact] + [Trait("Category", "Integration")] + public async Task Should_Correctly_Separate_Paid_And_Pending() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + // Paid + await InsertPaymentRequestAsync(siteId, correlationId, 1000m, + PaymentRequestStatus.Submitted, paymentStatus: "Fully Paid"); + await InsertPaymentRequestAsync(siteId, correlationId, 500m, + PaymentRequestStatus.Submitted, paymentStatus: "FULLY PAID"); + // Pending + await InsertPaymentRequestAsync(siteId, correlationId, 2000m, + PaymentRequestStatus.L1Pending); + await InsertPaymentRequestAsync(siteId, correlationId, 800m, + PaymentRequestStatus.Submitted, paymentStatus: null, invoiceStatus: null); + // Neither paid nor pending (declined) + await InsertPaymentRequestAsync(siteId, correlationId, 5000m, + PaymentRequestStatus.L1Declined); + + // Act + var results = await _paymentRequestRepository + .GetPaymentSummariesByCorrelationIdsAsync([correlationId]); + + // Assert + results.Count.ShouldBe(1); + results[0].TotalPaid.ShouldBe(1500m); // 1000 + 500 + results[0].TotalPending.ShouldBe(2800m); // 2000 + 800 + } + + [Fact] + [Trait("Category", "Integration")] + public async Task Should_Return_Summaries_For_Multiple_CorrelationIds() + { + // Arrange + var app1Id = Guid.NewGuid(); + var app2Id = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + // App 1 payments + await InsertPaymentRequestAsync(siteId, app1Id, 1000m, + PaymentRequestStatus.Submitted, paymentStatus: "Fully Paid"); + await InsertPaymentRequestAsync(siteId, app1Id, 500m, + PaymentRequestStatus.L1Pending); + // App 2 payments + await InsertPaymentRequestAsync(siteId, app2Id, 2000m, + PaymentRequestStatus.Submitted, paymentStatus: "fully paid"); + await InsertPaymentRequestAsync(siteId, app2Id, 300m, + PaymentRequestStatus.L2Pending); + + // Act + var results = await _paymentRequestRepository + .GetPaymentSummariesByCorrelationIdsAsync([app1Id, app2Id]); + + // Assert + results.Count.ShouldBe(2); + + var app1Summary = results.Find(r => r.ApplicationId == app1Id); + app1Summary.ShouldNotBeNull(); + app1Summary!.TotalPaid.ShouldBe(1000m); + app1Summary.TotalPending.ShouldBe(500m); + + var app2Summary = results.Find(r => r.ApplicationId == app2Id); + app2Summary.ShouldNotBeNull(); + app2Summary!.TotalPaid.ShouldBe(2000m); + app2Summary.TotalPending.ShouldBe(300m); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task Should_Return_Empty_For_Unknown_CorrelationIds() + { + // Arrange & Act + using var uow = _unitOfWorkManager.Begin(); + var results = await _paymentRequestRepository + .GetPaymentSummariesByCorrelationIdsAsync([Guid.NewGuid()]); + + // Assert + results.ShouldBeEmpty(); + } + + #endregion + + #region Helpers + + private async Task CreateSupplierAndSiteAsync() + { + using var uow = _unitOfWorkManager.Begin(); + var siteId = Guid.NewGuid(); + var supplier = new Supplier(Guid.NewGuid(), "TestSupplier", "SUP-001", + new Correlation(Guid.NewGuid(), "Test")); + supplier.AddSite(new Site(siteId, "001", PaymentGroup.EFT)); + await _supplierRepository.InsertAsync(supplier, true); + await uow.CompleteAsync(); + return siteId; + } + + private async Task InsertPaymentRequestAsync( + Guid siteId, + Guid correlationId, + decimal amount, + PaymentRequestStatus status, + string? paymentStatus = null, + string? invoiceStatus = null) + { + var dto = new CreatePaymentRequestDto + { + InvoiceNumber = $"INV-{Guid.NewGuid():N}", + Amount = amount, + PayeeName = "Test Payee", + ContractNumber = "0000000000", + SupplierNumber = "SUP-001", + SiteId = siteId, + CorrelationId = correlationId, + CorrelationProvider = "Test", + ReferenceNumber = $"REF-{Guid.NewGuid():N}", + BatchName = "TEST_BATCH", + BatchNumber = 1 + }; + + var paymentRequest = new PaymentRequest(Guid.NewGuid(), dto); + paymentRequest.SetPaymentRequestStatus(status); + + if (paymentStatus != null) + { + paymentRequest.SetPaymentStatus(paymentStatus); + } + + if (invoiceStatus != null) + { + paymentRequest.SetInvoiceStatus(invoiceStatus); + } + + await _paymentRequestRepository.InsertAsync(paymentRequest, true); + } + + #endregion +} From 97c8d6254401651ed317848908541a5325f62419 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:22:31 -0800 Subject: [PATCH 042/191] AB#31384 - Add integration tests for PaymentRequestRepository and clean up whitespace --- .../Repositories/PaymentRequestRepository.cs | 4 +- .../PaymentRequestAppService.cs | 6 +- .../PaymentInfo/PaymentInfoViewComponent.cs | 2 +- ...equestQueryManager_PaymentSummary_Tests.cs | 1 - .../PaymentRequestRepository_Tests.cs | 847 ++++++++++++++++++ .../PaymentInfoViewComponentTests.cs | 2 +- .../PaymentsTestBase.cs | 28 +- .../Contacts/ContactAppService.cs | 4 +- .../ApplicationLinksAppService.cs | 82 +- .../GrantApplicationAppService.cs | 10 +- 10 files changed, 916 insertions(+), 70 deletions(-) create mode 100644 applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequestRepository_Tests.cs diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/EntityFrameworkCore/Repositories/PaymentRequestRepository.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/EntityFrameworkCore/Repositories/PaymentRequestRepository.cs index 54bb015048..8c89f2be63 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/EntityFrameworkCore/Repositories/PaymentRequestRepository.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/EntityFrameworkCore/Repositories/PaymentRequestRepository.cs @@ -96,8 +96,8 @@ public async Task> GetPaymentPendingListByCorrelationIdAsyn .ToListAsync(); } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", - "CA1862:Use the 'StringComparison' method overloads to perform case-insensitive string comparisons", + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", + "CA1862:Use the 'StringComparison' method overloads to perform case-insensitive string comparisons", Justification = "EF Core does not support StringComparison - https://github.com/dotnet/efcore/issues/1222")] public async Task> GetPaymentSummariesByCorrelationIdsAsync(List correlationIds) { diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/PaymentRequestAppService.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/PaymentRequestAppService.cs index b73281088c..c9c33f89ca 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/PaymentRequestAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/PaymentRequestAppService.cs @@ -4,20 +4,20 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Unity.GrantManager.GrantApplications; using Unity.Payments.Domain.Exceptions; using Unity.Payments.Domain.PaymentRequests; using Unity.Payments.Domain.Services; using Unity.Payments.Domain.Shared; using Unity.Payments.Enums; +using Unity.Payments.PaymentRequests.Notifications; using Unity.Payments.Permissions; using Volo.Abp; using Volo.Abp.Application.Dtos; +using Volo.Abp.Authorization.Permissions; using Volo.Abp.Data; using Volo.Abp.Features; -using Volo.Abp.Authorization.Permissions; using Volo.Abp.Users; -using Unity.Payments.PaymentRequests.Notifications; -using Unity.GrantManager.GrantApplications; namespace Unity.Payments.PaymentRequests { diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/PaymentInfoViewComponent.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/PaymentInfoViewComponent.cs index b422c97832..f6db79533e 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/PaymentInfoViewComponent.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/PaymentInfoViewComponent.cs @@ -45,7 +45,7 @@ public async Task InvokeAsync(Guid applicationId, Guid app ApplicationFormVersionId = applicationFormVersionId, ApplicantId = application.Applicant.Id }; - + var summary = await _paymentRequestService.GetApplicationPaymentSummaryAsync(applicationId); model.TotalPaid = summary.TotalPaid; model.TotalPendingAmounts = summary.TotalPending; diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequestQueryManager_PaymentSummary_Tests.cs b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequestQueryManager_PaymentSummary_Tests.cs index 0d29fbd87e..d97ab38b32 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequestQueryManager_PaymentSummary_Tests.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequestQueryManager_PaymentSummary_Tests.cs @@ -5,7 +5,6 @@ using System.ComponentModel; using System.Linq; using System.Threading.Tasks; -using Unity.Payments.Domain.PaymentRequests; using Unity.Payments.Domain.Services; using Unity.Payments.Domain.Suppliers; using Unity.Payments.PaymentRequests; diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequestRepository_Tests.cs b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequestRepository_Tests.cs new file mode 100644 index 0000000000..58b82f1665 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequestRepository_Tests.cs @@ -0,0 +1,847 @@ +using Shouldly; +using System; +using System.ComponentModel; +using System.Linq; +using System.Threading.Tasks; +using Unity.Modules.Shared.Correlation; +using Unity.Payments.Codes; +using Unity.Payments.Domain.Suppliers; +using Unity.Payments.Enums; +using Unity.Payments.PaymentRequests; +using Volo.Abp.Uow; +using Xunit; + +namespace Unity.Payments.Domain.PaymentRequests; + +[Category("Integration")] +public class PaymentRequestRepository_Tests : PaymentsApplicationTestBase +{ + private readonly IPaymentRequestRepository _paymentRequestRepository; + private readonly ISupplierRepository _supplierRepository; + private readonly IUnitOfWorkManager _unitOfWorkManager; + + public PaymentRequestRepository_Tests() + { + _paymentRequestRepository = GetRequiredService(); + _supplierRepository = GetRequiredService(); + _unitOfWorkManager = GetRequiredService(); + } + + #region GetCountByCorrelationId + + [Fact] + [Trait("Category", "Integration")] + public async Task GetCountByCorrelationId_Should_Return_Zero_When_No_Payments_Exist() + { + // Arrange + var correlationId = Guid.NewGuid(); + + // Act + var count = await _paymentRequestRepository.GetCountByCorrelationId(correlationId); + + // Assert + count.ShouldBe(0); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetCountByCorrelationId_Should_Return_Correct_Count_For_Single_Payment() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 1000m, PaymentRequestStatus.L1Pending); + + // Act + var count = await _paymentRequestRepository.GetCountByCorrelationId(correlationId); + + // Assert + count.ShouldBe(1); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetCountByCorrelationId_Should_Return_Correct_Count_For_Multiple_Payments() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 1000m, PaymentRequestStatus.L1Pending); + await InsertPaymentRequestAsync(siteId, correlationId, 2000m, PaymentRequestStatus.L2Pending); + await InsertPaymentRequestAsync(siteId, correlationId, 3000m, PaymentRequestStatus.Submitted); + + // Act + var count = await _paymentRequestRepository.GetCountByCorrelationId(correlationId); + + // Assert + count.ShouldBe(3); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetCountByCorrelationId_Should_Not_Count_Other_CorrelationIds() + { + // Arrange + var correlationId1 = Guid.NewGuid(); + var correlationId2 = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId1, 1000m, PaymentRequestStatus.L1Pending); + await InsertPaymentRequestAsync(siteId, correlationId2, 2000m, PaymentRequestStatus.L1Pending); + + // Act + var count = await _paymentRequestRepository.GetCountByCorrelationId(correlationId1); + + // Assert + count.ShouldBe(1); + } + + #endregion + + #region GetPaymentRequestCountBySiteId + + [Fact] + [Trait("Category", "Integration")] + public async Task GetPaymentRequestCountBySiteId_Should_Return_Zero_When_No_Payments_Exist() + { + // Arrange + var siteId = await CreateSupplierAndSiteAsync(); + + // Act + var count = await _paymentRequestRepository.GetPaymentRequestCountBySiteId(siteId); + + // Assert + count.ShouldBe(0); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetPaymentRequestCountBySiteId_Should_Return_Correct_Count_For_Single_Payment() + { + // Arrange + var siteId = await CreateSupplierAndSiteAsync(); + var correlationId = Guid.NewGuid(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 1000m, PaymentRequestStatus.L1Pending); + + // Act + var count = await _paymentRequestRepository.GetPaymentRequestCountBySiteId(siteId); + + // Assert + count.ShouldBe(1); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetPaymentRequestCountBySiteId_Should_Return_Correct_Count_For_Multiple_Payments() + { + // Arrange + var siteId = await CreateSupplierAndSiteAsync(); + var correlationId1 = Guid.NewGuid(); + var correlationId2 = Guid.NewGuid(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId1, 1000m, PaymentRequestStatus.L1Pending); + await InsertPaymentRequestAsync(siteId, correlationId2, 2000m, PaymentRequestStatus.L2Pending); + + // Act + var count = await _paymentRequestRepository.GetPaymentRequestCountBySiteId(siteId); + + // Assert + count.ShouldBe(2); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetPaymentRequestCountBySiteId_Should_Not_Count_Other_Sites() + { + // Arrange + var siteId1 = await CreateSupplierAndSiteAsync(); + var siteId2 = await CreateSupplierAndSiteAsync(); + var correlationId = Guid.NewGuid(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId1, correlationId, 1000m, PaymentRequestStatus.L1Pending); + await InsertPaymentRequestAsync(siteId2, correlationId, 2000m, PaymentRequestStatus.L1Pending); + + // Act + var count = await _paymentRequestRepository.GetPaymentRequestCountBySiteId(siteId1); + + // Assert + count.ShouldBe(1); + } + + #endregion + + #region GetPaymentRequestByInvoiceNumber + + [Fact] + [Trait("Category", "Integration")] + public async Task GetPaymentRequestByInvoiceNumber_Should_Return_Null_When_Not_Found() + { + // Arrange + var invoiceNumber = "NONEXISTENT-INV-001"; + + // Act + var payment = await _paymentRequestRepository.GetPaymentRequestByInvoiceNumber(invoiceNumber); + + // Assert + payment.ShouldBeNull(); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetPaymentRequestByInvoiceNumber_Should_Return_Payment_When_Found() + { + // Arrange + var siteId = await CreateSupplierAndSiteAsync(); + var correlationId = Guid.NewGuid(); + var invoiceNumber = $"TEST-INV-{Guid.NewGuid():N}"; + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 1000m, PaymentRequestStatus.L1Pending, invoiceNumber); + + // Act + var payment = await _paymentRequestRepository.GetPaymentRequestByInvoiceNumber(invoiceNumber); + + // Assert + payment.ShouldNotBeNull(); + payment.InvoiceNumber.ShouldBe(invoiceNumber); + payment.Amount.ShouldBe(1000m); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetPaymentRequestByInvoiceNumber_Should_Return_Only_Matching_Invoice() + { + // Arrange + var siteId = await CreateSupplierAndSiteAsync(); + var correlationId = Guid.NewGuid(); + var invoiceNumber1 = $"TEST-INV-{Guid.NewGuid():N}"; + var invoiceNumber2 = $"TEST-INV-{Guid.NewGuid():N}"; + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 1000m, PaymentRequestStatus.L1Pending, invoiceNumber1); + await InsertPaymentRequestAsync(siteId, correlationId, 2000m, PaymentRequestStatus.L2Pending, invoiceNumber2); + + // Act + var payment = await _paymentRequestRepository.GetPaymentRequestByInvoiceNumber(invoiceNumber1); + + // Assert + payment.ShouldNotBeNull(); + payment.InvoiceNumber.ShouldBe(invoiceNumber1); + payment.Amount.ShouldBe(1000m); + } + + #endregion + + #region GetTotalPaymentRequestAmountByCorrelationIdAsync + + [Fact] + [Trait("Category", "Integration")] + public async Task GetTotalPaymentRequestAmountByCorrelationIdAsync_Should_Return_Zero_When_No_Payments() + { + // Arrange + var correlationId = Guid.NewGuid(); + + // Act + var total = await _paymentRequestRepository.GetTotalPaymentRequestAmountByCorrelationIdAsync(correlationId); + + // Assert + total.ShouldBe(0m); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetTotalPaymentRequestAmountByCorrelationIdAsync_Should_Sum_Single_Payment() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 1000m, PaymentRequestStatus.Submitted); + + // Act + var total = await _paymentRequestRepository.GetTotalPaymentRequestAmountByCorrelationIdAsync(correlationId); + + // Assert + total.ShouldBe(1000m); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetTotalPaymentRequestAmountByCorrelationIdAsync_Should_Sum_Multiple_Payments() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 1000m, PaymentRequestStatus.Submitted); + await InsertPaymentRequestAsync(siteId, correlationId, 2500m, PaymentRequestStatus.Submitted); + await InsertPaymentRequestAsync(siteId, correlationId, 3000m, PaymentRequestStatus.L1Pending); + + // Act + var total = await _paymentRequestRepository.GetTotalPaymentRequestAmountByCorrelationIdAsync(correlationId); + + // Assert + total.ShouldBe(6500m); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetTotalPaymentRequestAmountByCorrelationIdAsync_Should_Exclude_L1Declined() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 1000m, PaymentRequestStatus.Submitted); + await InsertPaymentRequestAsync(siteId, correlationId, 5000m, PaymentRequestStatus.L1Declined); + + // Act + var total = await _paymentRequestRepository.GetTotalPaymentRequestAmountByCorrelationIdAsync(correlationId); + + // Assert + total.ShouldBe(1000m); // Declined amount excluded + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetTotalPaymentRequestAmountByCorrelationIdAsync_Should_Exclude_L2Declined() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 1000m, PaymentRequestStatus.Submitted); + await InsertPaymentRequestAsync(siteId, correlationId, 3000m, PaymentRequestStatus.L2Declined); + + // Act + var total = await _paymentRequestRepository.GetTotalPaymentRequestAmountByCorrelationIdAsync(correlationId); + + // Assert + total.ShouldBe(1000m); // Declined amount excluded + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetTotalPaymentRequestAmountByCorrelationIdAsync_Should_Exclude_L3Declined() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 1000m, PaymentRequestStatus.Submitted); + await InsertPaymentRequestAsync(siteId, correlationId, 2000m, PaymentRequestStatus.L3Declined); + + // Act + var total = await _paymentRequestRepository.GetTotalPaymentRequestAmountByCorrelationIdAsync(correlationId); + + // Assert + total.ShouldBe(1000m); // Declined amount excluded + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetTotalPaymentRequestAmountByCorrelationIdAsync_Should_Exclude_NotFound_InvoiceStatus() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 1000m, PaymentRequestStatus.Submitted); + await InsertPaymentRequestAsync(siteId, correlationId, 4000m, PaymentRequestStatus.Submitted, + invoiceStatus: CasPaymentRequestStatus.NotFound); + + // Act + var total = await _paymentRequestRepository.GetTotalPaymentRequestAmountByCorrelationIdAsync(correlationId); + + // Assert + total.ShouldBe(1000m); // NotFound invoice excluded + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetTotalPaymentRequestAmountByCorrelationIdAsync_Should_Exclude_ErrorFromCas_InvoiceStatus() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 1000m, PaymentRequestStatus.Submitted); + await InsertPaymentRequestAsync(siteId, correlationId, 6000m, PaymentRequestStatus.Submitted, + invoiceStatus: CasPaymentRequestStatus.ErrorFromCas); + + // Act + var total = await _paymentRequestRepository.GetTotalPaymentRequestAmountByCorrelationIdAsync(correlationId); + + // Assert + total.ShouldBe(1000m); // ErrorFromCas invoice excluded + } + + #endregion + + #region GetPaymentRequestsBySentToCasStatusAsync + + [Fact] + [Trait("Category", "Integration")] + public async Task GetPaymentRequestsBySentToCasStatusAsync_Should_Return_Empty_When_No_Matching_Payments() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 1000m, PaymentRequestStatus.Submitted, + invoiceStatus: CasPaymentRequestStatus.Validated); + + // Act + var payments = await _paymentRequestRepository.GetPaymentRequestsBySentToCasStatusAsync(); + + // Assert + payments.ShouldBeEmpty(); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetPaymentRequestsBySentToCasStatusAsync_Should_Return_ServiceUnavailable_Status() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 1000m, PaymentRequestStatus.Submitted, + invoiceStatus: CasPaymentRequestStatus.ServiceUnavailable); + + // Act + var payments = await _paymentRequestRepository.GetPaymentRequestsBySentToCasStatusAsync(); + + // Assert + payments.Count.ShouldBe(1); + payments[0].InvoiceStatus.ShouldBe(CasPaymentRequestStatus.ServiceUnavailable); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetPaymentRequestsBySentToCasStatusAsync_Should_Return_SentToCas_Status() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 1000m, PaymentRequestStatus.Submitted, + invoiceStatus: CasPaymentRequestStatus.SentToCas); + + // Act + var payments = await _paymentRequestRepository.GetPaymentRequestsBySentToCasStatusAsync(); + + // Assert + payments.Count.ShouldBe(1); + payments[0].InvoiceStatus.ShouldBe(CasPaymentRequestStatus.SentToCas); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetPaymentRequestsBySentToCasStatusAsync_Should_Return_NeverValidated_Status() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 1000m, PaymentRequestStatus.Submitted, + invoiceStatus: CasPaymentRequestStatus.NeverValidated); + + // Act + var payments = await _paymentRequestRepository.GetPaymentRequestsBySentToCasStatusAsync(); + + // Assert + payments.Count.ShouldBe(1); + payments[0].InvoiceStatus.ShouldBe(CasPaymentRequestStatus.NeverValidated); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetPaymentRequestsBySentToCasStatusAsync_Should_Return_All_ReCheck_Statuses() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 1000m, PaymentRequestStatus.Submitted, + invoiceStatus: CasPaymentRequestStatus.ServiceUnavailable); + await InsertPaymentRequestAsync(siteId, correlationId, 2000m, PaymentRequestStatus.Submitted, + invoiceStatus: CasPaymentRequestStatus.SentToCas); + await InsertPaymentRequestAsync(siteId, correlationId, 3000m, PaymentRequestStatus.Submitted, + invoiceStatus: CasPaymentRequestStatus.NeverValidated); + + // Act + var payments = await _paymentRequestRepository.GetPaymentRequestsBySentToCasStatusAsync(); + + // Assert + payments.Count.ShouldBe(3); + payments.Any(p => p.InvoiceStatus == CasPaymentRequestStatus.ServiceUnavailable).ShouldBeTrue(); + payments.Any(p => p.InvoiceStatus == CasPaymentRequestStatus.SentToCas).ShouldBeTrue(); + payments.Any(p => p.InvoiceStatus == CasPaymentRequestStatus.NeverValidated).ShouldBeTrue(); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetPaymentRequestsBySentToCasStatusAsync_Should_Not_Return_Null_InvoiceStatus() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 1000m, PaymentRequestStatus.Submitted, + invoiceStatus: null); + await InsertPaymentRequestAsync(siteId, correlationId, 2000m, PaymentRequestStatus.Submitted, + invoiceStatus: CasPaymentRequestStatus.SentToCas); + + // Act + var payments = await _paymentRequestRepository.GetPaymentRequestsBySentToCasStatusAsync(); + + // Assert + payments.Count.ShouldBe(1); + payments[0].InvoiceStatus.ShouldBe(CasPaymentRequestStatus.SentToCas); + } + + #endregion + + #region GetPaymentRequestsByFailedsStatusAsync + + [Fact] + [Trait("Category", "Integration")] + public async Task GetPaymentRequestsByFailedsStatusAsync_Should_Return_Empty_When_No_Matching_Payments() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 1000m, PaymentRequestStatus.Submitted, + invoiceStatus: CasPaymentRequestStatus.Validated); + + // Act + var payments = await _paymentRequestRepository.GetPaymentRequestsByFailedsStatusAsync(); + + // Assert + payments.ShouldBeEmpty(); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetPaymentRequestsByFailedsStatusAsync_Should_Return_ServiceUnavailable_Status() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + var paymentRequest = await InsertAndGetPaymentRequestAsync(siteId, correlationId, 1000m, PaymentRequestStatus.Submitted, + invoiceStatus: CasPaymentRequestStatus.ServiceUnavailable); + + // Update to trigger LastModificationTime + paymentRequest.SetCasResponse("Test response"); + await _paymentRequestRepository.UpdateAsync(paymentRequest, true); + + // Act + var payments = await _paymentRequestRepository.GetPaymentRequestsByFailedsStatusAsync(); + + // Assert + payments.Count.ShouldBe(1); + payments[0].InvoiceStatus.ShouldBe(CasPaymentRequestStatus.ServiceUnavailable); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetPaymentRequestsByFailedsStatusAsync_Should_Return_ErrorFromCas_Status() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + var paymentRequest = await InsertAndGetPaymentRequestAsync(siteId, correlationId, 1000m, PaymentRequestStatus.Submitted, + invoiceStatus: CasPaymentRequestStatus.ErrorFromCas); + + // Update to trigger LastModificationTime + paymentRequest.SetCasResponse("Test response"); + await _paymentRequestRepository.UpdateAsync(paymentRequest, true); + + // Act + var payments = await _paymentRequestRepository.GetPaymentRequestsByFailedsStatusAsync(); + + // Assert + payments.Count.ShouldBe(1); + payments[0].InvoiceStatus.ShouldBe(CasPaymentRequestStatus.ErrorFromCas); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetPaymentRequestsByFailedsStatusAsync_Should_Return_Both_Failed_Statuses() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + var paymentRequest1 = await InsertAndGetPaymentRequestAsync(siteId, correlationId, 1000m, PaymentRequestStatus.Submitted, + invoiceStatus: CasPaymentRequestStatus.ServiceUnavailable); + var paymentRequest2 = await InsertAndGetPaymentRequestAsync(siteId, correlationId, 2000m, PaymentRequestStatus.Submitted, + invoiceStatus: CasPaymentRequestStatus.ErrorFromCas); + + // Update to trigger LastModificationTime + paymentRequest1.SetCasResponse("Test response"); + await _paymentRequestRepository.UpdateAsync(paymentRequest1, true); + paymentRequest2.SetCasResponse("Test response"); + await _paymentRequestRepository.UpdateAsync(paymentRequest2, true); + + // Act + var payments = await _paymentRequestRepository.GetPaymentRequestsByFailedsStatusAsync(); + + // Assert + payments.Count.ShouldBe(2); + payments.Any(p => p.InvoiceStatus == CasPaymentRequestStatus.ServiceUnavailable).ShouldBeTrue(); + payments.Any(p => p.InvoiceStatus == CasPaymentRequestStatus.ErrorFromCas).ShouldBeTrue(); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetPaymentRequestsByFailedsStatusAsync_Should_Not_Return_Null_InvoiceStatus() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + var paymentRequest1 = await InsertAndGetPaymentRequestAsync(siteId, correlationId, 1000m, PaymentRequestStatus.Submitted, + invoiceStatus: null); + var paymentRequest2 = await InsertAndGetPaymentRequestAsync(siteId, correlationId, 2000m, PaymentRequestStatus.Submitted, + invoiceStatus: CasPaymentRequestStatus.ErrorFromCas); + + // Update to trigger LastModificationTime (only update the one with ErrorFromCas) + paymentRequest2.SetCasResponse("Test response"); + await _paymentRequestRepository.UpdateAsync(paymentRequest2, true); + + // Act + var payments = await _paymentRequestRepository.GetPaymentRequestsByFailedsStatusAsync(); + + // Assert + payments.Count.ShouldBe(1); + payments[0].InvoiceStatus.ShouldBe(CasPaymentRequestStatus.ErrorFromCas); + } + + #endregion + + #region GetPaymentPendingListByCorrelationIdAsync + + [Fact] + [Trait("Category", "Integration")] + public async Task GetPaymentPendingListByCorrelationIdAsync_Should_Return_Empty_When_No_Pending() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 1000m, PaymentRequestStatus.Submitted); + + // Act + var payments = await _paymentRequestRepository.GetPaymentPendingListByCorrelationIdAsync(correlationId); + + // Assert + payments.ShouldBeEmpty(); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetPaymentPendingListByCorrelationIdAsync_Should_Return_L1Pending() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 1000m, PaymentRequestStatus.L1Pending); + + // Act + var payments = await _paymentRequestRepository.GetPaymentPendingListByCorrelationIdAsync(correlationId); + + // Assert + payments.Count.ShouldBe(1); + payments[0].Status.ShouldBe(PaymentRequestStatus.L1Pending); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetPaymentPendingListByCorrelationIdAsync_Should_Return_L2Pending() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 1000m, PaymentRequestStatus.L2Pending); + + // Act + var payments = await _paymentRequestRepository.GetPaymentPendingListByCorrelationIdAsync(correlationId); + + // Assert + payments.Count.ShouldBe(1); + payments[0].Status.ShouldBe(PaymentRequestStatus.L2Pending); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetPaymentPendingListByCorrelationIdAsync_Should_Return_All_Pending_Statuses() + { + // Arrange + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 1000m, PaymentRequestStatus.L1Pending); + await InsertPaymentRequestAsync(siteId, correlationId, 2000m, PaymentRequestStatus.L2Pending); + await InsertPaymentRequestAsync(siteId, correlationId, 3000m, PaymentRequestStatus.Submitted); + + // Act + var payments = await _paymentRequestRepository.GetPaymentPendingListByCorrelationIdAsync(correlationId); + + // Assert + payments.Count.ShouldBe(2); + payments.Any(p => p.Status == PaymentRequestStatus.L1Pending).ShouldBeTrue(); + payments.Any(p => p.Status == PaymentRequestStatus.L2Pending).ShouldBeTrue(); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetPaymentPendingListByCorrelationIdAsync_Should_Not_Return_L3Pending() + { + // Arrange - The method only returns L1 and L2 pending + var correlationId = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId, 1000m, PaymentRequestStatus.L3Pending); + await InsertPaymentRequestAsync(siteId, correlationId, 2000m, PaymentRequestStatus.L1Pending); + + // Act + var payments = await _paymentRequestRepository.GetPaymentPendingListByCorrelationIdAsync(correlationId); + + // Assert + payments.Count.ShouldBe(1); + payments[0].Status.ShouldBe(PaymentRequestStatus.L1Pending); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task GetPaymentPendingListByCorrelationIdAsync_Should_Only_Return_Matching_CorrelationId() + { + // Arrange + var correlationId1 = Guid.NewGuid(); + var correlationId2 = Guid.NewGuid(); + var siteId = await CreateSupplierAndSiteAsync(); + + using var uow = _unitOfWorkManager.Begin(); + await InsertPaymentRequestAsync(siteId, correlationId1, 1000m, PaymentRequestStatus.L1Pending); + await InsertPaymentRequestAsync(siteId, correlationId2, 2000m, PaymentRequestStatus.L1Pending); + + // Act + var payments = await _paymentRequestRepository.GetPaymentPendingListByCorrelationIdAsync(correlationId1); + + // Assert + payments.Count.ShouldBe(1); + payments[0].CorrelationId.ShouldBe(correlationId1); + } + + #endregion + + #region Helpers + + private async Task CreateSupplierAndSiteAsync() + { + using var uow = _unitOfWorkManager.Begin(); + var siteId = Guid.NewGuid(); + var supplier = new Supplier(Guid.NewGuid(), "TestSupplier", "SUP-001", + new Correlation(Guid.NewGuid(), "Test")); + supplier.AddSite(new Site(siteId, "001", PaymentGroup.EFT)); + await _supplierRepository.InsertAsync(supplier, true); + await uow.CompleteAsync(); + return siteId; + } + + private async Task InsertPaymentRequestAsync( + Guid siteId, + Guid correlationId, + decimal amount, + PaymentRequestStatus status, + string? customInvoiceNumber = null, + string? paymentStatus = null, + string? invoiceStatus = null) + { + await InsertAndGetPaymentRequestAsync(siteId, correlationId, amount, status, + customInvoiceNumber, paymentStatus, invoiceStatus); + } + + private async Task InsertAndGetPaymentRequestAsync( + Guid siteId, + Guid correlationId, + decimal amount, + PaymentRequestStatus status, + string? customInvoiceNumber = null, + string? paymentStatus = null, + string? invoiceStatus = null) + { + var invoiceNumber = customInvoiceNumber ?? $"INV-{Guid.NewGuid():N}"; + var dto = new CreatePaymentRequestDto + { + InvoiceNumber = invoiceNumber, + Amount = amount, + PayeeName = "Test Payee", + ContractNumber = "0000000000", + SupplierNumber = "SUP-001", + SiteId = siteId, + CorrelationId = correlationId, + CorrelationProvider = "Test", + ReferenceNumber = $"REF-{Guid.NewGuid():N}", + BatchName = "TEST_BATCH", + BatchNumber = 1 + }; + + var paymentRequest = new PaymentRequest(Guid.NewGuid(), dto); + paymentRequest.SetPaymentRequestStatus(status); + + if (paymentStatus != null) + { + paymentRequest.SetPaymentStatus(paymentStatus); + } + + if (invoiceStatus != null) + { + paymentRequest.SetInvoiceStatus(invoiceStatus); + } + + await _paymentRequestRepository.InsertAsync(paymentRequest, true); + return paymentRequest; + } + + #endregion +} diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/ViewComponents/PaymentInfoViewComponentTests.cs b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/ViewComponents/PaymentInfoViewComponentTests.cs index 18fba64ff6..549a5cab2e 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/ViewComponents/PaymentInfoViewComponentTests.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/ViewComponents/PaymentInfoViewComponentTests.cs @@ -331,7 +331,7 @@ private PaymentInfoViewComponent CreateViewComponent( }; var viewComponent = new PaymentInfoViewComponent( - appService, + appService, paymentRequestService, featureChecker) { diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.TestBase/PaymentsTestBase.cs b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.TestBase/PaymentsTestBase.cs index ce9e36c469..b223ff74de 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.TestBase/PaymentsTestBase.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.TestBase/PaymentsTestBase.cs @@ -1,20 +1,20 @@ -using System; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using System; using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; +using Unity.GrantManager.Applications; +using Unity.GrantManager.GrantApplications; +using Unity.Notifications.EmailGroups; +using Unity.Payments.Security; using Volo.Abp; -using Volo.Abp.Modularity; -using Volo.Abp.Uow; -using Volo.Abp.Testing; -using NSubstitute; using Volo.Abp.Features; -using Volo.Abp.Users; -using Unity.Payments.Security; +using Volo.Abp.Identity; +using Volo.Abp.Modularity; using Volo.Abp.SettingManagement; using Volo.Abp.TenantManagement; -using Unity.GrantManager.Applications; -using Unity.GrantManager.GrantApplications; -using Volo.Abp.Identity; -using Unity.Notifications.EmailGroups; +using Volo.Abp.Testing; +using Volo.Abp.Uow; +using Volo.Abp.Users; namespace Unity.Payments; @@ -84,8 +84,8 @@ protected override void AfterAddApplication(IServiceCollection services) var currentUser = Substitute.For(); currentUser.Id.Returns(ci => CurrentUserId); services.AddSingleton(currentUser); - - // We add a mock of this service to satisfy the IOC without having to spin up a whole settings table + + // We add a mock of this service to satisfy the IOC without having to spin up a whole settings table var settingManagerMock = Substitute.For(); // Mock required calls services.AddSingleton(settingManagerMock); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Contacts/ContactAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Contacts/ContactAppService.cs index 8a70946c5a..f8fcc31f74 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Contacts/ContactAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Contacts/ContactAppService.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; @@ -11,7 +11,7 @@ namespace Unity.GrantManager.Contacts; /// /// Generic contact management service. Manages contacts and their links to arbitrary entity types. -/// Currently marked as [RemoteService(false)] not exposed as an HTTP endpoint. +/// Currently marked as [RemoteService(false)] — not exposed as an HTTP endpoint. /// Authorization roles to be configured before enabling remote access. /// diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationLinksAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationLinksAppService.cs index fd765b4b0f..6e23ce9b63 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationLinksAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationLinksAppService.cs @@ -5,8 +5,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Unity.GrantManager.Applications; using Unity.GrantManager.ApplicationForms; +using Unity.GrantManager.Applications; using Volo.Abp.Application.Services; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories; @@ -32,7 +32,7 @@ public class ApplicationLinksAppService : CrudAppService< public IApplicationRepository ApplicationRepository { get; set; } = null!; public IApplicantRepository ApplicantRepository { get; set; } = null!; public IApplicationFormAppService ApplicationFormAppService { get; set; } = null!; - + public ApplicationLinksAppService(IRepository repository) : base(repository) { } public async Task> GetListByApplicationAsync(Guid applicationId) @@ -178,7 +178,7 @@ join applicant in applicantsQuery on application.ApplicantId equals applicant.Id public async Task GetCurrentApplicationInfoAsync(Guid applicationId) { Logger.LogInformation("GetCurrentApplicationInfoAsync called with applicationId: {ApplicationId}", applicationId); - + try { var applicationsQuery = await ApplicationRepository.GetQueryableAsync(); @@ -186,7 +186,7 @@ public async Task GetCurrentApplicationInfoAsync(Guid a .Include(a => a.ApplicationStatus) .Where(a => a.Id == applicationId) .FirstOrDefaultAsync(); - + if (application == null) { Logger.LogWarning("Application not found with ID: {ApplicationId}", applicationId); @@ -273,13 +273,13 @@ public async Task GetCurrentApplicationInfoAsync(Guid a LinkType = ApplicationLinkType.Related, FormVersion = formVersion }; - + return result; } catch (Exception ex) { Logger.LogError(ex, "Critical error in GetCurrentApplicationInfoAsync for applicationId: {ApplicationId}", applicationId); - + // If all else fails, return a basic structure return new ApplicationLinksInfoDto { @@ -300,16 +300,16 @@ public async Task DeleteWithPairAsync(Guid applicationLinkId) { // Get the link to find the paired record var link = await Repository.GetAsync(applicationLinkId); - + // Find the paired link (reverse direction) var applicationLinksQuery = await ApplicationLinksRepository.GetQueryableAsync(); var pairedLink = await applicationLinksQuery .Where(x => x.ApplicationId == link.LinkedApplicationId && x.LinkedApplicationId == link.ApplicationId) .FirstOrDefaultAsync(); - + // Delete both links await Repository.DeleteAsync(applicationLinkId); - + if (pairedLink != null) { await Repository.DeleteAsync(pairedLink.Id); @@ -319,7 +319,7 @@ public async Task DeleteWithPairAsync(Guid applicationLinkId) public async Task GetApplicationDetailsByReferenceAsync(string referenceNumber) { Logger.LogInformation("GetApplicationDetailsByReferenceAsync called with referenceNumber: {ReferenceNumber}", referenceNumber); - + try { var applicationsQuery = await ApplicationRepository.GetQueryableAsync(); @@ -327,7 +327,7 @@ public async Task GetApplicationDetailsByReferenceAsync .Include(a => a.ApplicationStatus) .Where(a => a.ReferenceNo == referenceNumber) .FirstOrDefaultAsync(); - + if (application == null) { Logger.LogWarning("Application not found with ReferenceNumber: {ReferenceNumber}", referenceNumber); @@ -399,7 +399,7 @@ public async Task GetApplicationDetailsByReferenceAsync catch (Exception ex) { Logger.LogError(ex, "Critical error in GetApplicationDetailsByReferenceAsync for referenceNumber: {ReferenceNumber}", referenceNumber); - + return new ApplicationLinksInfoDto { Id = Guid.Empty, @@ -418,46 +418,46 @@ public async Task GetApplicationDetailsByReferenceAsync public async Task UpdateLinkTypeAsync(Guid applicationLinkId, ApplicationLinkType newLinkType) { Logger.LogInformation("UpdateLinkTypeAsync called with linkId: {LinkId}, newLinkType: {LinkType}", applicationLinkId, newLinkType); - + // Get the existing link var link = await Repository.GetAsync(applicationLinkId); - + if (link != null) { // Update the link type link.LinkType = newLinkType; await Repository.UpdateAsync(link); - + Logger.LogInformation("Successfully updated link type for linkId: {LinkId}", applicationLinkId); } else { Logger.LogWarning("Link not found with ID: {LinkId}", applicationLinkId); } - + } public async Task ValidateApplicationLinksAsync( - Guid currentApplicationId, + Guid currentApplicationId, List proposedLinks) { var result = new ApplicationLinkValidationResult(); - + // Skip validation for empty or Related-only links var hierarchicalLinks = proposedLinks.Where(l => l.LinkType != ApplicationLinkType.Related).ToList(); if (hierarchicalLinks.Count == 0) { return result; } - + // Validate current app constraints var currentAppError = ValidateCurrentApplicationConstraints(hierarchicalLinks); - + // Process each proposed link foreach (var proposedLink in hierarchicalLinks) { var errorMessage = await ValidateLinkBasedOnType(currentApplicationId, proposedLink, currentAppError, hierarchicalLinks); - + if (!string.IsNullOrEmpty(errorMessage)) { result.ValidationErrors[proposedLink.ReferenceNumber] = true; @@ -468,31 +468,31 @@ public async Task ValidateApplicationLinksAsync result.ValidationErrors[proposedLink.ReferenceNumber] = false; } } - + return result; } - + private static string ValidateCurrentApplicationConstraints(List proposedLinks) { var parentCount = proposedLinks.Count(l => l.LinkType == ApplicationLinkType.Parent); - var hasParent = proposedLinks.Exists(l => l.LinkType == ApplicationLinkType.Parent); - var hasChild = proposedLinks.Exists(l => l.LinkType == ApplicationLinkType.Child); + var hasParent = proposedLinks.Exists(l => l.LinkType == ApplicationLinkType.Parent); + var hasChild = proposedLinks.Exists(l => l.LinkType == ApplicationLinkType.Child); if (parentCount > 1) { return ERROR_MULTIPLE_PARENTS; } - + if (hasParent && hasChild) { return ERROR_PARENT_WITH_CHILDREN; } - + return string.Empty; } - + private async Task ValidateLinkBasedOnType( - Guid currentApplicationId, + Guid currentApplicationId, ApplicationLinkValidationRequest proposedLink, string currentAppError, List allProposedLinks) @@ -505,25 +505,25 @@ private async Task ValidateLinkBasedOnType( { return currentAppError; } - + // Then check if the proposed parent is already a child of another app return await ValidateTargetCannotBeParentIfAlreadyChild(currentApplicationId, proposedLink); - + case ApplicationLinkType.Child: // Check if current app is trying to be both parent and child if (!string.IsNullOrEmpty(currentAppError) && allProposedLinks.Exists(l => l.LinkType == ApplicationLinkType.Parent)) { return ERROR_CURRENT_APP_IS_CHILD; } - + // Check target app conflicts return await ValidateTargetCanAcceptChildLink(currentApplicationId, proposedLink); - + default: return string.Empty; } } - + private async Task ValidateTargetCannotBeParentIfAlreadyChild(Guid currentApplicationId, ApplicationLinkValidationRequest proposedLink) { var targetLinks = await GetListByApplicationAsync(proposedLink.TargetApplicationId); @@ -537,29 +537,29 @@ private async Task ValidateTargetCannotBeParentIfAlreadyChild(Guid curre { return ERROR_TARGET_CHILD_CANNOT_BE_PARENT; } - + return string.Empty; } - + private async Task ValidateTargetCanAcceptChildLink(Guid currentApplicationId, ApplicationLinkValidationRequest proposedLink) { var targetLinks = await GetListByApplicationAsync(proposedLink.TargetApplicationId); // Exclude reverse links and self-references - var targetExternalLinks = targetLinks.Where(l => - l.ApplicationId != currentApplicationId && + var targetExternalLinks = targetLinks.Where(l => + l.ApplicationId != currentApplicationId && l.ApplicationId != proposedLink.TargetApplicationId).ToList(); - + if (targetExternalLinks.Exists(l => l.LinkType == ApplicationLinkType.Parent)) { return ERROR_TARGET_ALREADY_HAS_PARENT; } - + if (targetExternalLinks.Exists(l => l.LinkType == ApplicationLinkType.Child)) { return ERROR_TARGET_IS_PARENT_TO_OTHERS; } - + return string.Empty; } } \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs index 5b6df4c684..cca8d808c9 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs @@ -39,10 +39,10 @@ namespace Unity.GrantManager.GrantApplications; public class GrantApplicationAppService( IApplicationManager applicationManager, IApplicationRepository applicationRepository, - IApplicationStatusRepository applicationStatusRepository, + IApplicationStatusRepository applicationStatusRepository, IApplicationFormSubmissionRepository applicationFormSubmissionRepository, IApplicantRepository applicantRepository, - IApplicationFormRepository applicationFormRepository, + IApplicationFormRepository applicationFormRepository, IApplicantAgentRepository applicantAgentRepository, IApplicantAddressRepository applicantAddressRepository, IApplicantSupplierAppService applicantSupplierService, @@ -209,7 +209,7 @@ public async Task GetAsync(Guid id) public async Task GetApplicationFormAsync(Guid applicationFormId) { return await (await applicationFormRepository.GetQueryableAsync()).FirstOrDefaultAsync(s => s.Id == applicationFormId); - } + } [Authorize(UnitySelector.Review.AssessmentResults.Update.Default)] public async Task UpdateAssessmentResultsAsync(Guid id, CreateUpdateAssessmentResultsDto input) @@ -665,7 +665,7 @@ public async Task UpdateSupplierNumberAsync(Guid applicationId, string? supplier } return await applicantAgentRepository.UpdateAsync(applicantAgent); - } + } [Authorize(UnitySelector.Applicant.UpdatePolicy)] public async Task UpdateMergedApplicantAsync(Guid applicationId, CreateUpdateApplicantInfoDto input) @@ -810,7 +810,7 @@ public async Task UpdateApplicationStatus(Guid[] applicationIds, Guid statusId) Debug.WriteLine(ex.ToString()); } } - } + } public async Task> GetApplicationListAsync(List applicationIds) { From d729e49ea6a0ad1f8f6f1a1d5fbd8a29f8951b62 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:45:56 -0800 Subject: [PATCH 043/191] AB#31384 - Rename calculation objects from Payment Summary to Payment Rollup --- ...yDto.cs => ApplicationPaymentRollupDto.cs} | 2 +- .../IPaymentRequestAppService.cs | 4 +- .../IPaymentRequestRepository.cs | 2 +- .../Services/IPaymentRequestQueryManager.cs | 6 +- .../Services/PaymentRequestQueryManager.cs | 52 ++++++--- .../Repositories/PaymentRequestRepository.cs | 14 ++- .../PaymentRequestAppService.cs | 28 ++++- .../PaymentInfo/PaymentInfoViewComponent.cs | 6 +- ...equestQueryManager_PaymentRollup_Tests.cs} | 100 +++++++++--------- ...tRequestRepository_PaymentRollup_Tests.cs} | 48 ++++----- .../PaymentInfoViewComponentTests.cs | 38 +++---- .../GrantApplicationAppService.cs | 11 +- .../ApplicantSubmissionsViewComponent.cs | 10 +- 13 files changed, 188 insertions(+), 133 deletions(-) rename applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/{ApplicationPaymentSummaryDto.cs => ApplicationPaymentRollupDto.cs} (83%) rename applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/{PaymentRequestQueryManager_PaymentSummary_Tests.cs => PaymentRequestQueryManager_PaymentRollup_Tests.cs} (75%) rename applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/{PaymentRequestRepository_PaymentSummary_Tests.cs => PaymentRequestRepository_PaymentRollup_Tests.cs} (89%) diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/ApplicationPaymentSummaryDto.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/ApplicationPaymentRollupDto.cs similarity index 83% rename from applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/ApplicationPaymentSummaryDto.cs rename to applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/ApplicationPaymentRollupDto.cs index 0d6706572c..71053bd12f 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/ApplicationPaymentSummaryDto.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/ApplicationPaymentRollupDto.cs @@ -3,7 +3,7 @@ namespace Unity.Payments.PaymentRequests; [Serializable] -public class ApplicationPaymentSummaryDto +public class ApplicationPaymentRollupDto { public Guid ApplicationId { get; set; } public decimal TotalPaid { get; set; } diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/IPaymentRequestAppService.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/IPaymentRequestAppService.cs index ae1fc15a41..7d1295eee4 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/IPaymentRequestAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/IPaymentRequestAppService.cs @@ -21,7 +21,7 @@ public interface IPaymentRequestAppService : IApplicationService Task GetUserPaymentThresholdAsync(); Task ManuallyAddPaymentRequestsToReconciliationQueue(List paymentRequestIds); Task> GetPaymentPendingListByCorrelationIdAsync(Guid applicationId); - Task GetApplicationPaymentSummaryAsync(Guid applicationId); - Task> GetApplicationPaymentSummariesAsync(List applicationIds); + Task GetApplicationPaymentRollupAsync(Guid applicationId); + Task> GetApplicationPaymentRollupBatchAsync(List applicationIds); } } diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/PaymentRequests/IPaymentRequestRepository.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/PaymentRequests/IPaymentRequestRepository.cs index 9a27897a1f..e4d488fcfd 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/PaymentRequests/IPaymentRequestRepository.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/PaymentRequests/IPaymentRequestRepository.cs @@ -15,6 +15,6 @@ public interface IPaymentRequestRepository : IRepository Task GetPaymentRequestByInvoiceNumber(string invoiceNumber); Task> GetPaymentRequestsByFailedsStatusAsync(); Task> GetPaymentPendingListByCorrelationIdAsync(Guid correlationId); - Task> GetPaymentSummariesByCorrelationIdsAsync(List correlationIds); + Task> GetBatchPaymentRollupsByCorrelationIdsAsync(List correlationIds); } } diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/IPaymentRequestQueryManager.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/IPaymentRequestQueryManager.cs index d7c2c36cde..3578156da8 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/IPaymentRequestQueryManager.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/IPaymentRequestQueryManager.cs @@ -36,8 +36,8 @@ public interface IPaymentRequestQueryManager // Pending Payments Task> GetPaymentPendingListByCorrelationIdAsync(Guid applicationId); - // Payment Summaries (paid + pending aggregation) - Task GetApplicationPaymentSummaryAsync(Guid applicationId, List childApplicationIds); - Task> GetApplicationPaymentSummariesAsync(List applicationIds, Dictionary> childApplicationIdsByParent); + // Payment Rollups (paid + pending aggregation) + Task GetApplicationPaymentRollupAsync(Guid applicationId, List childApplicationIds); + Task> GetApplicationPaymentRollupBatchAsync(List applicationIds, Dictionary> childApplicationIdsByParent); } } diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/PaymentRequestQueryManager.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/PaymentRequestQueryManager.cs index b7b76ddcb2..30a4fdb7a7 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/PaymentRequestQueryManager.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/PaymentRequestQueryManager.cs @@ -223,14 +223,25 @@ public async Task> GetPaymentPendingListByCorrelationIdA return objectMapper.Map, List>(payments); } - public async Task GetApplicationPaymentSummaryAsync(Guid applicationId, List childApplicationIds) + /// + /// Retrieves a payment rollup for the specified application and its associated child applications. + /// + /// This method combines payment information from both the main application and its child + /// applications, providing an overall view of payment status. Use this method to obtain a single rollup when + /// displaying applications with related child records. + /// The unique identifier of the main application for which the payment rollup is requested. + /// A list of unique identifiers representing child applications whose payment data will be included in the + /// rollup. Cannot be null. + /// An instance of containing the aggregated total paid and pending + /// amounts for the main application and its child applications. + public async Task GetApplicationPaymentRollupAsync(Guid applicationId, List childApplicationIds) { var allCorrelationIds = new List { applicationId }; allCorrelationIds.AddRange(childApplicationIds); - var summaries = await paymentRequestRepository.GetPaymentSummariesByCorrelationIdsAsync(allCorrelationIds); + var summaries = await paymentRequestRepository.GetBatchPaymentRollupsByCorrelationIdsAsync(allCorrelationIds); - return new ApplicationPaymentSummaryDto + return new ApplicationPaymentRollupDto { ApplicationId = applicationId, TotalPaid = summaries.Sum(s => s.TotalPaid), @@ -238,7 +249,20 @@ public async Task GetApplicationPaymentSummaryAsyn }; } - public async Task> GetApplicationPaymentSummariesAsync( + /// + /// Retrieves batch payment rollup information for the specified application IDs, aggregating totals + /// for both paid and pending amounts from parent and child applications. + /// + /// This method performs a single database query to efficiently aggregate payment data + /// for all specified parent and child applications. The results include all relevant payment information + /// for each application ID provided. + /// A list of unique identifiers for the applications whose payment rollups are to be retrieved. Cannot be + /// null or empty. + /// A dictionary that maps each parent application ID to a list of its child application IDs. Must not be null + /// and should contain valid GUIDs. + /// A dictionary where each key is an application ID and the value is an containing + /// the total paid and pending amounts for that application, including amounts from any child applications. + public async Task> GetApplicationPaymentRollupBatchAsync( List applicationIds, Dictionary> childApplicationIdsByParent) { @@ -252,20 +276,20 @@ public async Task> GetApplication } } - var summaries = await paymentRequestRepository.GetPaymentSummariesByCorrelationIdsAsync(allCorrelationIds.ToList()); - var summaryLookup = summaries.ToDictionary(s => s.ApplicationId); + var paymentRollups = await paymentRequestRepository.GetBatchPaymentRollupsByCorrelationIdsAsync(allCorrelationIds.ToList()); + var rollupLookup = paymentRollups.ToDictionary(s => s.ApplicationId); - var result = new Dictionary(); + var result = new Dictionary(); foreach (var applicationId in applicationIds) { decimal totalPaid = 0; decimal totalPending = 0; // Add the parent application's own amounts - if (summaryLookup.TryGetValue(applicationId, out var parentSummary)) + if (rollupLookup.TryGetValue(applicationId, out var parentRollup)) { - totalPaid += parentSummary.TotalPaid; - totalPending += parentSummary.TotalPending; + totalPaid += parentRollup.TotalPaid; + totalPending += parentRollup.TotalPending; } // Add child application amounts @@ -273,15 +297,15 @@ public async Task> GetApplication { foreach (var childId in childIds) { - if (summaryLookup.TryGetValue(childId, out var childSummary)) + if (rollupLookup.TryGetValue(childId, out var childApplicationRollup)) { - totalPaid += childSummary.TotalPaid; - totalPending += childSummary.TotalPending; + totalPaid += childApplicationRollup.TotalPaid; + totalPending += childApplicationRollup.TotalPending; } } } - result[applicationId] = new ApplicationPaymentSummaryDto + result[applicationId] = new ApplicationPaymentRollupDto { ApplicationId = applicationId, TotalPaid = totalPaid, diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/EntityFrameworkCore/Repositories/PaymentRequestRepository.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/EntityFrameworkCore/Repositories/PaymentRequestRepository.cs index 8c89f2be63..95ecbb5c3a 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/EntityFrameworkCore/Repositories/PaymentRequestRepository.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/EntityFrameworkCore/Repositories/PaymentRequestRepository.cs @@ -96,17 +96,27 @@ public async Task> GetPaymentPendingListByCorrelationIdAsyn .ToListAsync(); } + /// + /// Asynchronously retrieves payment rollup information for each specified correlation ID. + /// + /// This method queries the database for payment records associated with the provided + /// correlation IDs and aggregates payment amounts based on their status. Ensure that the correlation IDs are + /// valid to avoid empty results. + /// A list of correlation IDs used to filter payment records. Each ID must be a valid GUID. + /// A task that represents the asynchronous operation. The task result contains a list of + /// ApplicationPaymentRollupDto objects, each summarizing the total paid and total pending amounts for the + /// corresponding correlation ID. [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1862:Use the 'StringComparison' method overloads to perform case-insensitive string comparisons", Justification = "EF Core does not support StringComparison - https://github.com/dotnet/efcore/issues/1222")] - public async Task> GetPaymentSummariesByCorrelationIdsAsync(List correlationIds) + public async Task> GetBatchPaymentRollupsByCorrelationIdsAsync(List correlationIds) { var dbSet = await GetDbSetAsync(); var results = await dbSet .Where(p => correlationIds.Contains(p.CorrelationId)) .GroupBy(p => p.CorrelationId) - .Select(g => new ApplicationPaymentSummaryDto + .Select(g => new ApplicationPaymentRollupDto { ApplicationId = g.Key, TotalPaid = g diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/PaymentRequestAppService.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/PaymentRequestAppService.cs index c9c33f89ca..8af0d4d534 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/PaymentRequestAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/PaymentRequestAppService.cs @@ -331,17 +331,37 @@ public async Task> GetPaymentPendingListByCorrelationIdA return await paymentRequestQueryManager.GetPaymentPendingListByCorrelationIdAsync(applicationId); } - public async Task GetApplicationPaymentSummaryAsync(Guid applicationId) + /// + /// Retrieves the payment rollup for the specified application, including any linked child + /// applications. + /// + /// This method first obtains any child applications associated with the given + /// application ID and then queries the payment rollup for both the application and its children. Use this + /// method to obtain a comprehensive payment overview when applications may be linked together. + /// The unique identifier of the application for which the payment rollup is requested. + /// An instance of containing the payment rollup details for the + /// specified application and its linked child applications. + public async Task GetApplicationPaymentRollupAsync(Guid applicationId) { var childLinks = await applicationLinksService.Value.GetChildApplications(applicationId); var childApplicationIds = childLinks.Select(l => l.LinkedApplicationId).ToList(); - return await paymentRequestQueryManager.GetApplicationPaymentSummaryAsync(applicationId, childApplicationIds); + return await paymentRequestQueryManager.GetApplicationPaymentRollupAsync(applicationId, childApplicationIds); } - public async Task> GetApplicationPaymentSummariesAsync(List applicationIds) + /// + /// Retrieves batch payment rollup information for the specified applications and their child applications. + /// + /// This method asynchronously resolves child applications linked to the provided + /// application identifiers before fetching a batch of payment rollups. Ensure that all application identifiers are valid + /// and exist in the system. + /// A list of application identifiers for which payment rollups are requested. This parameter cannot be null + /// or empty. + /// A dictionary mapping each application identifier to its corresponding . The dictionary + /// includes entries for both the specified applications and their child applications. + public async Task> GetApplicationPaymentRollupBatchAsync(List applicationIds) { var childApplicationIdsByParent = await applicationLinksService.Value.GetChildApplicationIdsByParentIdsAsync(applicationIds); - return await paymentRequestQueryManager.GetApplicationPaymentSummariesAsync(applicationIds, childApplicationIdsByParent); + return await paymentRequestQueryManager.GetApplicationPaymentRollupBatchAsync(applicationIds, childApplicationIdsByParent); } } } diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/PaymentInfoViewComponent.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/PaymentInfoViewComponent.cs index f6db79533e..32ddf14fee 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/PaymentInfoViewComponent.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/PaymentInfoViewComponent.cs @@ -46,9 +46,9 @@ public async Task InvokeAsync(Guid applicationId, Guid app ApplicantId = application.Applicant.Id }; - var summary = await _paymentRequestService.GetApplicationPaymentSummaryAsync(applicationId); - model.TotalPaid = summary.TotalPaid; - model.TotalPendingAmounts = summary.TotalPending; + var rollup = await _paymentRequestService.GetApplicationPaymentRollupAsync(applicationId); + model.TotalPaid = rollup.TotalPaid; + model.TotalPendingAmounts = rollup.TotalPending; model.RemainingAmount = application.ApprovedAmount - model.TotalPaid; return View(model); diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequestQueryManager_PaymentSummary_Tests.cs b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequestQueryManager_PaymentRollup_Tests.cs similarity index 75% rename from applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequestQueryManager_PaymentSummary_Tests.cs rename to applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequestQueryManager_PaymentRollup_Tests.cs index d97ab38b32..49c2c4bd1d 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequestQueryManager_PaymentSummary_Tests.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequestQueryManager_PaymentRollup_Tests.cs @@ -14,18 +14,18 @@ namespace Unity.Payments.Domain.PaymentRequests; [Category("Domain")] -public class PaymentRequestQueryManager_PaymentSummary_Tests +public class PaymentRequestQueryManager_PaymentRollup_Tests { - #region GetApplicationPaymentSummaryAsync (Single Application) + #region GetApplicationPaymentRollupAsync (Single Application) [Fact] - public async Task Should_Return_Summary_For_Single_Application_With_NoChildren() + public async Task Should_Return_Rollup_For_Single_Application_With_NoChildren() { // Arrange var appId = Guid.NewGuid(); var repo = Substitute.For(); - repo.GetPaymentSummariesByCorrelationIdsAsync(Arg.Any>()) - .Returns(new List + repo.GetBatchPaymentRollupsByCorrelationIdsAsync(Arg.Any>()) + .Returns(new List { new() { ApplicationId = appId, TotalPaid = 1500m, TotalPending = 2000m } }); @@ -33,7 +33,7 @@ public async Task Should_Return_Summary_For_Single_Application_With_NoChildren() var manager = CreateManager(repo); // Act - var result = await manager.GetApplicationPaymentSummaryAsync(appId, []); + var result = await manager.GetApplicationPaymentRollupAsync(appId, []); // Assert result.ShouldNotBeNull(); @@ -42,12 +42,12 @@ public async Task Should_Return_Summary_For_Single_Application_With_NoChildren() result.TotalPending.ShouldBe(2000m); // Verify repo was called with only the parent ID - await repo.Received(1).GetPaymentSummariesByCorrelationIdsAsync( + await repo.Received(1).GetBatchPaymentRollupsByCorrelationIdsAsync( Arg.Is>(ids => ids.Count == 1 && ids.Contains(appId))); } [Fact] - public async Task Should_Aggregate_Summary_From_Parent_And_Children() + public async Task Should_Aggregate_Rollup_From_Parent_And_Children() { // Arrange var parentId = Guid.NewGuid(); @@ -55,8 +55,8 @@ public async Task Should_Aggregate_Summary_From_Parent_And_Children() var child2Id = Guid.NewGuid(); var repo = Substitute.For(); - repo.GetPaymentSummariesByCorrelationIdsAsync(Arg.Any>()) - .Returns(new List + repo.GetBatchPaymentRollupsByCorrelationIdsAsync(Arg.Any>()) + .Returns(new List { new() { ApplicationId = parentId, TotalPaid = 1000m, TotalPending = 500m }, new() { ApplicationId = child1Id, TotalPaid = 300m, TotalPending = 200m }, @@ -66,7 +66,7 @@ public async Task Should_Aggregate_Summary_From_Parent_And_Children() var manager = CreateManager(repo); // Act - var result = await manager.GetApplicationPaymentSummaryAsync(parentId, [child1Id, child2Id]); + var result = await manager.GetApplicationPaymentRollupAsync(parentId, [child1Id, child2Id]); // Assert result.ShouldNotBeNull(); @@ -75,7 +75,7 @@ public async Task Should_Aggregate_Summary_From_Parent_And_Children() result.TotalPending.ShouldBe(800m); // 500 + 200 + 100 // Verify all IDs were sent to the repository - await repo.Received(1).GetPaymentSummariesByCorrelationIdsAsync( + await repo.Received(1).GetBatchPaymentRollupsByCorrelationIdsAsync( Arg.Is>(ids => ids.Count == 3 && ids.Contains(parentId) && ids.Contains(child1Id) && ids.Contains(child2Id))); } @@ -86,13 +86,13 @@ public async Task Should_Return_Zeros_When_No_Payment_Data_Exists() // Arrange var appId = Guid.NewGuid(); var repo = Substitute.For(); - repo.GetPaymentSummariesByCorrelationIdsAsync(Arg.Any>()) - .Returns(new List()); + repo.GetBatchPaymentRollupsByCorrelationIdsAsync(Arg.Any>()) + .Returns(new List()); var manager = CreateManager(repo); // Act - var result = await manager.GetApplicationPaymentSummaryAsync(appId, []); + var result = await manager.GetApplicationPaymentRollupAsync(appId, []); // Assert result.ShouldNotBeNull(); @@ -109,8 +109,8 @@ public async Task Should_Return_Zeros_When_Children_Have_No_Payments() var childId = Guid.NewGuid(); var repo = Substitute.For(); - repo.GetPaymentSummariesByCorrelationIdsAsync(Arg.Any>()) - .Returns(new List + repo.GetBatchPaymentRollupsByCorrelationIdsAsync(Arg.Any>()) + .Returns(new List { new() { ApplicationId = parentId, TotalPaid = 500m, TotalPending = 0m } // childId has no payments - not returned by repository @@ -119,7 +119,7 @@ public async Task Should_Return_Zeros_When_Children_Have_No_Payments() var manager = CreateManager(repo); // Act - var result = await manager.GetApplicationPaymentSummaryAsync(parentId, [childId]); + var result = await manager.GetApplicationPaymentRollupAsync(parentId, [childId]); // Assert result.TotalPaid.ShouldBe(500m); // Only parent amount @@ -134,8 +134,8 @@ public async Task Should_Handle_Single_Child_Application() var childId = Guid.NewGuid(); var repo = Substitute.For(); - repo.GetPaymentSummariesByCorrelationIdsAsync(Arg.Any>()) - .Returns(new List + repo.GetBatchPaymentRollupsByCorrelationIdsAsync(Arg.Any>()) + .Returns(new List { new() { ApplicationId = parentId, TotalPaid = 1000m, TotalPending = 0m }, new() { ApplicationId = childId, TotalPaid = 500m, TotalPending = 300m } @@ -144,7 +144,7 @@ public async Task Should_Handle_Single_Child_Application() var manager = CreateManager(repo); // Act - var result = await manager.GetApplicationPaymentSummaryAsync(parentId, [childId]); + var result = await manager.GetApplicationPaymentRollupAsync(parentId, [childId]); // Assert result.ApplicationId.ShouldBe(parentId); @@ -154,10 +154,10 @@ public async Task Should_Handle_Single_Child_Application() #endregion - #region GetApplicationPaymentSummariesAsync (Batch) + #region GetApplicationPaymentRollupsAsync (Batch) [Fact] - public async Task Should_Return_Batch_Summaries_For_Multiple_Applications_Without_Children() + public async Task Should_Return_Batch_Rollups_For_Multiple_Applications_Without_Children() { // Arrange var app1Id = Guid.NewGuid(); @@ -165,8 +165,8 @@ public async Task Should_Return_Batch_Summaries_For_Multiple_Applications_Withou var app3Id = Guid.NewGuid(); var repo = Substitute.For(); - repo.GetPaymentSummariesByCorrelationIdsAsync(Arg.Any>()) - .Returns(new List + repo.GetBatchPaymentRollupsByCorrelationIdsAsync(Arg.Any>()) + .Returns(new List { new() { ApplicationId = app1Id, TotalPaid = 1000m, TotalPending = 200m }, new() { ApplicationId = app2Id, TotalPaid = 500m, TotalPending = 100m }, @@ -176,7 +176,7 @@ public async Task Should_Return_Batch_Summaries_For_Multiple_Applications_Withou var manager = CreateManager(repo); // Act - var result = await manager.GetApplicationPaymentSummariesAsync( + var result = await manager.GetApplicationPaymentRollupBatchAsync( [app1Id, app2Id, app3Id], new Dictionary>()); @@ -191,7 +191,7 @@ public async Task Should_Return_Batch_Summaries_For_Multiple_Applications_Withou } [Fact] - public async Task Should_Aggregate_Child_Amounts_In_Batch_Summaries() + public async Task Should_Aggregate_Child_Amounts_In_Batch_Rollup() { // Arrange var parentAId = Guid.NewGuid(); @@ -207,8 +207,8 @@ public async Task Should_Aggregate_Child_Amounts_In_Batch_Summaries() }; var repo = Substitute.For(); - repo.GetPaymentSummariesByCorrelationIdsAsync(Arg.Any>()) - .Returns(new List + repo.GetBatchPaymentRollupsByCorrelationIdsAsync(Arg.Any>()) + .Returns(new List { new() { ApplicationId = parentAId, TotalPaid = 1000m, TotalPending = 100m }, new() { ApplicationId = childA1Id, TotalPaid = 200m, TotalPending = 50m }, @@ -220,7 +220,7 @@ public async Task Should_Aggregate_Child_Amounts_In_Batch_Summaries() var manager = CreateManager(repo); // Act - var result = await manager.GetApplicationPaymentSummariesAsync( + var result = await manager.GetApplicationPaymentRollupBatchAsync( [parentAId, parentBId], childMap); // Assert @@ -238,15 +238,15 @@ public async Task Should_Aggregate_Child_Amounts_In_Batch_Summaries() } [Fact] - public async Task Should_Handle_Application_With_No_Matching_Summary_In_Batch() + public async Task Should_Handle_Application_With_No_Matching_Rollup_In_Batch() { // Arrange var app1Id = Guid.NewGuid(); var app2Id = Guid.NewGuid(); var repo = Substitute.For(); - repo.GetPaymentSummariesByCorrelationIdsAsync(Arg.Any>()) - .Returns(new List + repo.GetBatchPaymentRollupsByCorrelationIdsAsync(Arg.Any>()) + .Returns(new List { // Only app1 has payment data, app2 doesn't new() { ApplicationId = app1Id, TotalPaid = 1000m, TotalPending = 500m } @@ -255,7 +255,7 @@ public async Task Should_Handle_Application_With_No_Matching_Summary_In_Batch() var manager = CreateManager(repo); // Act - var result = await manager.GetApplicationPaymentSummariesAsync( + var result = await manager.GetApplicationPaymentRollupBatchAsync( [app1Id, app2Id], new Dictionary>()); @@ -285,8 +285,8 @@ public async Task Should_Deduplicate_CorrelationIds_In_Batch_Repository_Call() }; var repo = Substitute.For(); - repo.GetPaymentSummariesByCorrelationIdsAsync(Arg.Any>()) - .Returns(new List + repo.GetBatchPaymentRollupsByCorrelationIdsAsync(Arg.Any>()) + .Returns(new List { new() { ApplicationId = parentAId, TotalPaid = 100m, TotalPending = 0m }, new() { ApplicationId = parentBId, TotalPaid = 200m, TotalPending = 0m }, @@ -296,12 +296,12 @@ public async Task Should_Deduplicate_CorrelationIds_In_Batch_Repository_Call() var manager = CreateManager(repo); // Act - var result = await manager.GetApplicationPaymentSummariesAsync( + var result = await manager.GetApplicationPaymentRollupBatchAsync( [parentAId, parentBId], childMap); // Assert // Verify repository was called with deduplicated IDs (3 unique, not 4) - await repo.Received(1).GetPaymentSummariesByCorrelationIdsAsync( + await repo.Received(1).GetBatchPaymentRollupsByCorrelationIdsAsync( Arg.Is>(ids => ids.Distinct().Count() == 3)); // Both parents should include the shared child's amounts @@ -325,16 +325,16 @@ public async Task Should_Make_Single_Repository_Call_For_Batch() }; var repo = Substitute.For(); - repo.GetPaymentSummariesByCorrelationIdsAsync(Arg.Any>()) - .Returns(new List()); + repo.GetBatchPaymentRollupsByCorrelationIdsAsync(Arg.Any>()) + .Returns(new List()); var manager = CreateManager(repo); // Act - await manager.GetApplicationPaymentSummariesAsync([app1Id, app2Id], childMap); + await manager.GetApplicationPaymentRollupBatchAsync([app1Id, app2Id], childMap); // Assert - should only call repository once (batch optimization) - await repo.Received(1).GetPaymentSummariesByCorrelationIdsAsync(Arg.Any>()); + await repo.Received(1).GetBatchPaymentRollupsByCorrelationIdsAsync(Arg.Any>()); } [Fact] @@ -342,13 +342,13 @@ public async Task Should_Return_Empty_Dictionary_For_Empty_Application_List() { // Arrange var repo = Substitute.For(); - repo.GetPaymentSummariesByCorrelationIdsAsync(Arg.Any>()) - .Returns(new List()); + repo.GetBatchPaymentRollupsByCorrelationIdsAsync(Arg.Any>()) + .Returns(new List()); var manager = CreateManager(repo); // Act - var result = await manager.GetApplicationPaymentSummariesAsync( + var result = await manager.GetApplicationPaymentRollupBatchAsync( [], new Dictionary>()); // Assert @@ -371,8 +371,8 @@ public async Task Should_Handle_Parent_Without_Children_In_Mixed_Batch() }; var repo = Substitute.For(); - repo.GetPaymentSummariesByCorrelationIdsAsync(Arg.Any>()) - .Returns(new List + repo.GetBatchPaymentRollupsByCorrelationIdsAsync(Arg.Any>()) + .Returns(new List { new() { ApplicationId = app1Id, TotalPaid = 1000m, TotalPending = 0m }, new() { ApplicationId = childId, TotalPaid = 500m, TotalPending = 100m }, @@ -382,7 +382,7 @@ public async Task Should_Handle_Parent_Without_Children_In_Mixed_Batch() var manager = CreateManager(repo); // Act - var result = await manager.GetApplicationPaymentSummariesAsync( + var result = await manager.GetApplicationPaymentRollupBatchAsync( [app1Id, app2Id], childMap); // Assert @@ -403,8 +403,8 @@ private static PaymentRequestQueryManager CreateManager(IPaymentRequestRepositor repo, Substitute.For(), Substitute.For(), - null!, // CasPaymentRequestCoordinator - not used by summary methods - null! // IObjectMapper - not used by summary methods + null!, // CasPaymentRequestCoordinator - not used by Rollup methods + null! // IObjectMapper - not used by Rollup methods ); } diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequestRepository_PaymentSummary_Tests.cs b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequestRepository_PaymentRollup_Tests.cs similarity index 89% rename from applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequestRepository_PaymentSummary_Tests.cs rename to applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequestRepository_PaymentRollup_Tests.cs index 0b8df1af66..17a7da11f0 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequestRepository_PaymentSummary_Tests.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequestRepository_PaymentRollup_Tests.cs @@ -12,13 +12,13 @@ namespace Unity.Payments.Domain.PaymentRequests; [Category("Integration")] -public class PaymentRequestRepository_PaymentSummary_Tests : PaymentsApplicationTestBase +public class PaymentRequestRepository_PaymentRollup_Tests : PaymentsApplicationTestBase { private readonly IPaymentRequestRepository _paymentRequestRepository; private readonly ISupplierRepository _supplierRepository; private readonly IUnitOfWorkManager _unitOfWorkManager; - public PaymentRequestRepository_PaymentSummary_Tests() + public PaymentRequestRepository_PaymentRollup_Tests() { _paymentRequestRepository = GetRequiredService(); _supplierRepository = GetRequiredService(); @@ -41,7 +41,7 @@ await InsertPaymentRequestAsync(siteId, correlationId, 1000m, // Act var results = await _paymentRequestRepository - .GetPaymentSummariesByCorrelationIdsAsync([correlationId]); + .GetBatchPaymentRollupsByCorrelationIdsAsync([correlationId]); // Assert results.Count.ShouldBe(1); @@ -63,7 +63,7 @@ await InsertPaymentRequestAsync(siteId, correlationId, 500m, // Act var results = await _paymentRequestRepository - .GetPaymentSummariesByCorrelationIdsAsync([correlationId]); + .GetBatchPaymentRollupsByCorrelationIdsAsync([correlationId]); // Assert results.Count.ShouldBe(1); @@ -84,7 +84,7 @@ await InsertPaymentRequestAsync(siteId, correlationId, 800m, // Act var results = await _paymentRequestRepository - .GetPaymentSummariesByCorrelationIdsAsync([correlationId]); + .GetBatchPaymentRollupsByCorrelationIdsAsync([correlationId]); // Assert results.Count.ShouldBe(1); @@ -105,7 +105,7 @@ await InsertPaymentRequestAsync(siteId, correlationId, 300m, // Act var results = await _paymentRequestRepository - .GetPaymentSummariesByCorrelationIdsAsync([correlationId]); + .GetBatchPaymentRollupsByCorrelationIdsAsync([correlationId]); // Assert results.Count.ShouldBe(1); @@ -132,7 +132,7 @@ await InsertPaymentRequestAsync(siteId, correlationId, 300m, // Act var results = await _paymentRequestRepository - .GetPaymentSummariesByCorrelationIdsAsync([correlationId]); + .GetBatchPaymentRollupsByCorrelationIdsAsync([correlationId]); // Assert results.Count.ShouldBe(1); @@ -153,7 +153,7 @@ await InsertPaymentRequestAsync(siteId, correlationId, 200m, // Act var results = await _paymentRequestRepository - .GetPaymentSummariesByCorrelationIdsAsync([correlationId]); + .GetBatchPaymentRollupsByCorrelationIdsAsync([correlationId]); // Assert results.Count.ShouldBe(1); @@ -182,7 +182,7 @@ await InsertPaymentRequestAsync(siteId, correlationId, 3000m, // Act var results = await _paymentRequestRepository - .GetPaymentSummariesByCorrelationIdsAsync([correlationId]); + .GetBatchPaymentRollupsByCorrelationIdsAsync([correlationId]); // Assert results.Count.ShouldBe(1); @@ -205,7 +205,7 @@ await InsertPaymentRequestAsync(siteId, correlationId, 300m, // Act var results = await _paymentRequestRepository - .GetPaymentSummariesByCorrelationIdsAsync([correlationId]); + .GetBatchPaymentRollupsByCorrelationIdsAsync([correlationId]); // Assert results.Count.ShouldBe(1); @@ -230,7 +230,7 @@ await InsertPaymentRequestAsync(siteId, correlationId, 200m, // Act var results = await _paymentRequestRepository - .GetPaymentSummariesByCorrelationIdsAsync([correlationId]); + .GetBatchPaymentRollupsByCorrelationIdsAsync([correlationId]); // Assert results.Count.ShouldBe(1); @@ -255,7 +255,7 @@ await InsertPaymentRequestAsync(siteId, correlationId, 3000m, // Act var results = await _paymentRequestRepository - .GetPaymentSummariesByCorrelationIdsAsync([correlationId]); + .GetBatchPaymentRollupsByCorrelationIdsAsync([correlationId]); // Assert results.Count.ShouldBe(1); @@ -292,7 +292,7 @@ await InsertPaymentRequestAsync(siteId, correlationId, 5000m, // Act var results = await _paymentRequestRepository - .GetPaymentSummariesByCorrelationIdsAsync([correlationId]); + .GetBatchPaymentRollupsByCorrelationIdsAsync([correlationId]); // Assert results.Count.ShouldBe(1); @@ -302,7 +302,7 @@ await InsertPaymentRequestAsync(siteId, correlationId, 5000m, [Fact] [Trait("Category", "Integration")] - public async Task Should_Return_Summaries_For_Multiple_CorrelationIds() + public async Task Should_Return_Rollup_For_Multiple_CorrelationIds() { // Arrange var app1Id = Guid.NewGuid(); @@ -323,20 +323,20 @@ await InsertPaymentRequestAsync(siteId, app2Id, 300m, // Act var results = await _paymentRequestRepository - .GetPaymentSummariesByCorrelationIdsAsync([app1Id, app2Id]); + .GetBatchPaymentRollupsByCorrelationIdsAsync([app1Id, app2Id]); // Assert results.Count.ShouldBe(2); - var app1Summary = results.Find(r => r.ApplicationId == app1Id); - app1Summary.ShouldNotBeNull(); - app1Summary!.TotalPaid.ShouldBe(1000m); - app1Summary.TotalPending.ShouldBe(500m); + var app1Rollup = results.Find(r => r.ApplicationId == app1Id); + app1Rollup.ShouldNotBeNull(); + app1Rollup!.TotalPaid.ShouldBe(1000m); + app1Rollup.TotalPending.ShouldBe(500m); - var app2Summary = results.Find(r => r.ApplicationId == app2Id); - app2Summary.ShouldNotBeNull(); - app2Summary!.TotalPaid.ShouldBe(2000m); - app2Summary.TotalPending.ShouldBe(300m); + var app2Rollup = results.Find(r => r.ApplicationId == app2Id); + app2Rollup.ShouldNotBeNull(); + app2Rollup!.TotalPaid.ShouldBe(2000m); + app2Rollup.TotalPending.ShouldBe(300m); } [Fact] @@ -346,7 +346,7 @@ public async Task Should_Return_Empty_For_Unknown_CorrelationIds() // Arrange & Act using var uow = _unitOfWorkManager.Begin(); var results = await _paymentRequestRepository - .GetPaymentSummariesByCorrelationIdsAsync([Guid.NewGuid()]); + .GetBatchPaymentRollupsByCorrelationIdsAsync([Guid.NewGuid()]); // Assert results.ShouldBeEmpty(); diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/ViewComponents/PaymentInfoViewComponentTests.cs b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/ViewComponents/PaymentInfoViewComponentTests.cs index 549a5cab2e..58d70e1c07 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/ViewComponents/PaymentInfoViewComponentTests.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/ViewComponents/PaymentInfoViewComponentTests.cs @@ -24,7 +24,7 @@ public PaymentInfoViewComponentTests() } [Fact] - public async Task PaymentInfo_Should_Display_TotalPaid_And_TotalPending_From_Summary() + public async Task PaymentInfo_Should_Display_TotalPaid_And_TotalPending_From_Rollup() { // Arrange var applicationId = Guid.NewGuid(); @@ -40,7 +40,7 @@ public async Task PaymentInfo_Should_Display_TotalPaid_And_TotalPending_From_Sum Applicant = new GrantApplicationApplicantDto { Id = applicantId } }; - var summary = new ApplicationPaymentSummaryDto + var rollup = new ApplicationPaymentRollupDto { ApplicationId = applicationId, TotalPaid = 1500m, @@ -52,7 +52,7 @@ public async Task PaymentInfo_Should_Display_TotalPaid_And_TotalPending_From_Sum var featureChecker = Substitute.For(); appService.GetAsync(applicationId).Returns(applicationDto); - paymentRequestService.GetApplicationPaymentSummaryAsync(applicationId).Returns(summary); + paymentRequestService.GetApplicationPaymentRollupAsync(applicationId).Returns(rollup); featureChecker.IsEnabledAsync("Unity.Payments").Returns(true); var viewComponent = CreateViewComponent(appService, paymentRequestService, featureChecker); @@ -83,7 +83,7 @@ public async Task PaymentInfo_Should_Calculate_RemainingAmount_From_ApprovedAmou Applicant = new GrantApplicationApplicantDto { Id = applicantId } }; - var summary = new ApplicationPaymentSummaryDto + var rollup = new ApplicationPaymentRollupDto { ApplicationId = applicationId, TotalPaid = 3500m, @@ -95,7 +95,7 @@ public async Task PaymentInfo_Should_Calculate_RemainingAmount_From_ApprovedAmou var featureChecker = Substitute.For(); appService.GetAsync(applicationId).Returns(applicationDto); - paymentRequestService.GetApplicationPaymentSummaryAsync(applicationId).Returns(summary); + paymentRequestService.GetApplicationPaymentRollupAsync(applicationId).Returns(rollup); featureChecker.IsEnabledAsync("Unity.Payments").Returns(true); var viewComponent = CreateViewComponent(appService, paymentRequestService, featureChecker); @@ -110,10 +110,10 @@ public async Task PaymentInfo_Should_Calculate_RemainingAmount_From_ApprovedAmou } [Fact] - public async Task PaymentInfo_Should_Include_Child_Application_Amounts_Via_Summary() + public async Task PaymentInfo_Should_Include_Child_Application_Amounts_By_Rollup() { // The ViewComponent now delegates child aggregation to the service layer. - // This test verifies it correctly uses the pre-aggregated summary. + // This test verifies it correctly uses the pre-aggregated rollup. // Arrange var parentAppId = Guid.NewGuid(); var applicationFormVersionId = Guid.NewGuid(); @@ -126,8 +126,8 @@ public async Task PaymentInfo_Should_Include_Child_Application_Amounts_Via_Summa Applicant = new GrantApplicationApplicantDto { Id = applicantId } }; - // Summary includes parent + child amounts (pre-aggregated by service) - var summary = new ApplicationPaymentSummaryDto + // Rollup includes parent + child amounts (pre-aggregated by service) + var rollup = new ApplicationPaymentRollupDto { ApplicationId = parentAppId, TotalPaid = 2300m, // e.g., 1000 (parent) + 500 (child1) + 800 (child2) @@ -139,7 +139,7 @@ public async Task PaymentInfo_Should_Include_Child_Application_Amounts_Via_Summa var featureChecker = Substitute.For(); appService.GetAsync(parentAppId).Returns(applicationDto); - paymentRequestService.GetApplicationPaymentSummaryAsync(parentAppId).Returns(summary); + paymentRequestService.GetApplicationPaymentRollupAsync(parentAppId).Returns(rollup); featureChecker.IsEnabledAsync("Unity.Payments").Returns(true); var viewComponent = CreateViewComponent(appService, paymentRequestService, featureChecker); @@ -170,7 +170,7 @@ public async Task PaymentInfo_Should_Handle_Zero_Payments() Applicant = new GrantApplicationApplicantDto { Id = applicantId } }; - var summary = new ApplicationPaymentSummaryDto + var rollup = new ApplicationPaymentRollupDto { ApplicationId = appId, TotalPaid = 0m, @@ -182,7 +182,7 @@ public async Task PaymentInfo_Should_Handle_Zero_Payments() var featureChecker = Substitute.For(); appService.GetAsync(appId).Returns(applicationDto); - paymentRequestService.GetApplicationPaymentSummaryAsync(appId).Returns(summary); + paymentRequestService.GetApplicationPaymentRollupAsync(appId).Returns(rollup); featureChecker.IsEnabledAsync("Unity.Payments").Returns(true); var viewComponent = CreateViewComponent(appService, paymentRequestService, featureChecker); @@ -215,7 +215,7 @@ public async Task PaymentInfo_Should_Map_RequestedAmount_And_RecommendedAmount() Applicant = new GrantApplicationApplicantDto { Id = applicantId } }; - var summary = new ApplicationPaymentSummaryDto + var rollup = new ApplicationPaymentRollupDto { ApplicationId = appId, TotalPaid = 0m, @@ -227,7 +227,7 @@ public async Task PaymentInfo_Should_Map_RequestedAmount_And_RecommendedAmount() var featureChecker = Substitute.For(); appService.GetAsync(appId).Returns(applicationDto); - paymentRequestService.GetApplicationPaymentSummaryAsync(appId).Returns(summary); + paymentRequestService.GetApplicationPaymentRollupAsync(appId).Returns(rollup); featureChecker.IsEnabledAsync("Unity.Payments").Returns(true); var viewComponent = CreateViewComponent(appService, paymentRequestService, featureChecker); @@ -273,11 +273,11 @@ public async Task PaymentInfo_Should_Return_Empty_View_When_Feature_Disabled() // Verify no service calls were made await appService.DidNotReceive().GetAsync(Arg.Any()); - await paymentRequestService.DidNotReceive().GetApplicationPaymentSummaryAsync(Arg.Any()); + await paymentRequestService.DidNotReceive().GetApplicationPaymentRollupAsync(Arg.Any()); } [Fact] - public async Task PaymentInfo_Should_Call_GetApplicationPaymentSummaryAsync_With_ApplicationId() + public async Task PaymentInfo_Should_Call_GetApplicationPaymentRollupAsync_With_ApplicationId() { // Arrange var appId = Guid.NewGuid(); @@ -291,7 +291,7 @@ public async Task PaymentInfo_Should_Call_GetApplicationPaymentSummaryAsync_With Applicant = new GrantApplicationApplicantDto { Id = applicantId } }; - var summary = new ApplicationPaymentSummaryDto + var rollup = new ApplicationPaymentRollupDto { ApplicationId = appId, TotalPaid = 100m, @@ -303,7 +303,7 @@ public async Task PaymentInfo_Should_Call_GetApplicationPaymentSummaryAsync_With var featureChecker = Substitute.For(); appService.GetAsync(appId).Returns(applicationDto); - paymentRequestService.GetApplicationPaymentSummaryAsync(appId).Returns(summary); + paymentRequestService.GetApplicationPaymentRollupAsync(appId).Returns(rollup); featureChecker.IsEnabledAsync("Unity.Payments").Returns(true); var viewComponent = CreateViewComponent(appService, paymentRequestService, featureChecker); @@ -312,7 +312,7 @@ public async Task PaymentInfo_Should_Call_GetApplicationPaymentSummaryAsync_With await viewComponent.InvokeAsync(appId, applicationFormVersionId); // Assert - Verify the correct service method was called with the right ID - await paymentRequestService.Received(1).GetApplicationPaymentSummaryAsync(appId); + await paymentRequestService.Received(1).GetApplicationPaymentRollupAsync(appId); } private PaymentInfoViewComponent CreateViewComponent( diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs index cca8d808c9..159f773369 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs @@ -62,13 +62,14 @@ public async Task> GetListAsync(GrantApplica var applicationIds = applications.Select(a => a.Id).ToList(); + // 2️ Fetch payment rollup batch if feature enabled bool paymentsFeatureEnabled = await FeatureChecker.IsEnabledAsync(PaymentConsts.UnityPaymentsFeature); - Dictionary paymentSummaries = []; + Dictionary paymentRollupBatch = []; if (paymentsFeatureEnabled && applicationIds.Count > 0) { - paymentSummaries = await paymentRequestService.GetApplicationPaymentSummariesAsync(applicationIds); + paymentRollupBatch = await paymentRequestService.GetApplicationPaymentRollupBatchAsync(applicationIds); } // 3️ Map applications to DTOs @@ -94,13 +95,13 @@ public async Task> GetListAsync(GrantApplica appDto.ContactCellPhone = app.ApplicantAgent?.Phone2; appDto.ApplicationLinks = ObjectMapper.Map, List>(app.ApplicationLinks?.ToList() ?? []); - if (paymentsFeatureEnabled && paymentSummaries.Count > 0) + if (paymentsFeatureEnabled && paymentRollupBatch.Count > 0) { - paymentSummaries.TryGetValue(app.Id, out var summary); + paymentRollupBatch.TryGetValue(app.Id, out var rollup); appDto.PaymentInfo = new PaymentInfoDto { ApprovedAmount = app.ApprovedAmount, - TotalPaid = summary?.TotalPaid ?? 0 + TotalPaid = rollup?.TotalPaid ?? 0 }; } return appDto; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantSubmissions/ApplicantSubmissionsViewComponent.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantSubmissions/ApplicantSubmissionsViewComponent.cs index 59cd647b90..7225e32ceb 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantSubmissions/ApplicantSubmissionsViewComponent.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantSubmissions/ApplicantSubmissionsViewComponent.cs @@ -56,10 +56,10 @@ public async Task InvokeAsync(Guid applicantId) var applicationIds = applications.Select(app => app.Id).ToList(); var paymentsFeatureEnabled = await _featureChecker.IsEnabledAsync(PaymentConsts.UnityPaymentsFeature); - Dictionary paymentSummaries = []; + Dictionary paymentRollupBatch = []; if (paymentsFeatureEnabled && applicationIds.Count > 0) { - paymentSummaries = await _paymentRequestService.GetApplicationPaymentSummariesAsync(applicationIds); + paymentRollupBatch = await _paymentRequestService.GetApplicationPaymentRollupBatchAsync(applicationIds); } // Map to DTOs (similar to GrantApplicationAppService.GetListAsync) @@ -126,13 +126,13 @@ public async Task InvokeAsync(Guid applicantId) } dto.Assignees = assigneeDtos; - if (paymentsFeatureEnabled && paymentSummaries.Count > 0) + if (paymentsFeatureEnabled && paymentRollupBatch.Count > 0) { - paymentSummaries.TryGetValue(app.Id, out var summary); + paymentRollupBatch.TryGetValue(app.Id, out var paymentRollup); dto.PaymentInfo = new PaymentInfoDto { ApprovedAmount = app.ApprovedAmount, - TotalPaid = summary?.TotalPaid ?? 0 + TotalPaid = paymentRollup?.TotalPaid ?? 0 }; } From 9074cf2f86f1a77327fa3f61b7fc244cd0c9f149 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Tue, 24 Feb 2026 09:39:34 -0800 Subject: [PATCH 044/191] AB#32001 Update gitignore for only code-workspace --- .gitignore | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 5ccccd8b9e..cf56f6bd31 100644 --- a/.gitignore +++ b/.gitignore @@ -24,7 +24,6 @@ bld/ [Oo]bj/ [Ll]og/ [Ll]ogs/ -*.log # Visual Studio cache/options directory .vs/ @@ -118,7 +117,4 @@ appsettings.json /applications/Orchestrator *.env -/applications/Unity.GrantManager/src/Unity.GrantManager.Web/package-lock.json - -# Local dev artifacts -/AGENTS.md +/applications/Unity.GrantManager/src/Unity.GrantManager.Web/package-lock.json \ No newline at end of file From d746aad57961c31b673502ce380bbcd22896cdfe Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Tue, 24 Feb 2026 11:52:12 -0800 Subject: [PATCH 045/191] AB#32001 Cache JsonSerializerOptions for AI payload log formatting --- .../src/Unity.GrantManager.Application/AI/OpenAIService.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 742a9abe05..a412edf268 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs @@ -26,6 +26,7 @@ public class OpenAIService : IAIService, ITransientDependency private readonly string NoKeyError = "OpenAI API key is not configured"; private const string AiPromptLogRelativePath = "logs/ai-prompts.log"; private static int _aiPromptLogInitialized; + private static readonly JsonSerializerOptions IndentedJsonLogOptions = new() { WriteIndented = true }; public OpenAIService(HttpClient httpClient, IConfiguration configuration, ILogger logger, ITextExtractionService textExtractionService) { @@ -497,7 +498,7 @@ private static string FormatPromptOutputForLog(string output) if (TryParseJsonObjectFromResponse(output, out var jsonObject)) { - return JsonSerializer.Serialize(jsonObject, new JsonSerializerOptions { WriteIndented = true }); + return JsonSerializer.Serialize(jsonObject, IndentedJsonLogOptions); } return output.Trim(); From c92ea2105cd199c2c6e01d21a6c0e45c211fe1e4 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Tue, 24 Feb 2026 12:01:15 -0800 Subject: [PATCH 046/191] AB#32001 Address Copilot feedback for AI payload logging safety and JSON cleanup --- .../AI/OpenAIService.cs | 88 ++++++++++++++----- 1 file changed, 65 insertions(+), 23 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 a412edf268..64ef58a1e5 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs @@ -25,7 +25,8 @@ public class OpenAIService : IAIService, ITransientDependency private bool LogPayloads => _configuration.GetValue("AI:Logging:LogPayloads") ?? false; private readonly string NoKeyError = "OpenAI API key is not configured"; private const string AiPromptLogRelativePath = "logs/ai-prompts.log"; - private static int _aiPromptLogInitialized; + private static readonly SemaphoreSlim AiPromptLogWriteSemaphore = new(1, 1); + private static bool _aiPromptLogInitialized; private static readonly JsonSerializerOptions IndentedJsonLogOptions = new() { WriteIndented = true }; public OpenAIService(HttpClient httpClient, IConfiguration configuration, ILogger logger, ITextExtractionService textExtractionService) @@ -133,9 +134,9 @@ public async Task GenerateAttachmentSummaryAsync(string fileName, byte[] 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."; } - LogPromptInput("AttachmentSummary", prompt, contentToAnalyze); + await LogPromptInput("AttachmentSummary", prompt, contentToAnalyze); var modelOutput = await GenerateSummaryAsync(contentToAnalyze, prompt, 150); - LogPromptOutput("AttachmentSummary", modelOutput); + await LogPromptOutput("AttachmentSummary", modelOutput); return modelOutput; } catch (Exception ex) @@ -213,9 +214,9 @@ public async Task AnalyzeApplicationAsync(string applicationContent, Lis Respond only with valid JSON in the exact format requested."; - LogPromptInput("ApplicationAnalysis", systemPrompt, analysisContent); + await LogPromptInput("ApplicationAnalysis", systemPrompt, analysisContent); var rawAnalysis = await GenerateSummaryAsync(analysisContent, systemPrompt, 1000); - LogPromptOutput("ApplicationAnalysis", rawAnalysis); + await LogPromptOutput("ApplicationAnalysis", rawAnalysis); // Post-process the AI response to add unique IDs to errors and warnings return AddIdsToAnalysisItems(rawAnalysis); @@ -333,9 +334,9 @@ Base your answers on the application content and attachment summaries provided. Be thorough, objective, and fair in your assessment. Base your answers strictly on the provided application content. Respond only with valid JSON in the exact format requested."; - LogPromptInput("ScoresheetAll", systemPrompt, analysisContent); + await LogPromptInput("ScoresheetAll", systemPrompt, analysisContent); var modelOutput = await GenerateSummaryAsync(analysisContent, systemPrompt, 2000); - LogPromptOutput("ScoresheetAll", modelOutput); + await LogPromptOutput("ScoresheetAll", modelOutput); return modelOutput; } catch (Exception ex) @@ -410,9 +411,9 @@ Always provide citations that reference specific parts of the application conten 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."; - LogPromptInput("ScoresheetSection", systemPrompt, analysisContent); + await LogPromptInput("ScoresheetSection", systemPrompt, analysisContent); var modelOutput = await GenerateSummaryAsync(analysisContent, systemPrompt, 2000); - LogPromptOutput("ScoresheetSection", modelOutput); + await LogPromptOutput("ScoresheetSection", modelOutput); return modelOutput; } catch (Exception ex) @@ -422,7 +423,7 @@ Always provide citations that reference specific parts of the application conten } } - private void LogPromptInput(string promptType, string? systemPrompt, string userPrompt) + private async Task LogPromptInput(string promptType, string? systemPrompt, string userPrompt) { if (!LogPayloads) { @@ -431,10 +432,10 @@ private void LogPromptInput(string promptType, string? systemPrompt, string user var formattedInput = FormatPromptInputForLog(systemPrompt, userPrompt); _logger.LogDebug("AI {PromptType} input payload: {PromptInput}", promptType, formattedInput); - WriteAiPromptLog(promptType, "INPUT", formattedInput); + await WriteAiPromptLog(promptType, "INPUT", formattedInput); } - private void LogPromptOutput(string promptType, string output) + private async Task LogPromptOutput(string promptType, string output) { if (!LogPayloads) { @@ -443,10 +444,10 @@ private void LogPromptOutput(string promptType, string output) var formattedOutput = FormatPromptOutputForLog(output); _logger.LogDebug("AI {PromptType} model output payload: {ModelOutput}", promptType, formattedOutput); - WriteAiPromptLog(promptType, "OUTPUT", formattedOutput); + await WriteAiPromptLog(promptType, "OUTPUT", formattedOutput); } - private void WriteAiPromptLog(string promptType, string payloadType, string payload) + private async Task WriteAiPromptLog(string promptType, string payloadType, string payload) { if (!LogPayloads) { @@ -455,12 +456,20 @@ private void WriteAiPromptLog(string promptType, string payloadType, string payl try { - var now = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss zzz"); - var logPath = Path.Combine(AppContext.BaseDirectory, AiPromptLogRelativePath); - EnsureAiPromptLogInitialized(logPath); + await AiPromptLogWriteSemaphore.WaitAsync(); + try + { + var now = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss zzz"); + var logPath = Path.Combine(AppContext.BaseDirectory, AiPromptLogRelativePath); + EnsureAiPromptLogInitialized(logPath); - var entry = $"{now} [{promptType}] {payloadType}\n{payload}\n\n"; - File.AppendAllText(logPath, entry); + var entry = $"{now} [{promptType}] {payloadType}\n{payload}\n\n"; + await File.AppendAllTextAsync(logPath, entry); + } + finally + { + AiPromptLogWriteSemaphore.Release(); + } } catch (Exception ex) { @@ -470,16 +479,19 @@ private void WriteAiPromptLog(string promptType, string payloadType, string payl private static void EnsureAiPromptLogInitialized(string logPath) { + if (_aiPromptLogInitialized) + { + return; + } + var directory = Path.GetDirectoryName(logPath); if (!string.IsNullOrWhiteSpace(directory)) { Directory.CreateDirectory(directory); } - if (Interlocked.Exchange(ref _aiPromptLogInitialized, 1) == 0) - { - File.WriteAllText(logPath, string.Empty); - } + File.WriteAllText(logPath, string.Empty); + _aiPromptLogInitialized = true; } private static string FormatPromptInputForLog(string? systemPrompt, string userPrompt) @@ -544,8 +556,20 @@ private static string CleanJsonResponse(string response) var startIndex = cleaned.IndexOf('\n'); if (startIndex >= 0) { + // Multi-line fenced code block: remove everything up to and including the first newline. cleaned = cleaned[(startIndex + 1)..]; } + else + { + // Single-line fenced JSON, e.g. ```json { ... } ``` or ```{ ... } ```. + // Strip everything before the first likely JSON payload token. + var jsonStart = FindFirstJsonTokenIndex(cleaned); + + if (jsonStart > 0) + { + cleaned = cleaned[jsonStart..]; + } + } } if (cleaned.EndsWith("```", StringComparison.Ordinal)) @@ -559,5 +583,23 @@ private static string CleanJsonResponse(string response) return cleaned.Trim(); } + + private static int FindFirstJsonTokenIndex(string value) + { + var objectStart = value.IndexOf('{'); + var arrayStart = value.IndexOf('['); + + if (objectStart >= 0 && arrayStart >= 0) + { + return Math.Min(objectStart, arrayStart); + } + + if (objectStart >= 0) + { + return objectStart; + } + + return arrayStart; + } } } From e6662e8762273b872585c8327d805004293f50ec Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Tue, 24 Feb 2026 13:45:57 -0800 Subject: [PATCH 047/191] AB#32001 Refine AI payload logging gates and local file logging behavior --- .../AI/OpenAIService.cs | 107 ++++++++---------- 1 file changed, 47 insertions(+), 60 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 64ef58a1e5..122673051e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs @@ -7,7 +7,6 @@ using System.Net.Http; using System.Text; using System.Text.Json; -using System.Threading; using System.Threading.Tasks; using Volo.Abp.DependencyInjection; @@ -22,14 +21,20 @@ public class OpenAIService : IAIService, ITransientDependency private string? ApiKey => _configuration["AI:OpenAI:ApiKey"]; private string? ApiUrl => _configuration["AI:OpenAI:ApiUrl"] ?? "https://api.openai.com/v1/chat/completions"; - private bool LogPayloads => _configuration.GetValue("AI:Logging:LogPayloads") ?? false; - private readonly string NoKeyError = "OpenAI API key is not configured"; - private const string AiPromptLogRelativePath = "logs/ai-prompts.log"; - private static readonly SemaphoreSlim AiPromptLogWriteSemaphore = new(1, 1); - private static bool _aiPromptLogInitialized; - private static readonly JsonSerializerOptions IndentedJsonLogOptions = new() { WriteIndented = true }; - - public OpenAIService(HttpClient httpClient, IConfiguration configuration, ILogger logger, ITextExtractionService textExtractionService) + private readonly string MissingApiKeyMessage = "OpenAI API key is not configured"; + + private const string PromptLogDirectoryName = "logs"; + private static readonly string PromptLogFileName = $"ai-prompts-{DateTime.UtcNow:yyyyMMdd-HHmmss}-{Environment.ProcessId}.log"; + private bool IsPayloadLoggingEnabled => _configuration.GetValue("AI:Logging:LogPayloads") ?? false; + private bool IsPromptFileLoggingEnabled => _configuration.GetValue("AI:Logging:EnablePromptFileLog") ?? false; + + private static readonly JsonSerializerOptions JsonLogOptions = new() { WriteIndented = true }; + + public OpenAIService( + HttpClient httpClient, + IConfiguration configuration, + ILogger logger, + ITextExtractionService textExtractionService) { _httpClient = httpClient; _configuration = configuration; @@ -41,7 +46,7 @@ public Task IsAvailableAsync() { if (string.IsNullOrEmpty(ApiKey)) { - _logger.LogWarning("Error: {Message}", NoKeyError); + _logger.LogWarning("Error: {Message}", MissingApiKeyMessage); return Task.FromResult(false); } @@ -52,7 +57,7 @@ public async Task GenerateSummaryAsync(string content, string? prompt = { if (string.IsNullOrEmpty(ApiKey)) { - _logger.LogWarning("Error: {Message}", NoKeyError); + _logger.LogWarning("Error: {Message}", MissingApiKeyMessage); return "AI analysis not available - service not configured."; } @@ -134,9 +139,9 @@ public async Task GenerateAttachmentSummaryAsync(string fileName, byte[] 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."; } - await LogPromptInput("AttachmentSummary", prompt, contentToAnalyze); + await LogPromptInputAsync("AttachmentSummary", prompt, contentToAnalyze); var modelOutput = await GenerateSummaryAsync(contentToAnalyze, prompt, 150); - await LogPromptOutput("AttachmentSummary", modelOutput); + await LogPromptOutputAsync("AttachmentSummary", modelOutput); return modelOutput; } catch (Exception ex) @@ -150,7 +155,7 @@ public async Task AnalyzeApplicationAsync(string applicationContent, Lis { if (string.IsNullOrEmpty(ApiKey)) { - _logger.LogWarning("{Message}", NoKeyError); + _logger.LogWarning("{Message}", MissingApiKeyMessage); return "AI analysis not available - service not configured."; } @@ -214,9 +219,9 @@ public async Task AnalyzeApplicationAsync(string applicationContent, Lis Respond only with valid JSON in the exact format requested."; - await LogPromptInput("ApplicationAnalysis", systemPrompt, analysisContent); + await LogPromptInputAsync("ApplicationAnalysis", systemPrompt, analysisContent); var rawAnalysis = await GenerateSummaryAsync(analysisContent, systemPrompt, 1000); - await LogPromptOutput("ApplicationAnalysis", rawAnalysis); + await LogPromptOutputAsync("ApplicationAnalysis", rawAnalysis); // Post-process the AI response to add unique IDs to errors and warnings return AddIdsToAnalysisItems(rawAnalysis); @@ -293,7 +298,7 @@ public async Task GenerateScoresheetAnswersAsync(string applicationConte { if (string.IsNullOrEmpty(ApiKey)) { - _logger.LogWarning("{Message}", NoKeyError); + _logger.LogWarning("{Message}", MissingApiKeyMessage); return "{}"; } @@ -334,9 +339,9 @@ Base your answers on the application content and attachment summaries provided. Be thorough, objective, and fair in your assessment. Base your answers strictly on the provided application content. Respond only with valid JSON in the exact format requested."; - await LogPromptInput("ScoresheetAll", systemPrompt, analysisContent); + await LogPromptInputAsync("ScoresheetAll", systemPrompt, analysisContent); var modelOutput = await GenerateSummaryAsync(analysisContent, systemPrompt, 2000); - await LogPromptOutput("ScoresheetAll", modelOutput); + await LogPromptOutputAsync("ScoresheetAll", modelOutput); return modelOutput; } catch (Exception ex) @@ -350,7 +355,7 @@ public async Task GenerateScoresheetSectionAnswersAsync(string applicati { if (string.IsNullOrEmpty(ApiKey)) { - _logger.LogWarning("{Message}", NoKeyError); + _logger.LogWarning("{Message}", MissingApiKeyMessage); return "{}"; } @@ -411,9 +416,9 @@ Always provide citations that reference specific parts of the application conten 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."; - await LogPromptInput("ScoresheetSection", systemPrompt, analysisContent); + await LogPromptInputAsync("ScoresheetSection", systemPrompt, analysisContent); var modelOutput = await GenerateSummaryAsync(analysisContent, systemPrompt, 2000); - await LogPromptOutput("ScoresheetSection", modelOutput); + await LogPromptOutputAsync("ScoresheetSection", modelOutput); return modelOutput; } catch (Exception ex) @@ -423,53 +428,46 @@ Always provide citations that reference specific parts of the application conten } } - private async Task LogPromptInput(string promptType, string? systemPrompt, string userPrompt) + private async Task LogPromptInputAsync(string promptType, string? systemPrompt, string userPrompt) { - if (!LogPayloads) + if (!IsPayloadLoggingEnabled) { return; } var formattedInput = FormatPromptInputForLog(systemPrompt, userPrompt); - _logger.LogDebug("AI {PromptType} input payload: {PromptInput}", promptType, formattedInput); - await WriteAiPromptLog(promptType, "INPUT", formattedInput); + _logger.LogInformation("AI {PromptType} input payload: {PromptInput}", promptType, formattedInput); + await WritePromptLogFileAsync(promptType, "INPUT", formattedInput); } - private async Task LogPromptOutput(string promptType, string output) + private async Task LogPromptOutputAsync(string promptType, string output) { - if (!LogPayloads) + if (!IsPayloadLoggingEnabled) { return; } var formattedOutput = FormatPromptOutputForLog(output); - _logger.LogDebug("AI {PromptType} model output payload: {ModelOutput}", promptType, formattedOutput); - await WriteAiPromptLog(promptType, "OUTPUT", formattedOutput); + _logger.LogInformation("AI {PromptType} model output payload: {ModelOutput}", promptType, formattedOutput); + await WritePromptLogFileAsync(promptType, "OUTPUT", formattedOutput); } - private async Task WriteAiPromptLog(string promptType, string payloadType, string payload) + private async Task WritePromptLogFileAsync(string promptType, string payloadType, string payload) { - if (!LogPayloads) + if (!CanWritePromptFileLog()) { return; } try { - await AiPromptLogWriteSemaphore.WaitAsync(); - try - { - var now = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss zzz"); - var logPath = Path.Combine(AppContext.BaseDirectory, AiPromptLogRelativePath); - EnsureAiPromptLogInitialized(logPath); + var now = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss zzz"); + var logDirectory = Path.Combine(AppContext.BaseDirectory, PromptLogDirectoryName); + Directory.CreateDirectory(logDirectory); - var entry = $"{now} [{promptType}] {payloadType}\n{payload}\n\n"; - await File.AppendAllTextAsync(logPath, entry); - } - finally - { - AiPromptLogWriteSemaphore.Release(); - } + var logPath = Path.Combine(logDirectory, PromptLogFileName); + var entry = $"{now} [{promptType}] {payloadType}\n{payload}\n\n"; + await File.AppendAllTextAsync(logPath, entry); } catch (Exception ex) { @@ -477,21 +475,10 @@ private async Task WriteAiPromptLog(string promptType, string payloadType, strin } } - private static void EnsureAiPromptLogInitialized(string logPath) + private bool CanWritePromptFileLog() { - if (_aiPromptLogInitialized) - { - return; - } - - var directory = Path.GetDirectoryName(logPath); - if (!string.IsNullOrWhiteSpace(directory)) - { - Directory.CreateDirectory(directory); - } - - File.WriteAllText(logPath, string.Empty); - _aiPromptLogInitialized = true; + return IsPayloadLoggingEnabled + && IsPromptFileLoggingEnabled; } private static string FormatPromptInputForLog(string? systemPrompt, string userPrompt) @@ -510,7 +497,7 @@ private static string FormatPromptOutputForLog(string output) if (TryParseJsonObjectFromResponse(output, out var jsonObject)) { - return JsonSerializer.Serialize(jsonObject, IndentedJsonLogOptions); + return JsonSerializer.Serialize(jsonObject, JsonLogOptions); } return output.Trim(); From 60b681b23428bbc85eb00b2e6190f6ee46269c6c Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Tue, 24 Feb 2026 14:14:49 -0800 Subject: [PATCH 048/191] AB#32001 AI logging single local file flag with comment --- .../AI/OpenAIService.cs | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 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 122673051e..2d343c739a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs @@ -23,10 +23,11 @@ public class OpenAIService : IAIService, ITransientDependency private string? ApiUrl => _configuration["AI:OpenAI:ApiUrl"] ?? "https://api.openai.com/v1/chat/completions"; private readonly string MissingApiKeyMessage = "OpenAI API key is not configured"; + // Optional local debugging sink for prompt payload logs to a local file. + // Not intended for deployed/shared environments. + private bool IsPromptFileLoggingEnabled => _configuration.GetValue("AI:Logging:EnablePromptFileLog") ?? false; private const string PromptLogDirectoryName = "logs"; private static readonly string PromptLogFileName = $"ai-prompts-{DateTime.UtcNow:yyyyMMdd-HHmmss}-{Environment.ProcessId}.log"; - private bool IsPayloadLoggingEnabled => _configuration.GetValue("AI:Logging:LogPayloads") ?? false; - private bool IsPromptFileLoggingEnabled => _configuration.GetValue("AI:Logging:EnablePromptFileLog") ?? false; private static readonly JsonSerializerOptions JsonLogOptions = new() { WriteIndented = true }; @@ -430,11 +431,6 @@ Always provide citations that reference specific parts of the application conten private async Task LogPromptInputAsync(string promptType, string? systemPrompt, string userPrompt) { - if (!IsPayloadLoggingEnabled) - { - return; - } - var formattedInput = FormatPromptInputForLog(systemPrompt, userPrompt); _logger.LogInformation("AI {PromptType} input payload: {PromptInput}", promptType, formattedInput); await WritePromptLogFileAsync(promptType, "INPUT", formattedInput); @@ -442,11 +438,6 @@ private async Task LogPromptInputAsync(string promptType, string? systemPrompt, private async Task LogPromptOutputAsync(string promptType, string output) { - if (!IsPayloadLoggingEnabled) - { - return; - } - var formattedOutput = FormatPromptOutputForLog(output); _logger.LogInformation("AI {PromptType} model output payload: {ModelOutput}", promptType, formattedOutput); await WritePromptLogFileAsync(promptType, "OUTPUT", formattedOutput); @@ -477,8 +468,7 @@ private async Task WritePromptLogFileAsync(string promptType, string payloadType private bool CanWritePromptFileLog() { - return IsPayloadLoggingEnabled - && IsPromptFileLoggingEnabled; + return IsPromptFileLoggingEnabled; } private static string FormatPromptInputForLog(string? systemPrompt, string userPrompt) From 6f0ca6de6e4e0bd6ddaddf200513675fd64c4cc5 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Tue, 24 Feb 2026 14:32:08 -0800 Subject: [PATCH 049/191] AB#001 Fix nullability warnings --- .../Unity.GrantManager.Application/AI/OpenAIService.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 2d343c739a..1ea3e93608 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs @@ -67,13 +67,14 @@ public async Task GenerateSummaryAsync(string content, string? prompt = try { var systemPrompt = prompt ?? "You are a professional grant analyst for the BC Government."; + var userPrompt = content ?? string.Empty; var requestBody = new { messages = new[] { new { role = "system", content = systemPrompt }, - new { role = "user", content = content } + new { role = "user", content = userPrompt } }, max_tokens = maxTokens, temperature = 0.3 @@ -99,6 +100,11 @@ public async Task GenerateSummaryAsync(string content, string? prompt = return "AI analysis failed - service temporarily unavailable."; } + if (string.IsNullOrWhiteSpace(responseContent)) + { + return "No summary generated."; + } + using var jsonDoc = JsonDocument.Parse(responseContent); var choices = jsonDoc.RootElement.GetProperty("choices"); if (choices.GetArrayLength() > 0) From 31b4a29b3c88398aca0538c4f256bfe8e945039c Mon Sep 17 00:00:00 2001 From: David Bright Date: Tue, 24 Feb 2026 14:43:07 -0800 Subject: [PATCH 050/191] Quick Date Range now displayed with blue border to match the original date range fields --- .../Views/Shared/Components/ActionBar/Default.cshtml | 2 +- .../Views/Shared/Components/ActionBar/Default.css | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.cshtml index bb425251da..1134892b19 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.cshtml @@ -18,7 +18,7 @@
- diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.css index cca218855d..5ccbfa7afb 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.css +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.css @@ -43,3 +43,11 @@ padding: 0px; margin: 0px; } + +.quick-date-input { + font-size: var(--bc-font-size); + color: var(--bc-colors-grey-text-500); + border-radius: var(--bc-layout-margin-small) !important; + border: 2px solid var(--bc-colors-blue-primary); + text-overflow: ellipsis; +} From ea29ca110a8c6506710e01a753488134950bba17 Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Tue, 24 Feb 2026 15:29:50 -0800 Subject: [PATCH 051/191] AB#30430 applicant profile address info provider --- .../ProfileData/AddressInfoItemDto.cs | 20 + .../ProfileData/ApplicantAddressInfoDto.cs | 4 + .../AddressInfoDataProvider.cs | 112 ++++- .../ApplicantProfile/OrgInfoDataProvider.cs | 6 + .../PaymentInfoDataProvider.cs | 6 + .../SubmissionInfoDataProvider.cs | 5 + .../AddressInfoDataProviderTests.cs | 437 ++++++++++++++++++ .../ApplicantProfileDataProviderTests.cs | 19 +- .../SubmissionInfoDataProviderTests.cs | 319 +++++++++++++ 9 files changed, 922 insertions(+), 6 deletions(-) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/AddressInfoItemDto.cs create mode 100644 applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/AddressInfoDataProviderTests.cs create mode 100644 applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/SubmissionInfoDataProviderTests.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/AddressInfoItemDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/AddressInfoItemDto.cs new file mode 100644 index 0000000000..5ad2a67c1f --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/AddressInfoItemDto.cs @@ -0,0 +1,20 @@ +using System; + +namespace Unity.GrantManager.ApplicantProfile.ProfileData +{ + public class AddressInfoItemDto + { + public Guid Id { get; set; } + public string AddressType { get; set; } = string.Empty; + public string Street { get; set; } = string.Empty; + public string Street2 { get; set; } = string.Empty; + public string Unit { get; set; } = string.Empty; + public string City { get; set; } = string.Empty; + public string Province { get; set; } = string.Empty; + public string PostalCode { get; set; } = string.Empty; + public string Country { get; set; } = string.Empty; + public bool IsPrimary { get; set; } + public bool IsEditable { get; set; } + public string? ReferenceNo { get; set; } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantAddressInfoDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantAddressInfoDto.cs index a532be4069..f7b956aba2 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantAddressInfoDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantAddressInfoDto.cs @@ -1,7 +1,11 @@ +using System.Collections.Generic; + namespace Unity.GrantManager.ApplicantProfile.ProfileData { public class ApplicantAddressInfoDto : ApplicantProfileDataDto { public override string DataType => "ADDRESSINFO"; + + public List Addresses { get; set; } = []; } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/AddressInfoDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/AddressInfoDataProvider.cs index f77a53000c..7e14f7ab13 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/AddressInfoDataProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/AddressInfoDataProvider.cs @@ -1,17 +1,123 @@ +using System; +using System.Linq; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; using Unity.GrantManager.ApplicantProfile.ProfileData; +using Unity.GrantManager.Applications; +using Unity.GrantManager.GrantApplications; +using Volo.Abp.Data; using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.MultiTenancy; namespace Unity.GrantManager.ApplicantProfile { + /// + /// Provides address information for the applicant profile by querying + /// application addresses linked to the applicant's form submissions. + /// Addresses are resolved via both the ApplicationId and ApplicantId + /// relationships, with duplicates removed. + /// [ExposeServices(typeof(IApplicantProfileDataProvider))] - public class AddressInfoDataProvider : IApplicantProfileDataProvider, ITransientDependency + public class AddressInfoDataProvider( + ICurrentTenant currentTenant, + IRepository applicationFormSubmissionRepository, + IRepository applicantAddressRepository, + IRepository applicationRepository) + : IApplicantProfileDataProvider, ITransientDependency { + /// public string Key => ApplicantProfileKeys.AddressInfo; - public Task GetDataAsync(ApplicantProfileInfoRequest request) + /// + public async Task GetDataAsync(ApplicantProfileInfoRequest request) { - return Task.FromResult(new ApplicantAddressInfoDto()); + var dto = new ApplicantAddressInfoDto + { + Addresses = [] + }; + + var subject = request.Subject ?? string.Empty; + var normalizedSubject = subject.Contains('@') + ? subject[..subject.IndexOf('@')].ToUpperInvariant() + : subject.ToUpperInvariant(); + + using (currentTenant.Change(request.TenantId)) + { + var submissionsQuery = await applicationFormSubmissionRepository.GetQueryableAsync(); + var addressesQuery = await applicantAddressRepository.GetQueryableAsync(); + var applicationsQuery = await applicationRepository.GetQueryableAsync(); + + var matchingSubmissions = submissionsQuery + .Where(s => s.OidcSub == normalizedSubject); + + // Addresses linked via ApplicationId — not editable (owned by an application) + var byApplicationId = + from submission in matchingSubmissions + join address in addressesQuery on submission.ApplicationId equals address.ApplicationId + join application in applicationsQuery on address.ApplicationId equals application.Id + select new { address, address.CreationTime, application.ReferenceNo, IsEditable = false }; + + // Addresses linked via ApplicantId — editable (directly from the applicant) + var byApplicantId = + from submission in matchingSubmissions + join address in addressesQuery on submission.ApplicantId equals address.ApplicantId + join application in applicationsQuery on address.ApplicationId equals application.Id into apps + from application in apps.DefaultIfEmpty() + select new { address, address.CreationTime, ReferenceNo = application != null ? application.ReferenceNo : null, IsEditable = true }; + + var results = await byApplicationId + .Concat(byApplicantId) + .ToListAsync(); + + // Deduplicate by address Id — application-linked (IsEditable = false) takes priority + var deduplicated = results + .GroupBy(r => r.address.Id) + .Select(g => g.OrderBy(r => r.IsEditable).First()) + .ToList(); + + var addressDtos = deduplicated.Select(r => new AddressInfoItemDto + { + Id = r.address.Id, + AddressType = GetAddressTypeName(r.address.AddressType), + Street = r.address.Street ?? string.Empty, + Street2 = r.address.Street2 ?? string.Empty, + Unit = r.address.Unit ?? string.Empty, + City = r.address.City ?? string.Empty, + Province = r.address.Province ?? string.Empty, + PostalCode = r.address.Postal ?? string.Empty, + Country = r.address.Country ?? string.Empty, + IsPrimary = r.address.HasProperty("isPrimary") && r.address.GetProperty("isPrimary"), + IsEditable = r.IsEditable, + ReferenceNo = r.ReferenceNo + }).ToList(); + + // If no address is marked as primary, mark the most recent one as primary + if (addressDtos.Any() && !addressDtos.Any(a => a.IsPrimary)) + { + var mostRecent = deduplicated.OrderByDescending(r => r.CreationTime).First(); + var mostRecentDto = addressDtos.First(a => a.Id == mostRecent.address.Id); + mostRecentDto.IsPrimary = true; + } + + dto.Addresses.AddRange(addressDtos); + } + + return dto; + } + + /// + /// Maps an enum value to a human-readable display name. + /// + private static string GetAddressTypeName(AddressType addressType) + { + return addressType switch + { + AddressType.PhysicalAddress => "Physical", + AddressType.MailingAddress => "Mailing", + AddressType.BusinessAddress => "Business", + _ => addressType.ToString() + }; } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/OrgInfoDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/OrgInfoDataProvider.cs index 2d1ced24c2..cc0bc93682 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/OrgInfoDataProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/OrgInfoDataProvider.cs @@ -4,11 +4,17 @@ namespace Unity.GrantManager.ApplicantProfile { + /// + /// Provides organization information for the applicant profile. + /// This is a placeholder provider for future implementation. + /// [ExposeServices(typeof(IApplicantProfileDataProvider))] public class OrgInfoDataProvider : IApplicantProfileDataProvider, ITransientDependency { + /// public string Key => ApplicantProfileKeys.OrgInfo; + /// public Task GetDataAsync(ApplicantProfileInfoRequest request) { return Task.FromResult(new ApplicantOrgInfoDto()); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/PaymentInfoDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/PaymentInfoDataProvider.cs index 8e1ddde042..9b7b86e62f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/PaymentInfoDataProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/PaymentInfoDataProvider.cs @@ -4,11 +4,17 @@ namespace Unity.GrantManager.ApplicantProfile { + /// + /// Provides payment information for the applicant profile. + /// This is a placeholder provider for future implementation. + /// [ExposeServices(typeof(IApplicantProfileDataProvider))] public class PaymentInfoDataProvider : IApplicantProfileDataProvider, ITransientDependency { + /// public string Key => ApplicantProfileKeys.PaymentInfo; + /// public Task GetDataAsync(ApplicantProfileInfoRequest request) { return Task.FromResult(new ApplicantPaymentInfoDto()); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/SubmissionInfoDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/SubmissionInfoDataProvider.cs index de1b1a29a7..60232d7e26 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/SubmissionInfoDataProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/SubmissionInfoDataProvider.cs @@ -109,6 +109,11 @@ private async Task ResolveFormViewUrlAsync() } } + /// + /// Extracts the submission timestamp from the CHEFS JSON createdAt field. + /// Falls back to the provided value if the field is + /// missing or the JSON cannot be parsed. + /// private DateTime ResolveSubmissionTime(string submissionJson, DateTime fallback) { try diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/AddressInfoDataProviderTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/AddressInfoDataProviderTests.cs new file mode 100644 index 0000000000..7b38a48e83 --- /dev/null +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/AddressInfoDataProviderTests.cs @@ -0,0 +1,437 @@ +using NSubstitute; +using Shouldly; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Unity.GrantManager.ApplicantProfile; +using Unity.GrantManager.ApplicantProfile.ProfileData; +using Unity.GrantManager.Applications; +using Unity.GrantManager.GrantApplications; +using Unity.GrantManager.TestHelpers; +using Volo.Abp.Data; +using Volo.Abp.Domain.Entities; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.MultiTenancy; +using Xunit; + +namespace Unity.GrantManager.Applicants +{ + public class AddressInfoDataProviderTests + { + private readonly ICurrentTenant _currentTenant; + private readonly IRepository _submissionRepo; + private readonly IRepository _addressRepo; + private readonly IRepository _applicationRepo; + private readonly AddressInfoDataProvider _provider; + + public AddressInfoDataProviderTests() + { + _currentTenant = Substitute.For(); + _currentTenant.Change(Arg.Any()).Returns(Substitute.For()); + _submissionRepo = Substitute.For>(); + _addressRepo = Substitute.For>(); + _applicationRepo = Substitute.For>(); + + SetupEmptyQueryables(); + + _provider = new AddressInfoDataProvider(_currentTenant, _submissionRepo, _addressRepo, _applicationRepo); + } + + private void SetupEmptyQueryables() + { + _submissionRepo.GetQueryableAsync() + .Returns(Task.FromResult(Enumerable.Empty().AsAsyncQueryable())); + _addressRepo.GetQueryableAsync() + .Returns(Task.FromResult(Enumerable.Empty().AsAsyncQueryable())); + _applicationRepo.GetQueryableAsync() + .Returns(Task.FromResult(Enumerable.Empty().AsAsyncQueryable())); + } + + private void SetupQueryables( + IEnumerable submissions, + IEnumerable addresses, + IEnumerable? applications = null) + { + _submissionRepo.GetQueryableAsync() + .Returns(Task.FromResult(submissions.AsAsyncQueryable())); + _addressRepo.GetQueryableAsync() + .Returns(Task.FromResult(addresses.AsAsyncQueryable())); + _applicationRepo.GetQueryableAsync() + .Returns(Task.FromResult((applications ?? []).AsAsyncQueryable())); + } + + private static ApplicantProfileInfoRequest CreateRequest() => new() + { + ProfileId = Guid.NewGuid(), + Subject = "testuser@idir", + TenantId = Guid.NewGuid(), + Key = ApplicantProfileKeys.AddressInfo + }; + + private static ApplicationFormSubmission CreateSubmission( + Guid applicationId, string oidcSub, Action? configure = null) + { + var entity = new ApplicationFormSubmission { ApplicationId = applicationId, OidcSub = oidcSub }; + EntityHelper.TrySetId(entity, () => Guid.NewGuid()); + configure?.Invoke(entity); + return entity; + } + + private static ApplicantAddress CreateAddress(Action configure) + { + var entity = new ApplicantAddress(); + EntityHelper.TrySetId(entity, () => Guid.NewGuid()); + configure(entity); + return entity; + } + + private static Application CreateApplication(Guid id, Action? configure = null) + { + var entity = new Application(); + EntityHelper.TrySetId(entity, () => id); + configure?.Invoke(entity); + return entity; + } + + [Fact] + public async Task GetDataAsync_ShouldChangeTenant() + { + // Arrange + var request = CreateRequest(); + + // Act + await _provider.GetDataAsync(request); + + // Assert + _currentTenant.Received(1).Change(request.TenantId); + } + + [Fact] + public async Task GetDataAsync_ShouldReturnCorrectDataType() + { + // Arrange + var request = CreateRequest(); + + // Act + var result = await _provider.GetDataAsync(request); + + // Assert + result.DataType.ShouldBe("ADDRESSINFO"); + } + + [Fact] + public async Task GetDataAsync_WithNoAddresses_ShouldReturnEmptyList() + { + // Arrange + var request = CreateRequest(); + + // Act + var result = await _provider.GetDataAsync(request); + + // Assert + var dto = result.ShouldBeOfType(); + dto.Addresses.ShouldBeEmpty(); + } + + [Fact] + public async Task GetDataAsync_ShouldMapAddressFields() + { + // Arrange + var request = CreateRequest(); + var applicationId = Guid.NewGuid(); + + SetupQueryables( + [CreateSubmission(applicationId, "TESTUSER")], + [CreateAddress(a => + { + a.ApplicationId = applicationId; + a.Street = "123 Main St"; + a.Street2 = "Suite 100"; + a.Unit = "4A"; + a.City = "Victoria"; + a.Province = "BC"; + a.Postal = "V8W 1A1"; + a.Country = "Canada"; + a.AddressType = AddressType.PhysicalAddress; + })], + [CreateApplication(applicationId, a => a.ReferenceNo = "REF-001")]); + + // Act + var result = await _provider.GetDataAsync(request); + + // Assert + var dto = result.ShouldBeOfType(); + dto.Addresses.Count.ShouldBe(1); + + var address = dto.Addresses[0]; + address.Street.ShouldBe("123 Main St"); + address.Street2.ShouldBe("Suite 100"); + address.Unit.ShouldBe("4A"); + address.City.ShouldBe("Victoria"); + address.Province.ShouldBe("BC"); + address.PostalCode.ShouldBe("V8W 1A1"); + address.Country.ShouldBe("Canada"); + address.AddressType.ShouldBe("Physical"); + address.ReferenceNo.ShouldBe("REF-001"); + address.IsEditable.ShouldBeFalse(); + } + + [Theory] + [InlineData(AddressType.PhysicalAddress, "Physical")] + [InlineData(AddressType.MailingAddress, "Mailing")] + [InlineData(AddressType.BusinessAddress, "Business")] + public async Task GetDataAsync_ShouldMapAddressTypeName(AddressType addressType, string expectedName) + { + // Arrange + var request = CreateRequest(); + var applicationId = Guid.NewGuid(); + + SetupQueryables( + [CreateSubmission(applicationId, "TESTUSER")], + [CreateAddress(a => { a.ApplicationId = applicationId; a.AddressType = addressType; })], + [CreateApplication(applicationId)]); + + // Act + var result = await _provider.GetDataAsync(request); + + // Assert + var dto = result.ShouldBeOfType(); + dto.Addresses[0].AddressType.ShouldBe(expectedName); + } + + [Fact] + public async Task GetDataAsync_ShouldReturnMultipleAddressesForSameSubmission() + { + // Arrange + var request = CreateRequest(); + var applicationId = Guid.NewGuid(); + + SetupQueryables( + [CreateSubmission(applicationId, "TESTUSER")], + [ + CreateAddress(a => { a.ApplicationId = applicationId; a.AddressType = AddressType.PhysicalAddress; a.City = "Victoria"; }), + CreateAddress(a => { a.ApplicationId = applicationId; a.AddressType = AddressType.MailingAddress; a.City = "Vancouver"; }) + ], + [CreateApplication(applicationId)]); + + // Act + var result = await _provider.GetDataAsync(request); + + // Assert + var dto = result.ShouldBeOfType(); + dto.Addresses.Count.ShouldBe(2); + } + + [Fact] + public async Task GetDataAsync_ShouldNotReturnAddressesForOtherSubjects() + { + // Arrange + var request = CreateRequest(); + var applicationId = Guid.NewGuid(); + + SetupQueryables( + [CreateSubmission(applicationId, "OTHERUSER")], + [CreateAddress(a => { a.ApplicationId = applicationId; a.City = "Victoria"; })], + [CreateApplication(applicationId)]); + + // Act + var result = await _provider.GetDataAsync(request); + + // Assert + var dto = result.ShouldBeOfType(); + dto.Addresses.ShouldBeEmpty(); + } + + [Fact] + public async Task GetDataAsync_ShouldHandleNullAddressFields() + { + // Arrange + var request = CreateRequest(); + var applicationId = Guid.NewGuid(); + + SetupQueryables( + [CreateSubmission(applicationId, "TESTUSER")], + [CreateAddress(a => + { + a.ApplicationId = applicationId; + a.Street = null; + a.Street2 = null; + a.Unit = null; + a.City = null; + a.Province = null; + a.Postal = null; + a.Country = null; + })], + [CreateApplication(applicationId)]); + + // Act + var result = await _provider.GetDataAsync(request); + + // Assert + var dto = result.ShouldBeOfType(); + var address = dto.Addresses[0]; + address.Street.ShouldBe(string.Empty); + address.Street2.ShouldBe(string.Empty); + address.Unit.ShouldBe(string.Empty); + address.City.ShouldBe(string.Empty); + address.Province.ShouldBe(string.Empty); + address.PostalCode.ShouldBe(string.Empty); + address.Country.ShouldBe(string.Empty); + } + + [Fact] + public async Task GetDataAsync_ShouldReturnAddressesLinkedByApplicantId() + { + // Arrange + var request = CreateRequest(); + var applicationId = Guid.NewGuid(); + var applicantId = Guid.NewGuid(); + + SetupQueryables( + [CreateSubmission(applicationId, "TESTUSER", s => s.ApplicantId = applicantId)], + [CreateAddress(a => + { + a.ApplicantId = applicantId; + a.City = "Kelowna"; + a.AddressType = AddressType.MailingAddress; + })]); + + // Act + var result = await _provider.GetDataAsync(request); + + // Assert + var dto = result.ShouldBeOfType(); + dto.Addresses.Count.ShouldBe(1); + dto.Addresses[0].City.ShouldBe("Kelowna"); + dto.Addresses[0].ReferenceNo.ShouldBeNull(); + dto.Addresses[0].IsEditable.ShouldBeTrue(); + } + + [Fact] + public async Task GetDataAsync_ShouldCombineAddressesFromBothLinks() + { + // Arrange + var request = CreateRequest(); + var applicationId = Guid.NewGuid(); + var applicantId = Guid.NewGuid(); + + SetupQueryables( + [CreateSubmission(applicationId, "TESTUSER", s => s.ApplicantId = applicantId)], + [ + CreateAddress(a => { a.ApplicationId = applicationId; a.City = "Victoria"; }), + CreateAddress(a => { a.ApplicantId = applicantId; a.City = "Kelowna"; }) + ], + [CreateApplication(applicationId, a => a.ReferenceNo = "REF-002")]); + + // Act + var result = await _provider.GetDataAsync(request); + + // Assert + var dto = result.ShouldBeOfType(); + dto.Addresses.Count.ShouldBe(2); + } + + [Fact] + public async Task GetDataAsync_ShouldDeduplicateAddressesMatchingBothLinks() + { + // Arrange + var request = CreateRequest(); + var applicationId = Guid.NewGuid(); + var applicantId = Guid.NewGuid(); + var addressId = Guid.NewGuid(); + + // Same address linked by both ApplicationId and ApplicantId + var sharedAddress = new ApplicantAddress + { + ApplicationId = applicationId, + ApplicantId = applicantId, + City = "Victoria" + }; + EntityHelper.TrySetId(sharedAddress, () => addressId); + + SetupQueryables( + [CreateSubmission(applicationId, "TESTUSER", s => s.ApplicantId = applicantId)], + [sharedAddress], + [CreateApplication(applicationId)]); + + // Act + var result = await _provider.GetDataAsync(request); + + // Assert — deduplicated to one entry, application-linked (not editable) wins + var dto = result.ShouldBeOfType(); + dto.Addresses.Count.ShouldBe(1); + dto.Addresses[0].City.ShouldBe("Victoria"); + dto.Addresses[0].IsEditable.ShouldBeFalse(); + } + + [Fact] + public async Task GetDataAsync_ShouldMarkMostRecentAddressAsPrimaryWhenNoneMarked() + { + // Arrange + var request = CreateRequest(); + var applicationId = Guid.NewGuid(); + var oldAddress = CreateAddress(a => + { + a.ApplicationId = applicationId; + a.City = "Vancouver"; + a.CreationTime = new DateTime(2023, 1, 1, 10, 0, 0, DateTimeKind.Utc); + }); + var recentAddress = CreateAddress(a => + { + a.ApplicationId = applicationId; + a.City = "Victoria"; + a.CreationTime = new DateTime(2023, 6, 15, 14, 30, 0, DateTimeKind.Utc); + }); + + SetupQueryables( + [CreateSubmission(applicationId, "TESTUSER")], + [oldAddress, recentAddress], + [CreateApplication(applicationId)]); + + // Act + var result = await _provider.GetDataAsync(request); + + // Assert + var dto = result.ShouldBeOfType(); + dto.Addresses.Count.ShouldBe(2); + var primary = dto.Addresses.Single(a => a.IsPrimary); + primary.City.ShouldBe("Victoria"); + } + + [Fact] + public async Task GetDataAsync_ShouldNotOverridePrimaryWhenAlreadySet() + { + // Arrange + var request = CreateRequest(); + var applicationId = Guid.NewGuid(); + var primaryAddress = CreateAddress(a => + { + a.ApplicationId = applicationId; + a.City = "Vancouver"; + a.CreationTime = new DateTime(2023, 1, 1, 10, 0, 0, DateTimeKind.Utc); + a.SetProperty("isPrimary", true); + }); + var recentAddress = CreateAddress(a => + { + a.ApplicationId = applicationId; + a.City = "Victoria"; + a.CreationTime = new DateTime(2023, 6, 15, 14, 30, 0, DateTimeKind.Utc); + }); + + SetupQueryables( + [CreateSubmission(applicationId, "TESTUSER")], + [primaryAddress, recentAddress], + [CreateApplication(applicationId)]); + + // Act + var result = await _provider.GetDataAsync(request); + + // Assert + var dto = result.ShouldBeOfType(); + dto.Addresses.Count.ShouldBe(2); + var primary = dto.Addresses.Single(a => a.IsPrimary); + primary.City.ShouldBe("Vancouver"); + } + } +} diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/ApplicantProfileDataProviderTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/ApplicantProfileDataProviderTests.cs index 51b1c94f24..cc8d41a68d 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/ApplicantProfileDataProviderTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/ApplicantProfileDataProviderTests.cs @@ -37,6 +37,19 @@ private static ContactInfoDataProvider CreateContactInfoDataProvider() return new ContactInfoDataProvider(currentTenant, applicantProfileContactService); } + private static AddressInfoDataProvider CreateAddressInfoDataProvider() + { + var currentTenant = Substitute.For(); + currentTenant.Change(Arg.Any()).Returns(Substitute.For()); + var submissionRepo = Substitute.For>(); + submissionRepo.GetQueryableAsync().Returns(Task.FromResult(Enumerable.Empty().AsAsyncQueryable())); + var addressRepo = Substitute.For>(); + addressRepo.GetQueryableAsync().Returns(Task.FromResult(Enumerable.Empty().AsAsyncQueryable())); + var applicationRepo = Substitute.For>(); + applicationRepo.GetQueryableAsync().Returns(Task.FromResult(Enumerable.Empty().AsAsyncQueryable())); + return new AddressInfoDataProvider(currentTenant, submissionRepo, addressRepo, applicationRepo); + } + private static SubmissionInfoDataProvider CreateSubmissionInfoDataProvider() { var currentTenant = Substitute.For(); @@ -88,14 +101,14 @@ public async Task OrgInfoDataProvider_GetDataAsync_ShouldReturnOrgInfoDto() [Fact] public void AddressInfoDataProvider_Key_ShouldMatchExpected() { - var provider = new AddressInfoDataProvider(); + var provider = CreateAddressInfoDataProvider(); provider.Key.ShouldBe(ApplicantProfileKeys.AddressInfo); } [Fact] public async Task AddressInfoDataProvider_GetDataAsync_ShouldReturnAddressInfoDto() { - var provider = new AddressInfoDataProvider(); + var provider = CreateAddressInfoDataProvider(); var result = await provider.GetDataAsync(CreateRequest(ApplicantProfileKeys.AddressInfo)); result.ShouldNotBeNull(); result.ShouldBeOfType(); @@ -140,7 +153,7 @@ public void AllProviders_ShouldHaveUniqueKeys() [ CreateContactInfoDataProvider(), new OrgInfoDataProvider(), - new AddressInfoDataProvider(), + CreateAddressInfoDataProvider(), CreateSubmissionInfoDataProvider(), new PaymentInfoDataProvider() ]; diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/SubmissionInfoDataProviderTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/SubmissionInfoDataProviderTests.cs new file mode 100644 index 0000000000..6e578e8963 --- /dev/null +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/SubmissionInfoDataProviderTests.cs @@ -0,0 +1,319 @@ +using Microsoft.Extensions.Logging; +using NSubstitute; +using Shouldly; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Unity.GrantManager.ApplicantProfile; +using Unity.GrantManager.ApplicantProfile.ProfileData; +using Unity.GrantManager.Applications; +using Unity.GrantManager.Integrations; +using Unity.GrantManager.TestHelpers; +using Volo.Abp.Domain.Entities; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.MultiTenancy; +using Xunit; + +namespace Unity.GrantManager.Applicants +{ + public class SubmissionInfoDataProviderTests + { + private readonly ICurrentTenant _currentTenant; + private readonly IRepository _submissionRepo; + private readonly IRepository _applicationRepo; + private readonly IRepository _statusRepo; + private readonly IEndpointManagementAppService _endpointManagementAppService; + private readonly ILogger _logger; + private readonly SubmissionInfoDataProvider _provider; + + public SubmissionInfoDataProviderTests() + { + _currentTenant = Substitute.For(); + _currentTenant.Change(Arg.Any()).Returns(Substitute.For()); + _submissionRepo = Substitute.For>(); + _applicationRepo = Substitute.For>(); + _statusRepo = Substitute.For>(); + _endpointManagementAppService = Substitute.For(); + _endpointManagementAppService.GetChefsApiBaseUrlAsync() + .Returns(Task.FromResult(string.Empty)); + _logger = Substitute.For>(); + + SetupEmptyQueryables(); + + _provider = new SubmissionInfoDataProvider( + _currentTenant, _submissionRepo, _applicationRepo, + _statusRepo, _endpointManagementAppService, _logger); + } + + private void SetupEmptyQueryables() + { + _submissionRepo.GetQueryableAsync() + .Returns(Task.FromResult(Enumerable.Empty().AsAsyncQueryable())); + _applicationRepo.GetQueryableAsync() + .Returns(Task.FromResult(Enumerable.Empty().AsAsyncQueryable())); + _statusRepo.GetQueryableAsync() + .Returns(Task.FromResult(Enumerable.Empty().AsAsyncQueryable())); + } + + private void SetupQueryables( + IEnumerable submissions, + IEnumerable applications, + IEnumerable statuses) + { + _submissionRepo.GetQueryableAsync() + .Returns(Task.FromResult(submissions.AsAsyncQueryable())); + _applicationRepo.GetQueryableAsync() + .Returns(Task.FromResult(applications.AsAsyncQueryable())); + _statusRepo.GetQueryableAsync() + .Returns(Task.FromResult(statuses.AsAsyncQueryable())); + } + + private static ApplicantProfileInfoRequest CreateRequest() => new() + { + ProfileId = Guid.NewGuid(), + Subject = "testuser@idir", + TenantId = Guid.NewGuid(), + Key = ApplicantProfileKeys.SubmissionInfo + }; + + private static ApplicationFormSubmission CreateSubmission( + Guid applicationId, string oidcSub, Action? configure = null) + { + var entity = new ApplicationFormSubmission + { + ApplicationId = applicationId, + OidcSub = oidcSub, + Submission = "{}" + }; + EntityHelper.TrySetId(entity, () => Guid.NewGuid()); + configure?.Invoke(entity); + return entity; + } + + private static Application CreateApplication(Guid id, Guid statusId, Action? configure = null) + { + var entity = new Application { ApplicationStatusId = statusId }; + EntityHelper.TrySetId(entity, () => id); + configure?.Invoke(entity); + return entity; + } + + private static ApplicationStatus CreateStatus(Guid id, string externalStatus) + { + var entity = new ApplicationStatus { ExternalStatus = externalStatus }; + EntityHelper.TrySetId(entity, () => id); + return entity; + } + + [Fact] + public async Task GetDataAsync_ShouldChangeTenant() + { + // Arrange + var request = CreateRequest(); + + // Act + await _provider.GetDataAsync(request); + + // Assert + _currentTenant.Received(1).Change(request.TenantId); + } + + [Fact] + public async Task GetDataAsync_ShouldReturnCorrectDataType() + { + // Arrange + var request = CreateRequest(); + + // Act + var result = await _provider.GetDataAsync(request); + + // Assert + result.DataType.ShouldBe("SUBMISSIONINFO"); + } + + [Fact] + public async Task GetDataAsync_WithNoSubmissions_ShouldReturnEmptyList() + { + // Arrange + var request = CreateRequest(); + + // Act + var result = await _provider.GetDataAsync(request); + + // Assert + var dto = result.ShouldBeOfType(); + dto.Submissions.ShouldBeEmpty(); + } + + [Fact] + public async Task GetDataAsync_ShouldMapSubmissionFields() + { + // Arrange + var request = CreateRequest(); + var applicationId = Guid.NewGuid(); + var statusId = Guid.NewGuid(); + var creationTime = new DateTime(2025, 1, 15, 10, 30, 0, DateTimeKind.Utc); + + SetupQueryables( + [CreateSubmission(applicationId, "TESTUSER", s => + { + s.ChefsSubmissionGuid = "abc-123"; + s.CreationTime = creationTime; + })], + [CreateApplication(applicationId, statusId, a => + { + a.ReferenceNo = "REF-001"; + a.ProjectName = "Test Project"; + })], + [CreateStatus(statusId, "Submitted")]); + + // Act + var result = await _provider.GetDataAsync(request); + + // Assert + var dto = result.ShouldBeOfType(); + dto.Submissions.Count.ShouldBe(1); + + var sub = dto.Submissions[0]; + sub.LinkId.ShouldBe("abc-123"); + sub.ReceivedTime.ShouldBe(creationTime); + sub.ReferenceNo.ShouldBe("REF-001"); + sub.ProjectName.ShouldBe("Test Project"); + sub.Status.ShouldBe("Submitted"); + } + + [Fact] + public async Task GetDataAsync_ShouldResolveSubmissionTimeFromJson() + { + // Arrange + var request = CreateRequest(); + var applicationId = Guid.NewGuid(); + var statusId = Guid.NewGuid(); + var creationTime = new DateTime(2025, 1, 15, 10, 30, 0, DateTimeKind.Utc); + var chefsCreatedAt = new DateTime(2025, 1, 14, 21, 37, 52, DateTimeKind.Utc); + + SetupQueryables( + [CreateSubmission(applicationId, "TESTUSER", s => + { + s.CreationTime = creationTime; + s.Submission = """{"createdAt": "2025-01-14T21:37:52.000Z"}"""; + })], + [CreateApplication(applicationId, statusId)], + [CreateStatus(statusId, "Submitted")]); + + // Act + var result = await _provider.GetDataAsync(request); + + // Assert + var dto = result.ShouldBeOfType(); + dto.Submissions[0].SubmissionTime.ShouldBe(chefsCreatedAt); + dto.Submissions[0].ReceivedTime.ShouldBe(creationTime); + } + + [Fact] + public async Task GetDataAsync_ShouldFallBackToCreationTimeWhenNoCreatedAt() + { + // Arrange + var request = CreateRequest(); + var applicationId = Guid.NewGuid(); + var statusId = Guid.NewGuid(); + var creationTime = new DateTime(2025, 1, 15, 10, 30, 0, DateTimeKind.Utc); + + SetupQueryables( + [CreateSubmission(applicationId, "TESTUSER", s => + { + s.CreationTime = creationTime; + s.Submission = """{"id": "some-id"}"""; + })], + [CreateApplication(applicationId, statusId)], + [CreateStatus(statusId, "Submitted")]); + + // Act + var result = await _provider.GetDataAsync(request); + + // Assert + var dto = result.ShouldBeOfType(); + dto.Submissions[0].SubmissionTime.ShouldBe(creationTime); + } + + [Fact] + public async Task GetDataAsync_ShouldFallBackToCreationTimeWhenInvalidJson() + { + // Arrange + var request = CreateRequest(); + var applicationId = Guid.NewGuid(); + var statusId = Guid.NewGuid(); + var creationTime = new DateTime(2025, 1, 15, 10, 30, 0, DateTimeKind.Utc); + + SetupQueryables( + [CreateSubmission(applicationId, "TESTUSER", s => + { + s.CreationTime = creationTime; + s.Submission = "not valid json"; + })], + [CreateApplication(applicationId, statusId)], + [CreateStatus(statusId, "Submitted")]); + + // Act + var result = await _provider.GetDataAsync(request); + + // Assert + var dto = result.ShouldBeOfType(); + dto.Submissions[0].SubmissionTime.ShouldBe(creationTime); + } + + [Fact] + public async Task GetDataAsync_ShouldResolveLinkSourceFromIntakeApiBase() + { + // Arrange + var request = CreateRequest(); + _endpointManagementAppService.GetChefsApiBaseUrlAsync() + .Returns(Task.FromResult("https://chefs-dev.apps.silver.devops.gov.bc.ca/app/api/v1")); + + // Act + var result = await _provider.GetDataAsync(request); + + // Assert + var dto = result.ShouldBeOfType(); + dto.LinkSource.ShouldBe("https://chefs-dev.apps.silver.devops.gov.bc.ca/app/form/view?s="); + } + + [Fact] + public async Task GetDataAsync_ShouldReturnEmptyLinkSourceWhenSettingFails() + { + // Arrange + var request = CreateRequest(); + _endpointManagementAppService.GetChefsApiBaseUrlAsync() + .Returns(x => throw new Exception("Not configured")); + + // Act + var result = await _provider.GetDataAsync(request); + + // Assert + var dto = result.ShouldBeOfType(); + dto.LinkSource.ShouldBeEmpty(); + } + + [Fact] + public async Task GetDataAsync_ShouldNotReturnSubmissionsForOtherSubjects() + { + // Arrange + var request = CreateRequest(); + var applicationId = Guid.NewGuid(); + var statusId = Guid.NewGuid(); + + SetupQueryables( + [CreateSubmission(applicationId, "OTHERUSER")], + [CreateApplication(applicationId, statusId)], + [CreateStatus(statusId, "Submitted")]); + + // Act + var result = await _provider.GetDataAsync(request); + + // Assert + var dto = result.ShouldBeOfType(); + dto.Submissions.ShouldBeEmpty(); + } + } +} From 2d484ed4f81e0c96dff18498a1f1359c008dfe07 Mon Sep 17 00:00:00 2001 From: David Bright Date: Tue, 24 Feb 2026 15:37:46 -0800 Subject: [PATCH 052/191] When using clear filter, the search field will be reset to empty, and the quick date range dropdown will be reset to 6 months --- .../wwwroot/themes/ux2/plugins/filterRow.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/plugins/filterRow.js b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/plugins/filterRow.js index 781616ad24..59f979ff51 100644 --- a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/plugins/filterRow.js +++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/plugins/filterRow.js @@ -389,6 +389,9 @@ $(externalSearchId).val(''); } + // Clear the search input field + $('#search').val(''); + // Clear custom filter inputs $('.custom-filter-input').val(''); @@ -398,8 +401,9 @@ // Clear order dt.order(initialSortOrder); - // Reload data - dt.ajax.reload(); + // If we want to reset quick date range dropdown to default (last 6 months) and trigger change + // The change event handler will reload the table, so would need to remove ajax.reload() here + $('#quickDateRange').val('last6months').trigger('change'); // Update button state this._updateButtonState(); From f5e4f75eeea8f771a262c63634e566644811ac8c Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Tue, 24 Feb 2026 15:45:51 -0800 Subject: [PATCH 053/191] AB#32046 Rename OpenAI config keys to Azure prefix --- .../src/Unity.GrantManager.Application/AI/OpenAIService.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 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 1ea3e93608..b2262ff7d8 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs @@ -19,13 +19,13 @@ public class OpenAIService : IAIService, ITransientDependency private readonly ILogger _logger; private readonly ITextExtractionService _textExtractionService; - private string? ApiKey => _configuration["AI:OpenAI:ApiKey"]; - private string? ApiUrl => _configuration["AI:OpenAI:ApiUrl"] ?? "https://api.openai.com/v1/chat/completions"; + private string? ApiKey => _configuration["Azure:OpenAI:ApiKey"]; + private string? ApiUrl => _configuration["Azure:OpenAI:ApiUrl"] ?? "https://api.openai.com/v1/chat/completions"; private readonly string MissingApiKeyMessage = "OpenAI API key is not configured"; // Optional local debugging sink for prompt payload logs to a local file. // Not intended for deployed/shared environments. - private bool IsPromptFileLoggingEnabled => _configuration.GetValue("AI:Logging:EnablePromptFileLog") ?? false; + private bool IsPromptFileLoggingEnabled => _configuration.GetValue("Azure:Logging:EnablePromptFileLog") ?? false; private const string PromptLogDirectoryName = "logs"; private static readonly string PromptLogFileName = $"ai-prompts-{DateTime.UtcNow:yyyyMMdd-HHmmss}-{Environment.ProcessId}.log"; From 2226ce4c6df3b28644cf11671929c1b0485ca865 Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Tue, 24 Feb 2026 15:50:45 -0800 Subject: [PATCH 054/191] AB#30430 sonarQube fixes --- .../ApplicantProfile/AddressInfoDataProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/AddressInfoDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/AddressInfoDataProvider.cs index 7e14f7ab13..44d54844b6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/AddressInfoDataProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/AddressInfoDataProvider.cs @@ -93,7 +93,7 @@ from application in apps.DefaultIfEmpty() }).ToList(); // If no address is marked as primary, mark the most recent one as primary - if (addressDtos.Any() && !addressDtos.Any(a => a.IsPrimary)) + if (addressDtos.Count > 0 && !addressDtos.Any(a => a.IsPrimary)) { var mostRecent = deduplicated.OrderByDescending(r => r.CreationTime).First(); var mostRecentDto = addressDtos.First(a => a.Id == mostRecent.address.Id); From 317bcf6efa65bc225837ee607b63a7c94f647a0c Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 20 Feb 2026 16:42:10 -0800 Subject: [PATCH 055/191] 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 1ea3e93608..ebc4d1e53e 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 676fd06b0fa0d3dae9f01833d78587f4eee9a298 Mon Sep 17 00:00:00 2001 From: David Bright Date: Tue, 24 Feb 2026 16:20:06 -0800 Subject: [PATCH 056/191] Applied logic to use a default
} - - From c4718df75218562c56b37afd258b2a40bf56fb74 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:17:31 -0800 Subject: [PATCH 060/191] AB#31384 - Add Application Links Comments --- .../ApplicationLinksAppService.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationLinksAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationLinksAppService.cs index 6e23ce9b63..f67026cfb5 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationLinksAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationLinksAppService.cs @@ -96,6 +96,15 @@ join applicant in applicantsQuery on application.ApplicantId equals applicant.Id return resultList; } + /// + /// Retrieves a list of application links of the specified type for a given application. + /// + /// Use this method to obtain links associated with a particular application and link type. The + /// returned list is mapped to ApplicationLinksDto for convenient consumption in client code. + /// The unique identifier of the application for which to retrieve links. Must be a valid GUID. + /// The type of application link to retrieve, specified by the enumeration. + /// A task that represents the asynchronous operation. The task result contains a list of + /// objects matching the specified application ID and link type. The list will be empty if no links are found. public async Task> GetApplicationLinksByType(Guid applicationId, ApplicationLinkType linkType) { var applicationLinksQuery = await ApplicationLinksRepository @@ -104,11 +113,30 @@ public async Task> GetApplicationLinksByType(Guid appl return ObjectMapper.Map, List>(applicationLinksQuery); } + /// + /// Retrieves a list of child application links associated with the specified applicationId. + /// + /// This method is asynchronous and may involve network or database calls, which could affect + /// performance. Ensure that the applicationId provided is valid to avoid exceptions. + /// The unique identifier of the application for which child applications are being retrieved. This parameter cannot + /// be an empty GUID. + /// A task that represents the asynchronous operation. The task result contains a list of + /// objects representing the child applications. The list will be empty if no child applications are found. public async Task> GetChildApplications(Guid applicationId) { return await GetApplicationLinksByType(applicationId, ApplicationLinkType.Child); } + /// + /// Retrieves a dictionary that maps each specified parent application ID to a list of its associated child application IDs. + /// + /// The method fetches application links from the repository and groups them by parent + /// application ID. Ensure that the provided parent application IDs exist in the repository to obtain meaningful + /// results. + /// A list of GUIDs representing the parent application IDs for which to retrieve child application IDs. This + /// parameter cannot be null or empty. + /// A dictionary where each key is a parent application ID and the corresponding value is a list of child + /// application IDs linked to that parent. The dictionary will be empty if no child applications are found. public async Task>> GetChildApplicationIdsByParentIdsAsync(List parentApplicationIds) { var links = await ApplicationLinksRepository From 9f3255560f11b8f2f056d2d6988340a544e79968 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:20:58 -0800 Subject: [PATCH 061/191] AB#31384 - Fix renaming for batch rollup --- .../Domain/Services/PaymentRequestQueryManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/PaymentRequestQueryManager.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/PaymentRequestQueryManager.cs index 30a4fdb7a7..41aa2b516c 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/PaymentRequestQueryManager.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/PaymentRequestQueryManager.cs @@ -239,13 +239,13 @@ public async Task GetApplicationPaymentRollupAsync( var allCorrelationIds = new List { applicationId }; allCorrelationIds.AddRange(childApplicationIds); - var summaries = await paymentRequestRepository.GetBatchPaymentRollupsByCorrelationIdsAsync(allCorrelationIds); + var batchRollup = await paymentRequestRepository.GetBatchPaymentRollupsByCorrelationIdsAsync(allCorrelationIds); return new ApplicationPaymentRollupDto { ApplicationId = applicationId, - TotalPaid = summaries.Sum(s => s.TotalPaid), - TotalPending = summaries.Sum(s => s.TotalPending) + TotalPaid = batchRollup.Sum(s => s.TotalPaid), + TotalPending = batchRollup.Sum(s => s.TotalPending) }; } From e70e47c16027c72d4ef29f690601ef55a7130986 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Tue, 24 Feb 2026 18:33:59 -0800 Subject: [PATCH 062/191] 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 ebc4d1e53e..f4af6e74a4 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 063/191] 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 0175753a60..212dc5bf7e 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 064/191] 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 c47190351d..4595cffcd1 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 0000000000..93c919dbf0 --- /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 065/191] 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 066/191] 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 5ecee640f4..6661f08fe8 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 067/191] 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 70596aa88d..5f33222ea5 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 f583143dcb..c0294fe062 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 c061a34c6d..3b7e9d91f0 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 f08a1749d5..aaadd97269 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 8199f1aba7..205a2c9d8c 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 4425d1952e..46240d8e93 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 7adc5859f8..856a9d6c79 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 b0359d7ce4..9dddbdd79b 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 0000000000..c758c444a2 --- /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 ec86d535d9..d5f3f67084 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 0000000000..fe0f05e3ed --- /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 073f94dde4..0000000000 --- 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 0000000000..e69de29bb2 diff --git a/database/crunchy-postgres/.helmignore b/database/crunchy-postgres/.helmignore deleted file mode 100644 index 0e8a0eb36f..0000000000 --- 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 ec6ceaee2c..0000000000 --- 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 3302e44b01..0000000000 --- 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 f8646e241e..0000000000 --- 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 cb6d0f61b0..0000000000 --- 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 1a758b08ec..0000000000 --- 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 9f71811f76..0000000000 --- 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 d60ad8ea69..0000000000 --- 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 b22a6b2fab..0000000000 --- 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 e2e2c08032..0000000000 --- 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 5c1aef2249..0000000000 --- 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 78ccf662cc..0000000000 --- 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 daf367a1ba..0000000000 --- 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 b7156b55fb..0000000000 --- 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 f1069148ce..0000000000 --- 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 d7ccfdbf58..0000000000 --- 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 3b0797bfac..0000000000 --- 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 e0ed02caf2..0000000000 --- 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 d7555a68e2..0000000000 --- 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 eb9eb63c2b..0000000000 --- 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 10e8e9218b..0000000000 --- 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 0e8a0eb36f..0000000000 --- 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 55e55af74d..0000000000 --- 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 dc01835b74..0000000000 --- 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 ba55149ace..0000000000 --- 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 1494e8e127..0000000000 --- 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 4a1b270353..0000000000 --- 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 8a4b50bb54..0000000000 --- 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 8c37fc3e2a..0000000000 --- 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 38a31fbea0..0000000000 --- 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 zcmcJSTW`}q5QX;{iT_}!JmMf;k$9-MsS>0WEj(31CTT+;u|sUPswjUQINy#p>$pxx zR8N9eIxv_g3HR%>DDCk-79=Sgm|i zXvt&;J(b>T*(JJ(-Ybs)esocKsx=}d9!`&hUoF3aj*FLFv#} zY85ZLw#+9(DUn}wqahJDcnuTgg?WsQbuW}yF($QsFO>4VWktA)I*{i$4j zlwv;cS$TO-%fx7iJ=Yg(!!e#rwSOb7`*{sg2RePW5nGFw48ljlWuZ4i%%Za|EQ1y- z5A@8Hp`bDq8-`?|??S7bS!x~RBT9T-N^`CAaDSzDcr@F#3#09fi#Y*bTk@W(A#+emIq6^s(KmtSW+i&QaN~BtzFC8cy44>Z_{KKDin) z1lhY85c({A1n$sBH3dG=``TkS^x6{j&cegYYR&4c(k?du{cfh!*s(YPI)%`&)lCSjGeyo#8h&=`RE6J>EI^->o}3K&a5D0bWwcZ$_8yNT&)KL*~+bSnubX+X0ViS+$!e#=o9Z z!^TrG>aaI=68CPUE96n|#-vUZ|9f|&ygyV89=T&TbYCR(QE#8L7o=iWMXrrI-qH^$ z)h|!G17D6jyVh&tjEARijK;pXG)@rP zI#rlR>8I_fG}=5>L^l1*R~Xyez&zBQXVBAT_KEu?4*N4bu={(Z7wOISCcB0ob+EtY zC_hlFwfJPJv3rGH8g38n9qvQ!Bxa(@-$xH;ubo45^4+6Lo4+TBEc^_i?GfIOpnHv5VRqT5)AGpJr5!dG@?ybTzxKL&l${1#ao6ae`-lyhf0A;xbNo8tf{cR8mhi~zqNa3sKXa(wk9 z&u?0-nH}veD`V#pmrI4T+MVg?dHQvFM*RCKUkztfm-a-!bP^f%Tu+`+>=^`ZALe(vcCy8H3lu)+y_8S6xCPDO)u2f99q z=#1l46~Sv0YP*`(sZjDB-%N1boZ`ytkONc);%=0UJPy~U+`>6Lma;OA4CS7N}UI^mKA-$CSjNBBF! zN4f+0T_v)hQMMW}wV9$-aM#o4o@TN$LCof5-%R3Q*%OcG(3!6GHH$-i1H)szAL}k{ zMpMKbALOtv%BO0N%+GX<9BpiSibVG!Qe5S_0AJ{#>9JvizAeNtclB=g??!sermxZ9 z^r?6%?e3|@ZPw*QX*d`kMk@U&9>Yk}=6g&1O%CJuoN*H>KdL8kty-D$lq?qCfJGh( z_atHFRcd5cX`^9B8XMhlv`X4wJr;M!==HQ!=}q8*SLuh$2GD|0L=ha#a?zjFIQE)G zK*n$M`AnZp-Uu7*g<1Pe@`VF@7%lGzC%g^wNq76vhEDXHG2Ke&H!FAdxA+d^20gr) z%M!Zt%pf1Flvta;KaBG^(8%-3g2++l8}TIAmst=k@IPaXgEu#;u;_q&nT?snK2r;H zI`>bs0G(r@V}7CZ_X*vX>iJ5o+GSmpDB5{2k)un==v<4wt&A-0k&+K`onFw$>zZMT zRLrupOuR$s#l}%r`s&{-1Nwj_8RwLeH*}iy7;efNZ3r)HFX8BYs_50(R5t|6 zIamF2ngO_xpQ3qtS^@O*z9=nOjJPf9^Vji@Cui~6?(yAMaG;RzZ<3V7w#)dq>RQqf z>w@xD)L-v`hRo;Z@|i-Vn+(95%!8HYp%7x+s{IjtiJ4{mw7jm_vzW97l8n!39{s(D zx@p8~>BOO4K0-at$s?fC_Z4SNzrUaMt7>%a4?OEEwR}mIJ-do-$NauTtEP-37kr~-N>=eA z_Vi>+L~kM(bvb z^lDn+kcFO!<9wxq?%~%)lACAHh7@%-yxn1x$4K9-KiMhI%%?0y|AM#K>7X%Nk{Gh} zS(MT58_~v}GXH9aLZ)yZL1IR<&g zAN98>>)MX*8>)b_2Iafw%-(x`U!Tj&XGk5+^q=)AwGHUEdv%w9%N#5DDezpM=$s?s z8i8KV;z{h>5XRnme*CV4FT-!b_3%Zw(gCF>!V;}>u97E%Z+x5~z8Sui&+;6aRUleu zt2&|yalm#MybY@o*Vr9djn$#BvsCNHLR`kKkzKU7o;QmX)B;nrnEa?ZLr2zUj2tXu z*@~5uyV7aqO4fpor?~MxvM95>ZOxa6YCLt35K)`D=bNZUIjZgl>d~+axJ1K8{?I6w zTy{s9we_Z6e~FsEI+x~qK7tIdh987fKm0@V`r)ry$MwVS!r#I_WeL9x=UKd>ase*3 zGi19d<{Qkn_$rz?wEGzMm8EyDmq@AUR9BN-;N#z4DzkpwdFw8T(i?ft`SrfkdW*AY zJ~S63tqtK{UB|FrIn*EfV5#mzK8a@`|6y-)DBB@F?(Q=V)*esbk+lV!4CDBWO4K6jwHk!;<`!z6sa)$aPxcqBI&E2QS1s@nz>#%6 zXr>*I+_zavTbX?;#PDyIi7#KuvW6WZ%RG2JuE_4=p;jH|V(h3KKaPHhRV-_;4}sva zJkkx(eKEC$*?qQS*^2MKqbF9cK=Y6@I!_i${3V7uV%5rN)83nTnd%6(1Gykt~OUhG&@PkvGQW#R{r^ zHIySJ}zN8U`%KvX)6_P-S~=DOCzDdXGTe8GuRno4jc3|55b<8OI{OO+Z zWO6}HjCqB89KMb`p=ZaDwo3tyb6cN3EBqX5=CJ}hQ4Q+;GTZ)C^4q#t<2yu-Q}dol z$`3&YUpwmybH6d0B|55nUQVx}!0Evkia+=+xL=MIm)lN?d-+}CzCLz&1gMKiC$i7F zh>Twz$(mU?u(D3)`KihfAIs5g4Mt4iq~)IK1J-V6p41=mUD2Gn)u{%e z%=`v1F6VvMM2mH1-p76a1KoGeRyR+ojy}7(=ith0I4#B~_~Ka&p%t$(8~9Fl?y0}+ zBK70CBaN2x>lFMm=c{=pGOBd7(jH}f!_09k9K zQiq4v|Ew<1SN+Y_WuFE*tmD`nrYg$*a1*{IGU&gZjzHT+qLEuj6BQ)%zpE);d%(h| zU{HtPvTB$-jf*x-b1so9^$GnqA7p1IX!Z$IHq228DvXQ14axm*4)*14cOJI|wJ6IJxJJV;CqFoEOih(FldVCn2yIUM;74ue(!C|0AiJWL>`;0=+UjrzF?OSXc6_)T$-KO%l@ z#d}wDX7q}*#3GutCxcIChU?F}o{Mr@J3Fy?F4{T$bVj>wq;wY1YnJJBGL+6EEly;z z3L$SGlX8rPAwc~h0NU1{Sn*RgZ;$o1p%D#)|@&lNYf@GgGMc4yH%tA)D{wBH@&8V1#Q zDfTDl#{Ap>GOCV1^>bO?>1FgR)EEwBZDa*0gLnI@=BId~^vfXgbJf)?-nhN4g}qTz ze6CY1ZM$S??%lO4$Ifye+mk#{hFhHp)bpI3>@WWva!#rAPP3HSBdY60yT9;OChzx~ zv(s>|`2JMtDXQWnB@66KnZ;gZZuK-W%D-J4M4LDy)gwNora-J9@}rR%(n$Z$`VJItPDEBRQq{ zYU19j%lNkA@Zra|noJu)aM#!A?4fF|UnuWt;=$XoR$5l;_I9hv^6WR-i}Eh429A{j z;b#w&tvpdacTc0<5KU2x(y}jkjNV~1gl3Gwb zUy6|oUec+__TC^d)Qb$9Y`$$2C{h1tY6G%o7tqN4VZJlIrQd_BtCG@08||&uUa&VJ zOMdd#bv*w>Q<_FCPu1rA{JJ_S-mBd2-xlX~I=5Oubqaj8ap4u&?sKo_!cL{mp1FQM zt~dqZKDirbRQJ%PW-W3=(0xBk^e0`vi|79KJM@GCzo@iIX|6$C`E?$0oygCYdj{ba z)HL@tiTVkSroQly^y;U@tqZUJ4b1$s%KrmqU3mRan33t{Ct};8Cu7n+o&3D0H9a_0 za4OrgQ*ia?LYsYv&l9`xbGA;a?VRK+MVqNbQaf>w?`*W?VD&pR_k{%&;XV0hqAdG1 ogULBYDj9>AtM#;(JDY0_^n5&%2lC%Ws7cs(_e7CuqE!k11I2VF1ONa4 diff --git a/openshift/unity-networkpolicy.yaml b/openshift/unity-networkpolicy.yaml deleted file mode 100644 index 530bdd06e8..0000000000 --- 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 a3b8ffac9e..0000000000 --- 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 6b99c636c7..0000000000 --- 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 068/191] 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 5ba2219590..35236847d0 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 069/191] 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 858bf96407..1cdcd6b497 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 13bca3efb9..d03af8505e 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 0000000000..9bbc4bfc9b --- /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 0000000000..78ac7c5b01 --- /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 0000000000..036802c5f2 --- /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 0000000000..487d80375b --- /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 fd2aa83735..be337f9d19 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 ae6e6d3eab..e6c29e5f42 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 070/191] 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 e6c29e5f42..34b740a911 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 071/191] 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 b60d8264f8..a7324e8678 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 b8b339ebfd..a37d8d7332 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 072/191] 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 28d5af2b4e..b73800d7d7 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 ecb9a894a0..8ec3e53bcd 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 073/191] 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 b73800d7d7..3c2b3f2b36 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 0cf1717ba4233c2435d0669b91bdacae6fcf2a96 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Thu, 26 Feb 2026 11:18:11 -0800 Subject: [PATCH 074/191] AB#29187 - Add text area resize handle to Email and Email Template editors --- .../NotificationsSettingGroup/Default.js | 15 ++++++++++++++- .../Shared/Components/EmailsWidget/Default.js | 16 ++++++++++++---- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.js b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.js index fbb5a29cb6..bf254581cd 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.js +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.js @@ -285,7 +285,10 @@ selector: `#${editorId}`, plugins: 'lists link image preview code', toolbar: 'undo redo | styles | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist | link image | code preview | variablesDropdownButton', - statusbar: false, + resize: true, + statusbar: true, + elementpath: false, + branding: false, promotion: false, content_css: false, skin: false, @@ -318,6 +321,16 @@ fetch: fetchVariablesMenuItems(dropdownItems, editor) }); + // Add a custom toolbar button to manually trigger autoresize + editor.ui.registry.addButton('autoResizeButton', { + text: 'Auto Fit', + icon: 'resize', + tooltip: 'Auto-resize to fit content', + onAction: function() { + editor.execCommand('mceAutoResize'); + } + }); + editor.on('init', function () { editor.mode.set(isPopulated ? 'readonly' : 'design'); if (data?.bodyHTML) { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js index 7ef97277ad..79fa979834 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js @@ -166,12 +166,16 @@ if (tinymce.get("EmailBody")) { tinymce.get("EmailBody").remove(); // remove existing instance } + tinymce.init({ license_key: 'gpl', - selector: `#EmailBody`, + selector: '#EmailBody', plugins: getPlugins(), toolbar: getToolbarOptions(), - statusbar: false, + resize: true, + statusbar: true, + elementpath: false, + branding: false, promotion: false, content_css: false, skin: false, @@ -608,12 +612,16 @@ if (tinymce.get("EmailBody")) { tinymce.get("EmailBody").remove(); // remove existing instance } + tinymce.init({ license_key: 'gpl', - selector: `#EmailBody`, + selector: '#EmailBody', plugins: getPlugins(), toolbar: getToolbarOptions(), - statusbar: false, + resize: true, + statusbar: true, + elementpath: false, + branding: false, promotion: false, content_css: false, skin: false, From a351d47398bb10c08c986bd9b1cd8b8ba2550354 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Thu, 26 Feb 2026 11:21:52 -0800 Subject: [PATCH 075/191] AB#29187 - Remove autoresize experimental code --- .../Settings/NotificationsSettingGroup/Default.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.js b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.js index bf254581cd..ca8c59cccd 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.js +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.js @@ -321,16 +321,6 @@ fetch: fetchVariablesMenuItems(dropdownItems, editor) }); - // Add a custom toolbar button to manually trigger autoresize - editor.ui.registry.addButton('autoResizeButton', { - text: 'Auto Fit', - icon: 'resize', - tooltip: 'Auto-resize to fit content', - onAction: function() { - editor.execCommand('mceAutoResize'); - } - }); - editor.on('init', function () { editor.mode.set(isPopulated ? 'readonly' : 'design'); if (data?.bodyHTML) { From a241bbe0b232add06a5b6680945d73f1594c8b71 Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Thu, 26 Feb 2026 12:42:09 -0800 Subject: [PATCH 076/191] AB#30429 add agent contacts --- .../IApplicantProfileContactService.cs | 14 ++- .../ProfileData/ContactInfoItemDto.cs | 1 + .../ApplicantProfileContactService.cs | 55 ++++++++- .../ContactInfoDataProvider.cs | 5 +- .../Contacts/ContactInfoServiceTests.cs | 112 +++++++++++++++++- .../applicant-profile-data-providers.md | 42 ++++++- 6 files changed, 214 insertions(+), 15 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileContactService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileContactService.cs index 671c497363..3db1d7dcd6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileContactService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileContactService.cs @@ -7,8 +7,9 @@ namespace Unity.GrantManager.ApplicantProfile; /// /// Provides applicant-profile-specific contact retrieval operations. -/// This service aggregates contacts from two sources: profile-linked contacts -/// and application-level contacts matched by OIDC subject. +/// This service aggregates contacts from three sources: profile-linked contacts, +/// application-level contacts matched by OIDC subject, and applicant agent +/// contacts derived from the submission login token. /// public interface IApplicantProfileContactService { @@ -26,4 +27,13 @@ public interface IApplicantProfileContactService /// The OIDC subject identifier (e.g. "user@idir"). /// A list of with IsEditable set to false. Task> GetApplicationContactsBySubjectAsync(string subject); + + /// + /// Retrieves contacts derived from applicant agents on applications whose form submissions + /// match the given OIDC subject. The join path is Submission → Application → ApplicantAgent. + /// The subject is normalized by stripping the domain portion (after @) and converting to upper case. + /// + /// The OIDC subject identifier (e.g. "user@idir"). + /// A list of with IsEditable set to false. + Task> GetApplicantAgentContactsBySubjectAsync(string subject); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ContactInfoItemDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ContactInfoItemDto.cs index 2be1b4ed0c..112eed817b 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ContactInfoItemDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ContactInfoItemDto.cs @@ -17,5 +17,6 @@ public class ContactInfoItemDto public bool IsPrimary { get; set; } public bool IsEditable { get; set; } public Guid? ApplicationId { get; set; } + public string? ReferenceNo { get; set; } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs index 46ef6e66f9..b53732447c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs @@ -13,15 +13,18 @@ namespace Unity.GrantManager.ApplicantProfile; /// -/// Applicant-profile-specific contact service. Retrieves contacts linked to applicant profiles -/// and application-level contacts matched by OIDC subject. This service operates independently -/// from the generic and queries repositories directly. +/// Applicant-profile-specific contact service. Retrieves contacts linked to applicant profiles, +/// application-level contacts matched by OIDC subject, and applicant agent contacts derived from +/// the submission login token. This service operates independently from the generic +/// and queries repositories directly. /// public class ApplicantProfileContactService( IContactRepository contactRepository, IContactLinkRepository contactLinkRepository, IRepository applicationFormSubmissionRepository, - IRepository applicationContactRepository) + IRepository applicationContactRepository, + IRepository applicantAgentRepository, + IRepository applicationRepository) : IApplicantProfileContactService, ITransientDependency { private const string ApplicantProfileEntityType = "ApplicantProfile"; @@ -52,7 +55,8 @@ join contact in contactsQuery on link.ContactId equals contact.Id Role = link.Role, IsPrimary = link.IsPrimary, IsEditable = true, - ApplicationId = null + ApplicationId = null, + ReferenceNo = null }).ToListAsync(); } @@ -65,10 +69,12 @@ public async Task> GetApplicationContactsBySubjectAsync var submissionsQuery = await applicationFormSubmissionRepository.GetQueryableAsync(); var applicationContactsQuery = await applicationContactRepository.GetQueryableAsync(); + var applicationsQuery = await applicationRepository.GetQueryableAsync(); var applicationContacts = await ( from submission in submissionsQuery join appContact in applicationContactsQuery on submission.ApplicationId equals appContact.ApplicationId + join application in applicationsQuery on submission.ApplicationId equals application.Id where submission.OidcSub == normalizedSubject select new ContactInfoItemDto { @@ -82,12 +88,49 @@ join appContact in applicationContactsQuery on submission.ApplicationId equals a ContactType = "Application", IsPrimary = false, IsEditable = false, - ApplicationId = appContact.ApplicationId + ApplicationId = appContact.ApplicationId, + ReferenceNo = application.ReferenceNo }).ToListAsync(); return applicationContacts; } + /// + public async Task> GetApplicantAgentContactsBySubjectAsync(string subject) + { + var normalizedSubject = subject.Contains('@') + ? subject[..subject.IndexOf('@')].ToUpperInvariant() + : subject.ToUpperInvariant(); + + var submissionsQuery = await applicationFormSubmissionRepository.GetQueryableAsync(); + var agentsQuery = await applicantAgentRepository.GetQueryableAsync(); + var applicationsQuery = await applicationRepository.GetQueryableAsync(); + + var agentContacts = await ( + from submission in submissionsQuery + join agent in agentsQuery on submission.ApplicationId equals agent.ApplicationId + join application in applicationsQuery on submission.ApplicationId equals application.Id + where submission.OidcSub == normalizedSubject + select new ContactInfoItemDto + { + ContactId = agent.Id, + Name = agent.Name, + Title = agent.Title, + Email = agent.Email, + WorkPhoneNumber = agent.Phone, + WorkPhoneExtension = agent.PhoneExtension, + MobilePhoneNumber = agent.Phone2, + Role = agent.RoleForApplicant, + ContactType = "ApplicantAgent", + IsPrimary = false, + IsEditable = false, + ApplicationId = agent.ApplicationId, + ReferenceNo = application.ReferenceNo + }).ToListAsync(); + + return agentContacts; + } + private static string GetMatchingRole(string contactType) { return ApplicationContactOptionList.ContactTypeList.TryGetValue(contactType, out string? value) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ContactInfoDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ContactInfoDataProvider.cs index 13bd414eeb..09b6245c75 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ContactInfoDataProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ContactInfoDataProvider.cs @@ -7,7 +7,7 @@ namespace Unity.GrantManager.ApplicantProfile { /// /// Provides contact information for the applicant profile by aggregating - /// profile-linked contacts and application-level contacts. + /// profile-linked contacts, application-level contacts, and applicant agent contacts. /// [ExposeServices(typeof(IApplicantProfileDataProvider))] public class ContactInfoDataProvider( @@ -35,6 +35,9 @@ public async Task GetDataAsync(ApplicantProfileInfoRequ var applicationContacts = await applicantProfileContactService.GetApplicationContactsBySubjectAsync(request.Subject); dto.Contacts.AddRange(applicationContacts); + + var agentContacts = await applicantProfileContactService.GetApplicantAgentContactsBySubjectAsync(request.Subject); + dto.Contacts.AddRange(agentContacts); } return dto; diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoServiceTests.cs index 48fadd7b97..2002e056d8 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoServiceTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoServiceTests.cs @@ -18,6 +18,8 @@ public class ApplicantProfileContactServiceTests private readonly IContactLinkRepository _contactLinkRepository; private readonly IRepository _submissionRepository; private readonly IRepository _applicationContactRepository; + private readonly IRepository _applicantAgentRepository; + private readonly IRepository _applicationRepository; private readonly ApplicantProfileContactService _service; public ApplicantProfileContactServiceTests() @@ -26,12 +28,16 @@ public ApplicantProfileContactServiceTests() _contactLinkRepository = Substitute.For(); _submissionRepository = Substitute.For>(); _applicationContactRepository = Substitute.For>(); + _applicantAgentRepository = Substitute.For>(); + _applicationRepository = Substitute.For>(); _service = new ApplicantProfileContactService( _contactRepository, _contactLinkRepository, _submissionRepository, - _applicationContactRepository); + _applicationContactRepository, + _applicantAgentRepository, + _applicationRepository); } private static T WithId(T entity, Guid id) where T : Entity @@ -143,8 +149,17 @@ public async Task GetApplicationContactsBySubjectAsync_WithMatchingSubmission_Sh }, appContactId) }.AsAsyncQueryable(); + var applications = new[] + { + WithId(new Application + { + ReferenceNo = "REF-001" + }, applicationId) + }.AsAsyncQueryable(); + _submissionRepository.GetQueryableAsync().Returns(submissions); _applicationContactRepository.GetQueryableAsync().Returns(applicationContacts); + _applicationRepository.GetQueryableAsync().Returns(applications); // Act var result = await _service.GetApplicationContactsBySubjectAsync("testuser@idir"); @@ -163,6 +178,7 @@ public async Task GetApplicationContactsBySubjectAsync_WithMatchingSubmission_Sh contact.IsPrimary.ShouldBeFalse(); contact.IsEditable.ShouldBeFalse(); contact.ApplicationId.ShouldBe(applicationId); + contact.ReferenceNo.ShouldBe("REF-001"); } [Fact] @@ -194,6 +210,8 @@ public async Task GetApplicationContactsBySubjectAsync_ShouldMatchCaseInsensitiv _submissionRepository.GetQueryableAsync().Returns(submissions); _applicationContactRepository.GetQueryableAsync().Returns(applicationContacts); + _applicationRepository.GetQueryableAsync().Returns( + new[] { WithId(new Application(), applicationId) }.AsAsyncQueryable()); // Act var result = await _service.GetApplicationContactsBySubjectAsync("testuser@IDIR"); @@ -231,6 +249,8 @@ public async Task GetApplicationContactsBySubjectAsync_ShouldStripDomainFromSubj _submissionRepository.GetQueryableAsync().Returns(submissions); _applicationContactRepository.GetQueryableAsync().Returns(applicationContacts); + _applicationRepository.GetQueryableAsync().Returns( + new[] { WithId(new Application(), applicationId) }.AsAsyncQueryable()); // Act var result = await _service.GetApplicationContactsBySubjectAsync("myuser@differentdomain"); @@ -269,6 +289,8 @@ public async Task GetApplicationContactsBySubjectAsync_WithSubjectWithoutAtSign_ _submissionRepository.GetQueryableAsync().Returns(submissions); _applicationContactRepository.GetQueryableAsync().Returns(applicationContacts); + _applicationRepository.GetQueryableAsync().Returns( + new[] { WithId(new Application(), applicationId) }.AsAsyncQueryable()); // Act var result = await _service.GetApplicationContactsBySubjectAsync("plainuser"); @@ -305,6 +327,8 @@ public async Task GetApplicationContactsBySubjectAsync_WithNonMatchingSubject_Sh _submissionRepository.GetQueryableAsync().Returns(submissions); _applicationContactRepository.GetQueryableAsync().Returns(applicationContacts); + _applicationRepository.GetQueryableAsync().Returns( + new[] { WithId(new Application(), applicationId) }.AsAsyncQueryable()); // Act var result = await _service.GetApplicationContactsBySubjectAsync("differentuser@idir"); @@ -321,6 +345,8 @@ public async Task GetApplicationContactsBySubjectAsync_WithNoSubmissions_ShouldR .Returns(Array.Empty().AsAsyncQueryable()); _applicationContactRepository.GetQueryableAsync() .Returns(Array.Empty().AsAsyncQueryable()); + _applicationRepository.GetQueryableAsync() + .Returns(Array.Empty().AsAsyncQueryable()); // Act var result = await _service.GetApplicationContactsBySubjectAsync("testuser@idir"); @@ -372,6 +398,12 @@ public async Task GetApplicationContactsBySubjectAsync_WithMultipleSubmissions_S _submissionRepository.GetQueryableAsync().Returns(submissions); _applicationContactRepository.GetQueryableAsync().Returns(applicationContacts); + _applicationRepository.GetQueryableAsync().Returns( + new[] + { + WithId(new Application(), appId1), + WithId(new Application(), appId2) + }.AsAsyncQueryable()); // Act var result = await _service.GetApplicationContactsBySubjectAsync("testuser@idir"); @@ -381,5 +413,83 @@ public async Task GetApplicationContactsBySubjectAsync_WithMultipleSubmissions_S result.ShouldAllBe(c => !c.IsEditable); result.ShouldAllBe(c => !c.IsPrimary); } + + [Fact] + public async Task GetApplicantAgentContactsBySubjectAsync_WithMatchingSubmission_ShouldReturnAgentContacts() + { + // Arrange + var applicationId = Guid.NewGuid(); + var agentId = Guid.NewGuid(); + + var submissions = new[] + { + new ApplicationFormSubmission + { + OidcSub = "TESTUSER", + ApplicationId = applicationId, + ApplicantId = Guid.NewGuid(), + ApplicationFormId = Guid.NewGuid() + } + }.AsAsyncQueryable(); + + var agents = new[] + { + WithId(new ApplicantAgent + { + ApplicationId = applicationId, + ApplicantId = Guid.NewGuid(), + Name = "Agent Smith", + Title = "Signing Authority", + Email = "agent@example.com", + Phone = "777-7777", + PhoneExtension = "201", + Phone2 = "888-8888", + RoleForApplicant = "Primary Contact" + }, agentId) + }.AsAsyncQueryable(); + + _submissionRepository.GetQueryableAsync().Returns(submissions); + _applicantAgentRepository.GetQueryableAsync().Returns(agents); + _applicationRepository.GetQueryableAsync().Returns( + new[] { WithId(new Application { ReferenceNo = "REF-AGENT-001" }, applicationId) }.AsAsyncQueryable()); + + // Act + var result = await _service.GetApplicantAgentContactsBySubjectAsync("testuser@idir"); + + // Assert + result.Count.ShouldBe(1); + var contact = result[0]; + contact.ContactId.ShouldBe(agentId); + contact.Name.ShouldBe("Agent Smith"); + contact.Title.ShouldBe("Signing Authority"); + contact.Email.ShouldBe("agent@example.com"); + contact.WorkPhoneNumber.ShouldBe("777-7777"); + contact.WorkPhoneExtension.ShouldBe("201"); + contact.MobilePhoneNumber.ShouldBe("888-8888"); + contact.Role.ShouldBe("Primary Contact"); + contact.ContactType.ShouldBe("ApplicantAgent"); + contact.IsPrimary.ShouldBeFalse(); + contact.IsEditable.ShouldBeFalse(); + contact.ApplicationId.ShouldBe(applicationId); + contact.ReferenceNo.ShouldBe("REF-AGENT-001"); + } + + [Fact] + public async Task GetApplicantAgentContactsBySubjectAsync_WithNoMatchingSubmissions_ShouldReturnEmpty() + { + // Arrange + _submissionRepository.GetQueryableAsync() + .Returns(Array.Empty().AsAsyncQueryable()); + _applicantAgentRepository.GetQueryableAsync() + .Returns(Array.Empty().AsAsyncQueryable()); + _applicationRepository.GetQueryableAsync() + .Returns(Array.Empty().AsAsyncQueryable()); + + // Act + var result = await _service.GetApplicantAgentContactsBySubjectAsync("testuser@idir"); + + // Assert + result.ShouldBeEmpty(); + } } } diff --git a/documentation/applicant-portal/applicant-profile-data-providers.md b/documentation/applicant-portal/applicant-profile-data-providers.md index 93c919dbf0..438f7a1bbf 100644 --- a/documentation/applicant-portal/applicant-profile-data-providers.md +++ b/documentation/applicant-portal/applicant-profile-data-providers.md @@ -101,7 +101,7 @@ sequenceDiagram ### 1. ContactInfoDataProvider (`CONTACTINFO`) -**Purpose:** Aggregates contact information from two sources — profile-linked contacts and application-level contacts. +**Purpose:** Aggregates contact information from three sources — profile-linked contacts, application-level contacts, and applicant agent contacts derived from the submission login token. **Dependencies:** - `ICurrentTenant` — for multi-tenant scoping @@ -112,7 +112,8 @@ sequenceDiagram 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. +4. Retrieves **applicant agent contacts** — contact information derived from `ApplicantAgent` records on applications whose form submissions match the normalized OIDC subject. The join path is `Submission → Application → ApplicantAgent`. These are **read-only** (`IsEditable = false`). +5. Merges all three 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. @@ -132,15 +133,27 @@ flowchart TD AC1["Normalize Subject
strip domain, uppercase"] AC2["Query ApplicationFormSubmission
WHERE OidcSub = normalizedSubject"] AC3["JOIN ApplicationContact
ON ApplicationId"] + AC3b["JOIN Application
ON ApplicationId
for ReferenceNo"] AC4["Map to ContactInfoItemDto
IsEditable = false"] - AC1 --> AC2 --> AC3 --> AC4 + AC1 --> AC2 --> AC3 --> AC3b --> AC4 + end + + subgraph AgentContacts["Applicant Agent Contacts - Read-Only"] + AG1["Normalize Subject
strip domain, uppercase"] + AG2["Query ApplicationFormSubmission
WHERE OidcSub = normalizedSubject"] + AG3["JOIN ApplicantAgent
ON ApplicationId"] + AG3b["JOIN Application
ON ApplicationId
for ReferenceNo"] + AG4["Map to ContactInfoItemDto
ContactType = 'ApplicantAgent'
IsEditable = false"] + AG1 --> AG2 --> AG3 --> AG3b --> AG4 end Start --> Tenant Tenant --> PC1 Tenant --> AC1 + Tenant --> AG1 PC3 --> Merge["Merge into Contacts list"] AC4 --> Merge + AG4 --> Merge Merge --> Return([Return ApplicantContactInfoDto]) ``` @@ -149,7 +162,26 @@ flowchart TD | Source | Entity | Join Path | Editable | |--------|--------|-----------|----------| | Profile Contacts | `ContactLink` → `Contact` | `ContactLink.RelatedEntityId = profileId` | ✅ Yes | -| Application Contacts | `ApplicationFormSubmission` → `ApplicationContact` | `Submission.OidcSub = normalizedSubject` | ❌ No | +| Application Contacts | `ApplicationFormSubmission` → `ApplicationContact` → `Application` | `Submission.OidcSub = normalizedSubject`, `Application.Id` for `ReferenceNo` | ❌ No | +| Applicant Agent Contacts | `ApplicationFormSubmission` → `ApplicantAgent` → `Application` | `Submission.ApplicationId = Agent.ApplicationId`, `Application.Id` for `ReferenceNo` | ❌ No | + +**Applicant Agent Field Mapping:** + +The `ApplicantAgent` entity is populated from the CHEFS submission login token during intake import. Its fields are mapped to `ContactInfoItemDto` as follows: + +| ApplicantAgent Field | ContactInfoItemDto Field | +|---------------------|-------------------------| +| `Id` | `ContactId` | +| `Name` | `Name` | +| `Title` | `Title` | +| `Email` | `Email` | +| `Phone` | `WorkPhoneNumber` | +| `PhoneExtension` | `WorkPhoneExtension` | +| `Phone2` | `MobilePhoneNumber` | +| `RoleForApplicant` | `Role` | +| `ApplicationId` | `ApplicationId` | +| `Application.ReferenceNo` | `ReferenceNo` | +| _(literal)_ `"ApplicantAgent"` | `ContactType` | --- @@ -356,7 +388,7 @@ Providers distinguish between **editable** and **read-only** data: | Provider | Editable Source | Read-Only Source | |----------|----------------|-----------------| -| ContactInfo | Profile-linked contacts | Application-level contacts | +| ContactInfo | Profile-linked contacts | Application-level contacts, Applicant agent contacts | | AddressInfo | Addresses linked via ApplicantId | Addresses linked via ApplicationId | --- From 1e081c3a7f65899f1ee479a6f85300cc7e730027 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Thu, 26 Feb 2026 12:48:10 -0800 Subject: [PATCH 077/191] AB#31851 - Bugfix - Standardize date rendering with DateUtils for PaymentRequests --- .../Pages/PaymentRequests/Index.js | 41 ++++++++----------- .../UnityThemeUX2GlobalScriptContributor.cs | 1 + .../Unity.Theme.UX2}/wwwroot/js/DateUtils.js | 0 .../Pages/GrantApplications/Index.cshtml | 1 - .../Pages/GrantApplications/Index.js | 4 +- .../ApplicantSubmissionsViewComponent.cs | 1 - .../ApplicantSubmissions/Default.js | 4 +- 7 files changed, 23 insertions(+), 29 deletions(-) rename applications/Unity.GrantManager/{src/Unity.GrantManager.Web => modules/Unity.Theme.UX2/src/Unity.Theme.UX2}/wwwroot/js/DateUtils.js (100%) diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/Index.js b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/Index.js index 5b7aff6ba2..b0d29d6df5 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/Index.js +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/Index.js @@ -486,9 +486,12 @@ $(function () { title: l('ApplicationPaymentListTable:RequestedOn'), name: 'requestedOn', data: 'creationTime', - className: 'data-table-header', + className: 'data-table-header text-nowrap', index: columnIndex, - render: DataTable.render.date('YYYY-MM-DD', abp.localization.currentCulture.name) + render: function (data, type) { + if (!data) return null; + return DateUtils.formatUtcDateToLocal(data, type); + } }; } function getUpdatedOnColumn(columnIndex) { @@ -496,9 +499,12 @@ $(function () { title: l('ApplicationPaymentListTable:UpdatedOn'), name: 'updatedOn', data: 'lastModificationTime', - className: 'data-table-header', + className: 'data-table-header text-nowrap', index: columnIndex, - render: DataTable.render.date('YYYY-MM-DD', abp.localization.currentCulture.name) + render: function(data, type) { + if (!data) return null; + return DateUtils.formatUtcDateToLocal(data, type); + } }; } function getPaidOnColumn(columnIndex) { @@ -506,18 +512,11 @@ $(function () { title: l('ApplicationPaymentListTable:PaidOn'), name: 'paidOn', data: 'paymentDate', - className: 'data-table-header', + className: 'data-table-header text-nowrap', index: columnIndex, - render: function (data) { + render: function (data, type) { if (!data) return null; - // Check if date is in DD-MMM-YYYY format - if (/^\d{2}-[A-Z]{3}-\d{4}$/.test(data)) { - // Parse and reformat - const date = luxon.DateTime.fromFormat(data, 'dd-MMM-yyyy'); - return date.toFormat('yyyy-MM-dd'); - } - // Use default render for other formats - return DataTable.render.date('YYYY-MM-DD', abp.localization.currentCulture.name)(data); + return DateUtils.formatUtcDateToLocal(data, type); } }; } @@ -575,11 +574,13 @@ $(function () { title: l(`ApplicationPaymentListTable:L${level}ApprovalDate`), name: `l${level}ApprovalDate`, data: 'expenseApprovals', - className: 'data-table-header', + className: 'data-table-header text-nowrap', index: columnIndex, - render: function (data) { + render: function (data, type) { let approval = getExpenseApprovalsDetails(data, level); - return formatDate(approval?.decisionDate); + const approvalDate = approval?.decisionDate; + if (!approvalDate) return null; + return DateUtils.formatUtcDateToLocal(approvalDate, type); } }; } @@ -711,12 +712,6 @@ $(function () { return expenseApprovals.find(x => x.type == type); } - function formatDate(data) { - return data != null ? luxon.DateTime.fromISO(data, { - locale: abp.localization.currentCulture.name, - }).toUTC().toLocaleString() : null; - } - $('#search').on('input', function () { let table = $('#PaymentRequestListTable').DataTable(); table.search($(this).val()).draw(); diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Bundling/UnityThemeUX2GlobalScriptContributor.cs b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Bundling/UnityThemeUX2GlobalScriptContributor.cs index 70d733e5a9..432c739046 100644 --- a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Bundling/UnityThemeUX2GlobalScriptContributor.cs +++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Bundling/UnityThemeUX2GlobalScriptContributor.cs @@ -49,5 +49,6 @@ public override void ConfigureBundle(BundleConfigurationContext context) context.Files.Add("/themes/ux2/plugins/filterRow.js"); context.Files.Add("/themes/ux2/plugins/colvisAlpha.js"); context.Files.Add("/themes/ux2/table-utils.js"); + context.Files.Add("/js/DateUtils.js"); } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/wwwroot/js/DateUtils.js b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/js/DateUtils.js similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Web/wwwroot/js/DateUtils.js rename to applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/js/DateUtils.js diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Index.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Index.cshtml index 9995d2c4c2..05cf01bc3b 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Index.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Index.cshtml @@ -18,7 +18,6 @@ @section scripts { - } 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 050bfb23b8..5d73ad10a0 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 @@ -559,8 +559,8 @@ const listColumns = getColumns(); className: 'data-table-header', index: columnIndex, render: function (data, type) { - const formattedDate = DateUtils.formatUtcDateToLocal(data, type); - return formattedDate ? String(formattedDate) : ''; + if (!data) return null; + return DateUtils.formatUtcDateToLocal(data, type); } }; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantSubmissions/ApplicantSubmissionsViewComponent.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantSubmissions/ApplicantSubmissionsViewComponent.cs index 7225e32ceb..db7b309121 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantSubmissions/ApplicantSubmissionsViewComponent.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantSubmissions/ApplicantSubmissionsViewComponent.cs @@ -169,7 +169,6 @@ public class ApplicantSubmissionsScriptBundleContributor : BundleContributor { public override void ConfigureBundle(BundleConfigurationContext context) { - context.Files.AddIfNotContains("/js/DateUtils.js"); context.Files.AddIfNotContains("/Views/Shared/Components/ApplicantSubmissions/Default.js"); } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantSubmissions/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantSubmissions/Default.js index 198501272d..221ddaf9aa 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantSubmissions/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantSubmissions/Default.js @@ -264,8 +264,8 @@ $(function () { className: 'data-table-header', index: columnIndex, render: function (data, type) { - const formattedDate = DateUtils.formatUtcDateToLocal(data, type); - return formattedDate ? String(formattedDate) : ''; + if (!data) return null; + return DateUtils.formatUtcDateToLocal(data, type); } }; } From 0afaca3b34111151cd89242b8ec8d0000d906851 Mon Sep 17 00:00:00 2001 From: Velang Date: Thu, 26 Feb 2026 12:56:51 -0800 Subject: [PATCH 078/191] adding the new date picker changes --- .../cypress/e2e/ApplicationsActionBar.cy.ts | 9 ++- .../Unity.AutoUI/cypress/e2e/chefsdata.cy.ts | 11 ++- .../cypress/pages/ApplicationsListPage.ts | 67 ++++++++++++++++++- 3 files changed, 75 insertions(+), 12 deletions(-) diff --git a/applications/Unity.AutoUI/cypress/e2e/ApplicationsActionBar.cy.ts b/applications/Unity.AutoUI/cypress/e2e/ApplicationsActionBar.cy.ts index 8a5e222e70..7bbdde7041 100644 --- a/applications/Unity.AutoUI/cypress/e2e/ApplicationsActionBar.cy.ts +++ b/applications/Unity.AutoUI/cypress/e2e/ApplicationsActionBar.cy.ts @@ -171,13 +171,12 @@ describe("Unity Login and check data from CHEFS", () => { page.switchToGrantProgram("Default Grants Program"); }); - it("Tests the existence and functionality of the Submitted Date From and Submitted Date To filters", () => { - // Set date filters using page object methods + it("Tests the existence and functionality of the Quick Date Range filter", () => { + // Select "All time" from quick date range dropdown and verify table refreshes page - .setSubmittedFromDate("2022-01-01") + .selectQuickDateRange("alltime") .waitForTableRefresh() - .setSubmittedToDate(page.getTodayIsoLocal()) - .waitForTableRefresh(); + .verifyQuickDateRangeValue("alltime"); }); // With no rows selected verify the visibility of Filter, Export, Save View, and Columns. diff --git a/applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts b/applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts index b9ec8948c9..05fe3e5a54 100644 --- a/applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts +++ b/applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts @@ -59,13 +59,12 @@ describe('Unity Login and check data from CHEFS', () => { // Ensure the search field exists cy.get('#search', { timeout: STANDARD_TIMEOUT }).should('exist') - // Conditionally widen Submitted Date range if the control exists + // Select "All time" from quick date range to widen the search cy.get('body', { timeout: STANDARD_TIMEOUT }).then(($body) => { - if ($body.find('input#submittedFromDate').length > 0) { - cy.get('input#submittedFromDate', { timeout: STANDARD_TIMEOUT }) - .should('exist') - .clear() - .type('2022-01-01') + if ($body.find('select#quickDateRange').length > 0) { + cy.get('select#quickDateRange', { timeout: STANDARD_TIMEOUT }) + .should('be.visible') + .select('alltime') } }) diff --git a/applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts b/applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts index ae424c36f3..aae8eb72bb 100644 --- a/applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts +++ b/applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts @@ -16,11 +16,23 @@ export class ApplicationsListPage extends ApplicationsPage { // Date filter selectors private readonly dateFilters = { + quickDateRange: "select#quickDateRange", submittedFromDate: "input#submittedFromDate", submittedToDate: "input#submittedToDate", spinner: 'div.spinner-grow[role="status"]', }; + // Quick date range option values + private readonly quickDateRangeOptions = { + today: "today", + last7Days: "last7days", + last30Days: "last30days", + last3Months: "last3months", + last6Months: "last6months", + allTime: "alltime", + custom: "custom", + }; + // Extended action bar selectors (beyond ApplicationsPage) private readonly extendedActionBar = { customButtons: "#app_custom_buttons", @@ -71,7 +83,60 @@ export class ApplicationsListPage extends ApplicationsPage { // ============ Date Filter Methods ============ /** - * Set the Submitted From Date filter + * Select a quick date range from the dropdown + * @param range - One of: "today", "last7days", "last30days", "last3months", "last6months", "alltime", "custom" + */ + selectQuickDateRange( + range: + | "today" + | "last7days" + | "last30days" + | "last3months" + | "last6months" + | "alltime" + | "custom" + ): this { + cy.get(this.dateFilters.quickDateRange, { timeout: this.STANDARD_TIMEOUT }) + .should("be.visible") + .select(range); + return this; + } + + /** + * Select "Last 6 months" from quick date range (default) + */ + selectLast6Months(): this { + return this.selectQuickDateRange("last6months"); + } + + /** + * Select "All time" from quick date range + */ + selectAllTime(): this { + return this.selectQuickDateRange("alltime"); + } + + /** + * Verify the quick date range dropdown has expected value + */ + verifyQuickDateRangeValue( + expectedValue: + | "today" + | "last7days" + | "last30days" + | "last3months" + | "last6months" + | "alltime" + | "custom" + ): this { + cy.get(this.dateFilters.quickDateRange, { timeout: this.STANDARD_TIMEOUT }) + .should("have.value", expectedValue); + return this; + } + + /** + * Set the Submitted From Date filter (for custom date range) + * @deprecated Use selectQuickDateRange() instead. This method is for custom date ranges only. */ setSubmittedFromDate(date: string): this { cy.get(this.dateFilters.submittedFromDate, { timeout: this.STANDARD_TIMEOUT }) From 061b953ab84ae2d20b83ce9a683ff87673e8ed4e Mon Sep 17 00:00:00 2001 From: Velang Date: Thu, 26 Feb 2026 13:16:33 -0800 Subject: [PATCH 079/191] adding support for observer loop --- applications/Unity.AutoUI/cypress/support/e2e.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/applications/Unity.AutoUI/cypress/support/e2e.ts b/applications/Unity.AutoUI/cypress/support/e2e.ts index 29ee2545ba..860eac1b25 100644 --- a/applications/Unity.AutoUI/cypress/support/e2e.ts +++ b/applications/Unity.AutoUI/cypress/support/e2e.ts @@ -6,4 +6,14 @@ // https://on.cypress.io/configuration // *********************************************************** -import '../support/commands.ts' +import '../support/commands' + +// Ignore ResizeObserver loop errors - these are benign browser notifications +// that occur when ResizeObserver callbacks don't complete in a single animation frame +Cypress.on('uncaught:exception', (err) => { + if (err.message.includes('ResizeObserver loop')) { + return false + } + // Return true to fail the test for other errors + return true +}) From 91369d06b13be7add8e0088ae5a5f8bf9d3f4a59 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Thu, 26 Feb 2026 13:23:27 -0800 Subject: [PATCH 080/191] AB#31851 - Set null return for formatUtcDateToLocal --- .../src/Unity.Payments.Web/Pages/PaymentRequests/Index.js | 4 ---- .../src/Unity.Theme.UX2/wwwroot/js/DateUtils.js | 4 ++-- .../Unity.GrantManager.Web/Pages/GrantApplications/Index.js | 1 - .../Views/Shared/Components/ApplicantSubmissions/Default.js | 1 - 4 files changed, 2 insertions(+), 8 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/Index.js b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/Index.js index b0d29d6df5..970d32b9a8 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/Index.js +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/Index.js @@ -489,7 +489,6 @@ $(function () { className: 'data-table-header text-nowrap', index: columnIndex, render: function (data, type) { - if (!data) return null; return DateUtils.formatUtcDateToLocal(data, type); } }; @@ -502,7 +501,6 @@ $(function () { className: 'data-table-header text-nowrap', index: columnIndex, render: function(data, type) { - if (!data) return null; return DateUtils.formatUtcDateToLocal(data, type); } }; @@ -515,7 +513,6 @@ $(function () { className: 'data-table-header text-nowrap', index: columnIndex, render: function (data, type) { - if (!data) return null; return DateUtils.formatUtcDateToLocal(data, type); } }; @@ -579,7 +576,6 @@ $(function () { render: function (data, type) { let approval = getExpenseApprovalsDetails(data, level); const approvalDate = approval?.decisionDate; - if (!approvalDate) return null; return DateUtils.formatUtcDateToLocal(approvalDate, type); } }; diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/js/DateUtils.js b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/js/DateUtils.js index 7050381455..651e25ebc5 100644 --- a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/js/DateUtils.js +++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/js/DateUtils.js @@ -9,11 +9,11 @@ const DateUtils = (function () { * @param {string|Date} dateUtc - The UTC date to format * @param {string} type - The type of formatting (for DataTables compatibility) * @param {object} options - Additional formatting options - * @returns {string|number} Formatted date string or timestamp for sorting + * @returns {string|number|null} Formatted date string or timestamp for sorting, null if input is invalid */ function formatUtcDateToLocal(dateUtc, type, options) { if (!dateUtc) { - return ''; + return null; } const date = new Date(dateUtc); 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 5d73ad10a0..1120afb5e5 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 @@ -559,7 +559,6 @@ const listColumns = getColumns(); className: 'data-table-header', index: columnIndex, render: function (data, type) { - if (!data) return null; return DateUtils.formatUtcDateToLocal(data, type); } }; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantSubmissions/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantSubmissions/Default.js index 221ddaf9aa..525bda3206 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantSubmissions/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantSubmissions/Default.js @@ -264,7 +264,6 @@ $(function () { className: 'data-table-header', index: columnIndex, render: function (data, type) { - if (!data) return null; return DateUtils.formatUtcDateToLocal(data, type); } }; From e200f12de2a733c2c2656e73a2df279acbed30a9 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Thu, 26 Feb 2026 14:00:37 -0800 Subject: [PATCH 081/191] AB#31851 - Add formatUtcDateToLocal use to PaymentInfo table --- .../Shared/Components/PaymentInfo/Default.js | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/Default.js b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/Default.js index 56cd6f0cb5..999b15e076 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/Default.js +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/Default.js @@ -336,8 +336,8 @@ data: 'creationTime', className: 'data-table-header', index: 5, - render: function (data) { - return formatDate(data); + render: function(data, type) { + return DateUtils.formatUtcDateToLocal(data, type); }, }; } @@ -349,8 +349,8 @@ data: 'lastModificationTime', className: 'data-table-header', index: 6, - render: function (data) { - return formatDate(data); + render: function(data, type) { + return DateUtils.formatUtcDateToLocal(data, type); }, }; } @@ -362,8 +362,8 @@ data: 'paidOn', className: 'data-table-header', index: 7, - render: function (data) { - return formatDate(data); + render: function (data, type) { + return DateUtils.formatUtcDateToLocal(data, type); }, }; } @@ -470,16 +470,6 @@ }; } - function formatDate(data) { - return data != null - ? luxon.DateTime.fromISO(data, { - locale: abp.localization.currentCulture.name, - }) - .toUTC() - .toLocaleString() - : '{Not Available}'; - } - PubSub.subscribe('refresh_application_list', (msg, data) => { dataTable.ajax.reload(null, false); PubSub.publish('clear_payment_application'); From 0dac2febd471a8fb9deb7c79d1a39ab3baef6a5d Mon Sep 17 00:00:00 2001 From: David Bright Date: Thu, 26 Feb 2026 14:05:16 -0800 Subject: [PATCH 082/191] 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 0d3372b1b7..2e9125f457 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 050bfb23b8..056d3ee72c 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 4c0fc3146bf62a4b9bdff5030daa165171f3c579 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Thu, 26 Feb 2026 14:44:40 -0800 Subject: [PATCH 083/191] AB#31851 - Revert changes to Payment Request Paid On column --- .../Pages/PaymentRequests/Index.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/Index.js b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/Index.js index 970d32b9a8..9454060749 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/Index.js +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/Index.js @@ -512,8 +512,16 @@ $(function () { data: 'paymentDate', className: 'data-table-header text-nowrap', index: columnIndex, - render: function (data, type) { - return DateUtils.formatUtcDateToLocal(data, type); + render: function (data) { + if (!data) return null; + // Check if date is in DD-MMM-YYYY format + if (/^\d{2}-[A-Z]{3}-\d{4}$/.test(data)) { + // Parse and reformat + const date = luxon.DateTime.fromFormat(data, 'dd-MMM-yyyy'); + return date.toFormat('yyyy-MM-dd'); + } + // Use default render for other formats + return DataTable.render.date('YYYY-MM-DD', abp.localization.currentCulture.name)(data); } }; } From 11193f3c90bbc4478c19541b4e612d5a5fa8cf30 Mon Sep 17 00:00:00 2001 From: David Bright Date: Thu, 26 Feb 2026 14:59:24 -0800 Subject: [PATCH 084/191] 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 056d3ee72c..7a4fab9bf5 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 8fe257a4c37d8ba00903153533656091fecdfc14 Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Thu, 26 Feb 2026 17:16:20 -0800 Subject: [PATCH 085/191] AB#26878: Applicant Profile History Tab - Initial Draft --- .../History/AuditHistoryDto.cs | 12 + .../History/CreateUpdateAuditHistoryDto.cs | 11 + .../History/CreateUpdateFundingHistoryDto.cs | 15 + .../History/CreateUpdateIssueTrackingDto.cs | 13 + .../History/FundingHistoryDto.cs | 16 + .../History/IApplicantHistoryAppService.cs | 29 + .../History/IssueTrackingDto.cs | 14 + .../History/SaveApplicantHistoryNotesDto.cs | 8 + .../History/ApplicantHistoryAppService.cs | 118 + ...rantManagerApplicationAutoMapperProfile.cs | 14 + .../Applications/Applicant.cs | 5 +- .../Applications/AuditHistory.cs | 14 + .../Applications/FundingHistory.cs | 18 + .../Applications/IAuditHistoryRepository.cs | 11 + .../Applications/IFundingHistoryRepository.cs | 11 + .../Applications/IIssueTrackingRepository.cs | 11 + .../Applications/IssueTracking.cs | 16 + .../GrantTenantDbContext.cs | 26 +- ...10_Add_ApplicantHistory_Tables.Designer.cs | 4788 +++++++++++++++++ ...60227001110_Add_ApplicantHistory_Tables.cs | 160 + .../GrantTenantDbContextModelSnapshot.cs | 219 +- .../Repositories/AuditHistoryRepository.cs | 29 + .../Repositories/FundingHistoryRepository.cs | 29 + .../Repositories/IssueTrackingRepository.cs | 29 + .../AuditHistoryModalViewModel.cs | 22 + .../CreateAuditHistoryModal.cshtml | 39 + .../CreateAuditHistoryModal.cshtml.cs | 42 + .../CreateFundingHistoryModal.cshtml | 80 + .../CreateFundingHistoryModal.cshtml.cs | 46 + .../CreateIssueTrackingModal.cshtml | 56 + .../CreateIssueTrackingModal.cshtml.cs | 44 + .../EditAuditHistoryModal.cshtml | 40 + .../EditAuditHistoryModal.cshtml.cs | 50 + .../EditFundingHistoryModal.cshtml | 81 + .../EditFundingHistoryModal.cshtml.cs | 58 + .../EditIssueTrackingModal.cshtml | 57 + .../EditIssueTrackingModal.cshtml.cs | 54 + .../FundingHistoryModalViewModel.cs | 36 + .../IssueTrackingModalViewModel.cs | 26 + .../Pages/Applicants/Details.cshtml | 13 +- .../ApplicantHistoryViewComponent.cs | 62 + .../ApplicantHistoryViewModel.cs | 11 + .../ApplicantHistory/Default.cshtml | 92 + .../Components/ApplicantHistory/Default.css | 40 + .../Components/ApplicantHistory/Default.js | 279 + 45 files changed, 6834 insertions(+), 10 deletions(-) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/AuditHistoryDto.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/CreateUpdateAuditHistoryDto.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/CreateUpdateFundingHistoryDto.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/CreateUpdateIssueTrackingDto.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/FundingHistoryDto.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/IApplicantHistoryAppService.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/IssueTrackingDto.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/SaveApplicantHistoryNotesDto.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/History/ApplicantHistoryAppService.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/AuditHistory.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/FundingHistory.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IAuditHistoryRepository.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IFundingHistoryRepository.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IIssueTrackingRepository.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IssueTracking.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260227001110_Add_ApplicantHistory_Tables.Designer.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260227001110_Add_ApplicantHistory_Tables.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/AuditHistoryRepository.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/FundingHistoryRepository.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/IssueTrackingRepository.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/AuditHistoryModalViewModel.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateAuditHistoryModal.cshtml create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateAuditHistoryModal.cshtml.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateFundingHistoryModal.cshtml create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateFundingHistoryModal.cshtml.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateIssueTrackingModal.cshtml create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateIssueTrackingModal.cshtml.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditAuditHistoryModal.cshtml create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditAuditHistoryModal.cshtml.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditFundingHistoryModal.cshtml create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditFundingHistoryModal.cshtml.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditIssueTrackingModal.cshtml create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditIssueTrackingModal.cshtml.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/FundingHistoryModalViewModel.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/IssueTrackingModalViewModel.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/ApplicantHistoryViewComponent.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/ApplicantHistoryViewModel.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.cshtml create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.css create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.js diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/AuditHistoryDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/AuditHistoryDto.cs new file mode 100644 index 0000000000..481563d524 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/AuditHistoryDto.cs @@ -0,0 +1,12 @@ +using System; +using Volo.Abp.Application.Dtos; + +namespace Unity.GrantManager.ApplicantProfile; + +public class AuditHistoryDto : AuditedEntityDto +{ + public Guid? ApplicantId { get; set; } + public string? AuditTrackingNumber { get; set; } + public DateTime? AuditDate { get; set; } + public string? AuditNote { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/CreateUpdateAuditHistoryDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/CreateUpdateAuditHistoryDto.cs new file mode 100644 index 0000000000..2857823606 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/CreateUpdateAuditHistoryDto.cs @@ -0,0 +1,11 @@ +using System; + +namespace Unity.GrantManager.ApplicantProfile; + +public class CreateUpdateAuditHistoryDto +{ + public Guid? ApplicantId { get; set; } + public string? AuditTrackingNumber { get; set; } + public DateTime? AuditDate { get; set; } + public string? AuditNote { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/CreateUpdateFundingHistoryDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/CreateUpdateFundingHistoryDto.cs new file mode 100644 index 0000000000..a1d9332cdf --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/CreateUpdateFundingHistoryDto.cs @@ -0,0 +1,15 @@ +using System; + +namespace Unity.GrantManager.ApplicantProfile; + +public class CreateUpdateFundingHistoryDto +{ + public Guid? ApplicantId { get; set; } + public string? GrantCategory { get; set; } + public int? FundingYear { get; set; } + public bool? RenewedFunding { get; set; } + public decimal? ApprovedAmount { get; set; } + public decimal? ReconsiderationAmount { get; set; } + public decimal? TotalGrantAmount { get; set; } + public string? FundingNotes { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/CreateUpdateIssueTrackingDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/CreateUpdateIssueTrackingDto.cs new file mode 100644 index 0000000000..a7aaae5e80 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/CreateUpdateIssueTrackingDto.cs @@ -0,0 +1,13 @@ +using System; + +namespace Unity.GrantManager.ApplicantProfile; + +public class CreateUpdateIssueTrackingDto +{ + public Guid? ApplicantId { get; set; } + public int? Year { get; set; } + public string? IssueHeading { get; set; } + public string? IssueDescription { get; set; } + public bool? Resolved { get; set; } + public string? ResolutionNote { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/FundingHistoryDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/FundingHistoryDto.cs new file mode 100644 index 0000000000..cdb292ba68 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/FundingHistoryDto.cs @@ -0,0 +1,16 @@ +using System; +using Volo.Abp.Application.Dtos; + +namespace Unity.GrantManager.ApplicantProfile; + +public class FundingHistoryDto : AuditedEntityDto +{ + public Guid? ApplicantId { get; set; } + public string? GrantCategory { get; set; } + public int? FundingYear { get; set; } + public bool? RenewedFunding { get; set; } + public decimal? ApprovedAmount { get; set; } + public decimal? ReconsiderationAmount { get; set; } + public decimal? TotalGrantAmount { get; set; } + public string? FundingNotes { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/IApplicantHistoryAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/IApplicantHistoryAppService.cs new file mode 100644 index 0000000000..8265fd590a --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/IApplicantHistoryAppService.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace Unity.GrantManager.ApplicantProfile; + +public interface IApplicantHistoryAppService : IApplicationService +{ + Task> GetFundingHistoryListAsync(Guid applicantId); + Task GetFundingHistoryAsync(Guid id); + Task CreateFundingHistoryAsync(CreateUpdateFundingHistoryDto input); + Task UpdateFundingHistoryAsync(Guid id, CreateUpdateFundingHistoryDto input); + Task DeleteFundingHistoryAsync(Guid id); + + Task> GetIssueTrackingListAsync(Guid applicantId); + Task GetIssueTrackingAsync(Guid id); + Task CreateIssueTrackingAsync(CreateUpdateIssueTrackingDto input); + Task UpdateIssueTrackingAsync(Guid id, CreateUpdateIssueTrackingDto input); + Task DeleteIssueTrackingAsync(Guid id); + + Task> GetAuditHistoryListAsync(Guid applicantId); + Task GetAuditHistoryAsync(Guid id); + Task CreateAuditHistoryAsync(CreateUpdateAuditHistoryDto input); + Task UpdateAuditHistoryAsync(Guid id, CreateUpdateAuditHistoryDto input); + Task DeleteAuditHistoryAsync(Guid id); + + Task SaveNotesAsync(Guid applicantId, SaveApplicantHistoryNotesDto input); +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/IssueTrackingDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/IssueTrackingDto.cs new file mode 100644 index 0000000000..4c2fb42e33 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/IssueTrackingDto.cs @@ -0,0 +1,14 @@ +using System; +using Volo.Abp.Application.Dtos; + +namespace Unity.GrantManager.ApplicantProfile; + +public class IssueTrackingDto : AuditedEntityDto +{ + public Guid? ApplicantId { get; set; } + public int? Year { get; set; } + public string? IssueHeading { get; set; } + public string? IssueDescription { get; set; } + public bool? Resolved { get; set; } + public string? ResolutionNote { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/SaveApplicantHistoryNotesDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/SaveApplicantHistoryNotesDto.cs new file mode 100644 index 0000000000..8e72c9c8c5 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/SaveApplicantHistoryNotesDto.cs @@ -0,0 +1,8 @@ +namespace Unity.GrantManager.ApplicantProfile; + +public class SaveApplicantHistoryNotesDto +{ + public string? FundingHistoryComments { get; set; } + public string? IssueTrackingComments { get; set; } + public string? AuditComments { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/History/ApplicantHistoryAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/History/ApplicantHistoryAppService.cs new file mode 100644 index 0000000000..04e6c0b14e --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/History/ApplicantHistoryAppService.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Unity.GrantManager.Applications; + +namespace Unity.GrantManager.ApplicantProfile; + +public class ApplicantHistoryAppService( + IFundingHistoryRepository fundingHistoryRepository, + IIssueTrackingRepository issueTrackingRepository, + IAuditHistoryRepository auditHistoryRepository, + IApplicantRepository applicantRepository) : GrantManagerAppService, IApplicantHistoryAppService +{ + public async Task> GetFundingHistoryListAsync(Guid applicantId) + { + var items = await fundingHistoryRepository.GetByApplicantIdAsync(applicantId); + return ObjectMapper.Map, List>(items); + } + + public async Task GetFundingHistoryAsync(Guid id) + { + var entity = await fundingHistoryRepository.GetAsync(id); + return ObjectMapper.Map(entity); + } + + public async Task CreateFundingHistoryAsync(CreateUpdateFundingHistoryDto input) + { + var entity = ObjectMapper.Map(input); + await fundingHistoryRepository.InsertAsync(entity, autoSave: true); + return ObjectMapper.Map(entity); + } + + public async Task UpdateFundingHistoryAsync(Guid id, CreateUpdateFundingHistoryDto input) + { + var entity = await fundingHistoryRepository.GetAsync(id); + ObjectMapper.Map(input, entity); + await fundingHistoryRepository.UpdateAsync(entity, autoSave: true); + return ObjectMapper.Map(entity); + } + + public async Task DeleteFundingHistoryAsync(Guid id) + { + await fundingHistoryRepository.DeleteAsync(id, autoSave: true); + } + + public async Task> GetIssueTrackingListAsync(Guid applicantId) + { + var items = await issueTrackingRepository.GetByApplicantIdAsync(applicantId); + return ObjectMapper.Map, List>(items); + } + + public async Task GetIssueTrackingAsync(Guid id) + { + var entity = await issueTrackingRepository.GetAsync(id); + return ObjectMapper.Map(entity); + } + + public async Task CreateIssueTrackingAsync(CreateUpdateIssueTrackingDto input) + { + var entity = ObjectMapper.Map(input); + await issueTrackingRepository.InsertAsync(entity, autoSave: true); + return ObjectMapper.Map(entity); + } + + public async Task UpdateIssueTrackingAsync(Guid id, CreateUpdateIssueTrackingDto input) + { + var entity = await issueTrackingRepository.GetAsync(id); + ObjectMapper.Map(input, entity); + await issueTrackingRepository.UpdateAsync(entity, autoSave: true); + return ObjectMapper.Map(entity); + } + + public async Task DeleteIssueTrackingAsync(Guid id) + { + await issueTrackingRepository.DeleteAsync(id, autoSave: true); + } + + public async Task> GetAuditHistoryListAsync(Guid applicantId) + { + var items = await auditHistoryRepository.GetByApplicantIdAsync(applicantId); + return ObjectMapper.Map, List>(items); + } + + public async Task GetAuditHistoryAsync(Guid id) + { + var entity = await auditHistoryRepository.GetAsync(id); + return ObjectMapper.Map(entity); + } + + public async Task CreateAuditHistoryAsync(CreateUpdateAuditHistoryDto input) + { + var entity = ObjectMapper.Map(input); + await auditHistoryRepository.InsertAsync(entity, autoSave: true); + return ObjectMapper.Map(entity); + } + + public async Task UpdateAuditHistoryAsync(Guid id, CreateUpdateAuditHistoryDto input) + { + var entity = await auditHistoryRepository.GetAsync(id); + ObjectMapper.Map(input, entity); + await auditHistoryRepository.UpdateAsync(entity, autoSave: true); + return ObjectMapper.Map(entity); + } + + public async Task DeleteAuditHistoryAsync(Guid id) + { + await auditHistoryRepository.DeleteAsync(id, autoSave: true); + } + + public async Task SaveNotesAsync(Guid applicantId, SaveApplicantHistoryNotesDto input) + { + var applicant = await applicantRepository.GetAsync(applicantId); + applicant.FundingHistoryComments = input.FundingHistoryComments; + applicant.IssueTrackingComments = input.IssueTrackingComments; + applicant.AuditComments = input.AuditComments; + await applicantRepository.UpdateAsync(applicant, autoSave: true); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationAutoMapperProfile.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationAutoMapperProfile.cs index fd3459abe7..a46909e6d6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationAutoMapperProfile.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationAutoMapperProfile.cs @@ -2,6 +2,7 @@ using System; using Unity.GrantManager.ApplicationForms; using Unity.GrantManager.Applications; +using Unity.GrantManager.ApplicantProfile; using Unity.GrantManager.Assessments; using Unity.GrantManager.Attachments; using Unity.GrantManager.Comments; @@ -94,6 +95,19 @@ public GrantManagerApplicationAutoMapperProfile() CreateMap() .ForMember(dest => dest.Tag, opt => opt.MapFrom(src => src.Tag)); + //-- APPLICANT HISTORY + CreateMap(); + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); + CreateMap(); + //-- PROJECT INFO CreateMap() .IgnoreNullAndDefaultValues(); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/Applicant.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/Applicant.cs index 4ded30e777..80d65ae852 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/Applicant.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/Applicant.cs @@ -32,5 +32,8 @@ public class Applicant : AuditedAggregateRoot, IMultiTenant public virtual Collection? ApplicantAddresses { get; set; } public decimal? MatchPercentage { get; set; } public string? NonRegOrgName { get; set; } - public bool? IsDuplicated { get; set; } + public bool? IsDuplicated { get; set; } + public string? FundingHistoryComments { get; set; } + public string? IssueTrackingComments { get; set; } + public string? AuditComments { get; set; } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/AuditHistory.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/AuditHistory.cs new file mode 100644 index 0000000000..84b26e59d5 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/AuditHistory.cs @@ -0,0 +1,14 @@ +using System; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; + +namespace Unity.GrantManager.Applications; + +public class AuditHistory : AuditedAggregateRoot, IMultiTenant +{ + public Guid? ApplicantId { get; set; } + public string? AuditTrackingNumber { get; set; } + public DateTime? AuditDate { get; set; } + public string? AuditNote { get; set; } + public Guid? TenantId { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/FundingHistory.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/FundingHistory.cs new file mode 100644 index 0000000000..e583e5ad61 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/FundingHistory.cs @@ -0,0 +1,18 @@ +using System; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; + +namespace Unity.GrantManager.Applications; + +public class FundingHistory : AuditedAggregateRoot, IMultiTenant +{ + public Guid? ApplicantId { get; set; } + public string? GrantCategory { get; set; } + public int? FundingYear { get; set; } + public bool? RenewedFunding { get; set; } + public decimal? ApprovedAmount { get; set; } + public decimal? ReconsiderationAmount { get; set; } + public decimal? TotalGrantAmount { get; set; } + public string? FundingNotes { get; set; } + public Guid? TenantId { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IAuditHistoryRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IAuditHistoryRepository.cs new file mode 100644 index 0000000000..373f4f613a --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IAuditHistoryRepository.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; + +namespace Unity.GrantManager.Applications; + +public interface IAuditHistoryRepository : IRepository +{ + Task> GetByApplicantIdAsync(Guid applicantId); +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IFundingHistoryRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IFundingHistoryRepository.cs new file mode 100644 index 0000000000..5be4218fc5 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IFundingHistoryRepository.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; + +namespace Unity.GrantManager.Applications; + +public interface IFundingHistoryRepository : IRepository +{ + Task> GetByApplicantIdAsync(Guid applicantId); +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IIssueTrackingRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IIssueTrackingRepository.cs new file mode 100644 index 0000000000..35dfde9085 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IIssueTrackingRepository.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; + +namespace Unity.GrantManager.Applications; + +public interface IIssueTrackingRepository : IRepository +{ + Task> GetByApplicantIdAsync(Guid applicantId); +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IssueTracking.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IssueTracking.cs new file mode 100644 index 0000000000..2e16b8debd --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IssueTracking.cs @@ -0,0 +1,16 @@ +using System; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; + +namespace Unity.GrantManager.Applications; + +public class IssueTracking : AuditedAggregateRoot, IMultiTenant +{ + public Guid? ApplicantId { get; set; } + public int? Year { get; set; } + public string? IssueHeading { get; set; } + public string? IssueDescription { get; set; } + public bool? Resolved { get; set; } + public string? ResolutionNote { get; set; } + public Guid? TenantId { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantTenantDbContext.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantTenantDbContext.cs index 3e042440fd..ee2bdd7ed2 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantTenantDbContext.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantTenantDbContext.cs @@ -46,6 +46,9 @@ public class GrantTenantDbContext : AbpDbContext public DbSet Tags { get; set; } public DbSet Contacts { get; set; } public DbSet ContactLinks { get; set; } + public DbSet FundingHistories { get; set; } + public DbSet IssueTrackings { get; set; } + public DbSet AuditHistories { get; set; } #endregion public GrantTenantDbContext(DbContextOptions options) : base(options) @@ -337,13 +340,34 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.HasOne().WithMany().HasForeignKey(x => x.ContactId).IsRequired(); - b.Property(x => x.RelatedEntityType).IsRequired().HasMaxLength(100); + b.Property(x => x.RelatedEntityType).IsRequired().HasMaxLength(100); b.Property(x => x.Role).HasMaxLength(100); b.HasIndex(x => new { x.ContactId, x.RelatedEntityType, x.RelatedEntityId }); b.HasIndex(x => new { x.RelatedEntityType, x.RelatedEntityId }); }); + modelBuilder.Entity(b => + { + b.ToTable(GrantManagerConsts.TenantTablePrefix + "FundingHistories", GrantManagerConsts.DbSchema); + b.ConfigureByConvention(); + b.HasOne().WithMany().HasForeignKey(x => x.ApplicantId).IsRequired(false); + }); + + modelBuilder.Entity(b => + { + b.ToTable(GrantManagerConsts.TenantTablePrefix + "IssueTrackings", GrantManagerConsts.DbSchema); + b.ConfigureByConvention(); + b.HasOne().WithMany().HasForeignKey(x => x.ApplicantId).IsRequired(false); + }); + + modelBuilder.Entity(b => + { + b.ToTable(GrantManagerConsts.TenantTablePrefix + "AuditHistories", GrantManagerConsts.DbSchema); + b.ConfigureByConvention(); + b.HasOne().WithMany().HasForeignKey(x => x.ApplicantId).IsRequired(false); + }); + var allEntityTypes = modelBuilder.Model.GetEntityTypes(); foreach (var type in allEntityTypes.Where(t => t.ClrType != typeof(ExtraPropertyDictionary)).Select(t => t.ClrType)) { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260227001110_Add_ApplicantHistory_Tables.Designer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260227001110_Add_ApplicantHistory_Tables.Designer.cs new file mode 100644 index 0000000000..86a126dc90 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260227001110_Add_ApplicantHistory_Tables.Designer.cs @@ -0,0 +1,4788 @@ +// +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("20260227001110_Add_ApplicantHistory_Tables")] + partial class Add_ApplicantHistory_Tables + { + /// + 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("AuditComments") + .HasColumnType("text"); + + b.Property("BusinessNumber") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FiscalDay") + .HasColumnType("integer"); + + b.Property("FiscalMonth") + .HasColumnType("text"); + + b.Property("FundingHistoryComments") + .HasColumnType("text"); + + b.Property("IndigenousOrgInd") + .HasColumnType("text"); + + b.Property("IsDuplicated") + .HasColumnType("boolean"); + + b.Property("IssueTrackingComments") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MatchPercentage") + .HasColumnType("numeric"); + + b.Property("NonRegOrgName") + .HasColumnType("text"); + + b.Property("NonRegisteredBusinessName") + .HasColumnType("text"); + + b.Property("OrgName") + .HasColumnType("text"); + + b.Property("OrgNumber") + .HasColumnType("text"); + + b.Property("OrgStatus") + .HasColumnType("text"); + + b.Property("OrganizationSize") + .HasColumnType("text"); + + b.Property("OrganizationType") + .HasColumnType("text"); + + b.Property("RedStop") + .HasColumnType("boolean"); + + b.Property("Sector") + .HasColumnType("text"); + + b.Property("SectorSubSectorIndustryDesc") + .HasColumnType("text"); + + b.Property("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") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PreventPayment") + .HasColumnType("boolean"); + + b.Property("RenderFormIoToHtml") + .HasColumnType("boolean"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("SuffixType") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IntakeId"); + + b.HasIndex("ParentFormId"); + + b.ToTable("ApplicationForms", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormSubmission", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormVersionId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ChefsSubmissionGuid") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormVersionId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OidcSub") + .IsRequired() + .HasColumnType("text"); + + b.Property("RenderedHTML") + .HasColumnType("text"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Submission") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormSubmissions", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormVersion", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("AvailableChefsFields") + .HasColumnType("text"); + + b.Property("ChefsApplicationFormGuid") + .HasColumnType("text"); + + b.Property("ChefsFormVersionGuid") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormSchema") + .HasColumnType("jsonb"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SubmissionHeaderMapping") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormVersion", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationLink", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LinkType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("Related"); + + b.Property("LinkedApplicationId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationLinks", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationStatus", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExternalStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("InternalStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("StatusCode") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("StatusCode") + .IsUnique(); + + b.ToTable("ApplicationStatuses", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationTags", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TagId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("TagId"); + + b.ToTable("ApplicationTags", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AssessmentAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AssessmentId"); + + b.ToTable("AssessmentAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AuditHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("AuditDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AuditNote") + .HasColumnType("text"); + + b.Property("AuditTrackingNumber") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("AuditHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.FundingHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApprovedAmount") + .HasColumnType("numeric"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FundingNotes") + .HasColumnType("text"); + + b.Property("FundingYear") + .HasColumnType("integer"); + + b.Property("GrantCategory") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReconsiderationAmount") + .HasColumnType("numeric"); + + b.Property("RenewedFunding") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TotalGrantAmount") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("FundingHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.IssueTracking", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IssueDescription") + .HasColumnType("text"); + + b.Property("IssueHeading") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ResolutionNote") + .HasColumnType("text"); + + b.Property("Resolved") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("IssueTrackings", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Assessments.Assessment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ApprovalRecommended") + .HasColumnType("boolean"); + + b.Property("AssessorId") + .HasColumnType("uuid"); + + b.Property("CleanGrowth") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("EconomicImpact") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FinancialAnalysis") + .HasColumnType("integer"); + + b.Property("InclusiveGrowth") + .HasColumnType("integer"); + + b.Property("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.Applications.AuditHistory", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.FundingHistory", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.IssueTracking", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Assessments.Assessment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("Assessments") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("AssessorId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.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/20260227001110_Add_ApplicantHistory_Tables.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260227001110_Add_ApplicantHistory_Tables.cs new file mode 100644 index 0000000000..6de3f30d88 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260227001110_Add_ApplicantHistory_Tables.cs @@ -0,0 +1,160 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + /// + public partial class Add_ApplicantHistory_Tables : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AuditComments", + table: "Applicants", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "FundingHistoryComments", + table: "Applicants", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "IssueTrackingComments", + table: "Applicants", + type: "text", + nullable: true); + + migrationBuilder.CreateTable( + name: "AuditHistories", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ApplicantId = table.Column(type: "uuid", nullable: true), + AuditTrackingNumber = table.Column(type: "text", nullable: true), + AuditDate = table.Column(type: "timestamp without time zone", nullable: true), + AuditNote = table.Column(type: "text", nullable: true), + TenantId = table.Column(type: "uuid", nullable: true), + ExtraProperties = table.Column(type: "text", nullable: false), + ConcurrencyStamp = table.Column(type: "character varying(40)", maxLength: 40, nullable: false), + CreationTime = table.Column(type: "timestamp without time zone", nullable: false), + CreatorId = table.Column(type: "uuid", nullable: true), + LastModificationTime = table.Column(type: "timestamp without time zone", nullable: true), + LastModifierId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AuditHistories", x => x.Id); + table.ForeignKey( + name: "FK_AuditHistories_Applicants_ApplicantId", + column: x => x.ApplicantId, + principalTable: "Applicants", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "FundingHistories", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ApplicantId = table.Column(type: "uuid", nullable: true), + GrantCategory = table.Column(type: "text", nullable: true), + FundingYear = table.Column(type: "integer", nullable: true), + RenewedFunding = table.Column(type: "boolean", nullable: true), + ApprovedAmount = table.Column(type: "numeric", nullable: true), + ReconsiderationAmount = table.Column(type: "numeric", nullable: true), + TotalGrantAmount = table.Column(type: "numeric", nullable: true), + FundingNotes = table.Column(type: "text", nullable: true), + TenantId = table.Column(type: "uuid", nullable: true), + ExtraProperties = table.Column(type: "text", nullable: false), + ConcurrencyStamp = table.Column(type: "character varying(40)", maxLength: 40, nullable: false), + CreationTime = table.Column(type: "timestamp without time zone", nullable: false), + CreatorId = table.Column(type: "uuid", nullable: true), + LastModificationTime = table.Column(type: "timestamp without time zone", nullable: true), + LastModifierId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_FundingHistories", x => x.Id); + table.ForeignKey( + name: "FK_FundingHistories_Applicants_ApplicantId", + column: x => x.ApplicantId, + principalTable: "Applicants", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "IssueTrackings", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ApplicantId = table.Column(type: "uuid", nullable: true), + Year = table.Column(type: "integer", nullable: true), + IssueHeading = table.Column(type: "text", nullable: true), + IssueDescription = table.Column(type: "text", nullable: true), + Resolved = table.Column(type: "boolean", nullable: true), + ResolutionNote = table.Column(type: "text", nullable: true), + TenantId = table.Column(type: "uuid", nullable: true), + ExtraProperties = table.Column(type: "text", nullable: false), + ConcurrencyStamp = table.Column(type: "character varying(40)", maxLength: 40, nullable: false), + CreationTime = table.Column(type: "timestamp without time zone", nullable: false), + CreatorId = table.Column(type: "uuid", nullable: true), + LastModificationTime = table.Column(type: "timestamp without time zone", nullable: true), + LastModifierId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_IssueTrackings", x => x.Id); + table.ForeignKey( + name: "FK_IssueTrackings_Applicants_ApplicantId", + column: x => x.ApplicantId, + principalTable: "Applicants", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_AuditHistories_ApplicantId", + table: "AuditHistories", + column: "ApplicantId"); + + migrationBuilder.CreateIndex( + name: "IX_FundingHistories_ApplicantId", + table: "FundingHistories", + column: "ApplicantId"); + + migrationBuilder.CreateIndex( + name: "IX_IssueTrackings_ApplicantId", + table: "IssueTrackings", + column: "ApplicantId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AuditHistories"); + + migrationBuilder.DropTable( + name: "FundingHistories"); + + migrationBuilder.DropTable( + name: "IssueTrackings"); + + migrationBuilder.DropColumn( + name: "AuditComments", + table: "Applicants"); + + migrationBuilder.DropColumn( + name: "FundingHistoryComments", + table: "Applicants"); + + migrationBuilder.DropColumn( + name: "IssueTrackingComments", + table: "Applicants"); + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/GrantTenantDbContextModelSnapshot.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/GrantTenantDbContextModelSnapshot.cs index be337f9d19..67f1c6df2f 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 @@ -798,6 +798,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ApproxNumberOfEmployees") .HasColumnType("text"); + b.Property("AuditComments") + .HasColumnType("text"); + b.Property("BusinessNumber") .HasColumnType("text"); @@ -827,12 +830,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("FiscalMonth") .HasColumnType("text"); + b.Property("FundingHistoryComments") + .HasColumnType("text"); + b.Property("IndigenousOrgInd") .HasColumnType("text"); b.Property("IsDuplicated") .HasColumnType("boolean"); + b.Property("IssueTrackingComments") + .HasColumnType("text"); + b.Property("LastModificationTime") .HasColumnType("timestamp without time zone") .HasColumnName("LastModificationTime"); @@ -1667,7 +1676,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("numeric"); b.Property("Prefix") - .HasColumnType("text"); + .HasMaxLength(100) + .HasColumnType("character varying(100)"); b.Property("PreventPayment") .HasColumnType("boolean"); @@ -2087,6 +2097,192 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AssessmentAttachments", (string)null); }); + modelBuilder.Entity("Unity.GrantManager.Applications.AuditHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("AuditDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AuditNote") + .HasColumnType("text"); + + b.Property("AuditTrackingNumber") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("AuditHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.FundingHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApprovedAmount") + .HasColumnType("numeric"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FundingNotes") + .HasColumnType("text"); + + b.Property("FundingYear") + .HasColumnType("integer"); + + b.Property("GrantCategory") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReconsiderationAmount") + .HasColumnType("numeric"); + + b.Property("RenewedFunding") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TotalGrantAmount") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("FundingHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.IssueTracking", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IssueDescription") + .HasColumnType("text"); + + b.Property("IssueHeading") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ResolutionNote") + .HasColumnType("text"); + + b.Property("Resolved") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("IssueTrackings", (string)null); + }); + modelBuilder.Entity("Unity.GrantManager.Assessments.Assessment", b => { b.Property("Id") @@ -4314,6 +4510,27 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired(); }); + modelBuilder.Entity("Unity.GrantManager.Applications.AuditHistory", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.FundingHistory", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.IssueTracking", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + modelBuilder.Entity("Unity.GrantManager.Assessments.Assessment", b => { b.HasOne("Unity.GrantManager.Applications.Application", "Application") diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/AuditHistoryRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/AuditHistoryRepository.cs new file mode 100644 index 0000000000..9e51bba158 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/AuditHistoryRepository.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Unity.GrantManager.Applications; +using Unity.GrantManager.EntityFrameworkCore; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +namespace Unity.GrantManager.Repositories; + +[Dependency(ReplaceServices = true)] +[ExposeServices(typeof(IAuditHistoryRepository))] +public class AuditHistoryRepository : EfCoreRepository, IAuditHistoryRepository +{ + public AuditHistoryRepository(IDbContextProvider dbContextProvider) : base(dbContextProvider) + { + } + + public async Task> GetByApplicantIdAsync(Guid applicantId) + { + var dbContext = await GetDbContextAsync(); + return await dbContext.AuditHistories + .Where(x => x.ApplicantId == applicantId) + .ToListAsync(); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/FundingHistoryRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/FundingHistoryRepository.cs new file mode 100644 index 0000000000..5f15cd1ab2 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/FundingHistoryRepository.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Unity.GrantManager.Applications; +using Unity.GrantManager.EntityFrameworkCore; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +namespace Unity.GrantManager.Repositories; + +[Dependency(ReplaceServices = true)] +[ExposeServices(typeof(IFundingHistoryRepository))] +public class FundingHistoryRepository : EfCoreRepository, IFundingHistoryRepository +{ + public FundingHistoryRepository(IDbContextProvider dbContextProvider) : base(dbContextProvider) + { + } + + public async Task> GetByApplicantIdAsync(Guid applicantId) + { + var dbContext = await GetDbContextAsync(); + return await dbContext.FundingHistories + .Where(x => x.ApplicantId == applicantId) + .ToListAsync(); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/IssueTrackingRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/IssueTrackingRepository.cs new file mode 100644 index 0000000000..98a37cd08f --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/IssueTrackingRepository.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Unity.GrantManager.Applications; +using Unity.GrantManager.EntityFrameworkCore; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +namespace Unity.GrantManager.Repositories; + +[Dependency(ReplaceServices = true)] +[ExposeServices(typeof(IIssueTrackingRepository))] +public class IssueTrackingRepository : EfCoreRepository, IIssueTrackingRepository +{ + public IssueTrackingRepository(IDbContextProvider dbContextProvider) : base(dbContextProvider) + { + } + + public async Task> GetByApplicantIdAsync(Guid applicantId) + { + var dbContext = await GetDbContextAsync(); + return await dbContext.IssueTrackings + .Where(x => x.ApplicantId == applicantId) + .ToListAsync(); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/AuditHistoryModalViewModel.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/AuditHistoryModalViewModel.cs new file mode 100644 index 0000000000..24d47fa4ad --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/AuditHistoryModalViewModel.cs @@ -0,0 +1,22 @@ +using System; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Mvc; + +namespace Unity.GrantManager.Web.Pages.ApplicantHistory; + +public class AuditHistoryModalViewModel +{ + [HiddenInput] + public Guid ApplicantId { get; set; } + + [DisplayName("Audit Tracking Number")] + public string? AuditTrackingNumber { get; set; } + + [DisplayName("Audit Date")] + [DataType(DataType.Date)] + public DateTime? AuditDate { get; set; } + + [DisplayName("Audit Note")] + public string? AuditNote { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateAuditHistoryModal.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateAuditHistoryModal.cshtml new file mode 100644 index 0000000000..730d7a81f5 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateAuditHistoryModal.cshtml @@ -0,0 +1,39 @@ +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@model Unity.GrantManager.Web.Pages.ApplicantHistory.CreateAuditHistoryModal +@{ + Layout = null; +} + +
+ + + + + +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + + + +
+
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateAuditHistoryModal.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateAuditHistoryModal.cshtml.cs new file mode 100644 index 0000000000..1e3c4f2ed5 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateAuditHistoryModal.cshtml.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Threading.Tasks; +using Unity.GrantManager.ApplicantProfile; +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; + +namespace Unity.GrantManager.Web.Pages.ApplicantHistory; + +public class CreateAuditHistoryModal : AbpPageModel +{ + [BindProperty] + public AuditHistoryModalViewModel? AuditHistoryForm { get; set; } + + private readonly IApplicantHistoryAppService _applicantHistoryAppService; + + public CreateAuditHistoryModal(IApplicantHistoryAppService applicantHistoryAppService) + { + _applicantHistoryAppService = applicantHistoryAppService; + } + + public void OnGet(Guid applicantId) + { + AuditHistoryForm = new AuditHistoryModalViewModel + { + ApplicantId = applicantId + }; + } + + public async Task OnPostAsync() + { + var dto = new CreateUpdateAuditHistoryDto + { + ApplicantId = AuditHistoryForm!.ApplicantId, + AuditTrackingNumber = AuditHistoryForm.AuditTrackingNumber, + AuditDate = AuditHistoryForm.AuditDate, + AuditNote = AuditHistoryForm.AuditNote + }; + + await _applicantHistoryAppService.CreateAuditHistoryAsync(dto); + return NoContent(); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateFundingHistoryModal.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateFundingHistoryModal.cshtml new file mode 100644 index 0000000000..ca90bc019b --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateFundingHistoryModal.cshtml @@ -0,0 +1,80 @@ +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@model Unity.GrantManager.Web.Pages.ApplicantHistory.CreateFundingHistoryModal +@{ + Layout = null; +} + +
+ + + + + +
+ + +
+ +
+ + +
+ +
+ +
+
+ + +
+
+
+ +
+
+ $ + + +
+
+ +
+
+ $ + + +
+
+ +
+
+ $ + + +
+
+ +
+ + +
+
+ + + + +
+
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateFundingHistoryModal.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateFundingHistoryModal.cshtml.cs new file mode 100644 index 0000000000..f8a9332304 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateFundingHistoryModal.cshtml.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Threading.Tasks; +using Unity.GrantManager.ApplicantProfile; +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; + +namespace Unity.GrantManager.Web.Pages.ApplicantHistory; + +public class CreateFundingHistoryModal : AbpPageModel +{ + [BindProperty] + public FundingHistoryModalViewModel? FundingHistoryForm { get; set; } + + private readonly IApplicantHistoryAppService _applicantHistoryAppService; + + public CreateFundingHistoryModal(IApplicantHistoryAppService applicantHistoryAppService) + { + _applicantHistoryAppService = applicantHistoryAppService; + } + + public void OnGet(Guid applicantId) + { + FundingHistoryForm = new FundingHistoryModalViewModel + { + ApplicantId = applicantId + }; + } + + public async Task OnPostAsync() + { + var dto = new CreateUpdateFundingHistoryDto + { + ApplicantId = FundingHistoryForm!.ApplicantId, + GrantCategory = FundingHistoryForm.GrantCategory, + FundingYear = FundingHistoryForm.FundingYear, + RenewedFunding = FundingHistoryForm.RenewedFunding, + ApprovedAmount = FundingHistoryForm.ApprovedAmount, + ReconsiderationAmount = FundingHistoryForm.ReconsiderationAmount, + TotalGrantAmount = FundingHistoryForm.TotalGrantAmount, + FundingNotes = FundingHistoryForm.FundingNotes + }; + + await _applicantHistoryAppService.CreateFundingHistoryAsync(dto); + return NoContent(); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateIssueTrackingModal.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateIssueTrackingModal.cshtml new file mode 100644 index 0000000000..823a0874ac --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateIssueTrackingModal.cshtml @@ -0,0 +1,56 @@ +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@model Unity.GrantManager.Web.Pages.ApplicantHistory.CreateIssueTrackingModal +@{ + Layout = null; +} + +
+ + + + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ + +
+
+
+ +
+ + +
+
+ + + + +
+
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateIssueTrackingModal.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateIssueTrackingModal.cshtml.cs new file mode 100644 index 0000000000..3bbf0f11ed --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateIssueTrackingModal.cshtml.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Threading.Tasks; +using Unity.GrantManager.ApplicantProfile; +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; + +namespace Unity.GrantManager.Web.Pages.ApplicantHistory; + +public class CreateIssueTrackingModal : AbpPageModel +{ + [BindProperty] + public IssueTrackingModalViewModel? IssueTrackingForm { get; set; } + + private readonly IApplicantHistoryAppService _applicantHistoryAppService; + + public CreateIssueTrackingModal(IApplicantHistoryAppService applicantHistoryAppService) + { + _applicantHistoryAppService = applicantHistoryAppService; + } + + public void OnGet(Guid applicantId) + { + IssueTrackingForm = new IssueTrackingModalViewModel + { + ApplicantId = applicantId + }; + } + + public async Task OnPostAsync() + { + var dto = new CreateUpdateIssueTrackingDto + { + ApplicantId = IssueTrackingForm!.ApplicantId, + Year = IssueTrackingForm.Year, + IssueHeading = IssueTrackingForm.IssueHeading, + IssueDescription = IssueTrackingForm.IssueDescription, + Resolved = IssueTrackingForm.Resolved, + ResolutionNote = IssueTrackingForm.ResolutionNote + }; + + await _applicantHistoryAppService.CreateIssueTrackingAsync(dto); + return NoContent(); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditAuditHistoryModal.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditAuditHistoryModal.cshtml new file mode 100644 index 0000000000..0f27446e20 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditAuditHistoryModal.cshtml @@ -0,0 +1,40 @@ +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@model Unity.GrantManager.Web.Pages.ApplicantHistory.EditAuditHistoryModal +@{ + Layout = null; +} + +
+ + + + + + +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + + + +
+
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditAuditHistoryModal.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditAuditHistoryModal.cshtml.cs new file mode 100644 index 0000000000..c34409c8b3 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditAuditHistoryModal.cshtml.cs @@ -0,0 +1,50 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Threading.Tasks; +using Unity.GrantManager.ApplicantProfile; +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; + +namespace Unity.GrantManager.Web.Pages.ApplicantHistory; + +public class EditAuditHistoryModal : AbpPageModel +{ + [BindProperty(SupportsGet = true)] + public Guid Id { get; set; } + + [BindProperty] + public AuditHistoryModalViewModel? AuditHistoryForm { get; set; } + + private readonly IApplicantHistoryAppService _applicantHistoryAppService; + + public EditAuditHistoryModal(IApplicantHistoryAppService applicantHistoryAppService) + { + _applicantHistoryAppService = applicantHistoryAppService; + } + + public async Task OnGetAsync(Guid id) + { + Id = id; + var record = await _applicantHistoryAppService.GetAuditHistoryAsync(id); + AuditHistoryForm = new AuditHistoryModalViewModel + { + ApplicantId = record.ApplicantId ?? Guid.Empty, + AuditTrackingNumber = record.AuditTrackingNumber, + AuditDate = record.AuditDate, + AuditNote = record.AuditNote + }; + } + + public async Task OnPostAsync() + { + var dto = new CreateUpdateAuditHistoryDto + { + ApplicantId = AuditHistoryForm!.ApplicantId, + AuditTrackingNumber = AuditHistoryForm.AuditTrackingNumber, + AuditDate = AuditHistoryForm.AuditDate, + AuditNote = AuditHistoryForm.AuditNote + }; + + await _applicantHistoryAppService.UpdateAuditHistoryAsync(Id, dto); + return NoContent(); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditFundingHistoryModal.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditFundingHistoryModal.cshtml new file mode 100644 index 0000000000..1deb5b35e0 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditFundingHistoryModal.cshtml @@ -0,0 +1,81 @@ +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@model Unity.GrantManager.Web.Pages.ApplicantHistory.EditFundingHistoryModal +@{ + Layout = null; +} + +
+ + + + + + +
+ + +
+ +
+ + +
+ +
+ +
+
+ + +
+
+
+ +
+
+ $ + + +
+
+ +
+
+ $ + + +
+
+ +
+
+ $ + + +
+
+ +
+ + +
+
+ + + + +
+
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditFundingHistoryModal.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditFundingHistoryModal.cshtml.cs new file mode 100644 index 0000000000..33e98e1f38 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditFundingHistoryModal.cshtml.cs @@ -0,0 +1,58 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Threading.Tasks; +using Unity.GrantManager.ApplicantProfile; +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; + +namespace Unity.GrantManager.Web.Pages.ApplicantHistory; + +public class EditFundingHistoryModal : AbpPageModel +{ + [BindProperty(SupportsGet = true)] + public Guid Id { get; set; } + + [BindProperty] + public FundingHistoryModalViewModel? FundingHistoryForm { get; set; } + + private readonly IApplicantHistoryAppService _applicantHistoryAppService; + + public EditFundingHistoryModal(IApplicantHistoryAppService applicantHistoryAppService) + { + _applicantHistoryAppService = applicantHistoryAppService; + } + + public async Task OnGetAsync(Guid id) + { + Id = id; + var record = await _applicantHistoryAppService.GetFundingHistoryAsync(id); + FundingHistoryForm = new FundingHistoryModalViewModel + { + ApplicantId = record.ApplicantId ?? Guid.Empty, + GrantCategory = record.GrantCategory, + FundingYear = record.FundingYear, + RenewedFunding = record.RenewedFunding, + ApprovedAmount = record.ApprovedAmount, + ReconsiderationAmount = record.ReconsiderationAmount, + TotalGrantAmount = record.TotalGrantAmount, + FundingNotes = record.FundingNotes + }; + } + + public async Task OnPostAsync() + { + var dto = new CreateUpdateFundingHistoryDto + { + ApplicantId = FundingHistoryForm!.ApplicantId, + GrantCategory = FundingHistoryForm.GrantCategory, + FundingYear = FundingHistoryForm.FundingYear, + RenewedFunding = FundingHistoryForm.RenewedFunding, + ApprovedAmount = FundingHistoryForm.ApprovedAmount, + ReconsiderationAmount = FundingHistoryForm.ReconsiderationAmount, + TotalGrantAmount = FundingHistoryForm.TotalGrantAmount, + FundingNotes = FundingHistoryForm.FundingNotes + }; + + await _applicantHistoryAppService.UpdateFundingHistoryAsync(Id, dto); + return NoContent(); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditIssueTrackingModal.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditIssueTrackingModal.cshtml new file mode 100644 index 0000000000..b0acc6b1e8 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditIssueTrackingModal.cshtml @@ -0,0 +1,57 @@ +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@model Unity.GrantManager.Web.Pages.ApplicantHistory.EditIssueTrackingModal +@{ + Layout = null; +} + +
+ + + + + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ + +
+
+
+ +
+ + +
+
+ + + + +
+
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditIssueTrackingModal.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditIssueTrackingModal.cshtml.cs new file mode 100644 index 0000000000..88c743afe6 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditIssueTrackingModal.cshtml.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Threading.Tasks; +using Unity.GrantManager.ApplicantProfile; +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; + +namespace Unity.GrantManager.Web.Pages.ApplicantHistory; + +public class EditIssueTrackingModal : AbpPageModel +{ + [BindProperty(SupportsGet = true)] + public Guid Id { get; set; } + + [BindProperty] + public IssueTrackingModalViewModel? IssueTrackingForm { get; set; } + + private readonly IApplicantHistoryAppService _applicantHistoryAppService; + + public EditIssueTrackingModal(IApplicantHistoryAppService applicantHistoryAppService) + { + _applicantHistoryAppService = applicantHistoryAppService; + } + + public async Task OnGetAsync(Guid id) + { + Id = id; + var record = await _applicantHistoryAppService.GetIssueTrackingAsync(id); + IssueTrackingForm = new IssueTrackingModalViewModel + { + ApplicantId = record.ApplicantId ?? Guid.Empty, + Year = record.Year, + IssueHeading = record.IssueHeading, + IssueDescription = record.IssueDescription, + Resolved = record.Resolved, + ResolutionNote = record.ResolutionNote + }; + } + + public async Task OnPostAsync() + { + var dto = new CreateUpdateIssueTrackingDto + { + ApplicantId = IssueTrackingForm!.ApplicantId, + Year = IssueTrackingForm.Year, + IssueHeading = IssueTrackingForm.IssueHeading, + IssueDescription = IssueTrackingForm.IssueDescription, + Resolved = IssueTrackingForm.Resolved, + ResolutionNote = IssueTrackingForm.ResolutionNote + }; + + await _applicantHistoryAppService.UpdateIssueTrackingAsync(Id, dto); + return NoContent(); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/FundingHistoryModalViewModel.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/FundingHistoryModalViewModel.cs new file mode 100644 index 0000000000..819265002b --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/FundingHistoryModalViewModel.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace Unity.GrantManager.Web.Pages.ApplicantHistory; + +public class FundingHistoryModalViewModel +{ + [HiddenInput] + public Guid ApplicantId { get; set; } + + [DisplayName("Grant Category")] + public string? GrantCategory { get; set; } + + [DisplayName("Funding Year")] + public int? FundingYear { get; set; } + + [DisplayName("Renewed Funding")] + public bool? RenewedFunding { get; set; } + + [DisplayName("Approved Amount")] + [DataType(DataType.Currency)] + public decimal? ApprovedAmount { get; set; } + + [DisplayName("Reconsideration Amount")] + [DataType(DataType.Currency)] + public decimal? ReconsiderationAmount { get; set; } + + [DisplayName("Total Grant Amount")] + [DataType(DataType.Currency)] + public decimal? TotalGrantAmount { get; set; } + + [DisplayName("Funding Notes")] + public string? FundingNotes { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/IssueTrackingModalViewModel.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/IssueTrackingModalViewModel.cs new file mode 100644 index 0000000000..2877253c28 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/IssueTrackingModalViewModel.cs @@ -0,0 +1,26 @@ +using System; +using System.ComponentModel; +using Microsoft.AspNetCore.Mvc; + +namespace Unity.GrantManager.Web.Pages.ApplicantHistory; + +public class IssueTrackingModalViewModel +{ + [HiddenInput] + public Guid ApplicantId { get; set; } + + [DisplayName("Year")] + public int? Year { get; set; } + + [DisplayName("Issue Heading")] + public string? IssueHeading { get; set; } + + [DisplayName("Issue Description")] + public string? IssueDescription { get; set; } + + [DisplayName("Resolved")] + public bool? Resolved { get; set; } + + [DisplayName("Resolution Note")] + public string? ResolutionNote { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml index d0cc9134f6..5e9f9aefa3 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml @@ -67,14 +67,13 @@ @*-------- Submissions Section END ---------*@ - @*-------- Payments Section ---------*@ - -
-
Under Construction
- @*-------- await Component.InvokeAsync("ApplicantPayments", new { applicantId = Model.ApplicantId }) ---------*@ + @*-------- History Section ---------*@ + +
+ @await Component.InvokeAsync("ApplicantHistory", new { applicantId = Model.ApplicantId })
- @*-------- Payments Section END ---------*@ + @*-------- History Section END ---------*@ @@ -177,4 +176,4 @@
-
\ No newline at end of file + diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/ApplicantHistoryViewComponent.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/ApplicantHistoryViewComponent.cs new file mode 100644 index 0000000000..8a97579040 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/ApplicantHistoryViewComponent.cs @@ -0,0 +1,62 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Unity.GrantManager.Applications; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace Unity.GrantManager.Web.Views.Shared.Components.ApplicantHistory +{ + [Widget( + RefreshUrl = "Widget/ApplicantHistory/Refresh", + ScriptTypes = new[] { typeof(ApplicantHistoryScriptBundleContributor) }, + StyleTypes = new[] { typeof(ApplicantHistoryStyleBundleContributor) }, + AutoInitialize = true)] + public class ApplicantHistoryViewComponent : AbpViewComponent + { + private readonly IApplicantRepository _applicantRepository; + + public ApplicantHistoryViewComponent(IApplicantRepository applicantRepository) + { + _applicantRepository = applicantRepository; + } + + public async Task InvokeAsync(Guid applicantId) + { + if (applicantId == Guid.Empty) + { + return View(new ApplicantHistoryViewModel { ApplicantId = applicantId }); + } + + var applicant = await _applicantRepository.GetAsync(applicantId); + + var viewModel = new ApplicantHistoryViewModel + { + ApplicantId = applicantId, + FundingHistoryComments = applicant.FundingHistoryComments, + IssueTrackingComments = applicant.IssueTrackingComments, + AuditComments = applicant.AuditComments + }; + + return View(viewModel); + } + } + + public class ApplicantHistoryScriptBundleContributor : BundleContributor + { + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("/Views/Shared/Components/ApplicantHistory/Default.js"); + } + } + + public class ApplicantHistoryStyleBundleContributor : BundleContributor + { + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("/Views/Shared/Components/ApplicantHistory/Default.css"); + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/ApplicantHistoryViewModel.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/ApplicantHistoryViewModel.cs new file mode 100644 index 0000000000..aaf1a25e63 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/ApplicantHistoryViewModel.cs @@ -0,0 +1,11 @@ +using System; + +namespace Unity.GrantManager.Web.Views.Shared.Components.ApplicantHistory; + +public class ApplicantHistoryViewModel +{ + public Guid ApplicantId { get; set; } + public string? FundingHistoryComments { get; set; } + public string? IssueTrackingComments { get; set; } + public string? AuditComments { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.cshtml new file mode 100644 index 0000000000..caf33c13a5 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.cshtml @@ -0,0 +1,92 @@ +@using Unity.GrantManager.Web.Views.Shared.Components.ApplicantHistory + +@model ApplicantHistoryViewModel + +@{ + Layout = null; +} + +
+ +
+ +
+ +
+ + + @* ── Funding History ── *@ +
+
+
Funding History
+
+
+ Previous Funding History +
+
+ +
+
+ + +
+ + + +
+
+ + @* ── Issue Tracking ── *@ +
+
+
Issue Tracking
+
+
+ Previous Issues +
+
+ +
+
+ + +
+ + + +
+
+ + @* ── Audit History ── (no search bar per mockup) *@ +
+
+
Audit History
+
+ + + +
+ + + +
+
+
+
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.css new file mode 100644 index 0000000000..d93e1286d3 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.css @@ -0,0 +1,40 @@ +/* ApplicantHistory Component Styles */ + +.applicant-info-container { + margin-bottom: 1.5rem; + position: relative; +} + +.applicant-info-container .save-button-container { + display: flex; + justify-content: flex-end; + position: sticky; + top: 72px; + padding-right: 24px; + z-index: 999; +} + +.applicant-info-container .save-button-container .floating-save-btn { + position: static; +} + +.history-section { + border-bottom: 1px solid #dee2e6; + margin-bottom: 1.5rem; + padding-bottom: 1.5rem; +} + +.history-section:last-child { + border-bottom: none; +} + +.history-section__toolbar { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.75rem; +} + +.history-section__toolbar .tbl-search { + max-width: 280px; +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.js new file mode 100644 index 0000000000..450ef326e0 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.js @@ -0,0 +1,279 @@ +$(function () { + const nullPlaceholder = '—'; + const formatter = typeof createNumberFormatter === 'function' ? createNumberFormatter() : null; + const formatCurrency = (val) => formatter ? formatter.format(val) : (val ?? nullPlaceholder); + + // ── Save button / dirty tracking ────────────────────────────────────────── + const form = $('#ApplicantHistoryNotesForm'); + const saveBtn = $('#saveApplicantHistoryBtn'); + let zoneForm = null; + + if (form.length && saveBtn.length && typeof UnityZoneForm === 'function') { + zoneForm = new UnityZoneForm(form, { saveButtonSelector: '#saveApplicantHistoryBtn' }); + zoneForm.init(); + } + + saveBtn.on('click', function (e) { + e.preventDefault(); + if (!zoneForm || zoneForm.modifiedFields.size === 0) return; + + const applicantId = $('#ApplicantHistory_ApplicantId').val(); + if (!applicantId) { + abp.notify.warn('Applicant identifier is missing.'); + return; + } + + unity.grantManager.applicantProfile.applicantHistory + .saveNotes(applicantId, { + fundingHistoryComments: $('#FundingHistoryComments').val(), + issueTrackingComments: $('#IssueTrackingComments').val(), + auditComments: $('#AuditComments').val() + }) + .done(function () { + abp.notify.success('History notes saved.'); + zoneForm.resetTracking(); + }) + .fail(function () { + abp.notify.error('Failed to save history notes.'); + }); + }); + + // ── Column definitions ──────────────────────────────────────────────────── + + function getFundingHistoryColumns() { + return [ + { title: 'Grant Category', data: 'grantCategory', name: 'grantCategory', className: 'data-table-header', render: (d) => d ?? nullPlaceholder }, + { title: 'Funding Year', data: 'fundingYear', name: 'fundingYear', className: 'data-table-header', render: (d) => d ?? nullPlaceholder }, + { title: 'Renewed Funding', data: 'renewedFunding', name: 'renewedFunding', className: 'data-table-header', render: (d) => d === true ? 'Yes' : d === false ? 'No' : nullPlaceholder }, + { title: 'Approved Amount', data: 'approvedAmount', name: 'approvedAmount', className: 'data-table-header currency-display', render: (d) => formatCurrency(d) }, + { title: 'Reconsideration Amount', data: 'reconsiderationAmount', name: 'reconsiderationAmount', className: 'data-table-header currency-display', render: (d) => formatCurrency(d) }, + { title: 'Total Grant Amount', data: 'totalGrantAmount', name: 'totalGrantAmount', className: 'data-table-header currency-display', render: (d) => formatCurrency(d) }, + { title: 'Notes', data: 'fundingNotes', name: 'fundingNotes', className: 'data-table-header', render: (d) => d ?? nullPlaceholder }, + { + title: 'Actions', data: null, name: 'actions', orderable: false, className: 'data-table-header', + render: function (data, type, row) { + return `` + + ``; + } + } + ].map(function (col, i) { col.index = i; col.targets = [i]; return col; }); + } + + function getIssueTrackingColumns() { + return [ + { title: 'Year', data: 'year', name: 'year', className: 'data-table-header', render: (d) => d ?? nullPlaceholder }, + { title: 'Issue Heading', data: 'issueHeading', name: 'issueHeading', className: 'data-table-header', render: (d) => d ?? nullPlaceholder }, + { title: 'Issue Description', data: 'issueDescription', name: 'issueDescription', className: 'data-table-header', render: (d) => d ?? nullPlaceholder }, + { title: 'Resolved', data: 'resolved', name: 'resolved', className: 'data-table-header', render: (d) => d === true ? 'Yes' : d === false ? 'No' : nullPlaceholder }, + { title: 'Resolution Note', data: 'resolutionNote', name: 'resolutionNote', className: 'data-table-header', render: (d) => d ?? nullPlaceholder }, + { + title: 'Actions', data: null, name: 'actions', orderable: false, className: 'data-table-header', + render: function (data, type, row) { + return `` + + ``; + } + } + ].map(function (col, i) { col.index = i; col.targets = [i]; return col; }); + } + + function getAuditHistoryColumns() { + return [ + { title: 'Tracking #', data: 'auditTrackingNumber', name: 'auditTrackingNumber', className: 'data-table-header', render: (d) => d ?? nullPlaceholder }, + { + title: 'Audit Date', data: 'auditDate', name: 'auditDate', className: 'data-table-header', + render: function (d) { + if (!d) return nullPlaceholder; + try { return luxon.DateTime.fromISO(d).toLocaleString(); } catch (e) { return d; } + } + }, + { title: 'Audit Note', data: 'auditNote', name: 'auditNote', className: 'data-table-header', render: (d) => d ?? nullPlaceholder }, + { + title: 'Actions', data: null, name: 'actions', orderable: false, className: 'data-table-header', + render: function (data, type, row) { + return `` + + ``; + } + } + ].map(function (col, i) { col.index = i; col.targets = [i]; return col; }); + } + + // ── Modals ──────────────────────────────────────────────────────────────── + + const getApplicantId = () => $('#ApplicantHistory_ApplicantId').val(); + + const createFundingModal = new abp.ModalManager(abp.appPath + 'ApplicantHistory/CreateFundingHistoryModal'); + const editFundingModal = new abp.ModalManager(abp.appPath + 'ApplicantHistory/EditFundingHistoryModal'); + + const createIssueModal = new abp.ModalManager(abp.appPath + 'ApplicantHistory/CreateIssueTrackingModal'); + const editIssueModal = new abp.ModalManager(abp.appPath + 'ApplicantHistory/EditIssueTrackingModal'); + + const createAuditModal = new abp.ModalManager(abp.appPath + 'ApplicantHistory/CreateAuditHistoryModal'); + const editAuditModal = new abp.ModalManager(abp.appPath + 'ApplicantHistory/EditAuditHistoryModal'); + + // ── DataTables ──────────────────────────────────────────────────────────── + + const fundingHistoryTable = initializeDataTable({ + dt: $('#FundingHistoryTable'), + defaultVisibleColumns: ['grantCategory', 'fundingYear', 'renewedFunding', 'approvedAmount', 'reconsiderationAmount', 'totalGrantAmount', 'fundingNotes', 'actions'], + listColumns: getFundingHistoryColumns(), + dataEndpoint: () => unity.grantManager.applicantProfile.applicantHistory.getFundingHistoryList(getApplicantId()), + data: () => ({}), + responseCallback: function (r) { return { recordsTotal: r.length, recordsFiltered: r.length, data: r }; }, + actionButtons: [ + { + text: 'ADD', + className: 'custom-table-btn flex-none btn btn-secondary', + action: function () { createFundingModal.open({ applicantId: getApplicantId() }); } + }, + { + extend: 'csv', + text: 'Export', + className: 'custom-table-btn flex-none btn btn-secondary', + exportOptions: { columns: ':visible:not(.notexport)' } + } + ], + serverSideEnabled: false, + pagingEnabled: true, + dataTableName: 'FundingHistoryTable', + dynamicButtonContainerId: 'fundingHistoryDynamicButtons' + }); + + if (fundingHistoryTable && typeof fundingHistoryTable.externalSearch === 'function') { + fundingHistoryTable.externalSearch('#funding-history-search', { delay: 300 }); + } + + const issueTrackingTable = initializeDataTable({ + dt: $('#IssueTrackingTable'), + defaultVisibleColumns: ['year', 'issueHeading', 'issueDescription', 'resolved', 'resolutionNote', 'actions'], + listColumns: getIssueTrackingColumns(), + dataEndpoint: () => unity.grantManager.applicantProfile.applicantHistory.getIssueTrackingList(getApplicantId()), + data: () => ({}), + responseCallback: function (r) { return { recordsTotal: r.length, recordsFiltered: r.length, data: r }; }, + actionButtons: [ + { + text: 'ADD', + className: 'custom-table-btn flex-none btn btn-secondary', + action: function () { createIssueModal.open({ applicantId: getApplicantId() }); } + }, + { + extend: 'csv', + text: 'Export', + className: 'custom-table-btn flex-none btn btn-secondary', + exportOptions: { columns: ':visible:not(.notexport)' } + } + ], + serverSideEnabled: false, + pagingEnabled: true, + dataTableName: 'IssueTrackingTable', + dynamicButtonContainerId: 'issueTrackingDynamicButtons' + }); + + if (issueTrackingTable && typeof issueTrackingTable.externalSearch === 'function') { + issueTrackingTable.externalSearch('#issue-tracking-search', { delay: 300 }); + } + + const auditHistoryTable = initializeDataTable({ + dt: $('#AuditHistoryTable'), + defaultVisibleColumns: ['auditTrackingNumber', 'auditDate', 'auditNote', 'actions'], + listColumns: getAuditHistoryColumns(), + dataEndpoint: () => unity.grantManager.applicantProfile.applicantHistory.getAuditHistoryList(getApplicantId()), + data: () => ({}), + responseCallback: function (r) { return { recordsTotal: r.length, recordsFiltered: r.length, data: r }; }, + actionButtons: [ + { + text: 'ADD', + className: 'custom-table-btn flex-none btn btn-secondary', + action: function () { createAuditModal.open({ applicantId: getApplicantId() }); } + }, + { + extend: 'csv', + text: 'Export', + className: 'custom-table-btn flex-none btn btn-secondary', + exportOptions: { columns: ':visible:not(.notexport)' } + } + ], + serverSideEnabled: false, + pagingEnabled: true, + dataTableName: 'AuditHistoryTable', + dynamicButtonContainerId: 'auditHistoryDynamicButtons' + }); + + // ── Modal result callbacks ───────────────────────────────────────────────── + + createFundingModal.onResult(function () { + fundingHistoryTable.ajax.reload(); + abp.notify.success('Funding history record added.'); + }); + + editFundingModal.onResult(function () { + fundingHistoryTable.ajax.reload(); + abp.notify.success('Funding history record updated.'); + }); + + createIssueModal.onResult(function () { + issueTrackingTable.ajax.reload(); + abp.notify.success('Issue tracking record added.'); + }); + + editIssueModal.onResult(function () { + issueTrackingTable.ajax.reload(); + abp.notify.success('Issue tracking record updated.'); + }); + + createAuditModal.onResult(function () { + auditHistoryTable.ajax.reload(); + abp.notify.success('Audit history record added.'); + }); + + editAuditModal.onResult(function () { + auditHistoryTable.ajax.reload(); + abp.notify.success('Audit history record updated.'); + }); + + // ── Edit / Delete — delegated handlers on table containers ──────────────── + + $('#FundingHistoryTable').on('click', '.funding-edit-btn', function () { + editFundingModal.open({ id: $(this).data('id') }); + }); + + $('#FundingHistoryTable').on('click', '.funding-delete-btn', function () { + const id = $(this).data('id'); + abp.message.confirm('Are you sure you want to delete this funding history record?', function (confirmed) { + if (confirmed) { + unity.grantManager.applicantProfile.applicantHistory.deleteFundingHistory(id) + .done(function () { fundingHistoryTable.ajax.reload(); abp.notify.success('Record deleted.'); }) + .fail(function () { abp.notify.error('Failed to delete record.'); }); + } + }); + }); + + $('#IssueTrackingTable').on('click', '.issue-edit-btn', function () { + editIssueModal.open({ id: $(this).data('id') }); + }); + + $('#IssueTrackingTable').on('click', '.issue-delete-btn', function () { + const id = $(this).data('id'); + abp.message.confirm('Are you sure you want to delete this issue tracking record?', function (confirmed) { + if (confirmed) { + unity.grantManager.applicantProfile.applicantHistory.deleteIssueTracking(id) + .done(function () { issueTrackingTable.ajax.reload(); abp.notify.success('Record deleted.'); }) + .fail(function () { abp.notify.error('Failed to delete record.'); }); + } + }); + }); + + $('#AuditHistoryTable').on('click', '.audit-edit-btn', function () { + editAuditModal.open({ id: $(this).data('id') }); + }); + + $('#AuditHistoryTable').on('click', '.audit-delete-btn', function () { + const id = $(this).data('id'); + abp.message.confirm('Are you sure you want to delete this audit history record?', function (confirmed) { + if (confirmed) { + unity.grantManager.applicantProfile.applicantHistory.deleteAuditHistory(id) + .done(function () { auditHistoryTable.ajax.reload(); abp.notify.success('Record deleted.'); }) + .fail(function () { abp.notify.error('Failed to delete record.'); }); + } + }); + }); +}); From a960047e47f09531979c47aafef63b2f3c4321a6 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 27 Feb 2026 10:55:54 -0800 Subject: [PATCH 086/191] AB#32008 Add Office document text extraction support (Word/Excel) # Conflicts: # applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/TextExtractionService.cs # applications/Unity.GrantManager/src/Unity.GrantManager.Application/Unity.GrantManager.Application.csproj --- .../AI/TextExtractionService.cs | 167 ++++++++++++++++-- .../Unity.GrantManager.Application.csproj | 1 + 2 files changed, 152 insertions(+), 16 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 3c2b3f2b36..8e7f3d41bb 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/TextExtractionService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/TextExtractionService.cs @@ -1,6 +1,10 @@ using Microsoft.Extensions.Logging; +using NPOI.SS.UserModel; +using NPOI.XWPF.UserModel; using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -12,6 +16,12 @@ namespace Unity.GrantManager.AI public class TextExtractionService : ITextExtractionService, ITransientDependency { private const int MaxExtractedTextLength = 50000; + private const int MaxExcelSheets = 10; + private const int MaxExcelRowsPerSheet = 2000; + private const int MaxExcelCellsPerRow = 50; + private const int MaxDocxParagraphs = 2000; + private const int MaxDocxTableRows = 2000; + private const int MaxDocxTableCellsPerRow = 50; private readonly ILogger _logger; public TextExtractionService(ILogger logger) @@ -29,13 +39,11 @@ public async Task ExtractTextAsync(string fileName, byte[] fileContent, try { - // Normalize content type 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" || extension == ".csv" || @@ -46,37 +54,37 @@ public async Task ExtractTextAsync(string fileName, byte[] fileContent, return NormalizeAndLimitText(rawText, fileName); } - // Handle PDF files if (normalizedContentType.Contains("pdf") || extension == ".pdf") { - rawText = await Task.FromResult(ExtractTextFromPdfFile(fileName, fileContent)); + rawText = ExtractTextFromPdfFile(fileName, fileContent); return NormalizeAndLimitText(rawText, fileName); } - // Handle Word documents if (normalizedContentType.Contains("word") || normalizedContentType.Contains("msword") || normalizedContentType.Contains("officedocument.wordprocessingml") || extension == ".doc" || extension == ".docx") { - // For now, return empty string - can be enhanced with Word parsing library - _logger.LogDebug("Word document text extraction not yet implemented for {FileName}", fileName); + if (extension == ".docx" || normalizedContentType.Contains("officedocument.wordprocessingml")) + { + rawText = ExtractTextFromWordDocx(fileContent); + return NormalizeAndLimitText(rawText, fileName); + } + + _logger.LogDebug("Legacy .doc extraction is not supported for {FileName}", fileName); return string.Empty; } - // Handle Excel files if (normalizedContentType.Contains("excel") || normalizedContentType.Contains("spreadsheet") || extension == ".xls" || extension == ".xlsx") { - // For now, return empty string - can be enhanced with Excel parsing library - _logger.LogDebug("Excel text extraction not yet implemented for {FileName}", fileName); - return string.Empty; + rawText = ExtractTextFromExcelFile(fileName, fileContent); + return NormalizeAndLimitText(rawText, fileName); } - // For other file types, return empty string _logger.LogDebug("No text extraction available for content type {ContentType} with extension {Extension}", contentType, extension); return string.Empty; @@ -92,17 +100,13 @@ private async Task ExtractTextFromTextFileAsync(byte[] fileContent) { try { - // Try UTF-8 first var text = Encoding.UTF8.GetString(fileContent); - // Check if the decoded text contains replacement characters (indicates encoding issue) if (text.Contains('\uFFFD')) { - // Try other encodings text = Encoding.ASCII.GetString(fileContent); } - // Limit the extracted text to a reasonable size. if (text.Length > MaxExtractedTextLength) { text = text.Substring(0, MaxExtractedTextLength); @@ -154,6 +158,137 @@ private string ExtractTextFromPdfFile(string fileName, byte[] fileContent) } } + private string ExtractTextFromWordDocx(byte[] fileContent) + { + try + { + using var stream = new MemoryStream(fileContent, writable: false); + using var document = new XWPFDocument(stream); + var parts = new List(); + + foreach (var paragraph in document.Paragraphs.Take(MaxDocxParagraphs)) + { + if (!string.IsNullOrWhiteSpace(paragraph.ParagraphText)) + { + parts.Add(paragraph.ParagraphText); + } + } + + foreach (var table in document.Tables) + { + foreach (var row in table.Rows.Take(MaxDocxTableRows)) + { + foreach (var cell in row.GetTableCells().Take(MaxDocxTableCellsPerRow)) + { + var text = cell.GetText(); + if (!string.IsNullOrWhiteSpace(text)) + { + parts.Add(text); + } + } + } + } + + var combined = string.Join(Environment.NewLine, parts); + if (combined.Length > MaxExtractedTextLength) + { + combined = combined.Substring(0, MaxExtractedTextLength); + } + + return combined; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Word (.docx) text extraction failed"); + return string.Empty; + } + } + + private string ExtractTextFromExcelFile(string fileName, byte[] fileContent) + { + try + { + using var stream = new MemoryStream(fileContent, writable: false); + using var workbook = WorkbookFactory.Create(stream); + var rows = new List(); + var totalLength = 0; + var sheetCount = Math.Min(workbook.NumberOfSheets, MaxExcelSheets); + + for (var sheetIndex = 0; sheetIndex < sheetCount; sheetIndex++) + { + var sheet = workbook.GetSheetAt(sheetIndex); + if (sheet == null) + { + continue; + } + + var processedRows = 0; + foreach (IRow row in sheet) + { + if (processedRows >= MaxExcelRowsPerSheet || totalLength >= MaxExtractedTextLength) + { + break; + } + + var cellTexts = row.Cells + .Take(MaxExcelCellsPerRow) + .Select(GetCellText) + .Where(value => !string.IsNullOrWhiteSpace(value)) + .ToList(); + + processedRows++; + + if (cellTexts.Count == 0) + { + continue; + } + + var rowText = string.Join(" | ", cellTexts); + rows.Add(rowText); + totalLength += rowText.Length; + } + + if (totalLength >= MaxExtractedTextLength) + { + break; + } + } + + var combined = string.Join(Environment.NewLine, rows); + if (combined.Length > MaxExtractedTextLength) + { + combined = combined.Substring(0, MaxExtractedTextLength); + } + + return combined; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Excel text extraction failed for {FileName}", fileName); + return string.Empty; + } + } + + private static string GetCellText(ICell cell) + { + if (cell == null) + { + return string.Empty; + } + + return (cell.CellType switch + { + CellType.String => cell.StringCellValue ?? string.Empty, + CellType.Numeric => DateUtil.IsCellDateFormatted(cell) + ? cell.DateCellValue.ToString() + : cell.NumericCellValue.ToString(), + CellType.Boolean => cell.BooleanCellValue ? "true" : "false", + CellType.Formula => cell.ToString(), + CellType.Blank => string.Empty, + _ => cell.ToString() ?? string.Empty + }) ?? string.Empty; + } + private string NormalizeAndLimitText(string text, string fileName) { var normalized = NormalizeExtractedText(text); 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 8ec3e53bcd..ff57bfd948 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 @@ -33,6 +33,7 @@ + From 7ed4d3f3a94e2700c9df5892b134203912b8b4cd Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 27 Feb 2026 12:01:21 -0800 Subject: [PATCH 087/191] AB#32008 Resolve ICell specificity error --- .../Unity.GrantManager.Application/AI/TextExtractionService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8e7f3d41bb..8de6b180ec 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/TextExtractionService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/TextExtractionService.cs @@ -269,7 +269,7 @@ private string ExtractTextFromExcelFile(string fileName, byte[] fileContent) } } - private static string GetCellText(ICell cell) + private static string GetCellText(NPOI.SS.UserModel.ICell cell) { if (cell == null) { From 39899ccaa044ae69b37683760689b460e88423ad Mon Sep 17 00:00:00 2001 From: Armin Hasanpour Date: Fri, 27 Feb 2026 13:02:26 -0800 Subject: [PATCH 088/191] Added new placeholders on appsettings. Added ESlint. --- applications/Unity.GrantManager/.env.example | 2 +- .../appsettings.Development.json | 58 ++- .../src/Unity.GrantManager.Web/package.json | 11 +- .../src/Unity.GrantManager.Web/yarn.lock | 488 +++++++++++++++++- 4 files changed, 528 insertions(+), 31 deletions(-) diff --git a/applications/Unity.GrantManager/.env.example b/applications/Unity.GrantManager/.env.example index 3a39780fa5..c2094e4c05 100644 --- a/applications/Unity.GrantManager/.env.example +++ b/applications/Unity.GrantManager/.env.example @@ -41,7 +41,7 @@ AuthServer__Realm="standard" #"unity-local" AuthServer__RequireHttpsMetadata="false" AuthServer__Audience="unity-4899" #"unity-web" AuthServer__ClientId="unity-4899" #"unity-web" -AuthServer__ClientSecret="="********"" +AuthServer__ClientSecret="********" AuthServer__IsBehindTlsTerminationProxy="false" AuthServer__SpecifyOidcParameters="true" AuthServer__OidcSignin="http://localhost:44342/signin-oidc" diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.Development.json b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.Development.json index 27323616ed..e41b752c50 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.Development.json +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.Development.json @@ -2,6 +2,18 @@ "App": { "SelfUrl": "https://localhost:44342" }, + "AuthServer": { + "ClientSecret": "" + }, + "S3": { + "AccessKeyId": "", + "Bucket": "", + "Endpoint": "", + "SecretAccessKey": "" + }, + "CssApi": { + "ClientSecret": "" + }, "ConnectionStrings": { "Default": "Host=localhost;port=5432;Database=UnityGrantManager;Username=postgres;", "Tenant": "Host=localhost;port=5432;Database=UnityGrantTenant;Username=postgres;" @@ -14,8 +26,29 @@ "Quartz": { "UseCluster": false, "IsAutoRegisterEnabled": false + }, + "IntakeResync": { + "Expression": "0 0 12 1/1 * ? *", + "NumDaysToCheck": "-2" + }, + "EmailResend": { + "RetryAttemptsMaximum": 2 + }, + "CasPaymentsReconciliation": { + "ProducerExpression": "0 0 22 1/1 * ? *" + }, + "CasFinancialNotificationSummary": { + "ProducerExpression": "0 0 23 1/1 * ? *" } }, + "RabbitMQ": { + "IsEnabled": false, + "HostName": "127.0.0.1", + "Port": 5672, + "UserName": "guest", + "Password": "guest", + "VirtualHost": "/" + }, "Intake": { "BaseUri": "https://chefs-test.apps.silver.devops.gov.bc.ca/app/api/v1", "FormId": "", @@ -28,16 +61,17 @@ "AllowUnregisteredVersions": true }, "Payments": { - "CasBaseUrl": "", - "CasClientId": "", - "CasClientSecret": "" + "CasBaseUrl": "", + "CasClientId": "", + "CasClientSecret": "" }, "Notifications": { "TeamsNotificationsWebhook": "", "ChesUrl": "https://ches-dev.api.gov.bc.ca/api/v1", "ChesTokenUrl": "https://dev.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", "ChesClientId": "", - "ChesClientSecret": "" + "ChesClientSecret": "", + "ChesBaseUri": "https://ches-dev.api.gov.bc.ca/api/v1" }, "DataProtection": { "IsEnabled": false @@ -97,5 +131,19 @@ }, "B2BAuth": { "ApiKey": "__SET_VIA_USER_SECRETS__" - } + }, + "ReportingAI": { + "JWTSecret": "" + }, + "Azure": { + "OpenAI": { + "ApiKey": "", + "ApiUrl": "", + "Model": "" + }, + "AgenticAPI": { + "Url": "http://localhost:5000/v1/completions" + } + } + } \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/package.json b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/package.json index 63cd5b81d7..ed970e1b7c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/package.json +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/package.json @@ -16,8 +16,10 @@ "datatables.net-select-bs5": "~3.1.0", "datatables.net-staterestore": "~1.4.2", "datatables.net-staterestore-dt": "~1.4.2", + "dompurify": "^3.3.1", "echarts": "~6.0.0", "formiojs": "4.17.4", + "handlebars": "~4.7.8", "html2canvas": "~1.4.1", "jquery-maskmoney": "~3.0.2", "jspdf": "~4.0.0", @@ -25,12 +27,11 @@ "popper.js": "~1.16.1", "pubsub-js": "~1.9.5", "sortablejs": "~1.15.6", - "tributejs": "~5.1.3", "tinymce": "~8.3.2", - "handlebars": "~4.7.8", - "dompurify": "^3.3.1" + "tributejs": "~5.1.3" }, "devDependencies": { - "@types/jquery": "~3.5.33" + "@types/jquery": "~3.5.33", + "eslint": "^10.0.2" } -} \ No newline at end of file +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/yarn.lock b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/yarn.lock index aa748a116e..ead5a4d639 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/yarn.lock +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/yarn.lock @@ -198,6 +198,54 @@ resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz" integrity sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA== +"@eslint-community/eslint-utils@^4.8.0": + version "4.9.1" + resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz" + integrity sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ== + dependencies: + eslint-visitor-keys "^3.4.3" + +"@eslint-community/regexpp@^4.12.2": + version "4.12.2" + resolved "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz" + integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew== + +"@eslint/config-array@^0.23.2": + version "0.23.2" + resolved "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.2.tgz" + integrity sha512-YF+fE6LV4v5MGWRGj7G404/OZzGNepVF8fxk7jqmqo3lrza7a0uUcDnROGRBG1WFC1omYUS/Wp1f42i0M+3Q3A== + dependencies: + "@eslint/object-schema" "^3.0.2" + debug "^4.3.1" + minimatch "^10.2.1" + +"@eslint/config-helpers@^0.5.2": + version "0.5.2" + resolved "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.2.tgz" + integrity sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ== + dependencies: + "@eslint/core" "^1.1.0" + +"@eslint/core@^1.1.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@eslint/core/-/core-1.1.0.tgz" + integrity sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw== + dependencies: + "@types/json-schema" "^7.0.15" + +"@eslint/object-schema@^3.0.2": + version "3.0.2" + resolved "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.2.tgz" + integrity sha512-HOy56KJt48Bx8KmJ+XGQNSUMT/6dZee/M54XyUyuvTvPXJmsERRvBchsUVx1UMe1WwIH49XLAczNC7V2INsuUw== + +"@eslint/plugin-kit@^0.6.0": + version "0.6.0" + resolved "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.0.tgz" + integrity sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ== + dependencies: + "@eslint/core" "^1.1.0" + levn "^0.4.1" + "@formio/bootstrap3@2.12.4-rc.1": version "2.12.4-rc.1" resolved "https://registry.npmjs.org/@formio/bootstrap3/-/bootstrap3-2.12.4-rc.1.tgz" @@ -234,7 +282,30 @@ resolved "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz" integrity sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA== -"@popperjs/core@^2.9.0": +"@humanfs/core@^0.19.1": + version "0.19.1" + resolved "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz" + integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA== + +"@humanfs/node@^0.16.6": + version "0.16.7" + resolved "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz" + integrity sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ== + dependencies: + "@humanfs/core" "^0.19.1" + "@humanwhocodes/retry" "^0.4.0" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/retry@^0.4.0", "@humanwhocodes/retry@^0.4.2": + version "0.4.3" + resolved "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz" + integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ== + +"@popperjs/core@^2.11.8", "@popperjs/core@^2.9.0": version "2.11.8" resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== @@ -244,6 +315,16 @@ resolved "https://registry.npmjs.org/@sphinxxxx/color-conversion/-/color-conversion-2.2.2.tgz" integrity sha512-XExJS3cLqgrmNBIP3bBw6+1oQ1ksGjFh0+oClDKFYpCCqx/hlqwWO5KO/S63fzUo67SxI9dMrF0y5T/Ey7h8Zw== +"@types/esrecurse@^4.3.1": + version "4.3.1" + resolved "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz" + integrity sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw== + +"@types/estree@^1.0.6", "@types/estree@^1.0.8": + version "1.0.8" + resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz" + integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== + "@types/jquery@~3.5.33": version "3.5.33" resolved "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.33.tgz" @@ -251,6 +332,11 @@ dependencies: "@types/sizzle" "*" +"@types/json-schema@^7.0.15": + version "7.0.15" + resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + "@types/pako@^2.0.3": version "2.0.4" resolved "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz" @@ -276,6 +362,26 @@ abortcontroller-polyfill@^1.7.5: resolved "https://registry.npmjs.org/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.8.tgz" integrity sha512-9f1iZ2uWh92VcrU9Y8x+LdM4DLj75VE0MJB8zuF1iUnroEptStw+DQ8EQPMUdfe5k+PkB1uUfDQfWbhstH8LrQ== +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.16.0: + version "8.16.0" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz" + integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw== + +ajv@^6.14.0: + version "6.14.0" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz" + integrity sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + animation-frame-polyfill@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/animation-frame-polyfill/-/animation-frame-polyfill-1.0.2.tgz" @@ -301,6 +407,11 @@ autocompleter@^7.0.1: resolved "https://registry.npmjs.org/autocompleter/-/autocompleter-7.1.0.tgz" integrity sha512-uCToOnq7eAD/GJAteDbYuQ7ksDtrYWOy5CIAq43wh0dT+5frMpPlyD9tp+y5fz8KIcsP+zR2MjzoTAdW5aJESw== +balanced-match@^4.0.2: + version "4.0.4" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz" + integrity sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA== + base64-arraybuffer@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz" @@ -331,11 +442,18 @@ bootstrap-select@~1.13.18: resolved "https://registry.npmjs.org/bootstrap-select/-/bootstrap-select-1.13.18.tgz" integrity sha512-V1IzK4rxBq5FrJtkzSH6RmFLFBsjx50byFbfAf8jYyXROWs7ZpprGjdHeoyq2HSsHyjJhMMwjsQhRoYAfxCGow== -bootstrap@^5.3.3: +bootstrap@^5.3.3, bootstrap@>=3.0.0: version "5.3.3" resolved "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz" integrity sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg== +brace-expansion@^5.0.2: + version "5.0.3" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz" + integrity sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA== + dependencies: + balanced-match "^4.0.2" + browser-cookies@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/browser-cookies/-/browser-cookies-1.2.0.tgz" @@ -392,6 +510,15 @@ create-point-cb@^1.0.0: dependencies: type-func "^1.0.1" +cross-spawn@^7.0.6: + version "7.0.6" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + crossvent@1.5.5: version "1.5.5" resolved "https://registry.npmjs.org/crossvent/-/crossvent-1.5.5.tgz" @@ -450,7 +577,7 @@ datatables.net-colreorder-bs5@~2.1.1: datatables.net-colreorder "2.1.2" jquery ">=1.7" -datatables.net-colreorder@2.1.2, datatables.net-colreorder@~2.1.1: +datatables.net-colreorder@~2.1.1, datatables.net-colreorder@2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/datatables.net-colreorder/-/datatables.net-colreorder-2.1.2.tgz" integrity sha512-lIsUyOt2nBm4sD2cSzDKZcIVrGgrZkh90Z2f03s8p7DYcZSfXMHAhFBrDYf9/eAK6wJnODN8EDMsrtPHfgoSXA== @@ -475,7 +602,7 @@ datatables.net-fixedheader-bs5@~4.0.3: datatables.net-fixedheader "4.0.5" jquery ">=1.7" -datatables.net-fixedheader@4.0.5, datatables.net-fixedheader@~4.0.3: +datatables.net-fixedheader@~4.0.3, datatables.net-fixedheader@4.0.5: version "4.0.5" resolved "https://registry.npmjs.org/datatables.net-fixedheader/-/datatables.net-fixedheader-4.0.5.tgz" integrity sha512-cobQhOhjzqIYXTvMRrHUulULS8Re+hd2mmgFiOGKcZwHV0mofIwBlgiU3Ol4LHikHUCvsGnTEXoI+C7Ozma5sA== @@ -509,7 +636,7 @@ datatables.net-staterestore-dt@~1.4.2: datatables.net-staterestore "1.4.3" jquery ">=1.7" -datatables.net-staterestore@1.4.3, datatables.net-staterestore@~1.4.2: +datatables.net-staterestore@~1.4.2, datatables.net-staterestore@1.4.3: version "1.4.3" resolved "https://registry.npmjs.org/datatables.net-staterestore/-/datatables.net-staterestore-1.4.3.tgz" integrity sha512-XSkCHwi+MZ8C5ZbZ1qlvIdIOs8YEJX4BVOk3GUMoSIta6xD4UsKTDV0SxfJWRYsNnDQwvCibQD0yJhK4Vk4xTw== @@ -517,7 +644,7 @@ datatables.net-staterestore@1.4.3, datatables.net-staterestore@~1.4.2: datatables.net "1.11 - 2" jquery ">=1.7" -"datatables.net@1.11 - 2", datatables.net@2.3.6, datatables.net@^2, datatables.net@^2.1.8: +datatables.net@^2, datatables.net@^2.1.8, "datatables.net@1.11 - 2", datatables.net@2.3.6: version "2.3.6" resolved "https://registry.npmjs.org/datatables.net/-/datatables.net-2.3.6.tgz" integrity sha512-xQ/dCxrjfxM0XY70wSIzakkTZ6ghERwlLmAPyCnu8Sk5cyt9YvOVyOsFNOa/BZ/lM63Q3i2YSSvp/o7GXZGsbg== @@ -531,6 +658,18 @@ datatables.net@2.2.1: dependencies: jquery ">=1.7" +debug@^4.3.1, debug@^4.3.2: + version "4.4.3" + resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + deepmerge@^4.2.2: version "4.3.1" resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz" @@ -574,7 +713,7 @@ dom-set@^1.0.1: is-array "^1.0.1" iselement "^1.1.4" -dompurify@^3.0.5, dompurify@^3.2.4: +dompurify@^3.0.5, dompurify@^3.2.4, dompurify@^3.3.1: version "3.3.1" resolved "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz" integrity sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q== @@ -602,6 +741,100 @@ echarts@~6.0.0: tslib "2.3.0" zrender "6.0.0" +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-scope@^9.1.1: + version "9.1.1" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.1.tgz" + integrity sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw== + dependencies: + "@types/esrecurse" "^4.3.1" + "@types/estree" "^1.0.8" + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint-visitor-keys@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz" + integrity sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA== + +eslint@^10.0.2, "eslint@^6.0.0 || ^7.0.0 || >=8.0.0": + version "10.0.2" + resolved "https://registry.npmjs.org/eslint/-/eslint-10.0.2.tgz" + integrity sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw== + dependencies: + "@eslint-community/eslint-utils" "^4.8.0" + "@eslint-community/regexpp" "^4.12.2" + "@eslint/config-array" "^0.23.2" + "@eslint/config-helpers" "^0.5.2" + "@eslint/core" "^1.1.0" + "@eslint/plugin-kit" "^0.6.0" + "@humanfs/node" "^0.16.6" + "@humanwhocodes/module-importer" "^1.0.1" + "@humanwhocodes/retry" "^0.4.2" + "@types/estree" "^1.0.6" + ajv "^6.14.0" + cross-spawn "^7.0.6" + debug "^4.3.2" + escape-string-regexp "^4.0.0" + eslint-scope "^9.1.1" + eslint-visitor-keys "^5.0.1" + espree "^11.1.1" + esquery "^1.7.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^8.0.0" + find-up "^5.0.0" + glob-parent "^6.0.2" + ignore "^5.2.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + json-stable-stringify-without-jsonify "^1.0.1" + minimatch "^10.2.1" + natural-compare "^1.4.0" + optionator "^0.9.3" + +espree@^11.1.1: + version "11.1.1" + resolved "https://registry.npmjs.org/espree/-/espree-11.1.1.tgz" + integrity sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ== + dependencies: + acorn "^8.16.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^5.0.1" + +esquery@^1.7.0: + version "1.7.0" + resolved "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz" + integrity sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + eventemitter3@^4.0.7: version "4.0.7" resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz" @@ -612,7 +845,7 @@ eventemitter3@^5.0.1: resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz" integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== -fast-deep-equal@^3.1.3: +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== @@ -627,6 +860,16 @@ fast-json-patch@^3.1.1: resolved "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz" integrity sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ== +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + fast-png@^6.2.0: version "6.4.0" resolved "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz" @@ -648,10 +891,38 @@ fflate@^0.8.1: resolved "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz" integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A== -formiojs@~4.17.4: - version "4.17.5" - resolved "https://registry.npmjs.org/formiojs/-/formiojs-4.17.5.tgz" - integrity sha512-vJW41GYhpJzmqYWJII8s48aZt+HQuYkh2jbqIQQtmQ6c4Eb1wlCXSrUNE2XeBmVCgEaC6YDHqnnvdhakZAAOOg== +file-entry-cache@^8.0.0: + version "8.0.0" + resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz" + integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== + dependencies: + flat-cache "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz" + integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== + dependencies: + flatted "^3.2.9" + keyv "^4.5.4" + +flatted@^3.2.9: + version "3.3.3" + resolved "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz" + integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== + +formiojs@4.17.4: + version "4.17.4" + resolved "https://registry.npmjs.org/formiojs/-/formiojs-4.17.4.tgz" + integrity sha512-1wUWPLKTJ6/FWa5jCtw5YfsUzgr85TYh8aMhXUWxxRNBtYxZMLg4RcFsArqDJP9Lfh9z4N8p/sLSaOik6iu7kA== dependencies: "@formio/bootstrap3" "2.12.4-rc.1" "@formio/choices.js" "10.2.0" @@ -696,6 +967,13 @@ fuse.js@^6.6.2: resolved "https://registry.npmjs.org/fuse.js/-/fuse.js-6.6.2.tgz" integrity sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA== +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + handlebars@~4.7.8: version "4.7.8" resolved "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz" @@ -728,11 +1006,21 @@ idb@^7.1.1: resolved "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz" integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ== +ignore@^5.2.0: + version "5.3.2" + resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + immediate@~3.0.5: version "3.0.6" resolved "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz" integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + inherits@~2.0.3: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" @@ -748,6 +1036,18 @@ is-array@^1.0.1: resolved "https://registry.npmjs.org/is-array/-/is-array-1.0.1.tgz" integrity sha512-gxiZ+y/u67AzpeFmAmo4CbtME/bs7J2C++su5zQzvQyaxUqVzkh69DI+jN+KZuSO6JaH6TIIU6M6LhqxMjxEpw== +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.0, is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + isarray@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" @@ -758,6 +1058,11 @@ iselement@^1.1.4: resolved "https://registry.npmjs.org/iselement/-/iselement-1.1.4.tgz" integrity sha512-4Q519eWmbHO1pbimiz7H1iJRUHVmAmfh0viSsUD+oAwVO4ntZt7gpf8i8AShVBTyOvRTZNYNBpUxOIvwZR+ffw== +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + ismobilejs@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/ismobilejs/-/ismobilejs-1.1.1.tgz" @@ -792,21 +1097,36 @@ jquery-validation-unobtrusive@^4.0.0: jquery "^3.6.0" jquery-validation ">=1.19" -jquery-validation@>=1.19, jquery-validation@^1.21.0: +jquery-validation@^1.21.0, jquery-validation@>=1.19: version "1.21.0" resolved "https://registry.npmjs.org/jquery-validation/-/jquery-validation-1.21.0.tgz" integrity sha512-xNot0rlUIgu7duMcQ5qb6MGkGL/Z1PQaRJQoZAURW9+a/2PGOUxY36o/WyNeP2T9R6jvWB8Z9lUVvvQWI/Zs5w== -jquery@>=1.10, jquery@>=1.12.0, jquery@>=1.2.6, "jquery@>=1.5.0 <4.0", jquery@>=1.6, jquery@>=1.7, jquery@>=1.7.2, "jquery@>=3.4.0 <4.0.0", jquery@^3.6.0, jquery@~3.7.1: +"jquery@^1.7 || ^2.0 || ^3.1", jquery@^3.6.0, jquery@>=1.10, jquery@>=1.12.0, jquery@>=1.2.6, "jquery@>=1.5.0 <4.0", jquery@>=1.6, jquery@>=1.7, jquery@>=1.7.2, "jquery@>=3.4.0 <4.0.0", jquery@~3.7.1, "jquery@1.9.1 - 3": version "3.7.1" resolved "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz" integrity sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg== +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + json-logic-js@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/json-logic-js/-/json-logic-js-2.0.2.tgz" integrity sha512-ZBtBdMJieqQcH7IX/LaBsr5pX+Y5JIW+EhejtM3Ffg2jdN9Iwf+Ht6TbHnvAZ/YtwyuhPaCBlnvzrwVeWdvGDQ== +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + jspdf@~4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/jspdf/-/jspdf-4.0.0.tgz" @@ -846,6 +1166,21 @@ jwt-decode@^3.1.2: resolved "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz" integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A== +keyv@^4.5.4: + version "4.5.4" + resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + lie@~3.3.0: version "3.3.0" resolved "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz" @@ -853,6 +1188,13 @@ lie@~3.3.0: dependencies: immediate "~3.0.5" +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + lodash-es@^4.17.21: version "4.17.21" resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz" @@ -885,6 +1227,13 @@ malihu-custom-scrollbar-plugin@^3.1.5: dependencies: jquery-mousewheel ">=3.0.6" +minimatch@^10.2.1: + version "10.2.2" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz" + integrity sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw== + dependencies: + brace-expansion "^5.0.2" + minimist@^1.2.5: version "1.2.8" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" @@ -902,11 +1251,21 @@ moment@^2.29.4, moment@^2.30.1, moment@^2.9.0: resolved "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz" integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + native-promise-only@^0.8.1: version "0.8.1" resolved "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz" integrity sha512-zkVhZUA3y8mbz652WrL5x0fB0ehrBkulWT3TomAQ9iDtyXZvzKeEA6GPxAItBYeNYl5yngKRX612qHOhvMkDeg== +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + neo-async@^2.6.2: version "2.6.2" resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" @@ -919,6 +1278,32 @@ node-fetch@~2.6.1: dependencies: whatwg-url "^5.0.0" +optionator@^0.9.3: + version "0.9.4" + resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.5" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + pako@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz" @@ -934,16 +1319,31 @@ parchment@^3.0.0: resolved "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz" integrity sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A== +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + performance-now@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== -popper.js@~1.16.1: +popper.js@^1.16.1, popper.js@~1.16.1: version "1.16.1" resolved "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz" integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ== +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" @@ -954,6 +1354,11 @@ pubsub-js@~1.9.5: resolved "https://registry.npmjs.org/pubsub-js/-/pubsub-js-1.9.5.tgz" integrity sha512-5MZ0I9i5JWVO7SizvOviKvZU2qaBbl2KQX150FAA+fJBwYpwOUId7aNygURWSdPzlsA/xZ/InUKXqBbzM0czTA== +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + quill-delta@^5.1.0: version "5.1.0" resolved "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz" @@ -1030,6 +1435,18 @@ setimmediate@^1.0.5: resolved "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz" integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + signature_pad@^4.1.4: version "4.1.6" resolved "https://registry.npmjs.org/signature_pad/-/signature_pad-4.1.6.tgz" @@ -1055,11 +1472,6 @@ stackblur-canvas@^2.0.0: resolved "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz" integrity sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ== -string-hash@^1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz" - integrity sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A== - string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" @@ -1067,6 +1479,11 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" +string-hash@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz" + integrity sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A== + svg-pathdata@^6.0.3: version "6.0.3" resolved "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz" @@ -1130,6 +1547,13 @@ tslib@2.3.0: resolved "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz" integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg== +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + type-func@^1.0.1: version "1.0.3" resolved "https://registry.npmjs.org/type-func/-/type-func-1.0.3.tgz" @@ -1140,6 +1564,13 @@ uglify-js@^3.1.4: resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz" integrity sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ== +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" @@ -1177,11 +1608,28 @@ whatwg-url@^5.0.0: tr46 "~0.0.3" webidl-conversions "^3.0.0" +which@^2.0.1: + version "2.0.2" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@^1.2.5: + version "1.2.5" + resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + wordwrap@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + zrender@6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz" From 739f395d897ef8cdb0eddc7c7832a7a4464c3a5b Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Fri, 27 Feb 2026 13:16:53 -0800 Subject: [PATCH 089/191] AB#30429 fix unit tests --- .../ApplicantProfileDataProviderTests.cs | 2 + .../Contacts/ContactInfoDataProviderTests.cs | 64 ++++++++++++------- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/ApplicantProfileDataProviderTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/ApplicantProfileDataProviderTests.cs index cc8d41a68d..7d7c20fc77 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/ApplicantProfileDataProviderTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/ApplicantProfileDataProviderTests.cs @@ -34,6 +34,8 @@ private static ContactInfoDataProvider CreateContactInfoDataProvider() .Returns(Task.FromResult(new List())); applicantProfileContactService.GetApplicationContactsBySubjectAsync(Arg.Any()) .Returns(Task.FromResult(new List())); + applicantProfileContactService.GetApplicantAgentContactsBySubjectAsync(Arg.Any()) + .Returns(Task.FromResult(new List())); return new ContactInfoDataProvider(currentTenant, applicantProfileContactService); } diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoDataProviderTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoDataProviderTests.cs index 976ad574c5..b7690ddfac 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoDataProviderTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoDataProviderTests.cs @@ -22,6 +22,14 @@ public ContactInfoDataProviderTests() _currentTenant = Substitute.For(); _currentTenant.Change(Arg.Any()).Returns(Substitute.For()); _applicantProfileContactService = Substitute.For(); + + _applicantProfileContactService.GetProfileContactsAsync(Arg.Any()) + .Returns(new List()); + _applicantProfileContactService.GetApplicationContactsBySubjectAsync(Arg.Any()) + .Returns(new List()); + _applicantProfileContactService.GetApplicantAgentContactsBySubjectAsync(Arg.Any()) + .Returns(new List()); + _provider = new ContactInfoDataProvider(_currentTenant, _applicantProfileContactService); } @@ -38,10 +46,6 @@ public async Task GetDataAsync_ShouldChangeTenant() { // Arrange var request = CreateRequest(); - _applicantProfileContactService.GetProfileContactsAsync(Arg.Any()) - .Returns(new List()); - _applicantProfileContactService.GetApplicationContactsBySubjectAsync(Arg.Any()) - .Returns(new List()); // Act await _provider.GetDataAsync(request); @@ -55,10 +59,6 @@ public async Task GetDataAsync_ShouldCallGetProfileContactsWithProfileId() { // Arrange var request = CreateRequest(); - _applicantProfileContactService.GetProfileContactsAsync(Arg.Any()) - .Returns(new List()); - _applicantProfileContactService.GetApplicationContactsBySubjectAsync(Arg.Any()) - .Returns(new List()); // Act await _provider.GetDataAsync(request); @@ -72,10 +72,6 @@ public async Task GetDataAsync_ShouldCallGetApplicationContactsWithSubject() { // Arrange var request = CreateRequest(); - _applicantProfileContactService.GetProfileContactsAsync(Arg.Any()) - .Returns(new List()); - _applicantProfileContactService.GetApplicationContactsBySubjectAsync(Arg.Any()) - .Returns(new List()); // Act await _provider.GetDataAsync(request); @@ -85,7 +81,20 @@ public async Task GetDataAsync_ShouldCallGetApplicationContactsWithSubject() } [Fact] - public async Task GetDataAsync_ShouldCombineBothContactSets() + public async Task GetDataAsync_ShouldCallGetApplicantAgentContactsWithSubject() + { + // Arrange + var request = CreateRequest(); + + // Act + await _provider.GetDataAsync(request); + + // Assert + await _applicantProfileContactService.Received(1).GetApplicantAgentContactsBySubjectAsync(request.Subject); + } + + [Fact] + public async Task GetDataAsync_ShouldCombineAllContactSets() { // Arrange var request = CreateRequest(); @@ -98,17 +107,22 @@ public async Task GetDataAsync_ShouldCombineBothContactSets() { new() { ContactId = Guid.NewGuid(), Name = "App Contact 1", IsEditable = false } }; + var agentContacts = new List + { + new() { ContactId = Guid.NewGuid(), Name = "Agent Contact 1", IsEditable = false, ContactType = "ApplicantAgent" } + }; _applicantProfileContactService.GetProfileContactsAsync(request.ProfileId).Returns(profileContacts); _applicantProfileContactService.GetApplicationContactsBySubjectAsync(request.Subject).Returns(appContacts); + _applicantProfileContactService.GetApplicantAgentContactsBySubjectAsync(request.Subject).Returns(agentContacts); // Act var result = await _provider.GetDataAsync(request); // Assert var dto = result.ShouldBeOfType(); - dto.Contacts.Count.ShouldBe(3); + dto.Contacts.Count.ShouldBe(4); dto.Contacts.Count(c => c.IsEditable).ShouldBe(2); - dto.Contacts.Count(c => !c.IsEditable).ShouldBe(1); + dto.Contacts.Count(c => !c.IsEditable).ShouldBe(2); } [Fact] @@ -116,10 +130,6 @@ public async Task GetDataAsync_WithNoContacts_ShouldReturnEmptyList() { // Arrange var request = CreateRequest(); - _applicantProfileContactService.GetProfileContactsAsync(Arg.Any()) - .Returns(new List()); - _applicantProfileContactService.GetApplicationContactsBySubjectAsync(Arg.Any()) - .Returns(new List()); // Act var result = await _provider.GetDataAsync(request); @@ -130,7 +140,7 @@ public async Task GetDataAsync_WithNoContacts_ShouldReturnEmptyList() } [Fact] - public async Task GetDataAsync_ProfileContactsShouldAppearBeforeApplicationContacts() + public async Task GetDataAsync_ContactsShouldAppearInExpectedOrder() { // Arrange var request = CreateRequest(); @@ -146,10 +156,19 @@ public async Task GetDataAsync_ProfileContactsShouldAppearBeforeApplicationConta Name = "App Second", IsEditable = false }; + var agentContact = new ContactInfoItemDto + { + ContactId = Guid.NewGuid(), + Name = "Agent Third", + IsEditable = false, + ContactType = "ApplicantAgent" + }; _applicantProfileContactService.GetProfileContactsAsync(request.ProfileId) .Returns(new List { profileContact }); _applicantProfileContactService.GetApplicationContactsBySubjectAsync(request.Subject) .Returns(new List { appContact }); + _applicantProfileContactService.GetApplicantAgentContactsBySubjectAsync(request.Subject) + .Returns(new List { agentContact }); // Act var result = await _provider.GetDataAsync(request); @@ -158,6 +177,7 @@ public async Task GetDataAsync_ProfileContactsShouldAppearBeforeApplicationConta var dto = result.ShouldBeOfType(); dto.Contacts[0].Name.ShouldBe("Profile First"); dto.Contacts[1].Name.ShouldBe("App Second"); + dto.Contacts[2].Name.ShouldBe("Agent Third"); } [Fact] @@ -165,10 +185,6 @@ public async Task GetDataAsync_ShouldReturnCorrectDataType() { // Arrange var request = CreateRequest(); - _applicantProfileContactService.GetProfileContactsAsync(Arg.Any()) - .Returns(new List()); - _applicantProfileContactService.GetApplicationContactsBySubjectAsync(Arg.Any()) - .Returns(new List()); // Act var result = await _provider.GetDataAsync(request); From 53fad35eb9e1123f73cafd5b43d4a9df8d679c04 Mon Sep 17 00:00:00 2001 From: Stephan McColm Date: Fri, 27 Feb 2026 13:52:32 -0800 Subject: [PATCH 090/191] update the Communities SQL Script and delete the Census Subdivisions script --- .../unitydb-census-subdivision-script.sql | 19 ------------------- .../scripts/unitydb-communities-script.sql | 9 ++++++--- 2 files changed, 6 insertions(+), 22 deletions(-) delete mode 100644 database/scripts/unitydb-census-subdivision-script.sql diff --git a/database/scripts/unitydb-census-subdivision-script.sql b/database/scripts/unitydb-census-subdivision-script.sql deleted file mode 100644 index ee74246b04..0000000000 --- a/database/scripts/unitydb-census-subdivision-script.sql +++ /dev/null @@ -1,19 +0,0 @@ - -DO $$ -DECLARE - json_data jsonb := '[{"CensusSubdivisionName":"Ahahswinis 1","Type":"Indian reserve","RegionalDistrictCode":"4"},{"CensusSubdivisionName":"Alberni 2","Type":"Indian reserve","RegionalDistrictCode":"4"},{"CensusSubdivisionName":"Alberni-Clayoquot A","Type":"Regional district electoral area","RegionalDistrictCode":"4"},{"CensusSubdivisionName":"Alberni-Clayoquot B","Type":"Regional district electoral area","RegionalDistrictCode":"4"},{"CensusSubdivisionName":"Alberni-Clayoquot C","Type":"Regional district electoral area","RegionalDistrictCode":"4"},{"CensusSubdivisionName":"Alberni-Clayoquot D","Type":"Regional district electoral area","RegionalDistrictCode":"4"},{"CensusSubdivisionName":"Alberni-Clayoquot E","Type":"Regional district electoral area","RegionalDistrictCode":"4"},{"CensusSubdivisionName":"Alberni-Clayoquot F","Type":"Regional district electoral area","RegionalDistrictCode":"4"},{"CensusSubdivisionName":"Anacla 12","Type":"Indian reserve","RegionalDistrictCode":"4"},{"CensusSubdivisionName":"Clakamucus 2","Type":"Indian reserve","RegionalDistrictCode":"4"},{"CensusSubdivisionName":"Elhlateese 2","Type":"Indian reserve","RegionalDistrictCode":"4"},{"CensusSubdivisionName":"Esowista 3","Type":"Indian reserve","RegionalDistrictCode":"4"},{"CensusSubdivisionName":"Hesquiat 1","Type":"Indian reserve","RegionalDistrictCode":"4"},{"CensusSubdivisionName":"Ittatsoo 1","Type":"Indian reserve","RegionalDistrictCode":"4"},{"CensusSubdivisionName":"Klehkoot 2","Type":"Indian reserve","RegionalDistrictCode":"4"},{"CensusSubdivisionName":"Macoah 1","Type":"Indian reserve","RegionalDistrictCode":"4"},{"CensusSubdivisionName":"Marktosis 15","Type":"Indian reserve","RegionalDistrictCode":"4"},{"CensusSubdivisionName":"Numukamis 1","Type":"Indian reserve","RegionalDistrictCode":"4"},{"CensusSubdivisionName":"Opitsat 1","Type":"Indian reserve","RegionalDistrictCode":"4"},{"CensusSubdivisionName":"Port Alberni","Type":"City","RegionalDistrictCode":"4"},{"CensusSubdivisionName":"Refuge Cove 6","Type":"Indian reserve","RegionalDistrictCode":"4"},{"CensusSubdivisionName":"Sachsa 4","Type":"Indian reserve","RegionalDistrictCode":"4"},{"CensusSubdivisionName":"Tin Wis 11","Type":"Indian reserve","RegionalDistrictCode":"4"},{"CensusSubdivisionName":"Tofino","Type":"District municipality","RegionalDistrictCode":"4"},{"CensusSubdivisionName":"Tsahaheh 1","Type":"Indian reserve","RegionalDistrictCode":"4"},{"CensusSubdivisionName":"Ucluelet","Type":"District municipality","RegionalDistrictCode":"4"},{"CensusSubdivisionName":"Babine 16","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Babine 25","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Babine 6","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Babine Lake 21B","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Binche 2","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Bulkley-Nechako A","Type":"Regional district electoral area","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Bulkley-Nechako B","Type":"Regional district electoral area","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Bulkley-Nechako C","Type":"Regional district electoral area","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Bulkley-Nechako D","Type":"Regional district electoral area","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Bulkley-Nechako E","Type":"Regional district electoral area","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Bulkley-Nechako F","Type":"Regional district electoral area","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Bulkley-Nechako G","Type":"Regional district electoral area","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Burns Lake","Type":"Village","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Burns Lake 18","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Cheslatta 1","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Duncan Lake 2","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Dzitline Lee 9","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Fort St. James","Type":"District municipality","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Francois Lake 7","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Fraser Lake","Type":"Village","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Granisle","Type":"Village","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Houston","Type":"District municipality","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Jean Baptiste 28","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Laketown 3","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Mission Lands 17","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Nak''azdli","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Nautley (Fort Fraser) 1","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Nedoats 11","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Noonla 6","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"North Tacla Lake","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Omineca 1","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Palling 1","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Poison Creek 17A","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Seaspunkut 4","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Skins Lake 16A","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Skins Lake 16B","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Smithers","Type":"Town","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Sowchea 3","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Stellaquo (Stella) 1","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Stony Creek 1","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Tache 1","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Tacla Lake (Ferry Landing) 9","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Tadinlay 15","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Tatla West 11","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Tatla''t East 2","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Telkwa","Type":"Village","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Uncha Lake 13A","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Vanderhoof","Type":"District municipality","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Williams Prairie Meadow 1A","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Woyenne 27","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Ye Koo Che 3","Type":"Indian reserve","RegionalDistrictCode":"26"},{"CensusSubdivisionName":"Becher Bay 1","Type":"Indian reserve","RegionalDistrictCode":"1"},{"CensusSubdivisionName":"Central Saanich","Type":"District municipality","RegionalDistrictCode":"1"},{"CensusSubdivisionName":"Cole Bay 3","Type":"Indian reserve","RegionalDistrictCode":"1"},{"CensusSubdivisionName":"Colwood","Type":"City","RegionalDistrictCode":"1"},{"CensusSubdivisionName":"East Saanich 2","Type":"Indian reserve","RegionalDistrictCode":"1"},{"CensusSubdivisionName":"Esquimalt - District municipality","Type":"District municipality","RegionalDistrictCode":"1"},{"CensusSubdivisionName":"Esquimalt - Indian reserve","Type":"Indian reserve","RegionalDistrictCode":"1"},{"CensusSubdivisionName":"Galiano Island 9","Type":"Indian reserve","RegionalDistrictCode":"1"},{"CensusSubdivisionName":"Gordon River 2","Type":"Indian reserve","RegionalDistrictCode":"1"},{"CensusSubdivisionName":"Highlands","Type":"District municipality","RegionalDistrictCode":"1"},{"CensusSubdivisionName":"Juan de Fuca (Part 1)","Type":"Regional district electoral area","RegionalDistrictCode":"1"},{"CensusSubdivisionName":"Juan de Fuca (Part 2)","Type":"Regional district electoral area","RegionalDistrictCode":"1"},{"CensusSubdivisionName":"Langford","Type":"City","RegionalDistrictCode":"1"},{"CensusSubdivisionName":"Metchosin","Type":"District municipality","RegionalDistrictCode":"1"},{"CensusSubdivisionName":"New Songhees 1A","Type":"Indian reserve","RegionalDistrictCode":"1"},{"CensusSubdivisionName":"North Saanich","Type":"District municipality","RegionalDistrictCode":"1"},{"CensusSubdivisionName":"Oak Bay","Type":"District municipality","RegionalDistrictCode":"1"},{"CensusSubdivisionName":"Saanich","Type":"District municipality","RegionalDistrictCode":"1"},{"CensusSubdivisionName":"Saltspring Island","Type":"Regional district electoral area","RegionalDistrictCode":"1"},{"CensusSubdivisionName":"Sidney","Type":"Town","RegionalDistrictCode":"1"},{"CensusSubdivisionName":"Sooke","Type":"District municipality","RegionalDistrictCode":"1"},{"CensusSubdivisionName":"South Saanich 1","Type":"Indian reserve","RegionalDistrictCode":"1"},{"CensusSubdivisionName":"Southern Gulf Islands","Type":"Regional district electoral area","RegionalDistrictCode":"1"},{"CensusSubdivisionName":"T''Sou-ke","Type":"Indian reserve","RegionalDistrictCode":"1"},{"CensusSubdivisionName":"Union Bay 4","Type":"Indian reserve","RegionalDistrictCode":"1"},{"CensusSubdivisionName":"Victoria","Type":"City","RegionalDistrictCode":"1"},{"CensusSubdivisionName":"View Royal","Type":"Town","RegionalDistrictCode":"1"},{"CensusSubdivisionName":"Agats Meadow 8","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Alexandria","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Alexis Creek 14","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Alexis Creek 16","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Alexis Creek 21","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Alexis Creek 34","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Alkali Lake 1","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Alkali Lake 4A","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Anahim''s Flat 1","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Anahim''s Meadow","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Baezaeko River 27","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Baptiste Meadow 2","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Betty Creek 18","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Canim Lake 1","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Canim Lake 2","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Canim Lake 4","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Canoe Creek 3","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Cariboo A","Type":"Regional district electoral area","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Cariboo B","Type":"Regional district electoral area","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Cariboo C","Type":"Regional district electoral area","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Cariboo D","Type":"Regional district electoral area","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Cariboo E","Type":"Regional district electoral area","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Cariboo F","Type":"Regional district electoral area","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Cariboo G","Type":"Regional district electoral area","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Cariboo H","Type":"Regional district electoral area","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Cariboo I","Type":"Regional district electoral area","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Cariboo J","Type":"Regional district electoral area","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Cariboo K","Type":"Regional district electoral area","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Cariboo L","Type":"Regional district electoral area","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Charley Boy''s Meadow 3","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Chilco Lake 1","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Chilco Lake 1A","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Coglistiko River 29","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Deep Creek 2","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Dog Creek 1","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Dog Creek 2","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Dragon Lake 3","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Euchinico Creek 17","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Fishtrap 19","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Garden","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Johny Sticks 2","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Kluskus 1","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Lezbye 6","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Little Springs","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Lohbiee 3","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Michel Gardens 36","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Nazco 20","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"One Hundred Mile House","Type":"District municipality","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Puntzi Lake 2","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Quesnel","Type":"City","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Quesnel 1","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Redstone Flat 1","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Salmon River Meadow 7","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Sandy Harry 4","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Soda Creek 1","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Squinas 2","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Stone 1","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Sundayman''s Meadow 3","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Swan Lake 3","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Tanakut 4","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Thomas Squinas Ranch 2A","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Toosey 1","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Towdystan Lake 3","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Trout Lake Alec 16","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Ulkatcho 13","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Ulkatcho 14A","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Wells","Type":"District municipality","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Williams Lake","Type":"City","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Williams Lake 1","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Windy Mouth 7","Type":"Indian reserve","RegionalDistrictCode":"22"},{"CensusSubdivisionName":"Bella Bella 1","Type":"Indian reserve","RegionalDistrictCode":"9"},{"CensusSubdivisionName":"Bella Coola 1","Type":"Indian reserve","RegionalDistrictCode":"9"},{"CensusSubdivisionName":"Central Coast A","Type":"Regional district electoral area","RegionalDistrictCode":"9"},{"CensusSubdivisionName":"Central Coast C","Type":"Regional district electoral area","RegionalDistrictCode":"9"},{"CensusSubdivisionName":"Central Coast D","Type":"Regional district electoral area","RegionalDistrictCode":"9"},{"CensusSubdivisionName":"Central Coast E","Type":"Regional district electoral area","RegionalDistrictCode":"9"},{"CensusSubdivisionName":"Katit 1","Type":"Indian reserve","RegionalDistrictCode":"9"},{"CensusSubdivisionName":"Castlegar","Type":"City","RegionalDistrictCode":"20"},{"CensusSubdivisionName":"Central Kootenay A","Type":"Regional district electoral area","RegionalDistrictCode":"20"},{"CensusSubdivisionName":"Central Kootenay B","Type":"Regional district electoral area","RegionalDistrictCode":"20"},{"CensusSubdivisionName":"Central Kootenay C","Type":"Regional district electoral area","RegionalDistrictCode":"20"},{"CensusSubdivisionName":"Central Kootenay D","Type":"Regional district electoral area","RegionalDistrictCode":"20"},{"CensusSubdivisionName":"Central Kootenay E","Type":"Regional district electoral area","RegionalDistrictCode":"20"},{"CensusSubdivisionName":"Central Kootenay F","Type":"Regional district electoral area","RegionalDistrictCode":"20"},{"CensusSubdivisionName":"Central Kootenay G","Type":"Regional district electoral area","RegionalDistrictCode":"20"},{"CensusSubdivisionName":"Central Kootenay H","Type":"Regional district electoral area","RegionalDistrictCode":"20"},{"CensusSubdivisionName":"Central Kootenay I","Type":"Regional district electoral area","RegionalDistrictCode":"20"},{"CensusSubdivisionName":"Central Kootenay J","Type":"Regional district electoral area","RegionalDistrictCode":"20"},{"CensusSubdivisionName":"Central Kootenay K","Type":"Regional district electoral area","RegionalDistrictCode":"20"},{"CensusSubdivisionName":"Creston","Type":"Town","RegionalDistrictCode":"20"},{"CensusSubdivisionName":"Creston 1","Type":"Indian reserve","RegionalDistrictCode":"20"},{"CensusSubdivisionName":"Kaslo","Type":"Village","RegionalDistrictCode":"20"},{"CensusSubdivisionName":"Nakusp","Type":"Village","RegionalDistrictCode":"20"},{"CensusSubdivisionName":"Nelson","Type":"City","RegionalDistrictCode":"20"},{"CensusSubdivisionName":"New Denver","Type":"Village","RegionalDistrictCode":"20"},{"CensusSubdivisionName":"Salmo","Type":"Village","RegionalDistrictCode":"20"},{"CensusSubdivisionName":"Silverton","Type":"Village","RegionalDistrictCode":"20"},{"CensusSubdivisionName":"Slocan","Type":"Village","RegionalDistrictCode":"20"},{"CensusSubdivisionName":"Central Okanagan","Type":"Regional district electoral area","RegionalDistrictCode":"16"},{"CensusSubdivisionName":"Central Okanagan West","Type":"Regional district electoral area","RegionalDistrictCode":"16"},{"CensusSubdivisionName":"Duck Lake 7","Type":"Indian reserve","RegionalDistrictCode":"16"},{"CensusSubdivisionName":"Kelowna","Type":"City","RegionalDistrictCode":"16"},{"CensusSubdivisionName":"Lake Country","Type":"District municipality","RegionalDistrictCode":"16"},{"CensusSubdivisionName":"Peachland","Type":"District municipality","RegionalDistrictCode":"16"},{"CensusSubdivisionName":"Tsinstikeptum 10","Type":"Indian reserve","RegionalDistrictCode":"16"},{"CensusSubdivisionName":"Tsinstikeptum 9","Type":"Indian reserve","RegionalDistrictCode":"16"},{"CensusSubdivisionName":"West Kelowna","Type":"City","RegionalDistrictCode":"16"},{"CensusSubdivisionName":"Chum Creek 2","Type":"Indian reserve","RegionalDistrictCode":"18"},{"CensusSubdivisionName":"Columbia-Shuswap A","Type":"Regional district electoral area","RegionalDistrictCode":"18"},{"CensusSubdivisionName":"Columbia-Shuswap B","Type":"Regional district electoral area","RegionalDistrictCode":"18"},{"CensusSubdivisionName":"Columbia-Shuswap C","Type":"Regional district electoral area","RegionalDistrictCode":"18"},{"CensusSubdivisionName":"Columbia-Shuswap D","Type":"Regional district electoral area","RegionalDistrictCode":"18"},{"CensusSubdivisionName":"Columbia-Shuswap E","Type":"Regional district electoral area","RegionalDistrictCode":"18"},{"CensusSubdivisionName":"Columbia-Shuswap F","Type":"Regional district electoral area","RegionalDistrictCode":"18"},{"CensusSubdivisionName":"Golden","Type":"Town","RegionalDistrictCode":"18"},{"CensusSubdivisionName":"Hustalen 1","Type":"Indian reserve","RegionalDistrictCode":"18"},{"CensusSubdivisionName":"North Bay 5","Type":"Indian reserve","RegionalDistrictCode":"18"},{"CensusSubdivisionName":"Okanagan (Part) 1 - Thompson/Okanagan","Type":"Indian reserve","RegionalDistrictCode":"18"},{"CensusSubdivisionName":"Quaaout 1","Type":"Indian reserve","RegionalDistrictCode":"18"},{"CensusSubdivisionName":"Revelstoke","Type":"City","RegionalDistrictCode":"18"},{"CensusSubdivisionName":"Salmon Arm","Type":"City","RegionalDistrictCode":"18"},{"CensusSubdivisionName":"Salmon River 1","Type":"Indian reserve","RegionalDistrictCode":"18"},{"CensusSubdivisionName":"Scotch Creek 4","Type":"Indian reserve","RegionalDistrictCode":"18"},{"CensusSubdivisionName":"Sicamous","Type":"District municipality","RegionalDistrictCode":"18"},{"CensusSubdivisionName":"Switsemalph","Type":"Indian reserve","RegionalDistrictCode":"18"},{"CensusSubdivisionName":"Switsemalph 3","Type":"Indian reserve","RegionalDistrictCode":"18"},{"CensusSubdivisionName":"Comox","Type":"Town","RegionalDistrictCode":"6"},{"CensusSubdivisionName":"Comox 1","Type":"Indian reserve","RegionalDistrictCode":"6"},{"CensusSubdivisionName":"Comox Valley A","Type":"Regional district electoral area","RegionalDistrictCode":"6"},{"CensusSubdivisionName":"Comox Valley B (Lazo North)","Type":"Regional district electoral area","RegionalDistrictCode":"6"},{"CensusSubdivisionName":"Comox Valley C (Puntledge - Black Creek)","Type":"Regional district electoral area","RegionalDistrictCode":"6"},{"CensusSubdivisionName":"Courtenay","Type":"City","RegionalDistrictCode":"6"},{"CensusSubdivisionName":"Cumberland","Type":"Village","RegionalDistrictCode":"6"},{"CensusSubdivisionName":"Pentledge 2","Type":"Indian reserve","RegionalDistrictCode":"6"},{"CensusSubdivisionName":"Chemainus 13","Type":"Indian reserve","RegionalDistrictCode":"2"},{"CensusSubdivisionName":"Cowichan","Type":"Indian reserve","RegionalDistrictCode":"2"},{"CensusSubdivisionName":"Cowichan Lake","Type":"Indian reserve","RegionalDistrictCode":"2"},{"CensusSubdivisionName":"Cowichan Valley A","Type":"Regional district electoral area","RegionalDistrictCode":"2"},{"CensusSubdivisionName":"Cowichan Valley B","Type":"Regional district electoral area","RegionalDistrictCode":"2"},{"CensusSubdivisionName":"Cowichan Valley C","Type":"Regional district electoral area","RegionalDistrictCode":"2"},{"CensusSubdivisionName":"Cowichan Valley D","Type":"Regional district electoral area","RegionalDistrictCode":"2"},{"CensusSubdivisionName":"Cowichan Valley E","Type":"Regional district electoral area","RegionalDistrictCode":"2"},{"CensusSubdivisionName":"Cowichan Valley F","Type":"Regional district electoral area","RegionalDistrictCode":"2"},{"CensusSubdivisionName":"Cowichan Valley G","Type":"Regional district electoral area","RegionalDistrictCode":"2"},{"CensusSubdivisionName":"Cowichan Valley H","Type":"Regional district electoral area","RegionalDistrictCode":"2"},{"CensusSubdivisionName":"Cowichan Valley I","Type":"Regional district electoral area","RegionalDistrictCode":"2"},{"CensusSubdivisionName":"Duncan","Type":"City","RegionalDistrictCode":"2"},{"CensusSubdivisionName":"Est-Patrolas 4","Type":"Indian reserve","RegionalDistrictCode":"2"},{"CensusSubdivisionName":"Halalt 2","Type":"Indian reserve","RegionalDistrictCode":"2"},{"CensusSubdivisionName":"Kil-pah-las 3","Type":"Indian reserve","RegionalDistrictCode":"2"},{"CensusSubdivisionName":"Ladysmith","Type":"Town","RegionalDistrictCode":"2"},{"CensusSubdivisionName":"Lake Cowichan","Type":"Town","RegionalDistrictCode":"2"},{"CensusSubdivisionName":"Lyacksun 3","Type":"Indian reserve","RegionalDistrictCode":"2"},{"CensusSubdivisionName":"Malachan 11","Type":"Indian reserve","RegionalDistrictCode":"2"},{"CensusSubdivisionName":"Malahat 11","Type":"Indian reserve","RegionalDistrictCode":"2"},{"CensusSubdivisionName":"North Cowichan","Type":"District municipality","RegionalDistrictCode":"2"},{"CensusSubdivisionName":"Oyster Bay 12","Type":"Indian reserve","RegionalDistrictCode":"2"},{"CensusSubdivisionName":"Penelakut Island 7","Type":"Indian reserve","RegionalDistrictCode":"2"},{"CensusSubdivisionName":"Portier Pass 5","Type":"Indian reserve","RegionalDistrictCode":"2"},{"CensusSubdivisionName":"Shingle Point 4","Type":"Indian reserve","RegionalDistrictCode":"2"},{"CensusSubdivisionName":"Squaw-Hay-One 11","Type":"Indian reserve","RegionalDistrictCode":"2"},{"CensusSubdivisionName":"Theik 2","Type":"Indian reserve","RegionalDistrictCode":"2"},{"CensusSubdivisionName":"Tsussie 6","Type":"Indian reserve","RegionalDistrictCode":"2"},{"CensusSubdivisionName":"Tzart-Lam 5","Type":"Indian reserve","RegionalDistrictCode":"2"},{"CensusSubdivisionName":"Canal Flats","Type":"Village","RegionalDistrictCode":"19"},{"CensusSubdivisionName":"Cassimayooks (Mayook) 5","Type":"Indian reserve","RegionalDistrictCode":"19"},{"CensusSubdivisionName":"Columbia Lake 3","Type":"Indian reserve","RegionalDistrictCode":"19"},{"CensusSubdivisionName":"Cranbrook","Type":"City","RegionalDistrictCode":"19"},{"CensusSubdivisionName":"East Kootenay A","Type":"Regional district electoral area","RegionalDistrictCode":"19"},{"CensusSubdivisionName":"East Kootenay B","Type":"Regional district electoral area","RegionalDistrictCode":"19"},{"CensusSubdivisionName":"East Kootenay C","Type":"Regional district electoral area","RegionalDistrictCode":"19"},{"CensusSubdivisionName":"East Kootenay E","Type":"Regional district electoral area","RegionalDistrictCode":"19"},{"CensusSubdivisionName":"East Kootenay F","Type":"Regional district electoral area","RegionalDistrictCode":"19"},{"CensusSubdivisionName":"East Kootenay G","Type":"Regional district electoral area","RegionalDistrictCode":"19"},{"CensusSubdivisionName":"Elkford","Type":"District municipality","RegionalDistrictCode":"19"},{"CensusSubdivisionName":"Fernie","Type":"City","RegionalDistrictCode":"19"},{"CensusSubdivisionName":"Invermere","Type":"District municipality","RegionalDistrictCode":"19"},{"CensusSubdivisionName":"Isidore''s Ranch 4","Type":"Indian reserve","RegionalDistrictCode":"19"},{"CensusSubdivisionName":"Kimberley","Type":"City","RegionalDistrictCode":"19"},{"CensusSubdivisionName":"Kootenay 1","Type":"Indian reserve","RegionalDistrictCode":"19"},{"CensusSubdivisionName":"Radium Hot Springs","Type":"Village","RegionalDistrictCode":"19"},{"CensusSubdivisionName":"Shuswap","Type":"Indian reserve","RegionalDistrictCode":"19"},{"CensusSubdivisionName":"Sparwood","Type":"District municipality","RegionalDistrictCode":"19"},{"CensusSubdivisionName":"St. Mary''s","Type":"Indian reserve","RegionalDistrictCode":"19"},{"CensusSubdivisionName":"Tobacco Plains 2","Type":"Indian reserve","RegionalDistrictCode":"19"},{"CensusSubdivisionName":"Abbotsford","Type":"City","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Aitchelitch 9","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Albert Flat 5","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Boothroyd 13","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Boston Bar 1A","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Bucktum 4","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Chawathil 4","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Cheam 1","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Chehalis 5","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Chilliwack","Type":"City","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Douglas 8","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Fraser Valley A","Type":"Regional district electoral area","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Fraser Valley B","Type":"Regional district electoral area","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Fraser Valley C","Type":"Regional district electoral area","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Fraser Valley D","Type":"Regional district electoral area","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Fraser Valley E","Type":"Regional district electoral area","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Fraser Valley F","Type":"Regional district electoral area","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Fraser Valley G","Type":"Regional district electoral area","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Fraser Valley H","Type":"Regional district electoral area","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Harrison Hot Springs","Type":"Village","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Holachten 8","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Hope","Type":"District municipality","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Inkahtsaph 6","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Kahmoose 4","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Kent","Type":"District municipality","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Kopchitchin 2","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Kwawkwawapilt 6","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Lakahahmen 11","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Langley 2","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Lukseetsissum 9","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Matsqui Main 2","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Mission","Type":"District municipality","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Ohamil 1","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Paqulh","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Peters 1","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Popkum 1","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Puckatholetchin 11","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Q''alatkú7em","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Ruby Creek 2","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Sachteen","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Saddle Rock 9","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Schelowat 1","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Schkam 2","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Scowlitz 1","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Seabird Island","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Skawahlook 1","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Skookumchuck 4","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Skowkale","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Skwah 4","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Skwali 3","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Skway 5","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Skweahm 10","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Soowahlie 14","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Speyum 3","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Spuzzum 1","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Squawkum Creek 3","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Squiaala","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Stullawheets 8","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Tipella 7","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Tseatah 2","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Tuckkwiowhum 1","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Tzeachten 13","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Upper Sumas 6","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Yakweakwioose 12","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Yale Town 1","Type":"Indian reserve","RegionalDistrictCode":"10"},{"CensusSubdivisionName":"Fort George 2","Type":"Indian reserve","RegionalDistrictCode":"23"},{"CensusSubdivisionName":"Fraser-Fort George A","Type":"Regional district electoral area","RegionalDistrictCode":"23"},{"CensusSubdivisionName":"Fraser-Fort George C","Type":"Regional district electoral area","RegionalDistrictCode":"23"},{"CensusSubdivisionName":"Fraser-Fort George D","Type":"Regional district electoral area","RegionalDistrictCode":"23"},{"CensusSubdivisionName":"Fraser-Fort George E","Type":"Regional district electoral area","RegionalDistrictCode":"23"},{"CensusSubdivisionName":"Fraser-Fort George F","Type":"Regional district electoral area","RegionalDistrictCode":"23"},{"CensusSubdivisionName":"Fraser-Fort George G","Type":"Regional district electoral area","RegionalDistrictCode":"23"},{"CensusSubdivisionName":"Fraser-Fort George H","Type":"Regional district electoral area","RegionalDistrictCode":"23"},{"CensusSubdivisionName":"Mackenzie","Type":"District municipality","RegionalDistrictCode":"23"},{"CensusSubdivisionName":"McBride","Type":"Village","RegionalDistrictCode":"23"},{"CensusSubdivisionName":"McLeod Lake 1","Type":"Indian reserve","RegionalDistrictCode":"23"},{"CensusSubdivisionName":"Prince George","Type":"City","RegionalDistrictCode":"23"},{"CensusSubdivisionName":"Valemount","Type":"Village","RegionalDistrictCode":"23"},{"CensusSubdivisionName":"Anmore","Type":"Village","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"Barnston Island 3","Type":"Indian reserve","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"Belcarra","Type":"Village","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"Bowen Island","Type":"Island municipality","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"Burnaby","Type":"City","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"Burrard Inlet 3","Type":"Indian reserve","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"Capilano 5","Type":"Indian reserve","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"Coquitlam","Type":"City","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"Coquitlam 1","Type":"Indian reserve","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"Coquitlam 2","Type":"Indian reserve","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"Delta","Type":"City","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"Katzie 1","Type":"Indian reserve","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"Katzie 2","Type":"Indian reserve","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"Langley - City","Type":"City","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"Langley - District municipality","Type":"District municipality","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"Lions Bay","Type":"Village","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"Maple Ridge","Type":"City","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"Matsqui 4","Type":"Indian reserve","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"McMillan Island 6","Type":"Indian reserve","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"Metro Vancouver A","Type":"Regional district electoral area","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"Mission 1","Type":"Indian reserve","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"Musqueam 2","Type":"Indian reserve","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"Musqueam 4","Type":"Indian reserve","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"New Westminster","Type":"City","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"North Vancouver - City","Type":"City","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"North Vancouver - District municipality","Type":"District municipality","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"Pitt Meadows","Type":"City","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"Port Coquitlam","Type":"City","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"Port Moody","Type":"City","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"Richmond","Type":"City","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"Semiahmoo","Type":"Indian reserve","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"Seymour Creek 2","Type":"Indian reserve","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"Surrey","Type":"City","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"Tsawwassen","Type":"Tsawwassen Lands","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"Vancouver","Type":"City","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"West Vancouver","Type":"District municipality","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"White Rock","Type":"City","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"Whonnock 1","Type":"Indian reserve","RegionalDistrictCode":"11"},{"CensusSubdivisionName":"Babine 17","Type":"Indian reserve","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Bulkley River 19","Type":"Indian reserve","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Coryatsaqua (Moricetown) 2","Type":"Indian reserve","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Dease Lake 9","Type":"Indian reserve","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Gitanmaax 1","Type":"Indian reserve","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Gitanyow 1","Type":"Indian reserve","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Gitsegukla 1","Type":"Indian reserve","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Gitwangak 1","Type":"Indian reserve","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Guhthe Tah 12","Type":"Indian reserve","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Hagwilget 1","Type":"Indian reserve","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Hazelton","Type":"Village","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Iskut 6","Type":"Indian reserve","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Kispiox 1","Type":"Indian reserve","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Kitamaat 2","Type":"Indian reserve","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Kitasoo 1","Type":"Indian reserve","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Kitimat","Type":"District municipality","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Kitimat-Stikine A","Type":"Regional district electoral area","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Kitimat-Stikine B","Type":"Regional district electoral area","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Kitimat-Stikine C (Part 1)","Type":"Regional district electoral area","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Kitimat-Stikine C (Part 2)","Type":"Regional district electoral area","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Kitimat-Stikine D","Type":"Regional district electoral area","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Kitimat-Stikine E","Type":"Regional district electoral area","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Kitimat-Stikine F","Type":"Regional district electoral area","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Kitselas 1","Type":"Indian reserve","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Kitsumkaylum 1","Type":"Indian reserve","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Kshish 4","Type":"Indian reserve","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Kulspai 6","Type":"Indian reserve","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Moricetown 1","Type":"Indian reserve","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"New Hazelton","Type":"District municipality","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Nisga''a","Type":"Nisga''a land","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Sik-e-dakh 2","Type":"Indian reserve","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Stewart","Type":"District municipality","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Tahltan 1","Type":"Indian reserve","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Telegraph Creek","Type":"Indian reserve","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Terrace","Type":"City","RegionalDistrictCode":"25"},{"CensusSubdivisionName":"Fruitvale","Type":"Village","RegionalDistrictCode":"21"},{"CensusSubdivisionName":"Grand Forks","Type":"City","RegionalDistrictCode":"21"},{"CensusSubdivisionName":"Greenwood","Type":"City","RegionalDistrictCode":"21"},{"CensusSubdivisionName":"Kootenay Boundary A","Type":"Regional district electoral area","RegionalDistrictCode":"21"},{"CensusSubdivisionName":"Kootenay Boundary B / Lower Columbia-Old-Glory","Type":"Regional district electoral area","RegionalDistrictCode":"21"},{"CensusSubdivisionName":"Kootenay Boundary C / Christina Lake","Type":"Regional district electoral area","RegionalDistrictCode":"21"},{"CensusSubdivisionName":"Kootenay Boundary D / Rural Grand Forks","Type":"Regional district electoral area","RegionalDistrictCode":"21"},{"CensusSubdivisionName":"Kootenay Boundary E / West Boundary","Type":"Regional district electoral area","RegionalDistrictCode":"21"},{"CensusSubdivisionName":"Midway","Type":"Village","RegionalDistrictCode":"21"},{"CensusSubdivisionName":"Montrose","Type":"Village","RegionalDistrictCode":"21"},{"CensusSubdivisionName":"Rossland","Type":"City","RegionalDistrictCode":"21"},{"CensusSubdivisionName":"Trail","Type":"City","RegionalDistrictCode":"21"},{"CensusSubdivisionName":"Warfield","Type":"Village","RegionalDistrictCode":"21"},{"CensusSubdivisionName":"Alert Bay - Indian reserve","Type":"Indian reserve","RegionalDistrictCode":"8"},{"CensusSubdivisionName":"Alert Bay - Village","Type":"Village","RegionalDistrictCode":"8"},{"CensusSubdivisionName":"Dead Point 5","Type":"Indian reserve","RegionalDistrictCode":"8"},{"CensusSubdivisionName":"Fort Rupert 1","Type":"Indian reserve","RegionalDistrictCode":"8"},{"CensusSubdivisionName":"Gwayasdums 1","Type":"Indian reserve","RegionalDistrictCode":"8"},{"CensusSubdivisionName":"Hope Island 1","Type":"Indian reserve","RegionalDistrictCode":"8"},{"CensusSubdivisionName":"Hopetown 10A","Type":"Indian reserve","RegionalDistrictCode":"8"},{"CensusSubdivisionName":"Kippase 2","Type":"Indian reserve","RegionalDistrictCode":"8"},{"CensusSubdivisionName":"Mount Waddington A","Type":"Regional district electoral area","RegionalDistrictCode":"8"},{"CensusSubdivisionName":"Mount Waddington B","Type":"Regional district electoral area","RegionalDistrictCode":"8"},{"CensusSubdivisionName":"Mount Waddington C","Type":"Regional district electoral area","RegionalDistrictCode":"8"},{"CensusSubdivisionName":"Mount Waddington D","Type":"Regional district electoral area","RegionalDistrictCode":"8"},{"CensusSubdivisionName":"Nimpkish 2","Type":"Indian reserve","RegionalDistrictCode":"8"},{"CensusSubdivisionName":"Port Alice","Type":"Village","RegionalDistrictCode":"8"},{"CensusSubdivisionName":"Port Hardy","Type":"District municipality","RegionalDistrictCode":"8"},{"CensusSubdivisionName":"Port McNeill","Type":"Town","RegionalDistrictCode":"8"},{"CensusSubdivisionName":"Quaee 7","Type":"Indian reserve","RegionalDistrictCode":"8"},{"CensusSubdivisionName":"Quatsino Subdivision 18","Type":"Indian reserve","RegionalDistrictCode":"8"},{"CensusSubdivisionName":"Thomas Point 5","Type":"Indian reserve","RegionalDistrictCode":"8"},{"CensusSubdivisionName":"Thomas Point 5A","Type":"Indian reserve","RegionalDistrictCode":"8"},{"CensusSubdivisionName":"Tsulquate 4","Type":"Indian reserve","RegionalDistrictCode":"8"},{"CensusSubdivisionName":"Lantzville","Type":"District municipality","RegionalDistrictCode":"3"},{"CensusSubdivisionName":"Nanaimo","Type":"City","RegionalDistrictCode":"3"},{"CensusSubdivisionName":"Nanaimo A","Type":"Regional district electoral area","RegionalDistrictCode":"3"},{"CensusSubdivisionName":"Nanaimo B","Type":"Regional district electoral area","RegionalDistrictCode":"3"},{"CensusSubdivisionName":"Nanaimo C","Type":"Regional district electoral area","RegionalDistrictCode":"3"},{"CensusSubdivisionName":"Nanaimo E","Type":"Regional district electoral area","RegionalDistrictCode":"3"},{"CensusSubdivisionName":"Nanaimo F","Type":"Regional district electoral area","RegionalDistrictCode":"3"},{"CensusSubdivisionName":"Nanaimo G","Type":"Regional district electoral area","RegionalDistrictCode":"3"},{"CensusSubdivisionName":"Nanaimo H","Type":"Regional district electoral area","RegionalDistrictCode":"3"},{"CensusSubdivisionName":"Nanaimo River","Type":"Indian reserve","RegionalDistrictCode":"3"},{"CensusSubdivisionName":"Nanaimo Town 1","Type":"Indian reserve","RegionalDistrictCode":"3"},{"CensusSubdivisionName":"Nanoose","Type":"Indian reserve","RegionalDistrictCode":"3"},{"CensusSubdivisionName":"Parksville","Type":"City","RegionalDistrictCode":"3"},{"CensusSubdivisionName":"Qualicum","Type":"Indian reserve","RegionalDistrictCode":"3"},{"CensusSubdivisionName":"Qualicum Beach","Type":"Town","RegionalDistrictCode":"3"},{"CensusSubdivisionName":"Armstrong","Type":"City","RegionalDistrictCode":"17"},{"CensusSubdivisionName":"Coldstream","Type":"District municipality","RegionalDistrictCode":"17"},{"CensusSubdivisionName":"Enderby","Type":"City","RegionalDistrictCode":"17"},{"CensusSubdivisionName":"Enderby 2","Type":"Indian reserve","RegionalDistrictCode":"17"},{"CensusSubdivisionName":"Harris 3","Type":"Indian reserve","RegionalDistrictCode":"17"},{"CensusSubdivisionName":"Lumby","Type":"Village","RegionalDistrictCode":"17"},{"CensusSubdivisionName":"North Okanagan B","Type":"Regional district electoral area","RegionalDistrictCode":"17"},{"CensusSubdivisionName":"North Okanagan C","Type":"Regional district electoral area","RegionalDistrictCode":"17"},{"CensusSubdivisionName":"North Okanagan D","Type":"Regional district electoral area","RegionalDistrictCode":"17"},{"CensusSubdivisionName":"North Okanagan E","Type":"Regional district electoral area","RegionalDistrictCode":"17"},{"CensusSubdivisionName":"North Okanagan F","Type":"Regional district electoral area","RegionalDistrictCode":"17"},{"CensusSubdivisionName":"Okanagan (Part) 1 - North Okanagan","Type":"Indian reserve","RegionalDistrictCode":"17"},{"CensusSubdivisionName":"Priest''s Valley 6","Type":"Indian reserve","RegionalDistrictCode":"17"},{"CensusSubdivisionName":"Spallumcheen","Type":"District municipality","RegionalDistrictCode":"17"},{"CensusSubdivisionName":"Vernon","Type":"City","RegionalDistrictCode":"17"},{"CensusSubdivisionName":"Fontas 1","Type":"Indian reserve","RegionalDistrictCode":"29"},{"CensusSubdivisionName":"Fort Nelson 2","Type":"Indian reserve","RegionalDistrictCode":"29"},{"CensusSubdivisionName":"Kahntah 3","Type":"Indian reserve","RegionalDistrictCode":"29"},{"CensusSubdivisionName":"Northern Rockies","Type":"Regional municipality","RegionalDistrictCode":"29"},{"CensusSubdivisionName":"Prophet River 4","Type":"Indian reserve","RegionalDistrictCode":"29"},{"CensusSubdivisionName":"Alexis 9","Type":"Indian reserve","RegionalDistrictCode":"14"},{"CensusSubdivisionName":"Ashnola 10","Type":"Indian reserve","RegionalDistrictCode":"14"},{"CensusSubdivisionName":"Blind Creek 6","Type":"Indian reserve","RegionalDistrictCode":"14"},{"CensusSubdivisionName":"Chopaka 7 & 8","Type":"Indian reserve","RegionalDistrictCode":"14"},{"CensusSubdivisionName":"Chuchuwayha 2","Type":"Indian reserve","RegionalDistrictCode":"14"},{"CensusSubdivisionName":"Keremeos","Type":"Village","RegionalDistrictCode":"14"},{"CensusSubdivisionName":"Lower Similkameen 2","Type":"Indian reserve","RegionalDistrictCode":"14"},{"CensusSubdivisionName":"Lulu 5","Type":"Indian reserve","RegionalDistrictCode":"14"},{"CensusSubdivisionName":"Okanagan-Similkameen A","Type":"Regional district electoral area","RegionalDistrictCode":"14"},{"CensusSubdivisionName":"Okanagan-Similkameen B","Type":"Regional district electoral area","RegionalDistrictCode":"14"},{"CensusSubdivisionName":"Okanagan-Similkameen C","Type":"Regional district electoral area","RegionalDistrictCode":"14"},{"CensusSubdivisionName":"Okanagan-Similkameen D","Type":"Regional district electoral area","RegionalDistrictCode":"14"},{"CensusSubdivisionName":"Okanagan-Similkameen E","Type":"Regional district electoral area","RegionalDistrictCode":"14"},{"CensusSubdivisionName":"Okanagan-Similkameen F","Type":"Regional district electoral area","RegionalDistrictCode":"14"},{"CensusSubdivisionName":"Okanagan-Similkameen G","Type":"Regional district electoral area","RegionalDistrictCode":"14"},{"CensusSubdivisionName":"Okanagan-Similkameen H","Type":"Regional district electoral area","RegionalDistrictCode":"14"},{"CensusSubdivisionName":"Okanagan-Similkameen I","Type":"Regional district electoral area","RegionalDistrictCode":"14"},{"CensusSubdivisionName":"Oliver","Type":"Town","RegionalDistrictCode":"14"},{"CensusSubdivisionName":"Osoyoos","Type":"Town","RegionalDistrictCode":"14"},{"CensusSubdivisionName":"Osoyoos 1","Type":"Indian reserve","RegionalDistrictCode":"14"},{"CensusSubdivisionName":"Penticton","Type":"City","RegionalDistrictCode":"14"},{"CensusSubdivisionName":"Penticton 1","Type":"Indian reserve","RegionalDistrictCode":"14"},{"CensusSubdivisionName":"Princeton","Type":"Town","RegionalDistrictCode":"14"},{"CensusSubdivisionName":"Summerland","Type":"District municipality","RegionalDistrictCode":"14"},{"CensusSubdivisionName":"Blueberry River 205","Type":"Indian reserve","RegionalDistrictCode":"28"},{"CensusSubdivisionName":"Chetwynd","Type":"District municipality","RegionalDistrictCode":"28"},{"CensusSubdivisionName":"Dawson Creek","Type":"City","RegionalDistrictCode":"28"},{"CensusSubdivisionName":"Doig River 206","Type":"Indian reserve","RegionalDistrictCode":"28"},{"CensusSubdivisionName":"East Moberly Lake 169","Type":"Indian reserve","RegionalDistrictCode":"28"},{"CensusSubdivisionName":"Finlay River 6","Type":"Indian reserve","RegionalDistrictCode":"28"},{"CensusSubdivisionName":"Fort St. John","Type":"City","RegionalDistrictCode":"28"},{"CensusSubdivisionName":"Fort Ware 1","Type":"Indian reserve","RegionalDistrictCode":"28"},{"CensusSubdivisionName":"Halfway River 168","Type":"Indian reserve","RegionalDistrictCode":"28"},{"CensusSubdivisionName":"Hudson''s Hope","Type":"District municipality","RegionalDistrictCode":"28"},{"CensusSubdivisionName":"Ingenika Point","Type":"Indian settlement","RegionalDistrictCode":"28"},{"CensusSubdivisionName":"Mesilinka 7","Type":"Indian reserve","RegionalDistrictCode":"28"},{"CensusSubdivisionName":"Peace River B","Type":"Regional district electoral area","RegionalDistrictCode":"28"},{"CensusSubdivisionName":"Peace River C","Type":"Regional district electoral area","RegionalDistrictCode":"28"},{"CensusSubdivisionName":"Peace River D","Type":"Regional district electoral area","RegionalDistrictCode":"28"},{"CensusSubdivisionName":"Peace River E","Type":"Regional district electoral area","RegionalDistrictCode":"28"},{"CensusSubdivisionName":"Pouce Coupe","Type":"Village","RegionalDistrictCode":"28"},{"CensusSubdivisionName":"Taylor","Type":"District municipality","RegionalDistrictCode":"28"},{"CensusSubdivisionName":"Tumbler Ridge","Type":"District municipality","RegionalDistrictCode":"28"},{"CensusSubdivisionName":"West Moberly Lake 168A","Type":"Indian reserve","RegionalDistrictCode":"28"},{"CensusSubdivisionName":"Powell River","Type":"City","RegionalDistrictCode":"7"},{"CensusSubdivisionName":"qathet A","Type":"Regional district electoral area","RegionalDistrictCode":"7"},{"CensusSubdivisionName":"qathet B","Type":"Regional district electoral area","RegionalDistrictCode":"7"},{"CensusSubdivisionName":"qathet C","Type":"Regional district electoral area","RegionalDistrictCode":"7"},{"CensusSubdivisionName":"qathet D","Type":"Regional district electoral area","RegionalDistrictCode":"7"},{"CensusSubdivisionName":"qathet E","Type":"Regional district electoral area","RegionalDistrictCode":"7"},{"CensusSubdivisionName":"Sechelt (Part) - qathet","Type":"Indian government district","RegionalDistrictCode":"7"},{"CensusSubdivisionName":"Sliammon 1","Type":"Tla''amin Lands","RegionalDistrictCode":"7"},{"CensusSubdivisionName":"Dolphin Island 1","Type":"Indian reserve","RegionalDistrictCode":"24"},{"CensusSubdivisionName":"Kulkayu (Hartley Bay) 4","Type":"Indian reserve","RegionalDistrictCode":"24"},{"CensusSubdivisionName":"Kulkayu (Hartley Bay) 4A","Type":"Indian reserve","RegionalDistrictCode":"24"},{"CensusSubdivisionName":"Lax Kw''alaams 1","Type":"Indian reserve","RegionalDistrictCode":"24"},{"CensusSubdivisionName":"Masset","Type":"Village","RegionalDistrictCode":"24"},{"CensusSubdivisionName":"Masset 1","Type":"Indian reserve","RegionalDistrictCode":"24"},{"CensusSubdivisionName":"North Coast A","Type":"Regional district electoral area","RegionalDistrictCode":"24"},{"CensusSubdivisionName":"North Coast C","Type":"Regional district electoral area","RegionalDistrictCode":"24"},{"CensusSubdivisionName":"North Coast D","Type":"Regional district electoral area","RegionalDistrictCode":"24"},{"CensusSubdivisionName":"North Coast E","Type":"Regional district electoral area","RegionalDistrictCode":"24"},{"CensusSubdivisionName":"Port Clements","Type":"Village","RegionalDistrictCode":"24"},{"CensusSubdivisionName":"Port Edward","Type":"District municipality","RegionalDistrictCode":"24"},{"CensusSubdivisionName":"Prince Rupert","Type":"City","RegionalDistrictCode":"24"},{"CensusSubdivisionName":"Queen Charlotte","Type":"Village","RegionalDistrictCode":"24"},{"CensusSubdivisionName":"S1/2 Tsimpsean 2","Type":"Indian reserve","RegionalDistrictCode":"24"},{"CensusSubdivisionName":"Skidegate 1","Type":"Indian reserve","RegionalDistrictCode":"24"},{"CensusSubdivisionName":"Tlaa Gaa Aawtlaas 28","Type":"Indian reserve","RegionalDistrictCode":"24"},{"CensusSubdivisionName":"Bridge River 1","Type":"Indian reserve","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Cayoosh Creek 1","Type":"Indian reserve","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Cheakamus 11","Type":"Indian reserve","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Chilhil 6","Type":"Indian reserve","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Fountain 1","Type":"Indian reserve","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Fountain 10","Type":"Indian reserve","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Fountain 11","Type":"Indian reserve","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Fountain 12","Type":"Indian reserve","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Fountain 1B","Type":"Indian reserve","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Fountain 1D","Type":"Indian reserve","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Fountain 3","Type":"Indian reserve","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Fountain 3A","Type":"Indian reserve","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Fountain Creek 8","Type":"Indian reserve","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Kowtain 17","Type":"Indian reserve","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Lillooet","Type":"District municipality","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Lillooet 1","Type":"Indian reserve","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"McCartney''s Flat 4","Type":"Indian reserve","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Mission 5","Type":"Indian reserve","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Mount Currie","Type":"Indian reserve","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Necait 6","Type":"Indian reserve","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Nequatque","Type":"Indian reserve","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Nesikep 6","Type":"Indian reserve","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Nesuch 3","Type":"Indian reserve","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Pashilqua 2","Type":"Indian reserve","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Pavilion 1","Type":"Indian reserve","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Pemberton","Type":"Village","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Seaichem 16","Type":"Indian reserve","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Seton Lake 5","Type":"Indian reserve","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Slosh 1","Type":"Indian reserve","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Squamish","Type":"District municipality","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Squamish-Lillooet A","Type":"Regional district electoral area","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Squamish-Lillooet B","Type":"Regional district electoral area","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Squamish-Lillooet C","Type":"Regional district electoral area","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Squamish-Lillooet D","Type":"Regional district electoral area","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Stawamus 24","Type":"Indian reserve","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Towinock 2","Type":"Indian reserve","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Waiwakum 14","Type":"Indian reserve","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Whistler","Type":"District municipality","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Yekwaupsum 18","Type":"Indian reserve","RegionalDistrictCode":"13"},{"CensusSubdivisionName":"Dease River 1","Type":"Indian reserve","RegionalDistrictCode":"27"},{"CensusSubdivisionName":"Five Mile Point 3","Type":"Indian reserve","RegionalDistrictCode":"27"},{"CensusSubdivisionName":"Good Hope Lake","Type":"Indian settlement","RegionalDistrictCode":"27"},{"CensusSubdivisionName":"Liard River 3","Type":"Indian reserve","RegionalDistrictCode":"27"},{"CensusSubdivisionName":"Lower Post","Type":"Indian settlement","RegionalDistrictCode":"27"},{"CensusSubdivisionName":"Stikine Region","Type":"Regional district electoral area","RegionalDistrictCode":"27"},{"CensusSubdivisionName":"Unnamed 10","Type":"Indian reserve","RegionalDistrictCode":"27"},{"CensusSubdivisionName":"Ahaminaquus 12","Type":"Indian reserve","RegionalDistrictCode":"5"},{"CensusSubdivisionName":"Campbell River","Type":"City","RegionalDistrictCode":"5"},{"CensusSubdivisionName":"Campbell River 11","Type":"Indian reserve","RegionalDistrictCode":"5"},{"CensusSubdivisionName":"Cape Mudge 10","Type":"Indian reserve","RegionalDistrictCode":"5"},{"CensusSubdivisionName":"Chenahkint 12","Type":"Indian reserve","RegionalDistrictCode":"5"},{"CensusSubdivisionName":"Ehatis 11","Type":"Indian reserve","RegionalDistrictCode":"5"},{"CensusSubdivisionName":"Gold River","Type":"Village","RegionalDistrictCode":"5"},{"CensusSubdivisionName":"Homalco 9","Type":"Indian reserve","RegionalDistrictCode":"5"},{"CensusSubdivisionName":"Houpsitas 6","Type":"Indian reserve","RegionalDistrictCode":"5"},{"CensusSubdivisionName":"Nenagwas 12","Type":"Indian reserve","RegionalDistrictCode":"5"},{"CensusSubdivisionName":"Oclucje 7","Type":"Indian reserve","RegionalDistrictCode":"5"},{"CensusSubdivisionName":"Quinsam 12","Type":"Indian reserve","RegionalDistrictCode":"5"},{"CensusSubdivisionName":"Sayward","Type":"Village","RegionalDistrictCode":"5"},{"CensusSubdivisionName":"Squirrel Cove 8","Type":"Indian reserve","RegionalDistrictCode":"5"},{"CensusSubdivisionName":"Strathcona A","Type":"Regional district electoral area","RegionalDistrictCode":"5"},{"CensusSubdivisionName":"Strathcona B","Type":"Regional district electoral area","RegionalDistrictCode":"5"},{"CensusSubdivisionName":"Strathcona C","Type":"Regional district electoral area","RegionalDistrictCode":"5"},{"CensusSubdivisionName":"Strathcona D (Oyster Bay - Buttle Lake)","Type":"Regional district electoral area","RegionalDistrictCode":"5"},{"CensusSubdivisionName":"Tahsis","Type":"Village","RegionalDistrictCode":"5"},{"CensusSubdivisionName":"Tork 7","Type":"Indian reserve","RegionalDistrictCode":"5"},{"CensusSubdivisionName":"Tsa Xana 18","Type":"Indian reserve","RegionalDistrictCode":"5"},{"CensusSubdivisionName":"Village Island 1","Type":"Indian reserve","RegionalDistrictCode":"5"},{"CensusSubdivisionName":"Yuquot 1","Type":"Indian reserve","RegionalDistrictCode":"5"},{"CensusSubdivisionName":"Zeballos","Type":"Village","RegionalDistrictCode":"5"},{"CensusSubdivisionName":"Chekwelp 26","Type":"Indian reserve","RegionalDistrictCode":"12"},{"CensusSubdivisionName":"Gibsons","Type":"Town","RegionalDistrictCode":"12"},{"CensusSubdivisionName":"Sechelt","Type":"District municipality","RegionalDistrictCode":"12"},{"CensusSubdivisionName":"Sechelt (Part) - Sunshine Coast","Type":"Indian government district","RegionalDistrictCode":"12"},{"CensusSubdivisionName":"Sunshine Coast A","Type":"Regional district electoral area","RegionalDistrictCode":"12"},{"CensusSubdivisionName":"Sunshine Coast B","Type":"Regional district electoral area","RegionalDistrictCode":"12"},{"CensusSubdivisionName":"Sunshine Coast D","Type":"Regional district electoral area","RegionalDistrictCode":"12"},{"CensusSubdivisionName":"Sunshine Coast E","Type":"Regional district electoral area","RegionalDistrictCode":"12"},{"CensusSubdivisionName":"Sunshine Coast F","Type":"Regional district electoral area","RegionalDistrictCode":"12"},{"CensusSubdivisionName":"105 Mile Post 2","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Ashcroft","Type":"Village","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Ashcroft 4","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Barriere","Type":"District municipality","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Basque 18","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Bonaparte 3","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Boothroyd 8A","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Cache Creek","Type":"Village","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Canoe Creek 1","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Canoe Creek 2","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Chase","Type":"Village","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Chuchhraischin","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Clearwater","Type":"District municipality","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Clinton","Type":"Village","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Coldwater 1","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Douglas Lake 3","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Entlqwekkinh 19","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Halhalaeden","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Hamilton Creek 2","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"High Bar 1","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Inkluckcheen","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Inklyuhkinatko 2","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Joeyaska 2","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Kamloops","Type":"City","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Kamloops 1","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Kanaka Bar","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Kitzowit 20","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Klahkamich 17","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Kleetlekut 22","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Klickkumcheen 18","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Kloklowuck 7","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Kumcheen 1","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Logan Lake","Type":"District municipality","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Louis Creek 4","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Lower Hat Creek 2","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Lytton","Type":"Village","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Lytton 4A","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Lytton 4E","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Lytton 9A","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Lytton 9B","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Marble Canyon 3","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Merritt","Type":"City","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Nekalliston 2","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Nekliptum 1","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Neskonlith","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Nickel Palm 4","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Nickeyeah 25","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Nicola Lake 1","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Nicola Mameet 1","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Nicomen 1","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Nkaih 10","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Nohomeen 23","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Nooaitch 10","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"North Thompson 1","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Nuuautin 2","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Oregon Jack Creek 5","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Papyum 27","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Paska Island 3","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Paul''s Basin 2","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Pemynoos 9","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Peq-Paq 22","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Sahhaltkum 4","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Seah 5","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Shackan 11","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Shawniken 4B","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Siska Flat","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Skeetchestn","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Skuppah 2A","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Skuppah 4","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Skwayaynope 26","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Spences Bridge","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Spintlum Flat 3","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Squaam 2","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Stequmwhulpa 5","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Stryen 9","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Sun Peaks Mountain","Type":"Village","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Thompson-Nicola A (Wells Gray Country)","Type":"Regional district electoral area","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Thompson-Nicola B (Thompson Headwaters)","Type":"Regional district electoral area","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Thompson-Nicola E (Bonaparte Plateau)","Type":"Regional district electoral area","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Thompson-Nicola I (Blue Sky Country)","Type":"Regional district electoral area","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Thompson-Nicola J (Copper Desert Country)","Type":"Regional district electoral area","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Thompson-Nicola L (Grasslands)","Type":"Regional district electoral area","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Thompson-Nicola M (Beautiful Nicola Valley - North)","Type":"Regional district electoral area","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Thompson-Nicola N (Beautiful Nicola Valley - South)","Type":"Regional district electoral area","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Thompson-Nicola O (Lower North Thompson)","Type":"Regional district electoral area","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Thompson-Nicola P (Rivers and the Peaks)","Type":"Regional district electoral area","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Tsaukan 12","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Twoyqhalsht 16","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Upper Hat Creek 1","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Upper Nepa 6","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Whispering Pines 4","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Yawaucht 11","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Zacht 5","Type":"Indian reserve","RegionalDistrictCode":"15"},{"CensusSubdivisionName":"Zoht 4","Type":"Indian reserve","RegionalDistrictCode":"15"}]'; -BEGIN - -- Insert into "CensusSubdivisions" table - INSERT INTO public."CensusSubdivisions" - ("Id", "CensusSubdivisionName", "Type","RegionalDistrictCode", "ExtraProperties", "ConcurrencyStamp", "CreationTime") - SELECT - gen_random_uuid(), - data->>'CensusSubdivisionName', - data->>'Type', - data->>'RegionalDistrictCode', - '', - '', - pg_catalog.now() - FROM jsonb_array_elements(json_data::jsonb) AS data; - -END $$; diff --git a/database/scripts/unitydb-communities-script.sql b/database/scripts/unitydb-communities-script.sql index 617be7f972..0f0d27b3ab 100644 --- a/database/scripts/unitydb-communities-script.sql +++ b/database/scripts/unitydb-communities-script.sql @@ -1,7 +1,10 @@ -DO $$ +DO $$ DECLARE - json_data jsonb := '[{"Name":"Ahahswinis 1","Type":"Indian reserve","RegionalDistrictCode":"4"},{"Name":"Alberni 2","Type":"Indian reserve","RegionalDistrictCode":"4"},{"Name":"Alberni-Clayoquot A","Type":"Regional district electoral area","RegionalDistrictCode":"4"},{"Name":"Alberni-Clayoquot B","Type":"Regional district electoral area","RegionalDistrictCode":"4"},{"Name":"Alberni-Clayoquot C","Type":"Regional district electoral area","RegionalDistrictCode":"4"},{"Name":"Alberni-Clayoquot D","Type":"Regional district electoral area","RegionalDistrictCode":"4"},{"Name":"Alberni-Clayoquot E","Type":"Regional district electoral area","RegionalDistrictCode":"4"},{"Name":"Alberni-Clayoquot F","Type":"Regional district electoral area","RegionalDistrictCode":"4"},{"Name":"Anacla 12","Type":"Indian reserve","RegionalDistrictCode":"4"},{"Name":"Clakamucus 2","Type":"Indian reserve","RegionalDistrictCode":"4"},{"Name":"Elhlateese 2","Type":"Indian reserve","RegionalDistrictCode":"4"},{"Name":"Esowista 3","Type":"Indian reserve","RegionalDistrictCode":"4"},{"Name":"Hesquiat 1","Type":"Indian reserve","RegionalDistrictCode":"4"},{"Name":"Ittatsoo 1","Type":"Indian reserve","RegionalDistrictCode":"4"},{"Name":"Klehkoot 2","Type":"Indian reserve","RegionalDistrictCode":"4"},{"Name":"Macoah 1","Type":"Indian reserve","RegionalDistrictCode":"4"},{"Name":"Marktosis 15","Type":"Indian reserve","RegionalDistrictCode":"4"},{"Name":"Numukamis 1","Type":"Indian reserve","RegionalDistrictCode":"4"},{"Name":"Opitsat 1","Type":"Indian reserve","RegionalDistrictCode":"4"},{"Name":"Port Alberni","Type":"City","RegionalDistrictCode":"4"},{"Name":"Refuge Cove 6","Type":"Indian reserve","RegionalDistrictCode":"4"},{"Name":"Sachsa 4","Type":"Indian reserve","RegionalDistrictCode":"4"},{"Name":"Tin Wis 11","Type":"Indian reserve","RegionalDistrictCode":"4"},{"Name":"Tofino","Type":"District municipality","RegionalDistrictCode":"4"},{"Name":"Tsahaheh 1","Type":"Indian reserve","RegionalDistrictCode":"4"},{"Name":"Ucluelet","Type":"District municipality","RegionalDistrictCode":"4"},{"Name":"Babine 16","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Babine 25","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Babine 6","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Babine Lake 21B","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Binche 2","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Bulkley-Nechako A","Type":"Regional district electoral area","RegionalDistrictCode":"26"},{"Name":"Bulkley-Nechako B","Type":"Regional district electoral area","RegionalDistrictCode":"26"},{"Name":"Bulkley-Nechako C","Type":"Regional district electoral area","RegionalDistrictCode":"26"},{"Name":"Bulkley-Nechako D","Type":"Regional district electoral area","RegionalDistrictCode":"26"},{"Name":"Bulkley-Nechako E","Type":"Regional district electoral area","RegionalDistrictCode":"26"},{"Name":"Bulkley-Nechako F","Type":"Regional district electoral area","RegionalDistrictCode":"26"},{"Name":"Bulkley-Nechako G","Type":"Regional district electoral area","RegionalDistrictCode":"26"},{"Name":"Burns Lake","Type":"Village","RegionalDistrictCode":"26"},{"Name":"Burns Lake 18","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Cheslatta 1","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Duncan Lake 2","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Dzitline Lee 9","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Fort St. James","Type":"District municipality","RegionalDistrictCode":"26"},{"Name":"Francois Lake 7","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Fraser Lake","Type":"Village","RegionalDistrictCode":"26"},{"Name":"Granisle","Type":"Village","RegionalDistrictCode":"26"},{"Name":"Houston","Type":"District municipality","RegionalDistrictCode":"26"},{"Name":"Jean Baptiste 28","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Laketown 3","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Mission Lands 17","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Nak''azdli","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Nautley (Fort Fraser) 1","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Nedoats 11","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Noonla 6","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"North Tacla Lake","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Omineca 1","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Palling 1","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Poison Creek 17A","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Seaspunkut 4","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Skins Lake 16A","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Skins Lake 16B","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Smithers","Type":"Town","RegionalDistrictCode":"26"},{"Name":"Sowchea 3","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Stellaquo (Stella) 1","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Stony Creek 1","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Tache 1","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Tacla Lake (Ferry Landing) 9","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Tadinlay 15","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Tatla West 11","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Tatla''t East 2","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Telkwa","Type":"Village","RegionalDistrictCode":"26"},{"Name":"Uncha Lake 13A","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Vanderhoof","Type":"District municipality","RegionalDistrictCode":"26"},{"Name":"Williams Prairie Meadow 1A","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Woyenne 27","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Ye Koo Che 3","Type":"Indian reserve","RegionalDistrictCode":"26"},{"Name":"Becher Bay 1","Type":"Indian reserve","RegionalDistrictCode":"1"},{"Name":"Central Saanich","Type":"District municipality","RegionalDistrictCode":"1"},{"Name":"Cole Bay 3","Type":"Indian reserve","RegionalDistrictCode":"1"},{"Name":"Colwood","Type":"City","RegionalDistrictCode":"1"},{"Name":"East Saanich 2","Type":"Indian reserve","RegionalDistrictCode":"1"},{"Name":"Esquimalt - District municipality","Type":"District municipality","RegionalDistrictCode":"1"},{"Name":"Esquimalt - Indian reserve","Type":"Indian reserve","RegionalDistrictCode":"1"},{"Name":"Galiano Island 9","Type":"Indian reserve","RegionalDistrictCode":"1"},{"Name":"Gordon River 2","Type":"Indian reserve","RegionalDistrictCode":"1"},{"Name":"Highlands","Type":"District municipality","RegionalDistrictCode":"1"},{"Name":"Juan de Fuca (Part 1)","Type":"Regional district electoral area","RegionalDistrictCode":"1"},{"Name":"Juan de Fuca (Part 2)","Type":"Regional district electoral area","RegionalDistrictCode":"1"},{"Name":"Langford","Type":"City","RegionalDistrictCode":"1"},{"Name":"Metchosin","Type":"District municipality","RegionalDistrictCode":"1"},{"Name":"New Songhees 1A","Type":"Indian reserve","RegionalDistrictCode":"1"},{"Name":"North Saanich","Type":"District municipality","RegionalDistrictCode":"1"},{"Name":"Oak Bay","Type":"District municipality","RegionalDistrictCode":"1"},{"Name":"Saanich","Type":"District municipality","RegionalDistrictCode":"1"},{"Name":"Saltspring Island","Type":"Regional district electoral area","RegionalDistrictCode":"1"},{"Name":"Sidney","Type":"Town","RegionalDistrictCode":"1"},{"Name":"Sooke","Type":"District municipality","RegionalDistrictCode":"1"},{"Name":"South Saanich 1","Type":"Indian reserve","RegionalDistrictCode":"1"},{"Name":"Southern Gulf Islands","Type":"Regional district electoral area","RegionalDistrictCode":"1"},{"Name":"T''Sou-ke","Type":"Indian reserve","RegionalDistrictCode":"1"},{"Name":"Union Bay 4","Type":"Indian reserve","RegionalDistrictCode":"1"},{"Name":"Victoria","Type":"City","RegionalDistrictCode":"1"},{"Name":"View Royal","Type":"Town","RegionalDistrictCode":"1"},{"Name":"Agats Meadow 8","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Alexandria","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Alexis Creek 14","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Alexis Creek 16","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Alexis Creek 21","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Alexis Creek 34","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Alkali Lake 1","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Alkali Lake 4A","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Anahim''s Flat 1","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Anahim''s Meadow","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Baezaeko River 27","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Baptiste Meadow 2","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Betty Creek 18","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Canim Lake 1","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Canim Lake 2","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Canim Lake 4","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Canoe Creek 3","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Cariboo A","Type":"Regional district electoral area","RegionalDistrictCode":"22"},{"Name":"Cariboo B","Type":"Regional district electoral area","RegionalDistrictCode":"22"},{"Name":"Cariboo C","Type":"Regional district electoral area","RegionalDistrictCode":"22"},{"Name":"Cariboo D","Type":"Regional district electoral area","RegionalDistrictCode":"22"},{"Name":"Cariboo E","Type":"Regional district electoral area","RegionalDistrictCode":"22"},{"Name":"Cariboo F","Type":"Regional district electoral area","RegionalDistrictCode":"22"},{"Name":"Cariboo G","Type":"Regional district electoral area","RegionalDistrictCode":"22"},{"Name":"Cariboo H","Type":"Regional district electoral area","RegionalDistrictCode":"22"},{"Name":"Cariboo I","Type":"Regional district electoral area","RegionalDistrictCode":"22"},{"Name":"Cariboo J","Type":"Regional district electoral area","RegionalDistrictCode":"22"},{"Name":"Cariboo K","Type":"Regional district electoral area","RegionalDistrictCode":"22"},{"Name":"Cariboo L","Type":"Regional district electoral area","RegionalDistrictCode":"22"},{"Name":"Charley Boy''s Meadow 3","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Chilco Lake 1","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Chilco Lake 1A","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Coglistiko River 29","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Deep Creek 2","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Dog Creek 1","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Dog Creek 2","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Dragon Lake 3","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Euchinico Creek 17","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Fishtrap 19","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Garden","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Johny Sticks 2","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Kluskus 1","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Lezbye 6","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Little Springs","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Lohbiee 3","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Michel Gardens 36","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Nazco 20","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"One Hundred Mile House","Type":"District municipality","RegionalDistrictCode":"22"},{"Name":"Puntzi Lake 2","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Quesnel","Type":"City","RegionalDistrictCode":"22"},{"Name":"Quesnel 1","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Redstone Flat 1","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Salmon River Meadow 7","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Sandy Harry 4","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Soda Creek 1","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Squinas 2","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Stone 1","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Sundayman''s Meadow 3","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Swan Lake 3","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Tanakut 4","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Thomas Squinas Ranch 2A","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Toosey 1","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Towdystan Lake 3","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Trout Lake Alec 16","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Ulkatcho 13","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Ulkatcho 14A","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Wells","Type":"District municipality","RegionalDistrictCode":"22"},{"Name":"Williams Lake","Type":"City","RegionalDistrictCode":"22"},{"Name":"Williams Lake 1","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Windy Mouth 7","Type":"Indian reserve","RegionalDistrictCode":"22"},{"Name":"Bella Bella 1","Type":"Indian reserve","RegionalDistrictCode":"9"},{"Name":"Bella Coola 1","Type":"Indian reserve","RegionalDistrictCode":"9"},{"Name":"Central Coast A","Type":"Regional district electoral area","RegionalDistrictCode":"9"},{"Name":"Central Coast C","Type":"Regional district electoral area","RegionalDistrictCode":"9"},{"Name":"Central Coast D","Type":"Regional district electoral area","RegionalDistrictCode":"9"},{"Name":"Central Coast E","Type":"Regional district electoral area","RegionalDistrictCode":"9"},{"Name":"Katit 1","Type":"Indian reserve","RegionalDistrictCode":"9"},{"Name":"Castlegar","Type":"City","RegionalDistrictCode":"20"},{"Name":"Central Kootenay A","Type":"Regional district electoral area","RegionalDistrictCode":"20"},{"Name":"Central Kootenay B","Type":"Regional district electoral area","RegionalDistrictCode":"20"},{"Name":"Central Kootenay C","Type":"Regional district electoral area","RegionalDistrictCode":"20"},{"Name":"Central Kootenay D","Type":"Regional district electoral area","RegionalDistrictCode":"20"},{"Name":"Central Kootenay E","Type":"Regional district electoral area","RegionalDistrictCode":"20"},{"Name":"Central Kootenay F","Type":"Regional district electoral area","RegionalDistrictCode":"20"},{"Name":"Central Kootenay G","Type":"Regional district electoral area","RegionalDistrictCode":"20"},{"Name":"Central Kootenay H","Type":"Regional district electoral area","RegionalDistrictCode":"20"},{"Name":"Central Kootenay I","Type":"Regional district electoral area","RegionalDistrictCode":"20"},{"Name":"Central Kootenay J","Type":"Regional district electoral area","RegionalDistrictCode":"20"},{"Name":"Central Kootenay K","Type":"Regional district electoral area","RegionalDistrictCode":"20"},{"Name":"Creston","Type":"Town","RegionalDistrictCode":"20"},{"Name":"Creston 1","Type":"Indian reserve","RegionalDistrictCode":"20"},{"Name":"Kaslo","Type":"Village","RegionalDistrictCode":"20"},{"Name":"Nakusp","Type":"Village","RegionalDistrictCode":"20"},{"Name":"Nelson","Type":"City","RegionalDistrictCode":"20"},{"Name":"New Denver","Type":"Village","RegionalDistrictCode":"20"},{"Name":"Salmo","Type":"Village","RegionalDistrictCode":"20"},{"Name":"Silverton","Type":"Village","RegionalDistrictCode":"20"},{"Name":"Slocan","Type":"Village","RegionalDistrictCode":"20"},{"Name":"Central Okanagan","Type":"Regional district electoral area","RegionalDistrictCode":"16"},{"Name":"Central Okanagan West","Type":"Regional district electoral area","RegionalDistrictCode":"16"},{"Name":"Duck Lake 7","Type":"Indian reserve","RegionalDistrictCode":"16"},{"Name":"Kelowna","Type":"City","RegionalDistrictCode":"16"},{"Name":"Lake Country","Type":"District municipality","RegionalDistrictCode":"16"},{"Name":"Peachland","Type":"District municipality","RegionalDistrictCode":"16"},{"Name":"Tsinstikeptum 10","Type":"Indian reserve","RegionalDistrictCode":"16"},{"Name":"Tsinstikeptum 9","Type":"Indian reserve","RegionalDistrictCode":"16"},{"Name":"West Kelowna","Type":"City","RegionalDistrictCode":"16"},{"Name":"Chum Creek 2","Type":"Indian reserve","RegionalDistrictCode":"18"},{"Name":"Columbia-Shuswap A","Type":"Regional district electoral area","RegionalDistrictCode":"18"},{"Name":"Columbia-Shuswap B","Type":"Regional district electoral area","RegionalDistrictCode":"18"},{"Name":"Columbia-Shuswap C","Type":"Regional district electoral area","RegionalDistrictCode":"18"},{"Name":"Columbia-Shuswap D","Type":"Regional district electoral area","RegionalDistrictCode":"18"},{"Name":"Columbia-Shuswap E","Type":"Regional district electoral area","RegionalDistrictCode":"18"},{"Name":"Columbia-Shuswap F","Type":"Regional district electoral area","RegionalDistrictCode":"18"},{"Name":"Golden","Type":"Town","RegionalDistrictCode":"18"},{"Name":"Hustalen 1","Type":"Indian reserve","RegionalDistrictCode":"18"},{"Name":"North Bay 5","Type":"Indian reserve","RegionalDistrictCode":"18"},{"Name":"Okanagan (Part) 1 - Thompson/Okanagan","Type":"Indian reserve","RegionalDistrictCode":"18"},{"Name":"Quaaout 1","Type":"Indian reserve","RegionalDistrictCode":"18"},{"Name":"Revelstoke","Type":"City","RegionalDistrictCode":"18"},{"Name":"Salmon Arm","Type":"City","RegionalDistrictCode":"18"},{"Name":"Salmon River 1","Type":"Indian reserve","RegionalDistrictCode":"18"},{"Name":"Scotch Creek 4","Type":"Indian reserve","RegionalDistrictCode":"18"},{"Name":"Sicamous","Type":"District municipality","RegionalDistrictCode":"18"},{"Name":"Switsemalph","Type":"Indian reserve","RegionalDistrictCode":"18"},{"Name":"Switsemalph 3","Type":"Indian reserve","RegionalDistrictCode":"18"},{"Name":"Comox","Type":"Town","RegionalDistrictCode":"6"},{"Name":"Comox 1","Type":"Indian reserve","RegionalDistrictCode":"6"},{"Name":"Comox Valley A","Type":"Regional district electoral area","RegionalDistrictCode":"6"},{"Name":"Comox Valley B (Lazo North)","Type":"Regional district electoral area","RegionalDistrictCode":"6"},{"Name":"Comox Valley C (Puntledge - Black Creek)","Type":"Regional district electoral area","RegionalDistrictCode":"6"},{"Name":"Courtenay","Type":"City","RegionalDistrictCode":"6"},{"Name":"Cumberland","Type":"Village","RegionalDistrictCode":"6"},{"Name":"Pentledge 2","Type":"Indian reserve","RegionalDistrictCode":"6"},{"Name":"Chemainus 13","Type":"Indian reserve","RegionalDistrictCode":"2"},{"Name":"Cowichan","Type":"Indian reserve","RegionalDistrictCode":"2"},{"Name":"Cowichan Lake","Type":"Indian reserve","RegionalDistrictCode":"2"},{"Name":"Cowichan Valley A","Type":"Regional district electoral area","RegionalDistrictCode":"2"},{"Name":"Cowichan Valley B","Type":"Regional district electoral area","RegionalDistrictCode":"2"},{"Name":"Cowichan Valley C","Type":"Regional district electoral area","RegionalDistrictCode":"2"},{"Name":"Cowichan Valley D","Type":"Regional district electoral area","RegionalDistrictCode":"2"},{"Name":"Cowichan Valley E","Type":"Regional district electoral area","RegionalDistrictCode":"2"},{"Name":"Cowichan Valley F","Type":"Regional district electoral area","RegionalDistrictCode":"2"},{"Name":"Cowichan Valley G","Type":"Regional district electoral area","RegionalDistrictCode":"2"},{"Name":"Cowichan Valley H","Type":"Regional district electoral area","RegionalDistrictCode":"2"},{"Name":"Cowichan Valley I","Type":"Regional district electoral area","RegionalDistrictCode":"2"},{"Name":"Duncan","Type":"City","RegionalDistrictCode":"2"},{"Name":"Est-Patrolas 4","Type":"Indian reserve","RegionalDistrictCode":"2"},{"Name":"Halalt 2","Type":"Indian reserve","RegionalDistrictCode":"2"},{"Name":"Kil-pah-las 3","Type":"Indian reserve","RegionalDistrictCode":"2"},{"Name":"Ladysmith","Type":"Town","RegionalDistrictCode":"2"},{"Name":"Lake Cowichan","Type":"Town","RegionalDistrictCode":"2"},{"Name":"Lyacksun 3","Type":"Indian reserve","RegionalDistrictCode":"2"},{"Name":"Malachan 11","Type":"Indian reserve","RegionalDistrictCode":"2"},{"Name":"Malahat 11","Type":"Indian reserve","RegionalDistrictCode":"2"},{"Name":"North Cowichan","Type":"District municipality","RegionalDistrictCode":"2"},{"Name":"Oyster Bay 12","Type":"Indian reserve","RegionalDistrictCode":"2"},{"Name":"Penelakut Island 7","Type":"Indian reserve","RegionalDistrictCode":"2"},{"Name":"Portier Pass 5","Type":"Indian reserve","RegionalDistrictCode":"2"},{"Name":"Shingle Point 4","Type":"Indian reserve","RegionalDistrictCode":"2"},{"Name":"Squaw-Hay-One 11","Type":"Indian reserve","RegionalDistrictCode":"2"},{"Name":"Theik 2","Type":"Indian reserve","RegionalDistrictCode":"2"},{"Name":"Tsussie 6","Type":"Indian reserve","RegionalDistrictCode":"2"},{"Name":"Tzart-Lam 5","Type":"Indian reserve","RegionalDistrictCode":"2"},{"Name":"Canal Flats","Type":"Village","RegionalDistrictCode":"19"},{"Name":"Cassimayooks (Mayook) 5","Type":"Indian reserve","RegionalDistrictCode":"19"},{"Name":"Columbia Lake 3","Type":"Indian reserve","RegionalDistrictCode":"19"},{"Name":"Cranbrook","Type":"City","RegionalDistrictCode":"19"},{"Name":"East Kootenay A","Type":"Regional district electoral area","RegionalDistrictCode":"19"},{"Name":"East Kootenay B","Type":"Regional district electoral area","RegionalDistrictCode":"19"},{"Name":"East Kootenay C","Type":"Regional district electoral area","RegionalDistrictCode":"19"},{"Name":"East Kootenay E","Type":"Regional district electoral area","RegionalDistrictCode":"19"},{"Name":"East Kootenay F","Type":"Regional district electoral area","RegionalDistrictCode":"19"},{"Name":"East Kootenay G","Type":"Regional district electoral area","RegionalDistrictCode":"19"},{"Name":"Elkford","Type":"District municipality","RegionalDistrictCode":"19"},{"Name":"Fernie","Type":"City","RegionalDistrictCode":"19"},{"Name":"Invermere","Type":"District municipality","RegionalDistrictCode":"19"},{"Name":"Isidore''s Ranch 4","Type":"Indian reserve","RegionalDistrictCode":"19"},{"Name":"Kimberley","Type":"City","RegionalDistrictCode":"19"},{"Name":"Kootenay 1","Type":"Indian reserve","RegionalDistrictCode":"19"},{"Name":"Radium Hot Springs","Type":"Village","RegionalDistrictCode":"19"},{"Name":"Shuswap","Type":"Indian reserve","RegionalDistrictCode":"19"},{"Name":"Sparwood","Type":"District municipality","RegionalDistrictCode":"19"},{"Name":"St. Mary''s","Type":"Indian reserve","RegionalDistrictCode":"19"},{"Name":"Tobacco Plains 2","Type":"Indian reserve","RegionalDistrictCode":"19"},{"Name":"Abbotsford","Type":"City","RegionalDistrictCode":"10"},{"Name":"Aitchelitch 9","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Albert Flat 5","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Boothroyd 13","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Boston Bar 1A","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Bucktum 4","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Chawathil 4","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Cheam 1","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Chehalis 5","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Chilliwack","Type":"City","RegionalDistrictCode":"10"},{"Name":"Douglas 8","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Fraser Valley A","Type":"Regional district electoral area","RegionalDistrictCode":"10"},{"Name":"Fraser Valley B","Type":"Regional district electoral area","RegionalDistrictCode":"10"},{"Name":"Fraser Valley C","Type":"Regional district electoral area","RegionalDistrictCode":"10"},{"Name":"Fraser Valley D","Type":"Regional district electoral area","RegionalDistrictCode":"10"},{"Name":"Fraser Valley E","Type":"Regional district electoral area","RegionalDistrictCode":"10"},{"Name":"Fraser Valley F","Type":"Regional district electoral area","RegionalDistrictCode":"10"},{"Name":"Fraser Valley G","Type":"Regional district electoral area","RegionalDistrictCode":"10"},{"Name":"Fraser Valley H","Type":"Regional district electoral area","RegionalDistrictCode":"10"},{"Name":"Harrison Hot Springs","Type":"Village","RegionalDistrictCode":"10"},{"Name":"Holachten 8","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Hope","Type":"District municipality","RegionalDistrictCode":"10"},{"Name":"Inkahtsaph 6","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Kahmoose 4","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Kent","Type":"District municipality","RegionalDistrictCode":"10"},{"Name":"Kopchitchin 2","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Kwawkwawapilt 6","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Lakahahmen 11","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Langley 2","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Lukseetsissum 9","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Matsqui Main 2","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Mission","Type":"District municipality","RegionalDistrictCode":"10"},{"Name":"Ohamil 1","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Paqulh","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Peters 1","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Popkum 1","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Puckatholetchin 11","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Q''alatkú7em","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Ruby Creek 2","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Sachteen","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Saddle Rock 9","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Schelowat 1","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Schkam 2","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Scowlitz 1","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Seabird Island","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Skawahlook 1","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Skookumchuck 4","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Skowkale","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Skwah 4","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Skwali 3","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Skway 5","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Skweahm 10","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Soowahlie 14","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Speyum 3","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Spuzzum 1","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Squawkum Creek 3","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Squiaala","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Stullawheets 8","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Tipella 7","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Tseatah 2","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Tuckkwiowhum 1","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Tzeachten 13","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Upper Sumas 6","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Yakweakwioose 12","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Yale Town 1","Type":"Indian reserve","RegionalDistrictCode":"10"},{"Name":"Fort George 2","Type":"Indian reserve","RegionalDistrictCode":"23"},{"Name":"Fraser-Fort George A","Type":"Regional district electoral area","RegionalDistrictCode":"23"},{"Name":"Fraser-Fort George C","Type":"Regional district electoral area","RegionalDistrictCode":"23"},{"Name":"Fraser-Fort George D","Type":"Regional district electoral area","RegionalDistrictCode":"23"},{"Name":"Fraser-Fort George E","Type":"Regional district electoral area","RegionalDistrictCode":"23"},{"Name":"Fraser-Fort George F","Type":"Regional district electoral area","RegionalDistrictCode":"23"},{"Name":"Fraser-Fort George G","Type":"Regional district electoral area","RegionalDistrictCode":"23"},{"Name":"Fraser-Fort George H","Type":"Regional district electoral area","RegionalDistrictCode":"23"},{"Name":"Mackenzie","Type":"District municipality","RegionalDistrictCode":"23"},{"Name":"McBride","Type":"Village","RegionalDistrictCode":"23"},{"Name":"McLeod Lake 1","Type":"Indian reserve","RegionalDistrictCode":"23"},{"Name":"Prince George","Type":"City","RegionalDistrictCode":"23"},{"Name":"Valemount","Type":"Village","RegionalDistrictCode":"23"},{"Name":"Anmore","Type":"Village","RegionalDistrictCode":"11"},{"Name":"Barnston Island 3","Type":"Indian reserve","RegionalDistrictCode":"11"},{"Name":"Belcarra","Type":"Village","RegionalDistrictCode":"11"},{"Name":"Bowen Island","Type":"Island municipality","RegionalDistrictCode":"11"},{"Name":"Burnaby","Type":"City","RegionalDistrictCode":"11"},{"Name":"Burrard Inlet 3","Type":"Indian reserve","RegionalDistrictCode":"11"},{"Name":"Capilano 5","Type":"Indian reserve","RegionalDistrictCode":"11"},{"Name":"Coquitlam","Type":"City","RegionalDistrictCode":"11"},{"Name":"Coquitlam 1","Type":"Indian reserve","RegionalDistrictCode":"11"},{"Name":"Coquitlam 2","Type":"Indian reserve","RegionalDistrictCode":"11"},{"Name":"Delta","Type":"City","RegionalDistrictCode":"11"},{"Name":"Katzie 1","Type":"Indian reserve","RegionalDistrictCode":"11"},{"Name":"Katzie 2","Type":"Indian reserve","RegionalDistrictCode":"11"},{"Name":"Langley - City","Type":"City","RegionalDistrictCode":"11"},{"Name":"Langley - District municipality","Type":"District municipality","RegionalDistrictCode":"11"},{"Name":"Lions Bay","Type":"Village","RegionalDistrictCode":"11"},{"Name":"Maple Ridge","Type":"City","RegionalDistrictCode":"11"},{"Name":"Matsqui 4","Type":"Indian reserve","RegionalDistrictCode":"11"},{"Name":"McMillan Island 6","Type":"Indian reserve","RegionalDistrictCode":"11"},{"Name":"Metro Vancouver A","Type":"Regional district electoral area","RegionalDistrictCode":"11"},{"Name":"Mission 1","Type":"Indian reserve","RegionalDistrictCode":"11"},{"Name":"Musqueam 2","Type":"Indian reserve","RegionalDistrictCode":"11"},{"Name":"Musqueam 4","Type":"Indian reserve","RegionalDistrictCode":"11"},{"Name":"New Westminster","Type":"City","RegionalDistrictCode":"11"},{"Name":"North Vancouver - City","Type":"City","RegionalDistrictCode":"11"},{"Name":"North Vancouver - District municipality","Type":"District municipality","RegionalDistrictCode":"11"},{"Name":"Pitt Meadows","Type":"City","RegionalDistrictCode":"11"},{"Name":"Port Coquitlam","Type":"City","RegionalDistrictCode":"11"},{"Name":"Port Moody","Type":"City","RegionalDistrictCode":"11"},{"Name":"Richmond","Type":"City","RegionalDistrictCode":"11"},{"Name":"Semiahmoo","Type":"Indian reserve","RegionalDistrictCode":"11"},{"Name":"Seymour Creek 2","Type":"Indian reserve","RegionalDistrictCode":"11"},{"Name":"Surrey","Type":"City","RegionalDistrictCode":"11"},{"Name":"Tsawwassen","Type":"Tsawwassen Lands","RegionalDistrictCode":"11"},{"Name":"Vancouver","Type":"City","RegionalDistrictCode":"11"},{"Name":"West Vancouver","Type":"District municipality","RegionalDistrictCode":"11"},{"Name":"White Rock","Type":"City","RegionalDistrictCode":"11"},{"Name":"Whonnock 1","Type":"Indian reserve","RegionalDistrictCode":"11"},{"Name":"Babine 17","Type":"Indian reserve","RegionalDistrictCode":"25"},{"Name":"Bulkley River 19","Type":"Indian reserve","RegionalDistrictCode":"25"},{"Name":"Coryatsaqua (Moricetown) 2","Type":"Indian reserve","RegionalDistrictCode":"25"},{"Name":"Dease Lake 9","Type":"Indian reserve","RegionalDistrictCode":"25"},{"Name":"Gitanmaax 1","Type":"Indian reserve","RegionalDistrictCode":"25"},{"Name":"Gitanyow 1","Type":"Indian reserve","RegionalDistrictCode":"25"},{"Name":"Gitsegukla 1","Type":"Indian reserve","RegionalDistrictCode":"25"},{"Name":"Gitwangak 1","Type":"Indian reserve","RegionalDistrictCode":"25"},{"Name":"Guhthe Tah 12","Type":"Indian reserve","RegionalDistrictCode":"25"},{"Name":"Hagwilget 1","Type":"Indian reserve","RegionalDistrictCode":"25"},{"Name":"Hazelton","Type":"Village","RegionalDistrictCode":"25"},{"Name":"Iskut 6","Type":"Indian reserve","RegionalDistrictCode":"25"},{"Name":"Kispiox 1","Type":"Indian reserve","RegionalDistrictCode":"25"},{"Name":"Kitamaat 2","Type":"Indian reserve","RegionalDistrictCode":"25"},{"Name":"Kitasoo 1","Type":"Indian reserve","RegionalDistrictCode":"25"},{"Name":"Kitimat","Type":"District municipality","RegionalDistrictCode":"25"},{"Name":"Kitimat-Stikine A","Type":"Regional district electoral area","RegionalDistrictCode":"25"},{"Name":"Kitimat-Stikine B","Type":"Regional district electoral area","RegionalDistrictCode":"25"},{"Name":"Kitimat-Stikine C (Part 1)","Type":"Regional district electoral area","RegionalDistrictCode":"25"},{"Name":"Kitimat-Stikine C (Part 2)","Type":"Regional district electoral area","RegionalDistrictCode":"25"},{"Name":"Kitimat-Stikine D","Type":"Regional district electoral area","RegionalDistrictCode":"25"},{"Name":"Kitimat-Stikine E","Type":"Regional district electoral area","RegionalDistrictCode":"25"},{"Name":"Kitimat-Stikine F","Type":"Regional district electoral area","RegionalDistrictCode":"25"},{"Name":"Kitselas 1","Type":"Indian reserve","RegionalDistrictCode":"25"},{"Name":"Kitsumkaylum 1","Type":"Indian reserve","RegionalDistrictCode":"25"},{"Name":"Kshish 4","Type":"Indian reserve","RegionalDistrictCode":"25"},{"Name":"Kulspai 6","Type":"Indian reserve","RegionalDistrictCode":"25"},{"Name":"Moricetown 1","Type":"Indian reserve","RegionalDistrictCode":"25"},{"Name":"New Hazelton","Type":"District municipality","RegionalDistrictCode":"25"},{"Name":"Nisga''a","Type":"Nisga''a land","RegionalDistrictCode":"25"},{"Name":"Sik-e-dakh 2","Type":"Indian reserve","RegionalDistrictCode":"25"},{"Name":"Stewart","Type":"District municipality","RegionalDistrictCode":"25"},{"Name":"Tahltan 1","Type":"Indian reserve","RegionalDistrictCode":"25"},{"Name":"Telegraph Creek","Type":"Indian reserve","RegionalDistrictCode":"25"},{"Name":"Terrace","Type":"City","RegionalDistrictCode":"25"},{"Name":"Fruitvale","Type":"Village","RegionalDistrictCode":"21"},{"Name":"Grand Forks","Type":"City","RegionalDistrictCode":"21"},{"Name":"Greenwood","Type":"City","RegionalDistrictCode":"21"},{"Name":"Kootenay Boundary A","Type":"Regional district electoral area","RegionalDistrictCode":"21"},{"Name":"Kootenay Boundary B / Lower Columbia-Old-Glory","Type":"Regional district electoral area","RegionalDistrictCode":"21"},{"Name":"Kootenay Boundary C / Christina Lake","Type":"Regional district electoral area","RegionalDistrictCode":"21"},{"Name":"Kootenay Boundary D / Rural Grand Forks","Type":"Regional district electoral area","RegionalDistrictCode":"21"},{"Name":"Kootenay Boundary E / West Boundary","Type":"Regional district electoral area","RegionalDistrictCode":"21"},{"Name":"Midway","Type":"Village","RegionalDistrictCode":"21"},{"Name":"Montrose","Type":"Village","RegionalDistrictCode":"21"},{"Name":"Rossland","Type":"City","RegionalDistrictCode":"21"},{"Name":"Trail","Type":"City","RegionalDistrictCode":"21"},{"Name":"Warfield","Type":"Village","RegionalDistrictCode":"21"},{"Name":"Alert Bay - Indian reserve","Type":"Indian reserve","RegionalDistrictCode":"8"},{"Name":"Alert Bay - Village","Type":"Village","RegionalDistrictCode":"8"},{"Name":"Dead Point 5","Type":"Indian reserve","RegionalDistrictCode":"8"},{"Name":"Fort Rupert 1","Type":"Indian reserve","RegionalDistrictCode":"8"},{"Name":"Gwayasdums 1","Type":"Indian reserve","RegionalDistrictCode":"8"},{"Name":"Hope Island 1","Type":"Indian reserve","RegionalDistrictCode":"8"},{"Name":"Hopetown 10A","Type":"Indian reserve","RegionalDistrictCode":"8"},{"Name":"Kippase 2","Type":"Indian reserve","RegionalDistrictCode":"8"},{"Name":"Mount Waddington A","Type":"Regional district electoral area","RegionalDistrictCode":"8"},{"Name":"Mount Waddington B","Type":"Regional district electoral area","RegionalDistrictCode":"8"},{"Name":"Mount Waddington C","Type":"Regional district electoral area","RegionalDistrictCode":"8"},{"Name":"Mount Waddington D","Type":"Regional district electoral area","RegionalDistrictCode":"8"},{"Name":"Nimpkish 2","Type":"Indian reserve","RegionalDistrictCode":"8"},{"Name":"Port Alice","Type":"Village","RegionalDistrictCode":"8"},{"Name":"Port Hardy","Type":"District municipality","RegionalDistrictCode":"8"},{"Name":"Port McNeill","Type":"Town","RegionalDistrictCode":"8"},{"Name":"Quaee 7","Type":"Indian reserve","RegionalDistrictCode":"8"},{"Name":"Quatsino Subdivision 18","Type":"Indian reserve","RegionalDistrictCode":"8"},{"Name":"Thomas Point 5","Type":"Indian reserve","RegionalDistrictCode":"8"},{"Name":"Thomas Point 5A","Type":"Indian reserve","RegionalDistrictCode":"8"},{"Name":"Tsulquate 4","Type":"Indian reserve","RegionalDistrictCode":"8"},{"Name":"Lantzville","Type":"District municipality","RegionalDistrictCode":"3"},{"Name":"Nanaimo","Type":"City","RegionalDistrictCode":"3"},{"Name":"Nanaimo A","Type":"Regional district electoral area","RegionalDistrictCode":"3"},{"Name":"Nanaimo B","Type":"Regional district electoral area","RegionalDistrictCode":"3"},{"Name":"Nanaimo C","Type":"Regional district electoral area","RegionalDistrictCode":"3"},{"Name":"Nanaimo E","Type":"Regional district electoral area","RegionalDistrictCode":"3"},{"Name":"Nanaimo F","Type":"Regional district electoral area","RegionalDistrictCode":"3"},{"Name":"Nanaimo G","Type":"Regional district electoral area","RegionalDistrictCode":"3"},{"Name":"Nanaimo H","Type":"Regional district electoral area","RegionalDistrictCode":"3"},{"Name":"Nanaimo River","Type":"Indian reserve","RegionalDistrictCode":"3"},{"Name":"Nanaimo Town 1","Type":"Indian reserve","RegionalDistrictCode":"3"},{"Name":"Nanoose","Type":"Indian reserve","RegionalDistrictCode":"3"},{"Name":"Parksville","Type":"City","RegionalDistrictCode":"3"},{"Name":"Qualicum","Type":"Indian reserve","RegionalDistrictCode":"3"},{"Name":"Qualicum Beach","Type":"Town","RegionalDistrictCode":"3"},{"Name":"Armstrong","Type":"City","RegionalDistrictCode":"17"},{"Name":"Coldstream","Type":"District municipality","RegionalDistrictCode":"17"},{"Name":"Enderby","Type":"City","RegionalDistrictCode":"17"},{"Name":"Enderby 2","Type":"Indian reserve","RegionalDistrictCode":"17"},{"Name":"Harris 3","Type":"Indian reserve","RegionalDistrictCode":"17"},{"Name":"Lumby","Type":"Village","RegionalDistrictCode":"17"},{"Name":"North Okanagan B","Type":"Regional district electoral area","RegionalDistrictCode":"17"},{"Name":"North Okanagan C","Type":"Regional district electoral area","RegionalDistrictCode":"17"},{"Name":"North Okanagan D","Type":"Regional district electoral area","RegionalDistrictCode":"17"},{"Name":"North Okanagan E","Type":"Regional district electoral area","RegionalDistrictCode":"17"},{"Name":"North Okanagan F","Type":"Regional district electoral area","RegionalDistrictCode":"17"},{"Name":"Okanagan (Part) 1 - North Okanagan","Type":"Indian reserve","RegionalDistrictCode":"17"},{"Name":"Priest''s Valley 6","Type":"Indian reserve","RegionalDistrictCode":"17"},{"Name":"Spallumcheen","Type":"District municipality","RegionalDistrictCode":"17"},{"Name":"Vernon","Type":"City","RegionalDistrictCode":"17"},{"Name":"Fontas 1","Type":"Indian reserve","RegionalDistrictCode":"29"},{"Name":"Fort Nelson 2","Type":"Indian reserve","RegionalDistrictCode":"29"},{"Name":"Kahntah 3","Type":"Indian reserve","RegionalDistrictCode":"29"},{"Name":"Northern Rockies","Type":"Regional municipality","RegionalDistrictCode":"29"},{"Name":"Prophet River 4","Type":"Indian reserve","RegionalDistrictCode":"29"},{"Name":"Alexis 9","Type":"Indian reserve","RegionalDistrictCode":"14"},{"Name":"Ashnola 10","Type":"Indian reserve","RegionalDistrictCode":"14"},{"Name":"Blind Creek 6","Type":"Indian reserve","RegionalDistrictCode":"14"},{"Name":"Chopaka 7 & 8","Type":"Indian reserve","RegionalDistrictCode":"14"},{"Name":"Chuchuwayha 2","Type":"Indian reserve","RegionalDistrictCode":"14"},{"Name":"Keremeos","Type":"Village","RegionalDistrictCode":"14"},{"Name":"Lower Similkameen 2","Type":"Indian reserve","RegionalDistrictCode":"14"},{"Name":"Lulu 5","Type":"Indian reserve","RegionalDistrictCode":"14"},{"Name":"Okanagan-Similkameen A","Type":"Regional district electoral area","RegionalDistrictCode":"14"},{"Name":"Okanagan-Similkameen B","Type":"Regional district electoral area","RegionalDistrictCode":"14"},{"Name":"Okanagan-Similkameen C","Type":"Regional district electoral area","RegionalDistrictCode":"14"},{"Name":"Okanagan-Similkameen D","Type":"Regional district electoral area","RegionalDistrictCode":"14"},{"Name":"Okanagan-Similkameen E","Type":"Regional district electoral area","RegionalDistrictCode":"14"},{"Name":"Okanagan-Similkameen F","Type":"Regional district electoral area","RegionalDistrictCode":"14"},{"Name":"Okanagan-Similkameen G","Type":"Regional district electoral area","RegionalDistrictCode":"14"},{"Name":"Okanagan-Similkameen H","Type":"Regional district electoral area","RegionalDistrictCode":"14"},{"Name":"Okanagan-Similkameen I","Type":"Regional district electoral area","RegionalDistrictCode":"14"},{"Name":"Oliver","Type":"Town","RegionalDistrictCode":"14"},{"Name":"Osoyoos","Type":"Town","RegionalDistrictCode":"14"},{"Name":"Osoyoos 1","Type":"Indian reserve","RegionalDistrictCode":"14"},{"Name":"Penticton","Type":"City","RegionalDistrictCode":"14"},{"Name":"Penticton 1","Type":"Indian reserve","RegionalDistrictCode":"14"},{"Name":"Princeton","Type":"Town","RegionalDistrictCode":"14"},{"Name":"Summerland","Type":"District municipality","RegionalDistrictCode":"14"},{"Name":"Blueberry River 205","Type":"Indian reserve","RegionalDistrictCode":"28"},{"Name":"Chetwynd","Type":"District municipality","RegionalDistrictCode":"28"},{"Name":"Dawson Creek","Type":"City","RegionalDistrictCode":"28"},{"Name":"Doig River 206","Type":"Indian reserve","RegionalDistrictCode":"28"},{"Name":"East Moberly Lake 169","Type":"Indian reserve","RegionalDistrictCode":"28"},{"Name":"Finlay River 6","Type":"Indian reserve","RegionalDistrictCode":"28"},{"Name":"Fort St. John","Type":"City","RegionalDistrictCode":"28"},{"Name":"Fort Ware 1","Type":"Indian reserve","RegionalDistrictCode":"28"},{"Name":"Halfway River 168","Type":"Indian reserve","RegionalDistrictCode":"28"},{"Name":"Hudson''s Hope","Type":"District municipality","RegionalDistrictCode":"28"},{"Name":"Ingenika Point","Type":"Indian settlement","RegionalDistrictCode":"28"},{"Name":"Mesilinka 7","Type":"Indian reserve","RegionalDistrictCode":"28"},{"Name":"Peace River B","Type":"Regional district electoral area","RegionalDistrictCode":"28"},{"Name":"Peace River C","Type":"Regional district electoral area","RegionalDistrictCode":"28"},{"Name":"Peace River D","Type":"Regional district electoral area","RegionalDistrictCode":"28"},{"Name":"Peace River E","Type":"Regional district electoral area","RegionalDistrictCode":"28"},{"Name":"Pouce Coupe","Type":"Village","RegionalDistrictCode":"28"},{"Name":"Taylor","Type":"District municipality","RegionalDistrictCode":"28"},{"Name":"Tumbler Ridge","Type":"District municipality","RegionalDistrictCode":"28"},{"Name":"West Moberly Lake 168A","Type":"Indian reserve","RegionalDistrictCode":"28"},{"Name":"Powell River","Type":"City","RegionalDistrictCode":"7"},{"Name":"qathet A","Type":"Regional district electoral area","RegionalDistrictCode":"7"},{"Name":"qathet B","Type":"Regional district electoral area","RegionalDistrictCode":"7"},{"Name":"qathet C","Type":"Regional district electoral area","RegionalDistrictCode":"7"},{"Name":"qathet D","Type":"Regional district electoral area","RegionalDistrictCode":"7"},{"Name":"qathet E","Type":"Regional district electoral area","RegionalDistrictCode":"7"},{"Name":"Sechelt (Part) - qathet","Type":"Indian government district","RegionalDistrictCode":"7"},{"Name":"Sliammon 1","Type":"Tla''amin Lands","RegionalDistrictCode":"7"},{"Name":"Dolphin Island 1","Type":"Indian reserve","RegionalDistrictCode":"24"},{"Name":"Kulkayu (Hartley Bay) 4","Type":"Indian reserve","RegionalDistrictCode":"24"},{"Name":"Kulkayu (Hartley Bay) 4A","Type":"Indian reserve","RegionalDistrictCode":"24"},{"Name":"Lax Kw''alaams 1","Type":"Indian reserve","RegionalDistrictCode":"24"},{"Name":"Masset","Type":"Village","RegionalDistrictCode":"24"},{"Name":"Masset 1","Type":"Indian reserve","RegionalDistrictCode":"24"},{"Name":"North Coast A","Type":"Regional district electoral area","RegionalDistrictCode":"24"},{"Name":"North Coast C","Type":"Regional district electoral area","RegionalDistrictCode":"24"},{"Name":"North Coast D","Type":"Regional district electoral area","RegionalDistrictCode":"24"},{"Name":"North Coast E","Type":"Regional district electoral area","RegionalDistrictCode":"24"},{"Name":"Port Clements","Type":"Village","RegionalDistrictCode":"24"},{"Name":"Port Edward","Type":"District municipality","RegionalDistrictCode":"24"},{"Name":"Prince Rupert","Type":"City","RegionalDistrictCode":"24"},{"Name":"Queen Charlotte","Type":"Village","RegionalDistrictCode":"24"},{"Name":"S1/2 Tsimpsean 2","Type":"Indian reserve","RegionalDistrictCode":"24"},{"Name":"Skidegate 1","Type":"Indian reserve","RegionalDistrictCode":"24"},{"Name":"Tlaa Gaa Aawtlaas 28","Type":"Indian reserve","RegionalDistrictCode":"24"},{"Name":"Bridge River 1","Type":"Indian reserve","RegionalDistrictCode":"13"},{"Name":"Cayoosh Creek 1","Type":"Indian reserve","RegionalDistrictCode":"13"},{"Name":"Cheakamus 11","Type":"Indian reserve","RegionalDistrictCode":"13"},{"Name":"Chilhil 6","Type":"Indian reserve","RegionalDistrictCode":"13"},{"Name":"Fountain 1","Type":"Indian reserve","RegionalDistrictCode":"13"},{"Name":"Fountain 10","Type":"Indian reserve","RegionalDistrictCode":"13"},{"Name":"Fountain 11","Type":"Indian reserve","RegionalDistrictCode":"13"},{"Name":"Fountain 12","Type":"Indian reserve","RegionalDistrictCode":"13"},{"Name":"Fountain 1B","Type":"Indian reserve","RegionalDistrictCode":"13"},{"Name":"Fountain 1D","Type":"Indian reserve","RegionalDistrictCode":"13"},{"Name":"Fountain 3","Type":"Indian reserve","RegionalDistrictCode":"13"},{"Name":"Fountain 3A","Type":"Indian reserve","RegionalDistrictCode":"13"},{"Name":"Fountain Creek 8","Type":"Indian reserve","RegionalDistrictCode":"13"},{"Name":"Kowtain 17","Type":"Indian reserve","RegionalDistrictCode":"13"},{"Name":"Lillooet","Type":"District municipality","RegionalDistrictCode":"13"},{"Name":"Lillooet 1","Type":"Indian reserve","RegionalDistrictCode":"13"},{"Name":"McCartney''s Flat 4","Type":"Indian reserve","RegionalDistrictCode":"13"},{"Name":"Mission 5","Type":"Indian reserve","RegionalDistrictCode":"13"},{"Name":"Mount Currie","Type":"Indian reserve","RegionalDistrictCode":"13"},{"Name":"Necait 6","Type":"Indian reserve","RegionalDistrictCode":"13"},{"Name":"Nequatque","Type":"Indian reserve","RegionalDistrictCode":"13"},{"Name":"Nesikep 6","Type":"Indian reserve","RegionalDistrictCode":"13"},{"Name":"Nesuch 3","Type":"Indian reserve","RegionalDistrictCode":"13"},{"Name":"Pashilqua 2","Type":"Indian reserve","RegionalDistrictCode":"13"},{"Name":"Pavilion 1","Type":"Indian reserve","RegionalDistrictCode":"13"},{"Name":"Pemberton","Type":"Village","RegionalDistrictCode":"13"},{"Name":"Seaichem 16","Type":"Indian reserve","RegionalDistrictCode":"13"},{"Name":"Seton Lake 5","Type":"Indian reserve","RegionalDistrictCode":"13"},{"Name":"Slosh 1","Type":"Indian reserve","RegionalDistrictCode":"13"},{"Name":"Squamish","Type":"District municipality","RegionalDistrictCode":"13"},{"Name":"Squamish-Lillooet A","Type":"Regional district electoral area","RegionalDistrictCode":"13"},{"Name":"Squamish-Lillooet B","Type":"Regional district electoral area","RegionalDistrictCode":"13"},{"Name":"Squamish-Lillooet C","Type":"Regional district electoral area","RegionalDistrictCode":"13"},{"Name":"Squamish-Lillooet D","Type":"Regional district electoral area","RegionalDistrictCode":"13"},{"Name":"Stawamus 24","Type":"Indian reserve","RegionalDistrictCode":"13"},{"Name":"Towinock 2","Type":"Indian reserve","RegionalDistrictCode":"13"},{"Name":"Waiwakum 14","Type":"Indian reserve","RegionalDistrictCode":"13"},{"Name":"Whistler","Type":"District municipality","RegionalDistrictCode":"13"},{"Name":"Yekwaupsum 18","Type":"Indian reserve","RegionalDistrictCode":"13"},{"Name":"Dease River 1","Type":"Indian reserve","RegionalDistrictCode":"27"},{"Name":"Five Mile Point 3","Type":"Indian reserve","RegionalDistrictCode":"27"},{"Name":"Good Hope Lake","Type":"Indian settlement","RegionalDistrictCode":"27"},{"Name":"Liard River 3","Type":"Indian reserve","RegionalDistrictCode":"27"},{"Name":"Lower Post","Type":"Indian settlement","RegionalDistrictCode":"27"},{"Name":"Stikine Region","Type":"Regional district electoral area","RegionalDistrictCode":"27"},{"Name":"Unnamed 10","Type":"Indian reserve","RegionalDistrictCode":"27"},{"Name":"Ahaminaquus 12","Type":"Indian reserve","RegionalDistrictCode":"5"},{"Name":"Campbell River","Type":"City","RegionalDistrictCode":"5"},{"Name":"Campbell River 11","Type":"Indian reserve","RegionalDistrictCode":"5"},{"Name":"Cape Mudge 10","Type":"Indian reserve","RegionalDistrictCode":"5"},{"Name":"Chenahkint 12","Type":"Indian reserve","RegionalDistrictCode":"5"},{"Name":"Ehatis 11","Type":"Indian reserve","RegionalDistrictCode":"5"},{"Name":"Gold River","Type":"Village","RegionalDistrictCode":"5"},{"Name":"Homalco 9","Type":"Indian reserve","RegionalDistrictCode":"5"},{"Name":"Houpsitas 6","Type":"Indian reserve","RegionalDistrictCode":"5"},{"Name":"Nenagwas 12","Type":"Indian reserve","RegionalDistrictCode":"5"},{"Name":"Oclucje 7","Type":"Indian reserve","RegionalDistrictCode":"5"},{"Name":"Quinsam 12","Type":"Indian reserve","RegionalDistrictCode":"5"},{"Name":"Sayward","Type":"Village","RegionalDistrictCode":"5"},{"Name":"Squirrel Cove 8","Type":"Indian reserve","RegionalDistrictCode":"5"},{"Name":"Strathcona A","Type":"Regional district electoral area","RegionalDistrictCode":"5"},{"Name":"Strathcona B","Type":"Regional district electoral area","RegionalDistrictCode":"5"},{"Name":"Strathcona C","Type":"Regional district electoral area","RegionalDistrictCode":"5"},{"Name":"Strathcona D (Oyster Bay - Buttle Lake)","Type":"Regional district electoral area","RegionalDistrictCode":"5"},{"Name":"Tahsis","Type":"Village","RegionalDistrictCode":"5"},{"Name":"Tork 7","Type":"Indian reserve","RegionalDistrictCode":"5"},{"Name":"Tsa Xana 18","Type":"Indian reserve","RegionalDistrictCode":"5"},{"Name":"Village Island 1","Type":"Indian reserve","RegionalDistrictCode":"5"},{"Name":"Yuquot 1","Type":"Indian reserve","RegionalDistrictCode":"5"},{"Name":"Zeballos","Type":"Village","RegionalDistrictCode":"5"},{"Name":"Chekwelp 26","Type":"Indian reserve","RegionalDistrictCode":"12"},{"Name":"Gibsons","Type":"Town","RegionalDistrictCode":"12"},{"Name":"Sechelt","Type":"District municipality","RegionalDistrictCode":"12"},{"Name":"Sechelt (Part) - Sunshine Coast","Type":"Indian government district","RegionalDistrictCode":"12"},{"Name":"Sunshine Coast A","Type":"Regional district electoral area","RegionalDistrictCode":"12"},{"Name":"Sunshine Coast B","Type":"Regional district electoral area","RegionalDistrictCode":"12"},{"Name":"Sunshine Coast D","Type":"Regional district electoral area","RegionalDistrictCode":"12"},{"Name":"Sunshine Coast E","Type":"Regional district electoral area","RegionalDistrictCode":"12"},{"Name":"Sunshine Coast F","Type":"Regional district electoral area","RegionalDistrictCode":"12"},{"Name":"105 Mile Post 2","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Ashcroft","Type":"Village","RegionalDistrictCode":"15"},{"Name":"Ashcroft 4","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Barriere","Type":"District municipality","RegionalDistrictCode":"15"},{"Name":"Basque 18","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Bonaparte 3","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Boothroyd 8A","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Cache Creek","Type":"Village","RegionalDistrictCode":"15"},{"Name":"Canoe Creek 1","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Canoe Creek 2","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Chase","Type":"Village","RegionalDistrictCode":"15"},{"Name":"Chuchhraischin","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Clearwater","Type":"District municipality","RegionalDistrictCode":"15"},{"Name":"Clinton","Type":"Village","RegionalDistrictCode":"15"},{"Name":"Coldwater 1","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Douglas Lake 3","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Entlqwekkinh 19","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Halhalaeden","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Hamilton Creek 2","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"High Bar 1","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Inkluckcheen","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Inklyuhkinatko 2","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Joeyaska 2","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Kamloops","Type":"City","RegionalDistrictCode":"15"},{"Name":"Kamloops 1","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Kanaka Bar","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Kitzowit 20","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Klahkamich 17","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Kleetlekut 22","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Klickkumcheen 18","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Kloklowuck 7","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Kumcheen 1","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Logan Lake","Type":"District municipality","RegionalDistrictCode":"15"},{"Name":"Louis Creek 4","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Lower Hat Creek 2","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Lytton","Type":"Village","RegionalDistrictCode":"15"},{"Name":"Lytton 4A","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Lytton 4E","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Lytton 9A","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Lytton 9B","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Marble Canyon 3","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Merritt","Type":"City","RegionalDistrictCode":"15"},{"Name":"Nekalliston 2","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Nekliptum 1","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Neskonlith","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Nickel Palm 4","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Nickeyeah 25","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Nicola Lake 1","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Nicola Mameet 1","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Nicomen 1","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Nkaih 10","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Nohomeen 23","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Nooaitch 10","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"North Thompson 1","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Nuuautin 2","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Oregon Jack Creek 5","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Papyum 27","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Paska Island 3","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Paul''s Basin 2","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Pemynoos 9","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Peq-Paq 22","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Sahhaltkum 4","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Seah 5","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Shackan 11","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Shawniken 4B","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Siska Flat","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Skeetchestn","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Skuppah 2A","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Skuppah 4","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Skwayaynope 26","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Spences Bridge","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Spintlum Flat 3","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Squaam 2","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Stequmwhulpa 5","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Stryen 9","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Sun Peaks Mountain","Type":"Village","RegionalDistrictCode":"15"},{"Name":"Thompson-Nicola A (Wells Gray Country)","Type":"Regional district electoral area","RegionalDistrictCode":"15"},{"Name":"Thompson-Nicola B (Thompson Headwaters)","Type":"Regional district electoral area","RegionalDistrictCode":"15"},{"Name":"Thompson-Nicola E (Bonaparte Plateau)","Type":"Regional district electoral area","RegionalDistrictCode":"15"},{"Name":"Thompson-Nicola I (Blue Sky Country)","Type":"Regional district electoral area","RegionalDistrictCode":"15"},{"Name":"Thompson-Nicola J (Copper Desert Country)","Type":"Regional district electoral area","RegionalDistrictCode":"15"},{"Name":"Thompson-Nicola L (Grasslands)","Type":"Regional district electoral area","RegionalDistrictCode":"15"},{"Name":"Thompson-Nicola M (Beautiful Nicola Valley - North)","Type":"Regional district electoral area","RegionalDistrictCode":"15"},{"Name":"Thompson-Nicola N (Beautiful Nicola Valley - South)","Type":"Regional district electoral area","RegionalDistrictCode":"15"},{"Name":"Thompson-Nicola O (Lower North Thompson)","Type":"Regional district electoral area","RegionalDistrictCode":"15"},{"Name":"Thompson-Nicola P (Rivers and the Peaks)","Type":"Regional district electoral area","RegionalDistrictCode":"15"},{"Name":"Tsaukan 12","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Twoyqhalsht 16","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Upper Hat Creek 1","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Upper Nepa 6","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Whispering Pines 4","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Yawaucht 11","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Zacht 5","Type":"Indian reserve","RegionalDistrictCode":"15"},{"Name":"Zoht 4","Type":"Indian reserve","RegionalDistrictCode":"15"}]'; +-- Cleanup existing data in "Communities" table + DELETE FROM public."Communities"; +-- Insert into "Communities" table. + json_data jsonb := '[{"Name" : "Becher Bay 1", "Type" : "", "RegionalDistrictCode" : "1"}, {"Name" : "Central Saanich", "Type" : "", "RegionalDistrictCode" : "1"}, {"Name" : "Cole Bay 3", "Type" : "", "RegionalDistrictCode" : "1"}, {"Name" : "Colwood", "Type" : "", "RegionalDistrictCode" : "1"}, {"Name" : "East Saanich 2", "Type" : "", "RegionalDistrictCode" : "1"}, {"Name" : "Esquimalt", "Type" : "", "RegionalDistrictCode" : "1"}, {"Name" : "Esquimalt", "Type" : "", "RegionalDistrictCode" : "1"}, {"Name" : "Galiano Island 9", "Type" : "", "RegionalDistrictCode" : "1"}, {"Name" : "Gordon River 2", "Type" : "", "RegionalDistrictCode" : "1"}, {"Name" : "Highlands", "Type" : "", "RegionalDistrictCode" : "1"}, {"Name" : "Juan de Fuca (Part 1)", "Type" : "", "RegionalDistrictCode" : "1"}, {"Name" : "Juan de Fuca (Part 2)", "Type" : "", "RegionalDistrictCode" : "1"}, {"Name" : "Langford", "Type" : "", "RegionalDistrictCode" : "1"}, {"Name" : "Metchosin", "Type" : "", "RegionalDistrictCode" : "1"}, {"Name" : "New Songhees 1A", "Type" : "", "RegionalDistrictCode" : "1"}, {"Name" : "North Saanich", "Type" : "", "RegionalDistrictCode" : "1"}, {"Name" : "Oak Bay", "Type" : "", "RegionalDistrictCode" : "1"}, {"Name" : "Saanich", "Type" : "", "RegionalDistrictCode" : "1"}, {"Name" : "Saltspring Island", "Type" : "", "RegionalDistrictCode" : "1"}, {"Name" : "Sidney", "Type" : "", "RegionalDistrictCode" : "1"}, {"Name" : "Sooke", "Type" : "", "RegionalDistrictCode" : "1"}, {"Name" : "Southern Gulf Islands", "Type" : "", "RegionalDistrictCode" : "1"}, {"Name" : "South Saanich 1", "Type" : "", "RegionalDistrictCode" : "1"}, {"Name" : "T''Sou-ke", "Type" : "", "RegionalDistrictCode" : "1"}, {"Name" : "Union Bay 4", "Type" : "", "RegionalDistrictCode" : "1"}, {"Name" : "Victoria", "Type" : "", "RegionalDistrictCode" : "1"}, {"Name" : "View Royal", "Type" : "", "RegionalDistrictCode" : "1"}, {"Name" : "Abbotsford", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Aitchelitch 9", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Albert Flat 5", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Boothrouyd 5B", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Boothroyd 13", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Boston Bar 1A", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Bucktum 4", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Chawathil 4", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Cheam 1", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Chehalis 5", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Chilliwack", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Coqualeetza", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Douglas 8", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Fraser Valley A", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Fraser Valley B", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Fraser Valley C", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Fraser Valley D", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Fraser Valley E", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Fraser Valley F", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Fraser Valley G", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Fraser Valley H", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Harrison Hot Springs", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Holachten 8", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Hope", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Inkahtsaph 6", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Kahmoose 4", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Kent", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Kopchitchin 2", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Kwawkwawapilt 6", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Lakahahmen 11", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Langley 2", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Lukseetsissum 9", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Matsqui Main 2", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Mission", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Ohamil 1", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Paqulh", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Peters 1", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Popkum 1", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Puckatholetchin 11", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Q''alatkú7em", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Ruby Creek 2", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Sachteen", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Saddle Rock 9", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Schelowat 1", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Schkam 2", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Scowlitz 1", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Seabird Island", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Skawahlook 1", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Skookumchuck 4", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Skookumchuck 4A", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Skowkale", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Skwah 4", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Skwali 3", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Skway 5", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Skweahm 10", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Soowahlie 14", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Speyum 3", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Spuzzum 1", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Squawkum Creek 3", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Squiaala", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Stullawheets 8", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Tipella 7", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Tseatah 2", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Tuckkwiowhum 1", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Tzeachten 13", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Upper Sumas 6", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Yakweakwioose 12", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Yale Town 1", "Type" : "", "RegionalDistrictCode" : "10"}, {"Name" : "Anmore", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Barnston Island 3", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Belcarra", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Bowen Island", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Burnaby", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Burrard Inlet 3", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Capilano 5", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Coquitlam", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Coquitlam 1", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Coquitlam 2", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Delta", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Katzie 1", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Katzie 2", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Kitsilano 6", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Langley", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Langley", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Lions Bay", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Maple Ridge", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Matsqui 4", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "McMillan Island 6", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Metro Vancouver A", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Mission 1", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Musqueam 2", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Musqueam 4", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "New Westminster", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "North Vancouver", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "North Vancouver", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Pitt Meadows", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Port Coquitlam", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Port Moody", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Richmond", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Semiahmoo", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Seymour Creek 2", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Surrey", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Tsawwassen", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Vancouver", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "West Vancouver", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "White Rock", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Whonnock 1", "Type" : "", "RegionalDistrictCode" : "11"}, {"Name" : "Chekwelp 26", "Type" : "", "RegionalDistrictCode" : "12"}, {"Name" : "Gibsons", "Type" : "", "RegionalDistrictCode" : "12"}, {"Name" : "Sechelt", "Type" : "", "RegionalDistrictCode" : "12"}, {"Name" : "Sechelt (Part)", "Type" : "", "RegionalDistrictCode" : "12"}, {"Name" : "Sunshine Coast A", "Type" : "", "RegionalDistrictCode" : "12"}, {"Name" : "Sunshine Coast B", "Type" : "", "RegionalDistrictCode" : "12"}, {"Name" : "Sunshine Coast D", "Type" : "", "RegionalDistrictCode" : "12"}, {"Name" : "Sunshine Coast E", "Type" : "", "RegionalDistrictCode" : "12"}, {"Name" : "Sunshine Coast F", "Type" : "", "RegionalDistrictCode" : "12"}, {"Name" : "Bridge River 1", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Cayoosh Creek 1", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Cheakamus 11", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Chilhil 6", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Fountain 1", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Fountain 10", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Fountain 11", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Fountain 12", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Fountain 1B", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Fountain 1D", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Fountain 3", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Fountain 3A", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Fountain Creek 8", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Kowtain 17", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Lillooet", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Lillooet 1", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "McCartney''s Flat 4", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Mission 5", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Mount Currie", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Necait 6", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Nequatque", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Nesikep 6", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Nesuch 3", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Pashilqua 2", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Pavilion 1", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Pemberton", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Seaichem 16", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Seton Lake 5", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Slosh 1", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Squamish", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Squamish-Lillooet A", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Squamish-Lillooet B", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Squamish-Lillooet C", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Squamish-Lillooet D", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Stawamus 24", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Towinock 2", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Waiwakum 14", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Whistler", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Whitecap 1", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Yekwaupsum 18", "Type" : "", "RegionalDistrictCode" : "13"}, {"Name" : "Alexis 9", "Type" : "", "RegionalDistrictCode" : "14"}, {"Name" : "Ashnola 10", "Type" : "", "RegionalDistrictCode" : "14"}, {"Name" : "Blind Creek 6", "Type" : "", "RegionalDistrictCode" : "14"}, {"Name" : "Chopaka 7 & 8", "Type" : "", "RegionalDistrictCode" : "14"}, {"Name" : "Chuchuwayha 2", "Type" : "", "RegionalDistrictCode" : "14"}, {"Name" : "Keremeos", "Type" : "", "RegionalDistrictCode" : "14"}, {"Name" : "Lower Similkameen 2", "Type" : "", "RegionalDistrictCode" : "14"}, {"Name" : "Lulu 5", "Type" : "", "RegionalDistrictCode" : "14"}, {"Name" : "Okanagan-Similkameen A", "Type" : "", "RegionalDistrictCode" : "14"}, {"Name" : "Okanagan-Similkameen B", "Type" : "", "RegionalDistrictCode" : "14"}, {"Name" : "Okanagan-Similkameen C", "Type" : "", "RegionalDistrictCode" : "14"}, {"Name" : "Okanagan-Similkameen D", "Type" : "", "RegionalDistrictCode" : "14"}, {"Name" : "Okanagan-Similkameen E", "Type" : "", "RegionalDistrictCode" : "14"}, {"Name" : "Okanagan-Similkameen F", "Type" : "", "RegionalDistrictCode" : "14"}, {"Name" : "Okanagan-Similkameen G", "Type" : "", "RegionalDistrictCode" : "14"}, {"Name" : "Okanagan-Similkameen H", "Type" : "", "RegionalDistrictCode" : "14"}, {"Name" : "Okanagan-Similkameen I", "Type" : "", "RegionalDistrictCode" : "14"}, {"Name" : "Oliver", "Type" : "", "RegionalDistrictCode" : "14"}, {"Name" : "Osoyoos", "Type" : "", "RegionalDistrictCode" : "14"}, {"Name" : "Osoyoos 1", "Type" : "", "RegionalDistrictCode" : "14"}, {"Name" : "Penticton", "Type" : "", "RegionalDistrictCode" : "14"}, {"Name" : "Penticton 1", "Type" : "", "RegionalDistrictCode" : "14"}, {"Name" : "Princeton", "Type" : "", "RegionalDistrictCode" : "14"}, {"Name" : "Summerland", "Type" : "", "RegionalDistrictCode" : "14"}, {"Name" : "105 Mile Post 2", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Ashcroft", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Ashcroft 4", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Barriere", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Basque 18", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Bonaparte 3", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Boothroyd 8A", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Cache Creek", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Canoe Creek 1", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Canoe Creek 2", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Chase", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Chuchhraischin", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Clearwater", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Clinton", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Coldwater 1", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Douglas Lake 3", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Entlqwekkinh 19", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Halhalaeden", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Hamilton Creek 2", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "High Bar 1", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Inkluckcheen", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Inklyuhkinatko 2", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Joeyaska 2", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Kamloops", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Kamloops 1", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Kanaka Bar", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Kitzowit 20", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Klahkamich 17", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Kleetlekut 22", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Klickkumcheen 18", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Kloklowuck 7", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Kumcheen 1", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Logan Lake", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Louis Creek 4", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Lower Hat Creek 2", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Lytton", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Lytton 4A", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Lytton 4E", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Lytton 9A", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Lytton 9B", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Marble Canyon 3", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Merritt", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Nekalliston 2", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Nekliptum 1", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Neskonlith", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Nickel Palm 4", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Nickeyeah 25", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Nicola Lake 1", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Nicola Mameet 1", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Nicomen 1", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Nkaih 10", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Nohomeen 23", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Nooaitch 10", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "North Thompson 1", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Nuuautin 2", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Oregon Jack Creek 5", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Papyum 27", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Paska Island 3", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Paul''s Basin 2", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Pemynoos 9", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Peq-Paq 22", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Sahhaltkum 4", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Seah 5", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Shackan 11", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Shawniken 4B", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Siska Flat", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Siska Flat 5B", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Skeetchestn", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Skuppah 2A", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Skuppah 4", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Skwayaynope 26", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Spences Bridge", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Spintlum Flat 3", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Squaam 2", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Stequmwhulpa 5", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Stryen 9", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Sun Peaks Mountain", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Thompson-Nicola A (Wells Gray Country)", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Thompson-Nicola B (Thompson Headwaters)", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Thompson-Nicola E (Bonaparte Plateau)", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Thompson-Nicola I (Blue Sky Country)", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Thompson-Nicola J (Copper Desert Country)", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Thompson-Nicola L (Grasslands)", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Thompson-Nicola M (Beautiful Nicola Valley - North)", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Thompson-Nicola N (Beautiful Nicola Valley - South)", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Thompson-Nicola O (Lower North Thompson)", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Thompson-Nicola P (Rivers and the Peaks)", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Toops 3", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Tsaukan 12", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Twoyqhalsht 16", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Upper Hat Creek 1", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Upper Nepa 6", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Whispering Pines 4", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Yawaucht 11", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Zacht 5", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Zoht 4", "Type" : "", "RegionalDistrictCode" : "15"}, {"Name" : "Central Okanagan", "Type" : "", "RegionalDistrictCode" : "16"}, {"Name" : "Central Okanagan West", "Type" : "", "RegionalDistrictCode" : "16"}, {"Name" : "Duck Lake 7", "Type" : "", "RegionalDistrictCode" : "16"}, {"Name" : "Kelowna", "Type" : "", "RegionalDistrictCode" : "16"}, {"Name" : "Lake Country", "Type" : "", "RegionalDistrictCode" : "16"}, {"Name" : "Peachland", "Type" : "", "RegionalDistrictCode" : "16"}, {"Name" : "Tsinstikeptum 10", "Type" : "", "RegionalDistrictCode" : "16"}, {"Name" : "Tsinstikeptum 9", "Type" : "", "RegionalDistrictCode" : "16"}, {"Name" : "West Kelowna", "Type" : "", "RegionalDistrictCode" : "16"}, {"Name" : "Armstrong", "Type" : "", "RegionalDistrictCode" : "17"}, {"Name" : "Coldstream", "Type" : "", "RegionalDistrictCode" : "17"}, {"Name" : "Enderby", "Type" : "", "RegionalDistrictCode" : "17"}, {"Name" : "Enderby 2", "Type" : "", "RegionalDistrictCode" : "17"}, {"Name" : "Harris 3", "Type" : "", "RegionalDistrictCode" : "17"}, {"Name" : "Lumby", "Type" : "", "RegionalDistrictCode" : "17"}, {"Name" : "North Okanagan B", "Type" : "", "RegionalDistrictCode" : "17"}, {"Name" : "North Okanagan C", "Type" : "", "RegionalDistrictCode" : "17"}, {"Name" : "North Okanagan D", "Type" : "", "RegionalDistrictCode" : "17"}, {"Name" : "North Okanagan E", "Type" : "", "RegionalDistrictCode" : "17"}, {"Name" : "North Okanagan F", "Type" : "", "RegionalDistrictCode" : "17"}, {"Name" : "Okanagan (Part) 1", "Type" : "", "RegionalDistrictCode" : "17"}, {"Name" : "Priest''s Valley 6", "Type" : "", "RegionalDistrictCode" : "17"}, {"Name" : "Spallumcheen", "Type" : "", "RegionalDistrictCode" : "17"}, {"Name" : "Vernon", "Type" : "", "RegionalDistrictCode" : "17"}, {"Name" : "Chum Creek 2", "Type" : "", "RegionalDistrictCode" : "18"}, {"Name" : "Columbia Shuswap A", "Type" : "", "RegionalDistrictCode" : "18"}, {"Name" : "Columbia Shuswap B", "Type" : "", "RegionalDistrictCode" : "18"}, {"Name" : "Columbia Shuswap C", "Type" : "", "RegionalDistrictCode" : "18"}, {"Name" : "Columbia Shuswap D", "Type" : "", "RegionalDistrictCode" : "18"}, {"Name" : "Columbia Shuswap E", "Type" : "", "RegionalDistrictCode" : "18"}, {"Name" : "Columbia Shuswap F", "Type" : "", "RegionalDistrictCode" : "18"}, {"Name" : "Columbia Shuswap G", "Type" : "", "RegionalDistrictCode" : "18"}, {"Name" : "Golden", "Type" : "", "RegionalDistrictCode" : "18"}, {"Name" : "Hustalen 1", "Type" : "", "RegionalDistrictCode" : "18"}, {"Name" : "North Bay 5", "Type" : "", "RegionalDistrictCode" : "18"}, {"Name" : "Okanagan (Part) 1", "Type" : "", "RegionalDistrictCode" : "18"}, {"Name" : "Quaaout 1", "Type" : "", "RegionalDistrictCode" : "18"}, {"Name" : "Revelstoke", "Type" : "", "RegionalDistrictCode" : "18"}, {"Name" : "Salmon Arm", "Type" : "", "RegionalDistrictCode" : "18"}, {"Name" : "Salmon River 1", "Type" : "", "RegionalDistrictCode" : "18"}, {"Name" : "Scotch Creek 4", "Type" : "", "RegionalDistrictCode" : "18"}, {"Name" : "Sicamous", "Type" : "", "RegionalDistrictCode" : "18"}, {"Name" : "Switsemalph", "Type" : "", "RegionalDistrictCode" : "18"}, {"Name" : "Switsemalph 3", "Type" : "", "RegionalDistrictCode" : "18"}, {"Name" : "Canal Flats", "Type" : "", "RegionalDistrictCode" : "19"}, {"Name" : "Cassimayooks (Mayook) 5", "Type" : "", "RegionalDistrictCode" : "19"}, {"Name" : "Columbia Lake 3", "Type" : "", "RegionalDistrictCode" : "19"}, {"Name" : "Cranbrook", "Type" : "", "RegionalDistrictCode" : "19"}, {"Name" : "East Kootenay A", "Type" : "", "RegionalDistrictCode" : "19"}, {"Name" : "East Kootenay B", "Type" : "", "RegionalDistrictCode" : "19"}, {"Name" : "East Kootenay C", "Type" : "", "RegionalDistrictCode" : "19"}, {"Name" : "East Kootenay E", "Type" : "", "RegionalDistrictCode" : "19"}, {"Name" : "East Kootenay F", "Type" : "", "RegionalDistrictCode" : "19"}, {"Name" : "East Kootenay G", "Type" : "", "RegionalDistrictCode" : "19"}, {"Name" : "Elkford", "Type" : "", "RegionalDistrictCode" : "19"}, {"Name" : "Fernie", "Type" : "", "RegionalDistrictCode" : "19"}, {"Name" : "Invermere", "Type" : "", "RegionalDistrictCode" : "19"}, {"Name" : "Isidore''s Ranch 4", "Type" : "", "RegionalDistrictCode" : "19"}, {"Name" : "Kimberley", "Type" : "", "RegionalDistrictCode" : "19"}, {"Name" : "Kootenay 1", "Type" : "", "RegionalDistrictCode" : "19"}, {"Name" : "Radium Hot Springs", "Type" : "", "RegionalDistrictCode" : "19"}, {"Name" : "Shuswap", "Type" : "", "RegionalDistrictCode" : "19"}, {"Name" : "Sparwood", "Type" : "", "RegionalDistrictCode" : "19"}, {"Name" : "St. Mary''s", "Type" : "", "RegionalDistrictCode" : "19"}, {"Name" : "Tobacco Plains 2", "Type" : "", "RegionalDistrictCode" : "19"}, {"Name" : "Chemainus 13", "Type" : "", "RegionalDistrictCode" : "2"}, {"Name" : "Cowichan", "Type" : "", "RegionalDistrictCode" : "2"}, {"Name" : "Cowichan Lake", "Type" : "", "RegionalDistrictCode" : "2"}, {"Name" : "Cowichan Valley A", "Type" : "", "RegionalDistrictCode" : "2"}, {"Name" : "Cowichan Valley B", "Type" : "", "RegionalDistrictCode" : "2"}, {"Name" : "Cowichan Valley C", "Type" : "", "RegionalDistrictCode" : "2"}, {"Name" : "Cowichan Valley D", "Type" : "", "RegionalDistrictCode" : "2"}, {"Name" : "Cowichan Valley E", "Type" : "", "RegionalDistrictCode" : "2"}, {"Name" : "Cowichan Valley F", "Type" : "", "RegionalDistrictCode" : "2"}, {"Name" : "Cowichan Valley G", "Type" : "", "RegionalDistrictCode" : "2"}, {"Name" : "Cowichan Valley H", "Type" : "", "RegionalDistrictCode" : "2"}, {"Name" : "Cowichan Valley I", "Type" : "", "RegionalDistrictCode" : "2"}, {"Name" : "Duncan", "Type" : "", "RegionalDistrictCode" : "2"}, {"Name" : "Est-Patrolas 4", "Type" : "", "RegionalDistrictCode" : "2"}, {"Name" : "Halalt 2", "Type" : "", "RegionalDistrictCode" : "2"}, {"Name" : "Kil-pah-las 3", "Type" : "", "RegionalDistrictCode" : "2"}, {"Name" : "Ladysmith", "Type" : "", "RegionalDistrictCode" : "2"}, {"Name" : "Lake Cowichan", "Type" : "", "RegionalDistrictCode" : "2"}, {"Name" : "Lyacksun 3", "Type" : "", "RegionalDistrictCode" : "2"}, {"Name" : "Malachan 11", "Type" : "", "RegionalDistrictCode" : "2"}, {"Name" : "Malahat 11", "Type" : "", "RegionalDistrictCode" : "2"}, {"Name" : "North Cowichan", "Type" : "", "RegionalDistrictCode" : "2"}, {"Name" : "Oyster Bay 12", "Type" : "", "RegionalDistrictCode" : "2"}, {"Name" : "Penelakut Island 7", "Type" : "", "RegionalDistrictCode" : "2"}, {"Name" : "Portier Pass 5", "Type" : "", "RegionalDistrictCode" : "2"}, {"Name" : "Shingle Point 4", "Type" : "", "RegionalDistrictCode" : "2"}, {"Name" : "Squaw-Hay-One 11", "Type" : "", "RegionalDistrictCode" : "2"}, {"Name" : "Theik 2", "Type" : "", "RegionalDistrictCode" : "2"}, {"Name" : "Tsussie 6", "Type" : "", "RegionalDistrictCode" : "2"}, {"Name" : "Tzart-Lam 5", "Type" : "", "RegionalDistrictCode" : "2"}, {"Name" : "Castlegar", "Type" : "", "RegionalDistrictCode" : "20"}, {"Name" : "Central Kootenay A", "Type" : "", "RegionalDistrictCode" : "20"}, {"Name" : "Central Kootenay B", "Type" : "", "RegionalDistrictCode" : "20"}, {"Name" : "Central Kootenay C", "Type" : "", "RegionalDistrictCode" : "20"}, {"Name" : "Central Kootenay D", "Type" : "", "RegionalDistrictCode" : "20"}, {"Name" : "Central Kootenay E", "Type" : "", "RegionalDistrictCode" : "20"}, {"Name" : "Central Kootenay F", "Type" : "", "RegionalDistrictCode" : "20"}, {"Name" : "Central Kootenay G", "Type" : "", "RegionalDistrictCode" : "20"}, {"Name" : "Central Kootenay H", "Type" : "", "RegionalDistrictCode" : "20"}, {"Name" : "Central Kootenay I", "Type" : "", "RegionalDistrictCode" : "20"}, {"Name" : "Central Kootenay J", "Type" : "", "RegionalDistrictCode" : "20"}, {"Name" : "Central Kootenay K", "Type" : "", "RegionalDistrictCode" : "20"}, {"Name" : "Creston", "Type" : "", "RegionalDistrictCode" : "20"}, {"Name" : "Creston 1", "Type" : "", "RegionalDistrictCode" : "20"}, {"Name" : "Kaslo", "Type" : "", "RegionalDistrictCode" : "20"}, {"Name" : "Lower Kootenay 1C", "Type" : "", "RegionalDistrictCode" : "20"}, {"Name" : "Lower Kootenay 5", "Type" : "", "RegionalDistrictCode" : "20"}, {"Name" : "Nakusp", "Type" : "", "RegionalDistrictCode" : "20"}, {"Name" : "Nelson", "Type" : "", "RegionalDistrictCode" : "20"}, {"Name" : "New Denver", "Type" : "", "RegionalDistrictCode" : "20"}, {"Name" : "Salmo", "Type" : "", "RegionalDistrictCode" : "20"}, {"Name" : "Silverton", "Type" : "", "RegionalDistrictCode" : "20"}, {"Name" : "Slocan", "Type" : "", "RegionalDistrictCode" : "20"}, {"Name" : "Fruitvale", "Type" : "", "RegionalDistrictCode" : "21"}, {"Name" : "Grand Forks", "Type" : "", "RegionalDistrictCode" : "21"}, {"Name" : "Greenwood", "Type" : "", "RegionalDistrictCode" : "21"}, {"Name" : "Kootenay Boundary A", "Type" : "", "RegionalDistrictCode" : "21"}, {"Name" : "Kootenay Boundary B / Lower Columbia-Old-Glory", "Type" : "", "RegionalDistrictCode" : "21"}, {"Name" : "Kootenay Boundary C / Christina Lake", "Type" : "", "RegionalDistrictCode" : "21"}, {"Name" : "Kootenay Boundary D / Rural Grand Forks", "Type" : "", "RegionalDistrictCode" : "21"}, {"Name" : "Kootenay Boundary E / West Boundary", "Type" : "", "RegionalDistrictCode" : "21"}, {"Name" : "Midway", "Type" : "", "RegionalDistrictCode" : "21"}, {"Name" : "Montrose", "Type" : "", "RegionalDistrictCode" : "21"}, {"Name" : "Rossland", "Type" : "", "RegionalDistrictCode" : "21"}, {"Name" : "Trail", "Type" : "", "RegionalDistrictCode" : "21"}, {"Name" : "Warfield", "Type" : "", "RegionalDistrictCode" : "21"}, {"Name" : "Agats Meadow 8", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Alexandria", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Alexis Creek 14", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Alexis Creek 16", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Alexis Creek 21", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Alexis Creek 34", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Alkali Lake 1", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Alkali Lake 4A", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Anahim''s Flat 1", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Anahim''s Meadow", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Baezaeko River 27", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Baptiste Meadow 2", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Betty Creek 18", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Canim Lake 1", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Canim Lake 2", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Canim Lake 4", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Canoe Creek 3", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Cariboo A", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Cariboo B", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Cariboo C", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Cariboo D", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Cariboo E", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Cariboo F", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Cariboo G", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Cariboo H", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Cariboo I", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Cariboo J", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Cariboo K", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Cariboo L", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Charley Boy''s Meadow 3", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Chilco Lake 1", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Chilco Lake 1A", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Coglistiko River 29", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Deep Creek 2", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Dog Creek 1", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Dog Creek 2", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Dragon Lake 3", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Euchinico Creek 17", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Fishtrap 19", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Garden", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Johny Sticks 2", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Kluskus 1", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Lezbye 6", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Little Springs", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Lohbiee 3", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Michel Gardens 36", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Nazco 20", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "One Hundred Mile House", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Puntzi Lake 2", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Quesnel", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Quesnel 1", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Redstone Flat 1", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Redstone Flat 1A", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Salmon River Meadow 7", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Sandy Harry 4", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Soda Creek 1", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Squinas 2", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Stone 1", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Sundayman''s Meadow 3", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Swan Lake 3", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Tanakut 4", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Thomas Squinas Ranch 2A", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Toosey 1", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Towdystan Lake 3", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Trout Lake Alec 16", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Ulkatcho 13", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Ulkatcho 14A", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Wells", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Williams Lake", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Williams Lake 1", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Windy Mouth 7", "Type" : "", "RegionalDistrictCode" : "22"}, {"Name" : "Fort George 2", "Type" : "", "RegionalDistrictCode" : "23"}, {"Name" : "Fraser-Fort George A", "Type" : "", "RegionalDistrictCode" : "23"}, {"Name" : "Fraser-Fort George C", "Type" : "", "RegionalDistrictCode" : "23"}, {"Name" : "Fraser-Fort George D", "Type" : "", "RegionalDistrictCode" : "23"}, {"Name" : "Fraser-Fort George E", "Type" : "", "RegionalDistrictCode" : "23"}, {"Name" : "Fraser-Fort George F", "Type" : "", "RegionalDistrictCode" : "23"}, {"Name" : "Fraser-Fort George G", "Type" : "", "RegionalDistrictCode" : "23"}, {"Name" : "Fraser-Fort George H", "Type" : "", "RegionalDistrictCode" : "23"}, {"Name" : "Mackenzie", "Type" : "", "RegionalDistrictCode" : "23"}, {"Name" : "McBride", "Type" : "", "RegionalDistrictCode" : "23"}, {"Name" : "McCleod Lake 5", "Type" : "", "RegionalDistrictCode" : "23"}, {"Name" : "McLeod Lake 1", "Type" : "", "RegionalDistrictCode" : "23"}, {"Name" : "Prince George", "Type" : "", "RegionalDistrictCode" : "23"}, {"Name" : "Valemount", "Type" : "", "RegionalDistrictCode" : "23"}, {"Name" : "Daajing Giids", "Type" : "", "RegionalDistrictCode" : "24"}, {"Name" : "Dolphin Island 1", "Type" : "", "RegionalDistrictCode" : "24"}, {"Name" : "Kulkayu (Hartley Bay) 4", "Type" : "", "RegionalDistrictCode" : "24"}, {"Name" : "Kulkayu (Hartley Bay) 4A", "Type" : "", "RegionalDistrictCode" : "24"}, {"Name" : "Lax Kw''alaams 1", "Type" : "", "RegionalDistrictCode" : "24"}, {"Name" : "Masset", "Type" : "", "RegionalDistrictCode" : "24"}, {"Name" : "Masset 1", "Type" : "", "RegionalDistrictCode" : "24"}, {"Name" : "North Coast A", "Type" : "", "RegionalDistrictCode" : "24"}, {"Name" : "North Coast C", "Type" : "", "RegionalDistrictCode" : "24"}, {"Name" : "North Coast D", "Type" : "", "RegionalDistrictCode" : "24"}, {"Name" : "North Coast E", "Type" : "", "RegionalDistrictCode" : "24"}, {"Name" : "Port Clements", "Type" : "", "RegionalDistrictCode" : "24"}, {"Name" : "Port Edward", "Type" : "", "RegionalDistrictCode" : "24"}, {"Name" : "Prince Rupert", "Type" : "", "RegionalDistrictCode" : "24"}, {"Name" : "S1/2 Tsimpsean 2", "Type" : "", "RegionalDistrictCode" : "24"}, {"Name" : "Skidegate 1", "Type" : "", "RegionalDistrictCode" : "24"}, {"Name" : "Tlaa Gaa Aawtlaas 28", "Type" : "", "RegionalDistrictCode" : "24"}, {"Name" : "Anlaw 4", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Babine 17", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Bulkley River 19", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Coryatsaqua (Moricetown) 2", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Dease Lake 9", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Gitanmaax 1", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Gitanyow 1", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Gitsegukla 1", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Gitwangak 1", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Guhthe Tah 12", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Hagwilget 1", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Hazelton", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Iskut 6", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Kispiox 1", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Kitamaat 2", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Kitasoo 1", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Kitimat", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Kitimat-Stikine A", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Kitimat-Stikine B", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Kitimat-Stikine C (Part 1)", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Kitimat-Stikine C (Part 2)", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Kitimat-Stikine D", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Kitimat-Stikine E", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Kitimat-Stikine F", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Kitselas 1", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Kitsumkaylum 1", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Kshish 4", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Kulspai 6", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Moricetown 1", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "New Hazelton", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Nisga''a", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Sik-e-dakh 2", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Stewart", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Tahltan 1", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Telegraph Creek", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Terrace", "Type" : "", "RegionalDistrictCode" : "25"}, {"Name" : "Babine 16", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Babine 25", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Babine 6", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Babine Lake 21B", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Binche 2", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Bulkley-Nechako A", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Bulkley-Nechako B", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Bulkley-Nechako C", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Bulkley-Nechako D", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Bulkley-Nechako E", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Bulkley-Nechako F", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Bulkley-Nechako G", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Burns Lake", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Burns Lake 18", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Cheslatta 1", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Duncan Lake 2", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Dzitline Lee 9", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Fort St. James", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Francois Lake 7", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Fraser Lake", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Granisle", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Houston", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Jean Baptiste 28", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Laketown 3", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Mission Lands 17", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Nak''azdli", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Nautley (Fort Fraser) 1", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Nedoats 11", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Noonla 6", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "North Tacla Lake", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Omineca 1", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Palling 1", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Poison Creek 17A", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Seaspunkut 4", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Skins Lake 16A", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Skins Lake 16B", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Smithers", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Sowchea 3", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Stellaquo (Stella) 1", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Stony Creek 1", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Tache 1", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Tacla Lake (Ferry Landing) 9", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Tadinlay 15", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Tatla''t East 2", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Tatla West 11", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Telkwa", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Uncha Lake 13A", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Vanderhoof", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Williams Prairie Meadow 1A", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Woyenne 27", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Ye Koo Che 3", "Type" : "", "RegionalDistrictCode" : "26"}, {"Name" : "Dease River 1", "Type" : "", "RegionalDistrictCode" : "27"}, {"Name" : "Five Mile Point 3", "Type" : "", "RegionalDistrictCode" : "27"}, {"Name" : "Good Hope Lake", "Type" : "", "RegionalDistrictCode" : "27"}, {"Name" : "Liard River 3", "Type" : "", "RegionalDistrictCode" : "27"}, {"Name" : "Lower Post", "Type" : "", "RegionalDistrictCode" : "27"}, {"Name" : "Stikine Region", "Type" : "", "RegionalDistrictCode" : "27"}, {"Name" : "Unnamed 10", "Type" : "", "RegionalDistrictCode" : "27"}, {"Name" : "Blueberry River 205", "Type" : "", "RegionalDistrictCode" : "28"}, {"Name" : "Chetwynd", "Type" : "", "RegionalDistrictCode" : "28"}, {"Name" : "Dawson Creek", "Type" : "", "RegionalDistrictCode" : "28"}, {"Name" : "Doig River 206", "Type" : "", "RegionalDistrictCode" : "28"}, {"Name" : "East Moberly Lake 169", "Type" : "", "RegionalDistrictCode" : "28"}, {"Name" : "Finlay River 6", "Type" : "", "RegionalDistrictCode" : "28"}, {"Name" : "Fort St. John", "Type" : "", "RegionalDistrictCode" : "28"}, {"Name" : "Fort Ware 1", "Type" : "", "RegionalDistrictCode" : "28"}, {"Name" : "Halfway River 168", "Type" : "", "RegionalDistrictCode" : "28"}, {"Name" : "Hudson''s Hope", "Type" : "", "RegionalDistrictCode" : "28"}, {"Name" : "Ingenika Point", "Type" : "", "RegionalDistrictCode" : "28"}, {"Name" : "Mesilinka 7", "Type" : "", "RegionalDistrictCode" : "28"}, {"Name" : "Peace River B", "Type" : "", "RegionalDistrictCode" : "28"}, {"Name" : "Peace River C", "Type" : "", "RegionalDistrictCode" : "28"}, {"Name" : "Peace River D", "Type" : "", "RegionalDistrictCode" : "28"}, {"Name" : "Peace River E", "Type" : "", "RegionalDistrictCode" : "28"}, {"Name" : "Pouce Coupe", "Type" : "", "RegionalDistrictCode" : "28"}, {"Name" : "Taylor", "Type" : "", "RegionalDistrictCode" : "28"}, {"Name" : "Tumbler Ridge", "Type" : "", "RegionalDistrictCode" : "28"}, {"Name" : "West Moberly Lake 168A", "Type" : "", "RegionalDistrictCode" : "28"}, {"Name" : "Wochiigii Nané?", "Type" : "", "RegionalDistrictCode" : "28"}, {"Name" : "Fontas 1", "Type" : "", "RegionalDistrictCode" : "29"}, {"Name" : "Fort Nelson 2", "Type" : "", "RegionalDistrictCode" : "29"}, {"Name" : "Kahntah 3", "Type" : "", "RegionalDistrictCode" : "29"}, {"Name" : "Northern Rockies", "Type" : "", "RegionalDistrictCode" : "29"}, {"Name" : "Prophet River 4", "Type" : "", "RegionalDistrictCode" : "29"}, {"Name" : "Lantzville", "Type" : "", "RegionalDistrictCode" : "3"}, {"Name" : "Nanaimo", "Type" : "", "RegionalDistrictCode" : "3"}, {"Name" : "Nanaimo A", "Type" : "", "RegionalDistrictCode" : "3"}, {"Name" : "Nanaimo B", "Type" : "", "RegionalDistrictCode" : "3"}, {"Name" : "Nanaimo C", "Type" : "", "RegionalDistrictCode" : "3"}, {"Name" : "Nanaimo E", "Type" : "", "RegionalDistrictCode" : "3"}, {"Name" : "Nanaimo F", "Type" : "", "RegionalDistrictCode" : "3"}, {"Name" : "Nanaimo G", "Type" : "", "RegionalDistrictCode" : "3"}, {"Name" : "Nanaimo H", "Type" : "", "RegionalDistrictCode" : "3"}, {"Name" : "Nanaimo River", "Type" : "", "RegionalDistrictCode" : "3"}, {"Name" : "Nanaimo Town 1", "Type" : "", "RegionalDistrictCode" : "3"}, {"Name" : "Nanoose", "Type" : "", "RegionalDistrictCode" : "3"}, {"Name" : "Parksville", "Type" : "", "RegionalDistrictCode" : "3"}, {"Name" : "Qualicum", "Type" : "", "RegionalDistrictCode" : "3"}, {"Name" : "Qualicum Beach", "Type" : "", "RegionalDistrictCode" : "3"}, {"Name" : "Ahahswinis 1", "Type" : "", "RegionalDistrictCode" : "4"}, {"Name" : "Alberni 2", "Type" : "", "RegionalDistrictCode" : "4"}, {"Name" : "Alberni-Clayoquot A", "Type" : "", "RegionalDistrictCode" : "4"}, {"Name" : "Alberni-Clayoquot B", "Type" : "", "RegionalDistrictCode" : "4"}, {"Name" : "Alberni-Clayoquot C", "Type" : "", "RegionalDistrictCode" : "4"}, {"Name" : "Alberni-Clayoquot D", "Type" : "", "RegionalDistrictCode" : "4"}, {"Name" : "Alberni-Clayoquot E", "Type" : "", "RegionalDistrictCode" : "4"}, {"Name" : "Alberni-Clayoquot F", "Type" : "", "RegionalDistrictCode" : "4"}, {"Name" : "Anacla 12", "Type" : "", "RegionalDistrictCode" : "4"}, {"Name" : "Clakamucus 2", "Type" : "", "RegionalDistrictCode" : "4"}, {"Name" : "Elhlateese 2", "Type" : "", "RegionalDistrictCode" : "4"}, {"Name" : "Esowista 3", "Type" : "", "RegionalDistrictCode" : "4"}, {"Name" : "Hesquiat 1", "Type" : "", "RegionalDistrictCode" : "4"}, {"Name" : "Ittatsoo 1", "Type" : "", "RegionalDistrictCode" : "4"}, {"Name" : "Klehkoot 2", "Type" : "", "RegionalDistrictCode" : "4"}, {"Name" : "Macoah 1", "Type" : "", "RegionalDistrictCode" : "4"}, {"Name" : "Marktosis 15", "Type" : "", "RegionalDistrictCode" : "4"}, {"Name" : "Numukamis 1", "Type" : "", "RegionalDistrictCode" : "4"}, {"Name" : "Opitsat 1", "Type" : "", "RegionalDistrictCode" : "4"}, {"Name" : "Port Alberni", "Type" : "", "RegionalDistrictCode" : "4"}, {"Name" : "Refuge Cove 6", "Type" : "", "RegionalDistrictCode" : "4"}, {"Name" : "Sachsa 4", "Type" : "", "RegionalDistrictCode" : "4"}, {"Name" : "Tin Wis 11", "Type" : "", "RegionalDistrictCode" : "4"}, {"Name" : "Tofino", "Type" : "", "RegionalDistrictCode" : "4"}, {"Name" : "Tsahaheh 1", "Type" : "", "RegionalDistrictCode" : "4"}, {"Name" : "Ucluelet", "Type" : "", "RegionalDistrictCode" : "4"}, {"Name" : "Ahaminaquus 12", "Type" : "", "RegionalDistrictCode" : "5"}, {"Name" : "Campbell River", "Type" : "", "RegionalDistrictCode" : "5"}, {"Name" : "Campbell River 11", "Type" : "", "RegionalDistrictCode" : "5"}, {"Name" : "Cape Mudge 10", "Type" : "", "RegionalDistrictCode" : "5"}, {"Name" : "Chenahkint 12", "Type" : "", "RegionalDistrictCode" : "5"}, {"Name" : "Ehatis 11", "Type" : "", "RegionalDistrictCode" : "5"}, {"Name" : "Gold River", "Type" : "", "RegionalDistrictCode" : "5"}, {"Name" : "Homalco 9", "Type" : "", "RegionalDistrictCode" : "5"}, {"Name" : "Houpsitas 6", "Type" : "", "RegionalDistrictCode" : "5"}, {"Name" : "Nenagwas 12", "Type" : "", "RegionalDistrictCode" : "5"}, {"Name" : "Oclucje 7", "Type" : "", "RegionalDistrictCode" : "5"}, {"Name" : "Quinsam 12", "Type" : "", "RegionalDistrictCode" : "5"}, {"Name" : "Sayward", "Type" : "", "RegionalDistrictCode" : "5"}, {"Name" : "Squirrel Cove 8", "Type" : "", "RegionalDistrictCode" : "5"}, {"Name" : "Strathcona A", "Type" : "", "RegionalDistrictCode" : "5"}, {"Name" : "Strathcona B", "Type" : "", "RegionalDistrictCode" : "5"}, {"Name" : "Strathcona C", "Type" : "", "RegionalDistrictCode" : "5"}, {"Name" : "Strathcona D (Oyster Bay - Buttle Lake)", "Type" : "", "RegionalDistrictCode" : "5"}, {"Name" : "Tahsis", "Type" : "", "RegionalDistrictCode" : "5"}, {"Name" : "Tork 7", "Type" : "", "RegionalDistrictCode" : "5"}, {"Name" : "Tsa Xana 18", "Type" : "", "RegionalDistrictCode" : "5"}, {"Name" : "Village Island 1", "Type" : "", "RegionalDistrictCode" : "5"}, {"Name" : "Yuquot 1", "Type" : "", "RegionalDistrictCode" : "5"}, {"Name" : "Zeballos", "Type" : "", "RegionalDistrictCode" : "5"}, {"Name" : "Comox", "Type" : "", "RegionalDistrictCode" : "6"}, {"Name" : "Comox 1", "Type" : "", "RegionalDistrictCode" : "6"}, {"Name" : "Comox Valley A", "Type" : "", "RegionalDistrictCode" : "6"}, {"Name" : "Comox Valley B (Lazo North)", "Type" : "", "RegionalDistrictCode" : "6"}, {"Name" : "Comox Valley C (Puntledge - Black Creek)", "Type" : "", "RegionalDistrictCode" : "6"}, {"Name" : "Courtenay", "Type" : "", "RegionalDistrictCode" : "6"}, {"Name" : "Cumberland", "Type" : "", "RegionalDistrictCode" : "6"}, {"Name" : "Pentledge 2", "Type" : "", "RegionalDistrictCode" : "6"}, {"Name" : "Powell River", "Type" : "", "RegionalDistrictCode" : "7"}, {"Name" : "qathet A", "Type" : "", "RegionalDistrictCode" : "7"}, {"Name" : "qathet B", "Type" : "", "RegionalDistrictCode" : "7"}, {"Name" : "qathet C", "Type" : "", "RegionalDistrictCode" : "7"}, {"Name" : "qathet D", "Type" : "", "RegionalDistrictCode" : "7"}, {"Name" : "qathet E", "Type" : "", "RegionalDistrictCode" : "7"}, {"Name" : "Sechelt (Part)", "Type" : "", "RegionalDistrictCode" : "7"}, {"Name" : "Sliammon 1", "Type" : "", "RegionalDistrictCode" : "7"}, {"Name" : "Alert Bay", "Type" : "", "RegionalDistrictCode" : "8"}, {"Name" : "Alert Bay", "Type" : "", "RegionalDistrictCode" : "8"}, {"Name" : "Dead Point 5", "Type" : "", "RegionalDistrictCode" : "8"}, {"Name" : "Fort Rupert 1", "Type" : "", "RegionalDistrictCode" : "8"}, {"Name" : "Gwayasdums 1", "Type" : "", "RegionalDistrictCode" : "8"}, {"Name" : "Hope Island 1", "Type" : "", "RegionalDistrictCode" : "8"}, {"Name" : "Hopetown 10A", "Type" : "", "RegionalDistrictCode" : "8"}, {"Name" : "Kippase 2", "Type" : "", "RegionalDistrictCode" : "8"}, {"Name" : "Mount Waddington A", "Type" : "", "RegionalDistrictCode" : "8"}, {"Name" : "Mount Waddington B", "Type" : "", "RegionalDistrictCode" : "8"}, {"Name" : "Mount Waddington C", "Type" : "", "RegionalDistrictCode" : "8"}, {"Name" : "Mount Waddington D", "Type" : "", "RegionalDistrictCode" : "8"}, {"Name" : "Nimpkish 2", "Type" : "", "RegionalDistrictCode" : "8"}, {"Name" : "Port Alice", "Type" : "", "RegionalDistrictCode" : "8"}, {"Name" : "Port Hardy", "Type" : "", "RegionalDistrictCode" : "8"}, {"Name" : "Port McNeill", "Type" : "", "RegionalDistrictCode" : "8"}, {"Name" : "Quaee 7", "Type" : "", "RegionalDistrictCode" : "8"}, {"Name" : "Quatsino Subdivision 18", "Type" : "", "RegionalDistrictCode" : "8"}, {"Name" : "Thomas Point 5", "Type" : "", "RegionalDistrictCode" : "8"}, {"Name" : "Thomas Point 5A", "Type" : "", "RegionalDistrictCode" : "8"}, {"Name" : "Tsulquate 4", "Type" : "", "RegionalDistrictCode" : "8"}, {"Name" : "Bella Bella 1", "Type" : "", "RegionalDistrictCode" : "9"}, {"Name" : "Bella Coola 1", "Type" : "", "RegionalDistrictCode" : "9"}, {"Name" : "Central Coast A", "Type" : "", "RegionalDistrictCode" : "9"}, {"Name" : "Central Coast C", "Type" : "", "RegionalDistrictCode" : "9"}, {"Name" : "Central Coast D", "Type" : "", "RegionalDistrictCode" : "9"}, {"Name" : "Central Coast E", "Type" : "", "RegionalDistrictCode" : "9"}, {"Name" : "Katit 1", "Type" : "", "RegionalDistrictCode" : "9"}]'; BEGIN -- Insert into "Communities" table INSERT INTO public."Communities" @@ -16,4 +19,4 @@ BEGIN pg_catalog.now() FROM jsonb_array_elements(json_data::jsonb) AS data; -END $$; +END $$; \ No newline at end of file From 0676e963f4c607bc30a67c98bf319a238740d754 Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Fri, 27 Feb 2026 13:57:58 -0800 Subject: [PATCH 091/191] AB#30429 codeQL suggestions --- .../ApplicantProfileContactService.cs | 12 ++---------- .../ApplicantProfile/ContactInfoDataProvider.cs | 8 ++++++-- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs index b53732447c..eba51fa136 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs @@ -63,10 +63,6 @@ join contact in contactsQuery on link.ContactId equals contact.Id /// public async Task> GetApplicationContactsBySubjectAsync(string subject) { - var normalizedSubject = subject.Contains('@') - ? subject[..subject.IndexOf('@')].ToUpperInvariant() - : subject.ToUpperInvariant(); - var submissionsQuery = await applicationFormSubmissionRepository.GetQueryableAsync(); var applicationContactsQuery = await applicationContactRepository.GetQueryableAsync(); var applicationsQuery = await applicationRepository.GetQueryableAsync(); @@ -75,7 +71,7 @@ public async Task> GetApplicationContactsBySubjectAsync from submission in submissionsQuery join appContact in applicationContactsQuery on submission.ApplicationId equals appContact.ApplicationId join application in applicationsQuery on submission.ApplicationId equals application.Id - where submission.OidcSub == normalizedSubject + where submission.OidcSub == subject select new ContactInfoItemDto { ContactId = appContact.Id, @@ -98,10 +94,6 @@ join application in applicationsQuery on submission.ApplicationId equals applica /// public async Task> GetApplicantAgentContactsBySubjectAsync(string subject) { - var normalizedSubject = subject.Contains('@') - ? subject[..subject.IndexOf('@')].ToUpperInvariant() - : subject.ToUpperInvariant(); - var submissionsQuery = await applicationFormSubmissionRepository.GetQueryableAsync(); var agentsQuery = await applicantAgentRepository.GetQueryableAsync(); var applicationsQuery = await applicationRepository.GetQueryableAsync(); @@ -110,7 +102,7 @@ public async Task> GetApplicantAgentContactsBySubjectAs from submission in submissionsQuery join agent in agentsQuery on submission.ApplicationId equals agent.ApplicationId join application in applicationsQuery on submission.ApplicationId equals application.Id - where submission.OidcSub == normalizedSubject + where submission.OidcSub == subject select new ContactInfoItemDto { ContactId = agent.Id, diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ContactInfoDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ContactInfoDataProvider.cs index 09b6245c75..e028bb1b27 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ContactInfoDataProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ContactInfoDataProvider.cs @@ -33,10 +33,14 @@ public async Task GetDataAsync(ApplicantProfileInfoRequ var profileContacts = await applicantProfileContactService.GetProfileContactsAsync(request.ProfileId); dto.Contacts.AddRange(profileContacts); - var applicationContacts = await applicantProfileContactService.GetApplicationContactsBySubjectAsync(request.Subject); + var normalizedSubject = request.Subject.Contains('@') + ? request.Subject[..request.Subject.IndexOf('@')].ToUpperInvariant() + : request.Subject.ToUpperInvariant(); + + var applicationContacts = await applicantProfileContactService.GetApplicationContactsBySubjectAsync(normalizedSubject); dto.Contacts.AddRange(applicationContacts); - var agentContacts = await applicantProfileContactService.GetApplicantAgentContactsBySubjectAsync(request.Subject); + var agentContacts = await applicantProfileContactService.GetApplicantAgentContactsBySubjectAsync(normalizedSubject); dto.Contacts.AddRange(agentContacts); } From 24a97259985924f97297a085741ea02d065967d5 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 27 Feb 2026 14:19:13 -0800 Subject: [PATCH 092/191] AB#32004 SonarQube fixes --- .../AI/OpenAIService.cs | 4 +- .../AI/TextExtractionService.cs | 53 +++++++++++++------ .../AssessmentScoresWidgetViewComponent.cs | 8 ++- 3 files changed, 42 insertions(+), 23 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 f6f9a72f40..ad7786c4b8 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs @@ -195,9 +195,9 @@ public async Task AnalyzeApplicationAsync(string applicationContent, Lis using var schemaDoc = JsonDocument.Parse(formFieldConfiguration); schemaPayload = schemaDoc.RootElement.Clone(); } - catch (JsonException) + catch (JsonException ex) { - _logger.LogWarning("Invalid form field configuration JSON. Using empty schema payload."); + _logger.LogWarning(ex, "Invalid form field configuration JSON. Using empty schema payload."); } } 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 3c2b3f2b36..d55efddd12 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/TextExtractionService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/TextExtractionService.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Logging; using System; using System.IO; +using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -9,7 +10,7 @@ namespace Unity.GrantManager.AI { - public class TextExtractionService : ITextExtractionService, ITransientDependency + public partial class TextExtractionService : ITextExtractionService, ITransientDependency { private const int MaxExtractedTextLength = 50000; private readonly ILogger _logger; @@ -126,16 +127,16 @@ private string ExtractTextFromPdfFile(string fileName, byte[] fileContent) using var document = PdfDocument.Open(stream); var builder = new StringBuilder(); - foreach (var page in document.GetPages()) + foreach (var pageText in document.GetPages().Select(page => page.Text)) { if (builder.Length >= MaxExtractedTextLength) { break; } - if (!string.IsNullOrWhiteSpace(page.Text)) + if (!string.IsNullOrWhiteSpace(pageText)) { - builder.AppendLine(page.Text); + builder.AppendLine(pageText); } } @@ -180,18 +181,14 @@ private static string NormalizeExtractedText(string text) .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"); + normalized = LowerToUpperWordBoundaryRegex().Replace(normalized, " "); + normalized = PunctuationToWordBoundaryRegex().Replace(normalized, " "); + normalized = ColonDashSpacingRegex().Replace(normalized, ": - "); + normalized = HyphenSpacingRegex().Replace(normalized, " - "); + normalized = KeywordBoundaryRegex().Replace(normalized, " "); + normalized = MultipleSpacesRegex().Replace(normalized, " "); + normalized = NewlineWhitespaceRegex().Replace(normalized, "\n"); + normalized = MultipleNewlinesRegex().Replace(normalized, "\n"); return normalized.Trim(); } @@ -229,5 +226,29 @@ private static string RemoveLeadingFileNameArtifact(string text, string fileName return text; } + + [GeneratedRegex(@"(?<=[a-z])(?=[A-Z])")] + private static partial Regex LowerToUpperWordBoundaryRegex(); + + [GeneratedRegex(@"(?<=[\.\,\:\;\)])(?=[A-Za-z0-9])")] + private static partial Regex PunctuationToWordBoundaryRegex(); + + [GeneratedRegex(@":-")] + private static partial Regex ColonDashSpacingRegex(); + + [GeneratedRegex(@"(?<=\S)- (?=[A-Za-z])")] + private static partial Regex HyphenSpacingRegex(); + + [GeneratedRegex(@"(?<=[a-z])(?=(project|funding|budget|community|summary|notes|details|planning|outcomes|background|services)\b)", RegexOptions.IgnoreCase)] + private static partial Regex KeywordBoundaryRegex(); + + [GeneratedRegex(@"[ \t]+")] + private static partial Regex MultipleSpacesRegex(); + + [GeneratedRegex(@"\n\s*")] + private static partial Regex NewlineWhitespaceRegex(); + + [GeneratedRegex(@"\n{2,}")] + private static partial Regex MultipleNewlinesRegex(); } } 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 35236847d0..4e9bb37e79 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 @@ -135,12 +135,10 @@ private static void ResolveAiAnswer(Dictionary aiAnswers, Q { question.AICitation = rationaleProp.ToString(); } - if (aiAnswerValue.TryGetProperty("confidence", out var confidenceProp)) + if (aiAnswerValue.TryGetProperty("confidence", out var confidenceProp) && + confidenceProp.TryGetInt32(out var confidence)) { - if (confidenceProp.TryGetInt32(out var confidence)) - { - question.AIConfidence = Math.Clamp(confidence, 0, 100); - } + question.AIConfidence = Math.Clamp(confidence, 0, 100); } } else From f98ae3feab25eeb19de74fb25f1f251075fb2ad8 Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Fri, 27 Feb 2026 14:56:03 -0800 Subject: [PATCH 093/191] AB#30429 fix unit tests after codeQL suggestions --- .../Contacts/ContactInfoDataProviderTests.cs | 12 +- .../Contacts/ContactInfoServiceTests.cs | 130 +----------------- 2 files changed, 12 insertions(+), 130 deletions(-) diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoDataProviderTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoDataProviderTests.cs index b7690ddfac..dde051f27b 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoDataProviderTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoDataProviderTests.cs @@ -77,7 +77,7 @@ public async Task GetDataAsync_ShouldCallGetApplicationContactsWithSubject() await _provider.GetDataAsync(request); // Assert - await _applicantProfileContactService.Received(1).GetApplicationContactsBySubjectAsync(request.Subject); + await _applicantProfileContactService.Received(1).GetApplicationContactsBySubjectAsync("TESTUSER"); } [Fact] @@ -90,7 +90,7 @@ public async Task GetDataAsync_ShouldCallGetApplicantAgentContactsWithSubject() await _provider.GetDataAsync(request); // Assert - await _applicantProfileContactService.Received(1).GetApplicantAgentContactsBySubjectAsync(request.Subject); + await _applicantProfileContactService.Received(1).GetApplicantAgentContactsBySubjectAsync("TESTUSER"); } [Fact] @@ -112,8 +112,8 @@ public async Task GetDataAsync_ShouldCombineAllContactSets() new() { ContactId = Guid.NewGuid(), Name = "Agent Contact 1", IsEditable = false, ContactType = "ApplicantAgent" } }; _applicantProfileContactService.GetProfileContactsAsync(request.ProfileId).Returns(profileContacts); - _applicantProfileContactService.GetApplicationContactsBySubjectAsync(request.Subject).Returns(appContacts); - _applicantProfileContactService.GetApplicantAgentContactsBySubjectAsync(request.Subject).Returns(agentContacts); + _applicantProfileContactService.GetApplicationContactsBySubjectAsync("TESTUSER").Returns(appContacts); + _applicantProfileContactService.GetApplicantAgentContactsBySubjectAsync("TESTUSER").Returns(agentContacts); // Act var result = await _provider.GetDataAsync(request); @@ -165,9 +165,9 @@ public async Task GetDataAsync_ContactsShouldAppearInExpectedOrder() }; _applicantProfileContactService.GetProfileContactsAsync(request.ProfileId) .Returns(new List { profileContact }); - _applicantProfileContactService.GetApplicationContactsBySubjectAsync(request.Subject) + _applicantProfileContactService.GetApplicationContactsBySubjectAsync("TESTUSER") .Returns(new List { appContact }); - _applicantProfileContactService.GetApplicantAgentContactsBySubjectAsync(request.Subject) + _applicantProfileContactService.GetApplicantAgentContactsBySubjectAsync("TESTUSER") .Returns(new List { agentContact }); // Act diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoServiceTests.cs index 2002e056d8..874f16fd3f 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoServiceTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoServiceTests.cs @@ -162,7 +162,7 @@ public async Task GetApplicationContactsBySubjectAsync_WithMatchingSubmission_Sh _applicationRepository.GetQueryableAsync().Returns(applications); // Act - var result = await _service.GetApplicationContactsBySubjectAsync("testuser@idir"); + var result = await _service.GetApplicationContactsBySubjectAsync("TESTUSER"); // Assert result.Count.ShouldBe(1); @@ -181,124 +181,6 @@ public async Task GetApplicationContactsBySubjectAsync_WithMatchingSubmission_Sh contact.ReferenceNo.ShouldBe("REF-001"); } - [Fact] - public async Task GetApplicationContactsBySubjectAsync_ShouldMatchCaseInsensitively() - { - // Arrange - var applicationId = Guid.NewGuid(); - - var submissions = new[] - { - new ApplicationFormSubmission - { - OidcSub = "TESTUSER", - ApplicationId = applicationId, - ApplicantId = Guid.NewGuid(), - ApplicationFormId = Guid.NewGuid() - } - }.AsAsyncQueryable(); - - var applicationContacts = new[] - { - WithId(new ApplicationContact - { - ApplicationId = applicationId, - ContactFullName = "Case Test", - ContactType = "ADDITIONAL_CONTACT" - }, Guid.NewGuid()) - }.AsAsyncQueryable(); - - _submissionRepository.GetQueryableAsync().Returns(submissions); - _applicationContactRepository.GetQueryableAsync().Returns(applicationContacts); - _applicationRepository.GetQueryableAsync().Returns( - new[] { WithId(new Application(), applicationId) }.AsAsyncQueryable()); - - // Act - var result = await _service.GetApplicationContactsBySubjectAsync("testuser@IDIR"); - - // Assert - result.Count.ShouldBe(1); - } - - [Fact] - public async Task GetApplicationContactsBySubjectAsync_ShouldStripDomainFromSubject() - { - // Arrange - var applicationId = Guid.NewGuid(); - - var submissions = new[] - { - new ApplicationFormSubmission - { - OidcSub = "MYUSER", - ApplicationId = applicationId, - ApplicantId = Guid.NewGuid(), - ApplicationFormId = Guid.NewGuid() - } - }.AsAsyncQueryable(); - - var applicationContacts = new[] - { - WithId(new ApplicationContact - { - ApplicationId = applicationId, - ContactFullName = "Domain Strip Test", - ContactType = "ADDITIONAL_CONTACT" - }, Guid.NewGuid()) - }.AsAsyncQueryable(); - - _submissionRepository.GetQueryableAsync().Returns(submissions); - _applicationContactRepository.GetQueryableAsync().Returns(applicationContacts); - _applicationRepository.GetQueryableAsync().Returns( - new[] { WithId(new Application(), applicationId) }.AsAsyncQueryable()); - - // Act - var result = await _service.GetApplicationContactsBySubjectAsync("myuser@differentdomain"); - - // Assert - result.Count.ShouldBe(1); - result[0].Name.ShouldBe("Domain Strip Test"); - } - - [Fact] - public async Task GetApplicationContactsBySubjectAsync_WithSubjectWithoutAtSign_ShouldStillMatch() - { - // Arrange - var applicationId = Guid.NewGuid(); - - var submissions = new[] - { - new ApplicationFormSubmission - { - OidcSub = "PLAINUSER", - ApplicationId = applicationId, - ApplicantId = Guid.NewGuid(), - ApplicationFormId = Guid.NewGuid() - } - }.AsAsyncQueryable(); - - var applicationContacts = new[] - { - WithId(new ApplicationContact - { - ApplicationId = applicationId, - ContactFullName = "Plain User Contact", - ContactType = "ADDITIONAL_CONTACT" - }, Guid.NewGuid()) - }.AsAsyncQueryable(); - - _submissionRepository.GetQueryableAsync().Returns(submissions); - _applicationContactRepository.GetQueryableAsync().Returns(applicationContacts); - _applicationRepository.GetQueryableAsync().Returns( - new[] { WithId(new Application(), applicationId) }.AsAsyncQueryable()); - - // Act - var result = await _service.GetApplicationContactsBySubjectAsync("plainuser"); - - // Assert - result.Count.ShouldBe(1); - } - [Fact] public async Task GetApplicationContactsBySubjectAsync_WithNonMatchingSubject_ShouldReturnEmpty() { @@ -331,7 +213,7 @@ public async Task GetApplicationContactsBySubjectAsync_WithNonMatchingSubject_Sh new[] { WithId(new Application(), applicationId) }.AsAsyncQueryable()); // Act - var result = await _service.GetApplicationContactsBySubjectAsync("differentuser@idir"); + var result = await _service.GetApplicationContactsBySubjectAsync("DIFFERENTUSER"); // Assert result.ShouldBeEmpty(); @@ -349,7 +231,7 @@ public async Task GetApplicationContactsBySubjectAsync_WithNoSubmissions_ShouldR .Returns(Array.Empty().AsAsyncQueryable()); // Act - var result = await _service.GetApplicationContactsBySubjectAsync("testuser@idir"); + var result = await _service.GetApplicationContactsBySubjectAsync("TESTUSER"); // Assert result.ShouldBeEmpty(); @@ -406,7 +288,7 @@ public async Task GetApplicationContactsBySubjectAsync_WithMultipleSubmissions_S }.AsAsyncQueryable()); // Act - var result = await _service.GetApplicationContactsBySubjectAsync("testuser@idir"); + var result = await _service.GetApplicationContactsBySubjectAsync("TESTUSER"); // Assert result.Count.ShouldBe(2); @@ -454,7 +336,7 @@ public async Task GetApplicantAgentContactsBySubjectAsync_WithMatchingSubmission new[] { WithId(new Application { ReferenceNo = "REF-AGENT-001" }, applicationId) }.AsAsyncQueryable()); // Act - var result = await _service.GetApplicantAgentContactsBySubjectAsync("testuser@idir"); + var result = await _service.GetApplicantAgentContactsBySubjectAsync("TESTUSER"); // Assert result.Count.ShouldBe(1); @@ -486,7 +368,7 @@ public async Task GetApplicantAgentContactsBySubjectAsync_WithNoMatchingSubmissi .Returns(Array.Empty().AsAsyncQueryable()); // Act - var result = await _service.GetApplicantAgentContactsBySubjectAsync("testuser@idir"); + var result = await _service.GetApplicantAgentContactsBySubjectAsync("TESTUSER"); // Assert result.ShouldBeEmpty(); From d3474e24cf903f7e4bba9cd0df5cec7aaba12b4f Mon Sep 17 00:00:00 2001 From: Armin Hasanpour Date: Fri, 27 Feb 2026 15:31:38 -0800 Subject: [PATCH 094/191] Added AI Scoring user. --- .../Assessments/AIScoringConstants.cs | 12 + .../Assessments/Assessment.cs | 10 +- .../GrantManagerDataSeederContributor.cs | 23 +- ..._AddIsAiAssessmentToAssessment.Designer.cs | 4575 +++++++++++++++++ ...227210826_AddIsAiAssessmentToAssessment.cs | 49 + .../GrantTenantDbContextModelSnapshot.cs | 6 +- 6 files changed, 4669 insertions(+), 6 deletions(-) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Assessments/AIScoringConstants.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260227210826_AddIsAiAssessmentToAssessment.Designer.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260227210826_AddIsAiAssessmentToAssessment.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Assessments/AIScoringConstants.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Assessments/AIScoringConstants.cs new file mode 100644 index 0000000000..d74b69ae8a --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Assessments/AIScoringConstants.cs @@ -0,0 +1,12 @@ +using System; + +namespace Unity.GrantManager.Assessments; + +public static class AIScoringConstants +{ + // Well-known fixed GUID for the AI Scoring Person record (one per tenant) + public static readonly Guid AiPersonId = new("00000000-0000-0000-0000-000000000001"); + public const string AiOidcSub = "ai-scoring"; + public const string AiDisplayName = "AI Scoring"; + public const string AiBadge = "AI"; +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Assessments/Assessment.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Assessments/Assessment.cs index e4c0edab9f..3f573a3e6b 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Assessments/Assessment.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Assessments/Assessment.cs @@ -27,11 +27,13 @@ public virtual Application Application public bool? ApprovalRecommended { get; set; } + public bool IsAiAssessment { get; set; } + public AssessmentState Status { get; private set; } - public int? FinancialAnalysis { get; set; } - public int? EconomicImpact { get; set; } - public int? InclusiveGrowth { get; set; } + public int? FinancialAnalysis { get; set; } + public int? EconomicImpact { get; set; } + public int? InclusiveGrowth { get; set; } public int? CleanGrowth { get; set; } @@ -86,4 +88,4 @@ private void OnReopened() EndDate = null; } } - + diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/GrantManagerDataSeederContributor.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/GrantManagerDataSeederContributor.cs index a3ee4b5a92..6291c358cb 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/GrantManagerDataSeederContributor.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/GrantManagerDataSeederContributor.cs @@ -1,7 +1,9 @@ using System.Collections.Generic; using System.Threading.Tasks; using Unity.GrantManager.Applications; +using Unity.GrantManager.Assessments; using Unity.GrantManager.GrantApplications; +using Unity.GrantManager.Identity; using Volo.Abp.Data; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories; @@ -9,7 +11,8 @@ namespace Unity.GrantManager; public class GrantManagerDataSeederContributor( - IApplicationStatusRepository applicationStatusRepository) : IDataSeedContributor, ITransientDependency + IApplicationStatusRepository applicationStatusRepository, + IPersonRepository personRepository) : IDataSeedContributor, ITransientDependency { public static class GrantApplicationStates { @@ -37,6 +40,7 @@ public async Task SeedAsync(DataSeedContext context) } await SeedApplicationStatusAsync(); + await SeedAiScoringPersonAsync(context.TenantId); } @@ -67,4 +71,21 @@ private async Task SeedApplicationStatusAsync() } } } + + private async Task SeedAiScoringPersonAsync(System.Guid? tenantId) + { + var existing = await personRepository.FirstOrDefaultAsync(p => p.Id == AIScoringConstants.AiPersonId); + if (existing == null) + { + await personRepository.InsertAsync(new Person + { + Id = AIScoringConstants.AiPersonId, + OidcSub = AIScoringConstants.AiOidcSub, + OidcDisplayName = AIScoringConstants.AiDisplayName, + FullName = AIScoringConstants.AiDisplayName, + Badge = AIScoringConstants.AiBadge, + TenantId = tenantId + }); + } + } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260227210826_AddIsAiAssessmentToAssessment.Designer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260227210826_AddIsAiAssessmentToAssessment.Designer.cs new file mode 100644 index 0000000000..c68720af3d --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260227210826_AddIsAiAssessmentToAssessment.Designer.cs @@ -0,0 +1,4575 @@ +// +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("20260227210826_AddIsAiAssessmentToAssessment")] + partial class AddIsAiAssessmentToAssessment + { + /// + 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") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PreventPayment") + .HasColumnType("boolean"); + + b.Property("RenderFormIoToHtml") + .HasColumnType("boolean"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("SuffixType") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IntakeId"); + + b.HasIndex("ParentFormId"); + + b.ToTable("ApplicationForms", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormSubmission", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormVersionId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ChefsSubmissionGuid") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormVersionId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OidcSub") + .IsRequired() + .HasColumnType("text"); + + b.Property("RenderedHTML") + .HasColumnType("text"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Submission") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormSubmissions", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormVersion", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("AvailableChefsFields") + .HasColumnType("text"); + + b.Property("ChefsApplicationFormGuid") + .HasColumnType("text"); + + b.Property("ChefsFormVersionGuid") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormSchema") + .HasColumnType("jsonb"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SubmissionHeaderMapping") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormVersion", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationLink", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LinkType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("Related"); + + b.Property("LinkedApplicationId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationLinks", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationStatus", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExternalStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("InternalStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("StatusCode") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("StatusCode") + .IsUnique(); + + b.ToTable("ApplicationStatuses", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationTags", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TagId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("TagId"); + + b.ToTable("ApplicationTags", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AssessmentAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AssessmentId"); + + b.ToTable("AssessmentAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Assessments.Assessment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ApprovalRecommended") + .HasColumnType("boolean"); + + b.Property("AssessorId") + .HasColumnType("uuid"); + + b.Property("CleanGrowth") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("EconomicImpact") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FinancialAnalysis") + .HasColumnType("integer"); + + b.Property("InclusiveGrowth") + .HasColumnType("integer"); + + b.Property("IsAiAssessment") + .HasColumnType("boolean"); + + b.Property("IsComplete") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("AssessorId"); + + b.ToTable("Assessments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.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/20260227210826_AddIsAiAssessmentToAssessment.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260227210826_AddIsAiAssessmentToAssessment.cs new file mode 100644 index 0000000000..1f02f87527 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260227210826_AddIsAiAssessmentToAssessment.cs @@ -0,0 +1,49 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + /// + public partial class AddIsAiAssessmentToAssessment : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsAiAssessment", + table: "Assessments", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AlterColumn( + name: "Prefix", + table: "ApplicationForms", + type: "character varying(100)", + maxLength: 100, + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsAiAssessment", + table: "Assessments"); + + migrationBuilder.AlterColumn( + name: "Prefix", + table: "ApplicationForms", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(100)", + oldMaxLength: 100, + oldNullable: true); + } + } +} 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 be337f9d19..a7ab067ffc 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 @@ -1667,7 +1667,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("numeric"); b.Property("Prefix") - .HasColumnType("text"); + .HasMaxLength(100) + .HasColumnType("character varying(100)"); b.Property("PreventPayment") .HasColumnType("boolean"); @@ -2136,6 +2137,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("InclusiveGrowth") .HasColumnType("integer"); + b.Property("IsAiAssessment") + .HasColumnType("boolean"); + b.Property("IsComplete") .HasColumnType("boolean"); From 8316a275fb49dfdd1d97deb6c78198671a8292c1 Mon Sep 17 00:00:00 2001 From: Armin Hasanpour Date: Fri, 27 Feb 2026 15:48:02 -0800 Subject: [PATCH 095/191] AB#31871: Add AI Scoring to Feature Flags --- .../GrantManagerFeaturesDefinitionProvider.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantManagerFeaturesDefinitionProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantManagerFeaturesDefinitionProvider.cs index 4a983ed329..c829cf27ea 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantManagerFeaturesDefinitionProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantManagerFeaturesDefinitionProvider.cs @@ -56,6 +56,12 @@ public override void Define(IFeatureDefinitionContext context) displayName: LocalizableString .Create("AI Reporting"), valueType: new ToggleStringValueType()); + + myGroup.AddFeature("Unity.AI.Scoring", + defaultValue: defaultValue, + displayName: LocalizableString + .Create("AI Scoring"), + valueType: new ToggleStringValueType()); } } } From c6cce846b4aba25e40061882350de2a3e65d3fbe Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Fri, 27 Feb 2026 16:19:26 -0800 Subject: [PATCH 096/191] AB#26878: Set font size standard for $ symbol for currencies --- .../src/Unity.Theme.UX2/wwwroot/themes/ux2/unity-styles.css | 1 + 1 file changed, 1 insertion(+) diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/unity-styles.css b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/unity-styles.css index 382cabb108..a054982d3c 100644 --- a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/unity-styles.css +++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/unity-styles.css @@ -265,6 +265,7 @@ table.dataTable { margin-top: 32px; margin-left: 12px; color: var(--bc-colors-grey-text-300); + font-size: var(--bc-font-size); } .unity-input-group input { From 36703dc1c53f3968a5c5fbf21851f9421b51dc74 Mon Sep 17 00:00:00 2001 From: David Bright Date: Fri, 27 Feb 2026 16:21:04 -0800 Subject: [PATCH 097/191] Moved the Scoresheet dropdown from under Available Worksheets to be above the Assessment Info fields Added a read only Select input that displays the selected form version that is selected on the Mapping tab. --- .../CustomFields/CustomFieldsViewComponent.cs | 1 + .../CustomFields/CustomFieldsViewModel.cs | 1 + .../Components/CustomFields/Default.cshtml | 30 ++++++++++++------- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/CustomFieldsViewComponent.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/CustomFieldsViewComponent.cs index 66ab7c583e..5510427687 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/CustomFieldsViewComponent.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/CustomFieldsViewComponent.cs @@ -40,6 +40,7 @@ public async Task InvokeAsync(string? formVersionId, strin var formVersion = await applicationFormVersionAppService.GetByChefsFormVersionId(model.ChefsFormVersionId); model.Version = formVersion?.Version?.ToString(); + model.ChefsFormPublished = formVersion?.Published; model.WorksheetLinks = await worksheetLinkAppService.GetListByCorrelationAsync(formVersion?.Id ?? Guid.Empty, CorrelationConsts.FormVersion); model.PublishedWorksheets = [.. (await worksheetListAppService.GetListAsync()) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/CustomFieldsViewModel.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/CustomFieldsViewModel.cs index 246c37200f..95169c2de8 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/CustomFieldsViewModel.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/CustomFieldsViewModel.cs @@ -16,6 +16,7 @@ public class CustomFieldsViewModel [JsonRequired] public Guid ChefsFormVersionId { get; set; } + public bool? ChefsFormPublished { get; set; } public string? FormName { get; set; } public string? Version { get; set; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/Default.cshtml index 253c1c48b4..ae5195af02 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/Default.cshtml @@ -5,7 +5,26 @@ Layout = null; } -

Selected Form Version: @Model.Version

+
+
+ +
+ + @if (Model?.ChefsFormPublished == true) + { + + } +
+
+
+ + + + +
+
@@ -29,15 +48,6 @@
} - -
-
- - - - -
-
From 6d2c5c6245bfff91dd28b79b2e5c1b8aabbe0269 Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Fri, 27 Feb 2026 16:22:09 -0800 Subject: [PATCH 098/191] AB#32111 use x-unit test collections --- .../CheckboxGroupDefinitionWidgetTests.cs | 9 ++-- .../Components/CheckboxGroupWidgetTests.cs | 9 ++-- .../Components/CheckboxWidgetTests.cs | 9 ++-- .../Components/ComponentTestFixture.cs | 44 ++++++++++++++++++ .../CurrencyDefinitionWidgetTests.cs | 10 ++--- .../Components/CurrencyWidgetTests.cs | 9 ++-- .../DataGridDefinitionWidgetTests.cs | 9 ++-- .../Components/DataGridWidgetTests.cs | 9 ++-- .../Components/RadioWidgetTests.cs | 9 ++-- .../SelectListDefinitionWidgetTests.cs | 9 ++-- .../Components/SelectListWidgetTests.cs | 9 ++-- .../TextAreaDefinitionWidgetTests.cs | 9 ++-- .../Components/TextAreaWidgetTests.cs | 9 ++-- .../Components/TextDefinitionWidgetTests.cs | 9 ++-- .../Components/YesNoWidgetTests.cs | 9 ++-- .../Components/ApplicantInfoWidgetTests.cs | 8 ++-- .../ApplicationContactWidgetTests.cs | 8 +--- .../Components/ApplicationTagWidgetTests.cs | 2 +- .../Components/AssessmentScoresWidgetTests.cs | 2 +- .../Components/AttachmentControllerTests.cs | 2 +- .../FundingAgreementInfoWidgetTests.cs | 8 ++-- .../Components/ProjectInfoWidgetTests.cs | 28 +++--------- .../Components/SummaryWidgetTests.cs | 8 ++-- .../Pages/Index_Tests.cs | 20 +++++++-- .../WebTestFixture.cs | 45 +++++++++++++++++++ 25 files changed, 201 insertions(+), 101 deletions(-) create mode 100644 applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/ComponentTestFixture.cs create mode 100644 applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/WebTestFixture.cs diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/CheckboxGroupDefinitionWidgetTests.cs b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/CheckboxGroupDefinitionWidgetTests.cs index 3ae0ec372f..2f6a2b4c0b 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/CheckboxGroupDefinitionWidgetTests.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/CheckboxGroupDefinitionWidgetTests.cs @@ -1,22 +1,23 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewComponents; +using Microsoft.Extensions.DependencyInjection; using Shouldly; using System.Text.Json; using Unity.Flex.Web.Views.Shared.Components.CheckboxGroupDefinitionWidget; using Unity.Flex.Worksheets.Definitions; -using Unity.GrantManager; using Volo.Abp.DependencyInjection; namespace Unity.Flex.Web.Tests.Components { - public class CheckboxGroupDefinitionWidgetTests : GrantManagerWebTestBase + [Collection(ComponentTestCollection.Name)] + public class CheckboxGroupDefinitionWidgetTests { private readonly IAbpLazyServiceProvider lazyServiceProvider; - public CheckboxGroupDefinitionWidgetTests() + public CheckboxGroupDefinitionWidgetTests(ComponentTestFixture fixture) { - lazyServiceProvider = GetRequiredService(); + lazyServiceProvider = fixture.Services.GetRequiredService(); } [Fact] diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/CheckboxGroupWidgetTests.cs b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/CheckboxGroupWidgetTests.cs index 2dad6b56e1..ce6dc3d95a 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/CheckboxGroupWidgetTests.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/CheckboxGroupWidgetTests.cs @@ -1,21 +1,22 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewComponents; +using Microsoft.Extensions.DependencyInjection; using Shouldly; using Unity.Flex.Web.Views.Shared.Components.CheckboxGroupWidget; using Unity.Flex.Web.Views.Shared.Components.WorksheetInstanceWidget.ViewModels; -using Unity.GrantManager; using Volo.Abp.DependencyInjection; namespace Unity.Flex.Web.Tests.Components { - public class CheckboxGroupWidgetTests : GrantManagerWebTestBase + [Collection(ComponentTestCollection.Name)] + public class CheckboxGroupWidgetTests { private readonly IAbpLazyServiceProvider lazyServiceProvider; - public CheckboxGroupWidgetTests() + public CheckboxGroupWidgetTests(ComponentTestFixture fixture) { - lazyServiceProvider = GetRequiredService(); + lazyServiceProvider = fixture.Services.GetRequiredService(); } [Fact] diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/CheckboxWidgetTests.cs b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/CheckboxWidgetTests.cs index d50ed7d576..6a9353f98d 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/CheckboxWidgetTests.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/CheckboxWidgetTests.cs @@ -1,21 +1,22 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewComponents; +using Microsoft.Extensions.DependencyInjection; using Shouldly; using Unity.Flex.Web.Views.Shared.Components.CheckboxWidget; using Unity.Flex.Web.Views.Shared.Components.WorksheetInstanceWidget.ViewModels; -using Unity.GrantManager; using Volo.Abp.DependencyInjection; namespace Unity.Flex.Web.Tests.Components { - public class CheckboxWidgetTests : GrantManagerWebTestBase + [Collection(ComponentTestCollection.Name)] + public class CheckboxWidgetTests { private readonly IAbpLazyServiceProvider lazyServiceProvider; - public CheckboxWidgetTests() + public CheckboxWidgetTests(ComponentTestFixture fixture) { - lazyServiceProvider = GetRequiredService(); + lazyServiceProvider = fixture.Services.GetRequiredService(); } [Fact] diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/ComponentTestFixture.cs b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/ComponentTestFixture.cs new file mode 100644 index 0000000000..1baa8c9e8f --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/ComponentTestFixture.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Unity.GrantManager; +using Unity.Modules.Shared.MessageBrokers.RabbitMQ.Interfaces; + +namespace Unity.Flex.Web.Tests.Components; + +public class ComponentTestFixture : WebApplicationFactory +{ + protected override void ConfigureWebHost(Microsoft.AspNetCore.Hosting.IWebHostBuilder builder) + { + builder.ConfigureServices(services => + { + var hostedServices = services + .Where(d => typeof(IHostedService) + .IsAssignableFrom(d.ServiceType)) + .ToList(); + + foreach (var descriptor in hostedServices) + { + if (descriptor.ImplementationType?.Namespace?.Contains("RabbitMQ") == true) + { + services.Remove(descriptor); + } + } + +#if WINDOWS + services.RemoveAll(); +#endif + + services.Replace( + ServiceDescriptor.Singleton() + ); + }); + } +} + +[CollectionDefinition(ComponentTestCollection.Name)] +public class ComponentTestCollection : ICollectionFixture +{ + public const string Name = "ComponentTests"; +} diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/CurrencyDefinitionWidgetTests.cs b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/CurrencyDefinitionWidgetTests.cs index 8815b47308..54a780f2f8 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/CurrencyDefinitionWidgetTests.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/CurrencyDefinitionWidgetTests.cs @@ -1,23 +1,23 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewComponents; +using Microsoft.Extensions.DependencyInjection; using Shouldly; using System.Text.Json; using Unity.Flex.Web.Views.Shared.Components.CurrencyDefinitionWidget; using Unity.Flex.Worksheets.Definitions; -using Unity.GrantManager; using Volo.Abp.DependencyInjection; namespace Unity.Flex.Web.Tests.Components { - public class CurrencyDefinitionWidgetTests : GrantManagerWebTestBase + [Collection(ComponentTestCollection.Name)] + public class CurrencyDefinitionWidgetTests { private readonly IAbpLazyServiceProvider lazyServiceProvider; - public CurrencyDefinitionWidgetTests() + public CurrencyDefinitionWidgetTests(ComponentTestFixture fixture) { - // Lazily resolve services needed by the ViewComponent - lazyServiceProvider = GetRequiredService(); + lazyServiceProvider = fixture.Services.GetRequiredService(); } [Fact] diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/CurrencyWidgetTests.cs b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/CurrencyWidgetTests.cs index d566f5dc24..8389402f30 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/CurrencyWidgetTests.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/CurrencyWidgetTests.cs @@ -1,21 +1,22 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewComponents; +using Microsoft.Extensions.DependencyInjection; using Shouldly; using Unity.Flex.Web.Views.Shared.Components.CurrencyWidget; using Unity.Flex.Web.Views.Shared.Components.WorksheetInstanceWidget.ViewModels; -using Unity.GrantManager; using Volo.Abp.DependencyInjection; namespace Unity.Flex.Web.Tests.Components { - public class CurrencyWidgetTests : GrantManagerWebTestBase + [Collection(ComponentTestCollection.Name)] + public class CurrencyWidgetTests { private readonly IAbpLazyServiceProvider lazyServiceProvider; - public CurrencyWidgetTests() + public CurrencyWidgetTests(ComponentTestFixture fixture) { - lazyServiceProvider = GetRequiredService(); + lazyServiceProvider = fixture.Services.GetRequiredService(); } [Fact] diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/DataGridDefinitionWidgetTests.cs b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/DataGridDefinitionWidgetTests.cs index 62ec7d7221..35e7663b12 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/DataGridDefinitionWidgetTests.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/DataGridDefinitionWidgetTests.cs @@ -1,22 +1,23 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewComponents; +using Microsoft.Extensions.DependencyInjection; using Shouldly; using System.Text.Json; using Unity.Flex.Web.Views.Shared.Components.DataGridDefinitionWidget; using Unity.Flex.Worksheets.Definitions; -using Unity.GrantManager; using Volo.Abp.DependencyInjection; namespace Unity.Flex.Web.Tests.Components { - public class DataGridDefinitionWidgetTests : GrantManagerWebTestBase + [Collection(ComponentTestCollection.Name)] + public class DataGridDefinitionWidgetTests { private readonly IAbpLazyServiceProvider lazyServiceProvider; - public DataGridDefinitionWidgetTests() + public DataGridDefinitionWidgetTests(ComponentTestFixture fixture) { - lazyServiceProvider = GetRequiredService(); + lazyServiceProvider = fixture.Services.GetRequiredService(); } [Fact] diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/DataGridWidgetTests.cs b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/DataGridWidgetTests.cs index 6e18ab44d9..0fab52c5df 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/DataGridWidgetTests.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/DataGridWidgetTests.cs @@ -1,21 +1,22 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewComponents; +using Microsoft.Extensions.DependencyInjection; using Shouldly; using Unity.Flex.Web.Views.Shared.Components.DataGridWidget; using Unity.Flex.Web.Views.Shared.Components.WorksheetInstanceWidget.ViewModels; -using Unity.GrantManager; using Volo.Abp.DependencyInjection; namespace Unity.Flex.Web.Tests.Components { - public class DataGridWidgetTests : GrantManagerWebTestBase + [Collection(ComponentTestCollection.Name)] + public class DataGridWidgetTests { private readonly IAbpLazyServiceProvider lazyServiceProvider; - public DataGridWidgetTests() + public DataGridWidgetTests(ComponentTestFixture fixture) { - lazyServiceProvider = GetRequiredService(); + lazyServiceProvider = fixture.Services.GetRequiredService(); } [Fact] diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/RadioWidgetTests.cs b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/RadioWidgetTests.cs index 67c69a0182..c79d831814 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/RadioWidgetTests.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/RadioWidgetTests.cs @@ -1,21 +1,22 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewComponents; +using Microsoft.Extensions.DependencyInjection; using Shouldly; using Unity.Flex.Web.Views.Shared.Components.RadioWidget; using Unity.Flex.Web.Views.Shared.Components.WorksheetInstanceWidget.ViewModels; -using Unity.GrantManager; using Volo.Abp.DependencyInjection; namespace Unity.Flex.Web.Tests.Components { - public class RadioWidgetTests : GrantManagerWebTestBase + [Collection(ComponentTestCollection.Name)] + public class RadioWidgetTests { private readonly IAbpLazyServiceProvider lazyServiceProvider; - public RadioWidgetTests() + public RadioWidgetTests(ComponentTestFixture fixture) { - lazyServiceProvider = GetRequiredService(); + lazyServiceProvider = fixture.Services.GetRequiredService(); } [Fact] diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/SelectListDefinitionWidgetTests.cs b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/SelectListDefinitionWidgetTests.cs index a63c2c36ea..95ef22e1d9 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/SelectListDefinitionWidgetTests.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/SelectListDefinitionWidgetTests.cs @@ -1,22 +1,23 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewComponents; +using Microsoft.Extensions.DependencyInjection; using Shouldly; using System.Text.Json; using Unity.Flex.Web.Views.Shared.Components.SelectListDefinitionWidget; using Unity.Flex.Worksheets.Definitions; -using Unity.GrantManager; using Volo.Abp.DependencyInjection; namespace Unity.Flex.Web.Tests.Components { - public class SelectListDefinitionWidgetTests : GrantManagerWebTestBase + [Collection(ComponentTestCollection.Name)] + public class SelectListDefinitionWidgetTests { private readonly IAbpLazyServiceProvider lazyServiceProvider; - public SelectListDefinitionWidgetTests() + public SelectListDefinitionWidgetTests(ComponentTestFixture fixture) { - lazyServiceProvider = GetRequiredService(); + lazyServiceProvider = fixture.Services.GetRequiredService(); } [Fact] diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/SelectListWidgetTests.cs b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/SelectListWidgetTests.cs index 167b2d1825..245fb1e37a 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/SelectListWidgetTests.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/SelectListWidgetTests.cs @@ -1,21 +1,22 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewComponents; +using Microsoft.Extensions.DependencyInjection; using Shouldly; using Unity.Flex.Web.Views.Shared.Components.SelectListWidget; using Unity.Flex.Web.Views.Shared.Components.WorksheetInstanceWidget.ViewModels; -using Unity.GrantManager; using Volo.Abp.DependencyInjection; namespace Unity.Flex.Web.Tests.Components { - public class SelectListWidgetTests : GrantManagerWebTestBase + [Collection(ComponentTestCollection.Name)] + public class SelectListWidgetTests { private readonly IAbpLazyServiceProvider lazyServiceProvider; - public SelectListWidgetTests() + public SelectListWidgetTests(ComponentTestFixture fixture) { - lazyServiceProvider = GetRequiredService(); + lazyServiceProvider = fixture.Services.GetRequiredService(); } [Fact] diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/TextAreaDefinitionWidgetTests.cs b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/TextAreaDefinitionWidgetTests.cs index 029d545867..66d091906a 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/TextAreaDefinitionWidgetTests.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/TextAreaDefinitionWidgetTests.cs @@ -1,22 +1,23 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewComponents; +using Microsoft.Extensions.DependencyInjection; using Shouldly; using System.Text.Json; using Unity.Flex.Web.Views.Shared.Components.TextAreaDefinitionWidget; using Unity.Flex.Worksheets.Definitions; -using Unity.GrantManager; using Volo.Abp.DependencyInjection; namespace Unity.Flex.Web.Tests.Components { - public class TextAreaDefinitionWidgetTests : GrantManagerWebTestBase + [Collection(ComponentTestCollection.Name)] + public class TextAreaDefinitionWidgetTests { private readonly IAbpLazyServiceProvider lazyServiceProvider; - public TextAreaDefinitionWidgetTests() + public TextAreaDefinitionWidgetTests(ComponentTestFixture fixture) { - lazyServiceProvider = GetRequiredService(); + lazyServiceProvider = fixture.Services.GetRequiredService(); } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/TextAreaWidgetTests.cs b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/TextAreaWidgetTests.cs index f8cbf63d70..5e1f095186 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/TextAreaWidgetTests.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/TextAreaWidgetTests.cs @@ -1,21 +1,22 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewComponents; +using Microsoft.Extensions.DependencyInjection; using Shouldly; using Unity.Flex.Web.Views.Shared.Components.TextAreaWidget; using Unity.Flex.Web.Views.Shared.Components.WorksheetInstanceWidget.ViewModels; -using Unity.GrantManager; using Volo.Abp.DependencyInjection; namespace Unity.Flex.Web.Tests.Components { - public class TextAreaWidgetTests : GrantManagerWebTestBase + [Collection(ComponentTestCollection.Name)] + public class TextAreaWidgetTests { private readonly IAbpLazyServiceProvider lazyServiceProvider; - public TextAreaWidgetTests() + public TextAreaWidgetTests(ComponentTestFixture fixture) { - lazyServiceProvider = GetRequiredService(); + lazyServiceProvider = fixture.Services.GetRequiredService(); } [Fact] diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/TextDefinitionWidgetTests.cs b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/TextDefinitionWidgetTests.cs index 065af83a54..ea5de73024 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/TextDefinitionWidgetTests.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/TextDefinitionWidgetTests.cs @@ -1,22 +1,23 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewComponents; +using Microsoft.Extensions.DependencyInjection; using Shouldly; using System.Text.Json; using Unity.Flex.Web.Views.Shared.Components.TextDefinitionWidget; using Unity.Flex.Worksheets.Definitions; -using Unity.GrantManager; using Volo.Abp.DependencyInjection; namespace Unity.Flex.Web.Tests.Components { - public class TextDefinitionWidgetTests : GrantManagerWebTestBase + [Collection(ComponentTestCollection.Name)] + public class TextDefinitionWidgetTests { private readonly IAbpLazyServiceProvider lazyServiceProvider; - public TextDefinitionWidgetTests() + public TextDefinitionWidgetTests(ComponentTestFixture fixture) { - lazyServiceProvider = GetRequiredService(); + lazyServiceProvider = fixture.Services.GetRequiredService(); } [Fact] diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/YesNoWidgetTests.cs b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/YesNoWidgetTests.cs index 145cb3ea91..c56a199807 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/YesNoWidgetTests.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/YesNoWidgetTests.cs @@ -1,21 +1,22 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewComponents; +using Microsoft.Extensions.DependencyInjection; using Shouldly; using Unity.Flex.Web.Views.Shared.Components.WorksheetInstanceWidget.ViewModels; using Unity.Flex.Web.Views.Shared.Components.YesNoWidget; -using Unity.GrantManager; using Volo.Abp.DependencyInjection; namespace Unity.Flex.Web.Tests.Components { - public class YesNoWidgetTests : GrantManagerWebTestBase + [Collection(ComponentTestCollection.Name)] + public class YesNoWidgetTests { private readonly IAbpLazyServiceProvider lazyServiceProvider; - public YesNoWidgetTests() + public YesNoWidgetTests(ComponentTestFixture fixture) { - lazyServiceProvider = GetRequiredService(); + lazyServiceProvider = fixture.Services.GetRequiredService(); } [Fact] diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/ApplicantInfoWidgetTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/ApplicantInfoWidgetTests.cs index 77551b3d5f..018a689117 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/ApplicantInfoWidgetTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/ApplicantInfoWidgetTests.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewComponents; +using Microsoft.Extensions.DependencyInjection; using NSubstitute; using Shouldly; using System; @@ -15,13 +16,14 @@ namespace Unity.GrantManager.Components { - public class ApplicantInfoWidgetTests : GrantManagerWebTestBase + [Collection(WebTestCollection.Name)] + public class ApplicantInfoWidgetTests { private readonly IAbpLazyServiceProvider lazyServiceProvider; - public ApplicantInfoWidgetTests() + public ApplicantInfoWidgetTests(WebTestFixture fixture) { - lazyServiceProvider = GetRequiredService(); + lazyServiceProvider = fixture.Services.GetRequiredService(); } [Fact] diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/ApplicationContactWidgetTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/ApplicationContactWidgetTests.cs index 9b59458b0f..ce359e4af3 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/ApplicationContactWidgetTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/ApplicationContactWidgetTests.cs @@ -12,14 +12,8 @@ namespace Unity.GrantManager.Components { - public class ApplicationContactWidgetTests : GrantManagerWebTestBase + public class ApplicationContactWidgetTests { - public ApplicationContactWidgetTests() - { - // Disable logging to avoid disposed logger errors during tests - Environment.SetEnvironmentVariable("Logging:LogLevel:Default", "None"); - } - [Fact] public async Task ApplicationContactWidgetReturnsStatus() { diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/ApplicationTagWidgetTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/ApplicationTagWidgetTests.cs index 42d1b1006c..ea2d82a8bc 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/ApplicationTagWidgetTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/ApplicationTagWidgetTests.cs @@ -13,7 +13,7 @@ namespace Unity.GrantManager.Components { - public class ApplicationTagsWidgetTests : GrantManagerWebTestBase + public class ApplicationTagsWidgetTests { [Fact] public async Task ApplicationTagsWidgetReturnsStatus() diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/AssessmentScoresWidgetTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/AssessmentScoresWidgetTests.cs index b46bac9677..d100c709b3 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/AssessmentScoresWidgetTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/AssessmentScoresWidgetTests.cs @@ -14,7 +14,7 @@ namespace Unity.GrantManager.Components { - public class AssessmentScoresWidgetTests : GrantManagerWebTestBase + public class AssessmentScoresWidgetTests { [Fact] public async Task AssessmentScoresWidgetReturnsStatus() diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/AttachmentControllerTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/AttachmentControllerTests.cs index bf2318ffb0..5433be8771 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/AttachmentControllerTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/AttachmentControllerTests.cs @@ -13,7 +13,7 @@ namespace Unity.GrantManager.Components { - public class AttachmentControllerTests : GrantManagerWebTestBase + public class AttachmentControllerTests { [Fact] public async Task UploadApplicationAttachments_InvalidInput_ReturnsBadRequest() diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/FundingAgreementInfoWidgetTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/FundingAgreementInfoWidgetTests.cs index 3e301ff662..e7b1b902dd 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/FundingAgreementInfoWidgetTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/FundingAgreementInfoWidgetTests.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewComponents; +using Microsoft.Extensions.DependencyInjection; using NSubstitute; using Shouldly; using System; @@ -12,13 +13,14 @@ namespace Unity.GrantManager.Components { - public class FundingAgreementInfoWidgetTests : GrantManagerWebTestBase + [Collection(WebTestCollection.Name)] + public class FundingAgreementInfoWidgetTests { private readonly IAbpLazyServiceProvider lazyServiceProvider; - public FundingAgreementInfoWidgetTests() + public FundingAgreementInfoWidgetTests(WebTestFixture fixture) { - lazyServiceProvider = GetRequiredService(); + lazyServiceProvider = fixture.Services.GetRequiredService(); } [Fact] diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/ProjectInfoWidgetTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/ProjectInfoWidgetTests.cs index 1ef14f6c3c..44a4ed0ea3 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/ProjectInfoWidgetTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/ProjectInfoWidgetTests.cs @@ -11,31 +11,20 @@ using Volo.Abp.DependencyInjection; using Xunit; using Microsoft.AspNetCore.Authorization; - using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; namespace Unity.GrantManager.Components { - public class ProjectInfoWidgetTests : GrantManagerWebTestBase + [Collection(WebTestCollection.Name)] + public class ProjectInfoWidgetTests { private readonly IAbpLazyServiceProvider lazyServiceProvider; + private readonly IAuthorizationService authorizationService; - public ProjectInfoWidgetTests() + public ProjectInfoWidgetTests(WebTestFixture fixture) { - // Remove EventLog logger provider to prevent ObjectDisposedException during tests - var loggerFactory = GetRequiredService(); - foreach (var provider in loggerFactory - .GetType() - .GetField("_providers", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) - ?.GetValue(loggerFactory) as System.Collections.Generic.List ?? new System.Collections.Generic.List()) - { - if (provider.GetType().Name.Contains("EventLog")) - { - provider.Dispose(); - } - } - lazyServiceProvider = GetRequiredService(); + lazyServiceProvider = fixture.Services.GetRequiredService(); + authorizationService = fixture.Services.GetRequiredService(); } [Fact] @@ -56,9 +45,6 @@ public async Task ContactInfoReturnsStatus() var electoralDistrictService = Substitute.For(); var regionalDistrictService = Substitute.For(); var communitiesService = Substitute.For(); - var authorizationService = GetRequiredService(); - - var viewContext = new ViewContext { HttpContext = new DefaultHttpContext() @@ -68,7 +54,7 @@ public async Task ContactInfoReturnsStatus() ViewContext = viewContext }; - var viewComponent = new ProjectInfoViewComponent(appService, economicRegionService, electoralDistrictService, regionalDistrictService, communitiesService, authorizationService) + var viewComponent = new ProjectInfoViewComponent(appService, economicRegionService, electoralDistrictService, regionalDistrictService, communitiesService, this.authorizationService) { ViewComponentContext = viewComponentContext, LazyServiceProvider = lazyServiceProvider diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/SummaryWidgetTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/SummaryWidgetTests.cs index 6844c62bce..11d6f24612 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/SummaryWidgetTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/SummaryWidgetTests.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewComponents; +using Microsoft.Extensions.DependencyInjection; using NSubstitute; using Shouldly; using System; @@ -14,13 +15,14 @@ namespace Unity.GrantManager.Components { - public class SummaryWidgetTests : GrantManagerWebTestBase + [Collection(WebTestCollection.Name)] + public class SummaryWidgetTests { private readonly IAbpLazyServiceProvider lazyServiceProvider; - public SummaryWidgetTests() + public SummaryWidgetTests(WebTestFixture fixture) { - lazyServiceProvider = GetRequiredService(); + lazyServiceProvider = fixture.Services.GetRequiredService(); } [Fact] diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Pages/Index_Tests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Pages/Index_Tests.cs index 367e6cde38..7d69880307 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Pages/Index_Tests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Pages/Index_Tests.cs @@ -1,15 +1,27 @@ -using System.Threading.Tasks; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; using Shouldly; using Xunit; namespace Unity.GrantManager.Pages; -public class Index_Tests : GrantManagerWebTestBase +[Collection(WebTestCollection.Name)] +public class Index_Tests { + private readonly HttpClient _client; + + public Index_Tests(WebTestFixture fixture) + { + _client = fixture.CreateClient(); + } + [Fact] public async Task Welcome_Page() { - var response = await GetResponseAsStringAsync("/"); - response.ShouldNotBeNull(); + var response = await _client.GetAsync("/"); + response.StatusCode.ShouldBe(HttpStatusCode.OK); + var content = await response.Content.ReadAsStringAsync(); + content.ShouldNotBeNull(); } } diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/WebTestFixture.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/WebTestFixture.cs new file mode 100644 index 0000000000..1f8912471d --- /dev/null +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/WebTestFixture.cs @@ -0,0 +1,45 @@ +using System.Linq; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Unity.Modules.Shared.MessageBrokers.RabbitMQ.Interfaces; +using Xunit; + +namespace Unity.GrantManager; + +public class WebTestFixture : WebApplicationFactory +{ + protected override void ConfigureWebHost(Microsoft.AspNetCore.Hosting.IWebHostBuilder builder) + { + builder.ConfigureServices(services => + { + var hostedServices = services + .Where(d => typeof(IHostedService) + .IsAssignableFrom(d.ServiceType)) + .ToList(); + + foreach (var descriptor in hostedServices) + { + if (descriptor.ImplementationType?.Namespace?.Contains("RabbitMQ") == true) + { + services.Remove(descriptor); + } + } + +#if WINDOWS + services.RemoveAll(); +#endif + + services.Replace( + ServiceDescriptor.Singleton() + ); + }); + } +} + +[CollectionDefinition(WebTestCollection.Name)] +public class WebTestCollection : ICollectionFixture +{ + public const string Name = "WebTests"; +} From ce760cb67827dc4a9693dc0499af77ad5d1aa0d6 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 27 Feb 2026 16:25:11 -0800 Subject: [PATCH 099/191] AB#32008 Sonar fix simplify docx paragraph extraction loop --- .../AI/TextExtractionService.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 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 8de6b180ec..0f55a62e33 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/TextExtractionService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/TextExtractionService.cs @@ -166,11 +166,11 @@ private string ExtractTextFromWordDocx(byte[] fileContent) using var document = new XWPFDocument(stream); var parts = new List(); - foreach (var paragraph in document.Paragraphs.Take(MaxDocxParagraphs)) + foreach (var paragraphText in document.Paragraphs.Take(MaxDocxParagraphs).Select(paragraph => paragraph.ParagraphText)) { - if (!string.IsNullOrWhiteSpace(paragraph.ParagraphText)) + if (!string.IsNullOrWhiteSpace(paragraphText)) { - parts.Add(paragraph.ParagraphText); + parts.Add(paragraphText); } } From 608863d206734c26e7c56d662ff6dd9694f12221 Mon Sep 17 00:00:00 2001 From: Armin Hasanpour Date: Fri, 27 Feb 2026 16:25:17 -0800 Subject: [PATCH 100/191] AB#32021: Added AI Scoring to permissions --- .../GrantApplicationPermissionDefinitionProvider.cs | 5 +++++ .../Localization/GrantManager/en.json | 1 + .../Permissions/GrantApplicationPermissions.cs | 5 +++++ 3 files changed, 11 insertions(+) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantApplications/GrantApplicationPermissionDefinitionProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantApplications/GrantApplicationPermissionDefinitionProvider.cs index 428f8a5d69..54b25c9e1f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantApplications/GrantApplicationPermissionDefinitionProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantApplications/GrantApplicationPermissionDefinitionProvider.cs @@ -130,6 +130,11 @@ public override void Define(IPermissionDefinitionContext context) GrantApplicationPermissions.AI.AttachmentSummary.Default, L("Permission:AI.AttachmentSummary")) .RequireFeatures("Unity.AI.AttachmentSummaries"); + + aiPermissionsGroup.AddPermission( + GrantApplicationPermissions.AI.ScoringAssistant.Default, + L("Permission:AI.ScoringAssistant")) + .RequireFeatures("Unity.AI.Scoring"); } private static LocalizableString L(string name) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json index 667bc316f6..48cfff2966 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json @@ -154,6 +154,7 @@ "Permission:AI.Reporting": "AI Reporting", "Permission:AI.ApplicationAnalysis": "AI Application Analysis", "Permission:AI.AttachmentSummary": "AI Attachment Summary", + "Permission:AI.ScoringAssistant": "AI Scoring Assistant", "ApplicationForms": "Forms", "ApplicationForms:Description": "Description", diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/GrantApplicationPermissions.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/GrantApplicationPermissions.cs index b46f5c310c..4ce0b818e8 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/GrantApplicationPermissions.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/GrantApplicationPermissions.cs @@ -58,6 +58,11 @@ public static class AttachmentSummary { public const string Default = GroupName + ".AttachmentSummary"; } + + public static class ScoringAssistant + { + public const string Default = GroupName + ".ScoringAssistant"; + } } public static class Assignments From 253eabc16250f82efb8bb5a8b529120bd53de155 Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Fri, 27 Feb 2026 16:42:15 -0800 Subject: [PATCH 101/191] AB#32111 codeQL suggestions --- .../Components/ComponentTestFixture.cs | 7 ++++--- .../GrantManagerWebTestBase.cs | 11 ++++++----- .../Unity.GrantManager.Web.Tests/Pages/Index_Tests.cs | 2 +- .../Unity.GrantManager.Web.Tests/WebTestFixture.cs | 8 +++++--- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/ComponentTestFixture.cs b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/ComponentTestFixture.cs index 1baa8c9e8f..731cb41c9c 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/ComponentTestFixture.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/ComponentTestFixture.cs @@ -26,9 +26,10 @@ protected override void ConfigureWebHost(Microsoft.AspNetCore.Hosting.IWebHostBu } } -#if WINDOWS - services.RemoveAll(); -#endif +if (OperatingSystem.IsWindows()) + { + services.RemoveAll(); + } services.Replace( ServiceDescriptor.Singleton() diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/GrantManagerWebTestBase.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/GrantManagerWebTestBase.cs index 219a06211a..e543631bf6 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/GrantManagerWebTestBase.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/GrantManagerWebTestBase.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using System.Net; using System.Net.Http; using System.Text.Json; @@ -39,10 +40,10 @@ protected override void ConfigureServices(IServiceCollection services) } } -#if WINDOWS - // 🔹 Remove EventLog logger to avoid ObjectDisposedException in tests - services.RemoveAll(); -#endif +if (OperatingSystem.IsWindows()) + { + services.RemoveAll(); + } // // 🔹 Replace real channel provider with fake diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Pages/Index_Tests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Pages/Index_Tests.cs index 7d69880307..9e1c80056b 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Pages/Index_Tests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Pages/Index_Tests.cs @@ -22,6 +22,6 @@ public async Task Welcome_Page() var response = await _client.GetAsync("/"); response.StatusCode.ShouldBe(HttpStatusCode.OK); var content = await response.Content.ReadAsStringAsync(); - content.ShouldNotBeNull(); + content.ShouldNotBeNullOrEmpty(); } } diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/WebTestFixture.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/WebTestFixture.cs index 1f8912471d..1c3cc469b8 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/WebTestFixture.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/WebTestFixture.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; @@ -27,9 +28,10 @@ protected override void ConfigureWebHost(Microsoft.AspNetCore.Hosting.IWebHostBu } } -#if WINDOWS - services.RemoveAll(); -#endif +if (OperatingSystem.IsWindows()) + { + services.RemoveAll(); + } services.Replace( ServiceDescriptor.Singleton() From f6a73540a2f98468c2d3885131c2479554f07133 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 20 Feb 2026 17:25:33 -0800 Subject: [PATCH 102/191] AB#32010 Refactor AI contracts and add compatibility request wrappers --- .../AI/AIAttachmentPromptItem.cs | 8 +++ .../AI/AICompletionRequest.cs | 10 ++++ .../AI/AIJsonKeys.cs | 20 +++++++ .../AI/ApplicationAnalysisRequest.cs | 19 +++++++ .../AI/ApplicationAnalysisResponse.cs | 35 ++++++++++++ .../AI/AttachmentSummaryRequest.cs | 9 ++++ .../AI/IAIService.cs | 7 +++ .../AI/ScoresheetSectionRequest.cs | 13 +++++ .../AI/OpenAIService.cs | 54 +++++++++++++++++++ 9 files changed, 175 insertions(+) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AIAttachmentPromptItem.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AICompletionRequest.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AIJsonKeys.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/ApplicationAnalysisRequest.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/ApplicationAnalysisResponse.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AttachmentSummaryRequest.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/ScoresheetSectionRequest.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AIAttachmentPromptItem.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AIAttachmentPromptItem.cs new file mode 100644 index 0000000000..2918f03e89 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AIAttachmentPromptItem.cs @@ -0,0 +1,8 @@ +namespace Unity.GrantManager.AI +{ + public class AIAttachmentPromptItem + { + public string Name { get; set; } = string.Empty; + public string Summary { get; set; } = string.Empty; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AICompletionRequest.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AICompletionRequest.cs new file mode 100644 index 0000000000..8598d33002 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AICompletionRequest.cs @@ -0,0 +1,10 @@ +namespace Unity.GrantManager.AI +{ + public class AICompletionRequest + { + public string UserPrompt { get; set; } = string.Empty; + public string? SystemPrompt { get; set; } + public int MaxTokens { get; set; } = 150; + public double? Temperature { get; set; } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AIJsonKeys.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AIJsonKeys.cs new file mode 100644 index 0000000000..5ebbf4df9b --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AIJsonKeys.cs @@ -0,0 +1,20 @@ +namespace Unity.GrantManager.AI +{ + public static class AIJsonKeys + { + public const string Rating = "rating"; + public const string Errors = "errors"; + public const string Warnings = "warnings"; + public const string Summaries = "summaries"; + public const string Dismissed = "dismissed"; + + public const string Id = "id"; + public const string Title = "title"; + public const string Detail = "detail"; + public const string Summary = "summary"; + + public const string Answer = "answer"; + public const string Rationale = "rationale"; + public const string Confidence = "confidence"; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/ApplicationAnalysisRequest.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/ApplicationAnalysisRequest.cs new file mode 100644 index 0000000000..bf676f06ad --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/ApplicationAnalysisRequest.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Text.Json; + +namespace Unity.GrantManager.AI +{ + public class ApplicationAnalysisRequest + { + public JsonElement Schema { get; set; } + public JsonElement Data { get; set; } + public List Attachments { get; set; } = new(); + public string? Rubric { get; set; } + } + + public class ApplicationAnalysisAttachment + { + public string Name { get; set; } = string.Empty; + public string Summary { get; set; } = string.Empty; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/ApplicationAnalysisResponse.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/ApplicationAnalysisResponse.cs new file mode 100644 index 0000000000..f766a1f4d8 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/ApplicationAnalysisResponse.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Unity.GrantManager.AI +{ + public class ApplicationAnalysisResponse + { + [JsonPropertyName(AIJsonKeys.Rating)] + public string? Rating { get; set; } + + [JsonPropertyName(AIJsonKeys.Errors)] + public List Errors { get; set; } = new(); + + [JsonPropertyName(AIJsonKeys.Warnings)] + public List Warnings { get; set; } = new(); + + [JsonPropertyName(AIJsonKeys.Summaries)] + public List Summaries { get; set; } = new(); + + [JsonPropertyName(AIJsonKeys.Dismissed)] + public List Dismissed { get; set; } = new(); + } + + public class ApplicationAnalysisFinding + { + [JsonPropertyName(AIJsonKeys.Id)] + public string? Id { get; set; } + + [JsonPropertyName(AIJsonKeys.Title)] + public string? Title { get; set; } + + [JsonPropertyName(AIJsonKeys.Detail)] + public string? Detail { get; set; } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AttachmentSummaryRequest.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AttachmentSummaryRequest.cs new file mode 100644 index 0000000000..2cce56ae77 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AttachmentSummaryRequest.cs @@ -0,0 +1,9 @@ +namespace Unity.GrantManager.AI +{ + public class AttachmentSummaryRequest + { + public string FileName { get; set; } = string.Empty; + public byte[] FileContent { get; set; } = System.Array.Empty(); + public string ContentType { get; set; } = "application/octet-stream"; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/IAIService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/IAIService.cs index e4c3d26ac8..b8cb35632c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/IAIService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/IAIService.cs @@ -6,6 +6,13 @@ namespace Unity.GrantManager.AI public interface IAIService { Task IsAvailableAsync(); + + Task GenerateCompletionAsync(AICompletionRequest request); + Task GenerateAttachmentSummaryAsync(AttachmentSummaryRequest request); + Task GenerateApplicationAnalysisAsync(ApplicationAnalysisRequest request); + Task GenerateScoresheetSectionAnswersAsync(ScoresheetSectionRequest request); + + // Legacy compatibility methods retained until flow orchestration refactor. Task GenerateSummaryAsync(string content, string? prompt = null, int maxTokens = 150); Task GenerateAttachmentSummaryAsync(string fileName, byte[] fileContent, string contentType); Task AnalyzeApplicationAsync(string applicationContent, List attachmentSummaries, string rubric, string? formFieldConfiguration = null); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/ScoresheetSectionRequest.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/ScoresheetSectionRequest.cs new file mode 100644 index 0000000000..f20d4935e8 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/ScoresheetSectionRequest.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Text.Json; + +namespace Unity.GrantManager.AI +{ + public class ScoresheetSectionRequest + { + public JsonElement Data { get; set; } + public List Attachments { get; set; } = new(); + public string SectionName { get; set; } = string.Empty; + public JsonElement SectionSchema { get; set; } + } +} 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 ad7786c4b8..3547fc8dcc 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs @@ -54,6 +54,60 @@ public Task IsAvailableAsync() return Task.FromResult(true); } + public Task GenerateCompletionAsync(AICompletionRequest request) + { + return GenerateSummaryAsync( + request?.UserPrompt ?? string.Empty, + request?.SystemPrompt, + request?.MaxTokens ?? 150); + } + + public Task GenerateAttachmentSummaryAsync(AttachmentSummaryRequest request) + { + return GenerateAttachmentSummaryAsync( + request?.FileName ?? string.Empty, + request?.FileContent ?? Array.Empty(), + request?.ContentType ?? "application/octet-stream"); + } + + public Task GenerateApplicationAnalysisAsync(ApplicationAnalysisRequest request) + { + var dataJson = JsonSerializer.Serialize(request.Data, new JsonSerializerOptions { WriteIndented = true }); + var schemaJson = JsonSerializer.Serialize(request.Schema, new JsonSerializerOptions { WriteIndented = true }); + + var attachmentSummaries = request.Attachments + .Select(a => $"{a.Name}: {a.Summary}") + .ToList(); + + var applicationContent = $@"DATA +{dataJson}"; + + var formFieldConfiguration = $@"SCHEMA +{schemaJson}"; + + return AnalyzeApplicationAsync( + applicationContent, + attachmentSummaries, + request.Rubric ?? string.Empty, + formFieldConfiguration); + } + + public Task GenerateScoresheetSectionAnswersAsync(ScoresheetSectionRequest request) + { + var dataJson = JsonSerializer.Serialize(request.Data, new JsonSerializerOptions { WriteIndented = true }); + var sectionJson = JsonSerializer.Serialize(request.SectionSchema, new JsonSerializerOptions { WriteIndented = true }); + + var attachmentSummaries = request.Attachments + .Select(a => $"{a.Name}: {a.Summary}") + .ToList(); + + return GenerateScoresheetSectionAnswersAsync( + dataJson, + attachmentSummaries, + sectionJson, + request.SectionName); + } + public async Task GenerateSummaryAsync(string content, string? prompt = null, int maxTokens = 150) { if (string.IsNullOrEmpty(ApiKey)) From d6d0cf8d2d2dc24154706fe7f489504c7222e8c8 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 20 Feb 2026 17:56:10 -0800 Subject: [PATCH 103/191] Add typed AI analysis data to GrantApplicationDto --- .../GrantApplications/GrantApplicationDto.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/GrantApplicationDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/GrantApplicationDto.cs index 31a35a8739..f2906d1166 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/GrantApplicationDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/GrantApplicationDto.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Unity.GrantManager.AI; using Unity.GrantManager.ApplicationForms; using Volo.Abp.Application.Dtos; @@ -83,4 +84,5 @@ public class GrantApplicationDto : AuditedEntityDto public string? UnityApplicationId { get; set; } public string? ApplicantElectoralDistrict { get; set; } public string? AIAnalysis { get; set; } + public ApplicationAnalysisResponse? AIAnalysisData { get; set; } } From ceeb83fd9349f16ff8d083c16145deb514bb5982 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 27 Feb 2026 17:00:22 -0800 Subject: [PATCH 104/191] AB#32010 Sonar fixes for overload adjacency and serializer options reuse --- .../AI/IAIService.cs | 4 +- .../AI/OpenAIService.cs | 52 +++++++++---------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/IAIService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/IAIService.cs index b8cb35632c..ffddbbe6c9 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/IAIService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/IAIService.cs @@ -9,14 +9,14 @@ public interface IAIService Task GenerateCompletionAsync(AICompletionRequest request); Task GenerateAttachmentSummaryAsync(AttachmentSummaryRequest request); + Task GenerateAttachmentSummaryAsync(string fileName, byte[] fileContent, string contentType); Task GenerateApplicationAnalysisAsync(ApplicationAnalysisRequest request); Task GenerateScoresheetSectionAnswersAsync(ScoresheetSectionRequest request); + Task GenerateScoresheetSectionAnswersAsync(string applicationContent, List attachmentSummaries, string sectionJson, string sectionName); // Legacy compatibility methods retained until flow orchestration refactor. Task GenerateSummaryAsync(string content, string? prompt = null, int maxTokens = 150); - Task GenerateAttachmentSummaryAsync(string fileName, byte[] fileContent, string contentType); Task AnalyzeApplicationAsync(string applicationContent, List attachmentSummaries, string rubric, string? formFieldConfiguration = null); Task GenerateScoresheetAnswersAsync(string applicationContent, List attachmentSummaries, string scoresheetQuestions); - Task GenerateScoresheetSectionAnswersAsync(string applicationContent, List attachmentSummaries, string sectionJson, string sectionName); } } 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 3547fc8dcc..3830cd75e5 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs @@ -62,18 +62,10 @@ public Task GenerateCompletionAsync(AICompletionRequest request) request?.MaxTokens ?? 150); } - public Task GenerateAttachmentSummaryAsync(AttachmentSummaryRequest request) - { - return GenerateAttachmentSummaryAsync( - request?.FileName ?? string.Empty, - request?.FileContent ?? Array.Empty(), - request?.ContentType ?? "application/octet-stream"); - } - public Task GenerateApplicationAnalysisAsync(ApplicationAnalysisRequest request) { - var dataJson = JsonSerializer.Serialize(request.Data, new JsonSerializerOptions { WriteIndented = true }); - var schemaJson = JsonSerializer.Serialize(request.Schema, new JsonSerializerOptions { WriteIndented = true }); + var dataJson = JsonSerializer.Serialize(request.Data, JsonLogOptions); + var schemaJson = JsonSerializer.Serialize(request.Schema, JsonLogOptions); var attachmentSummaries = request.Attachments .Select(a => $"{a.Name}: {a.Summary}") @@ -92,22 +84,6 @@ public Task GenerateApplicationAnalysisAsync(ApplicationAnalysisRequest formFieldConfiguration); } - public Task GenerateScoresheetSectionAnswersAsync(ScoresheetSectionRequest request) - { - var dataJson = JsonSerializer.Serialize(request.Data, new JsonSerializerOptions { WriteIndented = true }); - var sectionJson = JsonSerializer.Serialize(request.SectionSchema, new JsonSerializerOptions { WriteIndented = true }); - - var attachmentSummaries = request.Attachments - .Select(a => $"{a.Name}: {a.Summary}") - .ToList(); - - return GenerateScoresheetSectionAnswersAsync( - dataJson, - attachmentSummaries, - sectionJson, - request.SectionName); - } - public async Task GenerateSummaryAsync(string content, string? prompt = null, int maxTokens = 150) { if (string.IsNullOrEmpty(ApiKey)) @@ -231,6 +207,14 @@ Produce a concise reviewer-facing summary of the provided attachment context. } } + public Task GenerateAttachmentSummaryAsync(AttachmentSummaryRequest request) + { + return GenerateAttachmentSummaryAsync( + request?.FileName ?? string.Empty, + request?.FileContent ?? Array.Empty(), + request?.ContentType ?? "application/octet-stream"); + } + public async Task AnalyzeApplicationAsync(string applicationContent, List attachmentSummaries, string rubric, string? formFieldConfiguration = null) { if (string.IsNullOrEmpty(ApiKey)) @@ -566,6 +550,22 @@ public async Task GenerateScoresheetSectionAnswersAsync(string applicati } } + public Task GenerateScoresheetSectionAnswersAsync(ScoresheetSectionRequest request) + { + var dataJson = JsonSerializer.Serialize(request.Data, JsonLogOptions); + var sectionJson = JsonSerializer.Serialize(request.SectionSchema, JsonLogOptions); + + var attachmentSummaries = request.Attachments + .Select(a => $"{a.Name}: {a.Summary}") + .ToList(); + + return GenerateScoresheetSectionAnswersAsync( + dataJson, + attachmentSummaries, + sectionJson, + request.SectionName); + } + private async Task LogPromptInputAsync(string promptType, string? systemPrompt, string userPrompt) { var formattedInput = FormatPromptInputForLog(systemPrompt, userPrompt); From 8f576e46d2295ee3877f0ec4a767eee2fe923782 Mon Sep 17 00:00:00 2001 From: David Bright Date: Fri, 27 Feb 2026 17:01:10 -0800 Subject: [PATCH 105/191] Quick suggested updates - Label for="" field and option value="" field --- .../Views/Shared/Components/CustomFields/Default.cshtml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/Default.cshtml index ae5195af02..9f001b64b6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/Default.cshtml @@ -7,10 +7,10 @@
- +
@if (Model?.ChefsFormPublished == true) { From 5e68d3978f9adbdc17a3504a6238d364a1ae860a Mon Sep 17 00:00:00 2001 From: David Bright Date: Fri, 27 Feb 2026 17:12:13 -0800 Subject: [PATCH 106/191] Added ! to @Model!.ScoresheetId to assert non-null value --- .../Views/Shared/Components/CustomFields/Default.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/Default.cshtml index 9f001b64b6..7e80979d48 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/Default.cshtml @@ -19,7 +19,7 @@
- + From 3dc5b40bcbbcb814c36aecc23d328f7f4c647e63 Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Fri, 27 Feb 2026 18:46:17 -0800 Subject: [PATCH 107/191] AB#26878: Applicant Profile Funding History Fix Bugs and Layout --- .../CreateFundingHistoryModal.cshtml | 57 ++++++++++++++++--- .../EditFundingHistoryModal.cshtml | 57 ++++++++++++++++--- .../ApplicantHistory/Default.cshtml | 3 + .../Components/ApplicantHistory/Default.js | 2 +- 4 files changed, 102 insertions(+), 17 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateFundingHistoryModal.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateFundingHistoryModal.cshtml index ca90bc019b..1e166143b2 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateFundingHistoryModal.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateFundingHistoryModal.cshtml @@ -5,31 +5,33 @@ Layout = null; } - +
- + + class="form-control" value="@Model.FundingHistoryForm?.GrantCategory" required /> +
Grant Category is required.
- + + class="form-control" inputmode="numeric" maxlength="4" + value="@Model.FundingHistoryForm?.FundingYear" required /> +
Funding Year is required.
-
+
@@ -78,3 +80,42 @@ + + diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditFundingHistoryModal.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditFundingHistoryModal.cshtml index 1deb5b35e0..39ec5fcf8f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditFundingHistoryModal.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditFundingHistoryModal.cshtml @@ -5,7 +5,7 @@ Layout = null; } -
+ @@ -13,24 +13,26 @@
- + + class="form-control" value="@Model.FundingHistoryForm?.GrantCategory" required /> +
Grant Category is required.
- + + class="form-control" inputmode="numeric" maxlength="4" + value="@Model.FundingHistoryForm?.FundingYear" required /> +
Funding Year is required.
-
+
@@ -79,3 +81,42 @@ + + diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.cshtml index caf33c13a5..25911b45b8 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.cshtml @@ -37,6 +37,7 @@
+