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/.gitignore b/.gitignore
index 841973049b..cf56f6bd31 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,7 @@
*.rsuser
*.userosscache
*.sln.docstates
+*.code-workspace
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
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.AutoUI/cypress/e2e/ApplicationsActionBar.cy.ts b/applications/Unity.AutoUI/cypress/e2e/ApplicationsActionBar.cy.ts
index 6d3cdd6eac..7bbdde7041 100644
--- a/applications/Unity.AutoUI/cypress/e2e/ApplicationsActionBar.cy.ts
+++ b/applications/Unity.AutoUI/cypress/e2e/ApplicationsActionBar.cy.ts
@@ -1,647 +1,260 @@
///
import { loginIfNeeded } from "../support/auth";
+import { ApplicationsListPage } from "../pages/ApplicationsListPage";
describe("Unity Login and check data from CHEFS", () => {
- const STANDARD_TIMEOUT = 20000;
-
- 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("Verifies the expected action buttons are visible when no rows are selected", () => {
- cy.get("#GrantApplicationsTable", { timeout: STANDARD_TIMEOUT }).should(
- "exist",
- );
-
- // Ensure we start from a clean selection state (0 selected)
- // (Using same "select all / deselect all" toggle approach as the working 1-row test)
- cy.get("div.dt-scroll-head thead input", { timeout: STANDARD_TIMEOUT })
- .should("exist")
- .click({ force: true })
- .click({ force: true });
-
- cy.get("#GrantApplicationsTable tbody tr.selected", {
- timeout: STANDARD_TIMEOUT,
- }).should("have.length", 0);
-
- // Filter button (left action bar group)
- cy.get("#btn-toggle-filter", { timeout: STANDARD_TIMEOUT }).should(
- "be.visible",
- );
-
- // Right-side buttons
- 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");
-
- // Optional sanity: action buttons that require selection should be disabled when none selected
- cy.get("#externalLink", { timeout: STANDARD_TIMEOUT }).should(
- "be.disabled",
- ); // Open
- cy.get("#assignApplication", { timeout: STANDARD_TIMEOUT }).should(
- "be.disabled",
- ); // Assign
- cy.get("#approveApplications", { timeout: STANDARD_TIMEOUT }).should(
- "be.disabled",
- ); // Approve
- cy.get("#tagApplication", { timeout: STANDARD_TIMEOUT }).should(
- "be.disabled",
- ); // Tags
- cy.get("#applicationPaymentRequest", {
- timeout: STANDARD_TIMEOUT,
- }).should("be.disabled"); // Payment
- cy.get("#applicationLink", { timeout: STANDARD_TIMEOUT }).should(
- "be.disabled",
- ); // Info
- });
-
- // With one row selected verify the visibility of Open, Assign, Approve, Tags, Payment, Info, Filter, Export, Save View, and Columns.
- it("Verifies the expected action buttons are visible when only one row is selected", () => {
- cy.get("#GrantApplicationsTable", { timeout: STANDARD_TIMEOUT }).should(
- "exist",
- );
-
- //Ensure we start from a clean selection state (0 selected)
- cy.get("div.dt-scroll-head thead input", { timeout: STANDARD_TIMEOUT })
- .should("exist")
- .click({ force: true })
- .click({ force: true });
-
- cy.get("#GrantApplicationsTable tbody tr.selected", {
- timeout: STANDARD_TIMEOUT,
- }).should("have.length", 0);
-
- // Select exactly 1 row (click a non-link cell, matching your earlier helper logic)
- cy.get("#GrantApplicationsTable tbody tr", { timeout: STANDARD_TIMEOUT })
- .should("have.length.greaterThan", 0)
- .first()
- .find("td")
- .not(":has(a)")
- .first()
- .click({ force: true });
-
- cy.get("#GrantApplicationsTable tbody tr.selected", {
- timeout: STANDARD_TIMEOUT,
- }).should("have.length", 1);
-
- // Action bar (left group)
- cy.get("#app_custom_buttons", { timeout: STANDARD_TIMEOUT })
- .should("exist")
- .scrollIntoView();
-
- // Left-side action buttons (actual IDs on this page)
- cy.get("#externalLink", { timeout: STANDARD_TIMEOUT }).should("be.visible"); // Open
- cy.get("#assignApplication", { timeout: STANDARD_TIMEOUT }).should(
- "be.visible",
- ); // Assign
- cy.get("#approveApplications", { timeout: STANDARD_TIMEOUT }).should(
- "be.visible",
- ); // Approve
- cy.get("#tagApplication", { timeout: STANDARD_TIMEOUT }).should("be.visible"); // Tags
- cy.get("#applicationPaymentRequest", {
- timeout: STANDARD_TIMEOUT,
- }).should("be.visible"); // Payment
- cy.get("#applicationLink", { timeout: STANDARD_TIMEOUT }).should(
- "be.visible",
- ); // Info
-
- // Filter button
- cy.get("#btn-toggle-filter", { timeout: STANDARD_TIMEOUT }).should(
- "be.visible",
- );
-
- // Right-side buttons
- 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");
- });
-
- it("Verifies the expected action buttons are visible when two rows are selected", () => {
- 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");
+ const page = new ApplicationsListPage();
+
+ // Column visibility test data - organized by scroll position for maintainability
+ const COLUMN_VISIBILITY_DATA = {
+ scrollPosition0: [
+ "Applicant Name",
+ "Category",
+ "Submission #",
+ "Submission Date",
+ "Status",
+ "Sub-Status",
+ "Community",
+ "Requested Amount",
+ "Approved Amount",
+ "Project Name",
+ "Applicant Id",
+ ],
+ scrollPosition1500: [
+ "Tags",
+ "Assignee",
+ "SubSector",
+ "Economic Region",
+ "Regional District",
+ "Registered Organization Number",
+ "Org Book Status",
+ ],
+ scrollPosition3000: [
+ "Project Start Date",
+ "Project End Date",
+ "Projected Funding Total",
+ "Total Paid Amount $",
+ "Project Electoral District",
+ "Applicant Electoral District",
+ ],
+ scrollPosition4500: [
+ "Forestry or Non-Forestry",
+ "Forestry Focus",
+ "Acquisition",
+ "City",
+ "Community Population",
+ "Likelihood of Funding",
+ "Total Score",
+ ],
+ scrollPosition6000: [
+ "Assessment Result",
+ "Recommended Amount",
+ "Due Date",
+ "Owner",
+ "Decision Date",
+ "Project Summary",
+ "Organization Type",
+ "Business Number",
+ ],
+ scrollPosition7500: [
+ "Due Diligence Status",
+ "Decline Rationale",
+ "Contact Full Name",
+ "Contact Title",
+ "Contact Email",
+ "Contact Business Phone",
+ "Contact Cell Phone",
+ ],
+ scrollPosition9000: [
+ "Signing Authority Full Name",
+ "Signing Authority Title",
+ "Signing Authority Email",
+ "Signing Authority Business Phone",
+ "Signing Authority Cell Phone",
+ "Place",
+ "Risk Ranking",
+ "Notes",
+ "Red-Stop",
+ "Indigenous",
+ "FYE Day",
+ "FYE Month",
+ "Payout",
+ "Unity Application Id",
+ ],
+ };
+
+ // Columns to toggle during the test - organized for scalability
+ const COLUMNS_TO_TOGGLE = {
+ // Columns that need single toggle (off by default, turn on)
+ 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",
+ ],
+ // Columns that need double toggle (on by default, toggle off then on)
+ 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 Quick Date Range filter", () => {
+ // Select "All time" from quick date range dropdown and verify table refreshes
+ page
+ .selectQuickDateRange("alltime")
+ .waitForTableRefresh()
+ .verifyQuickDateRangeValue("alltime");
+ });
+
+ // 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 using page object
+ page
+ .verifyTableHasData()
+ .selectMultipleRows([0, 1])
+ .verifyActionBarExists()
+ .clickPaymentButtonWithWait()
+ .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 (off by default, turn on)
+ 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);
});
- // 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 escapeRegex = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
-
- const clickColumnsItem = (label: string) => {
- // Case-insensitive exact match so DEV "ID" and PROD "Id" both work
- const re = new RegExp(`^\\s*${escapeRegex(label)}\\s*$`, "i");
- cy.contains("a.dropdown-item", re, { timeout: STANDARD_TIMEOUT })
- .should("exist")
- .scrollIntoView()
- .click({ force: true });
- };
-
- const normalize = (s: string) =>
- (s || "").replace(/\s+/g, " ").trim().toLowerCase();
-
- 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) => {
- const normTitles = titles.map(normalize);
- expected.forEach((e) => {
- const target = normalize(e);
- expect(
- normTitles,
- `visible headers should include "${e}" (case-insensitive)`,
- ).to.include(target);
- });
- });
- };
-
- 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");
+ // Close the columns menu
+ page.closeColumnsMenu();
- clickColumnsItem("Assessment Result");
+ // Verify columns by scrolling through the table horizontally
+ page
+ .scrollTableHorizontally(0)
+ .assertVisibleHeadersInclude(COLUMN_VISIBILITY_DATA.scrollPosition0);
- clickColumnsItem("Assignee");
- clickColumnsItem("Assignee");
+ page
+ .scrollTableHorizontally(1500)
+ .assertVisibleHeadersInclude(COLUMN_VISIBILITY_DATA.scrollPosition1500);
- clickColumnsItem("Business Number");
+ page
+ .scrollTableHorizontally(3000)
+ .assertVisibleHeadersInclude(COLUMN_VISIBILITY_DATA.scrollPosition3000);
- clickColumnsItem("Category");
- clickColumnsItem("Category");
+ page
+ .scrollTableHorizontally(4500)
+ .assertVisibleHeadersInclude(COLUMN_VISIBILITY_DATA.scrollPosition4500);
- clickColumnsItem("City");
+ page
+ .scrollTableHorizontally(6000)
+ .assertVisibleHeadersInclude(COLUMN_VISIBILITY_DATA.scrollPosition6000);
- clickColumnsItem("Community");
- clickColumnsItem("Community");
+ page
+ .scrollTableHorizontally(7500)
+ .assertVisibleHeadersInclude(COLUMN_VISIBILITY_DATA.scrollPosition7500);
- 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");
+ page
+ .scrollTableHorizontally(9000)
+ .assertVisibleHeadersInclude(COLUMN_VISIBILITY_DATA.scrollPosition9000);
+ });
- 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([
- "Applicant Name",
- "Category",
- "Submission #",
- "Submission Date",
- "Status",
- "Sub-Status",
- "Community",
- "Requested Amount",
- "Approved Amount",
- "Project Name",
- "Applicant Id",
- ]);
-
- scrollX(1500);
- assertVisibleHeadersInclude([
- "Tags",
- "Assignee",
- "SubSector",
- "Economic Region",
- "Regional District",
- "Registered Organization Number",
- "Org Book Status",
- ]);
-
- scrollX(3000);
- assertVisibleHeadersInclude([
- "Project Start Date",
- "Project End Date",
- "Projected Funding Total",
- "Total Paid Amount $",
- "Project Electoral District",
- "Applicant Electoral District",
- ]);
-
- scrollX(4500);
- assertVisibleHeadersInclude([
- "Forestry or Non-Forestry",
- "Forestry Focus",
- "Acquisition",
- "City",
- "Community Population",
- "Likelihood of Funding",
- "Total Score",
- ]);
-
- scrollX(6000);
- assertVisibleHeadersInclude([
- "Assessment Result",
- "Recommended Amount",
- "Due Date",
- "Owner",
- "Decision Date",
- "Project Summary",
- "Organization Type",
- "Business Number",
- ]);
-
- scrollX(7500);
- assertVisibleHeadersInclude([
- "Due Diligence Status",
- "Decline Rationale",
- "Contact Full Name",
- "Contact Title",
- "Contact Email",
- "Contact Business Phone",
- "Contact Cell Phone",
- ]);
-
- scrollX(9000);
- assertVisibleHeadersInclude([
- "Signing Authority Full Name",
- "Signing Authority Title",
- "Signing Authority Email",
- "Signing Authority Business Phone",
- "Signing Authority Cell Phone",
- "Place",
- "Risk Ranking",
- "Notes",
- "Red-Stop",
- "Indigenous",
- "FYE Day",
- "FYE Month",
- "Payout",
- "Unity Application Id",
- ]);
- });
-
- it("Verify Logout", () => {
- cy.logout();
- });
-});
\ No newline at end of file
+ it("Verify Logout", () => {
+ cy.logout();
+ });
+});
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/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/ApplicationDetailsPage.ts b/applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts
index 8415064169..6194b02f1f 100644
--- a/applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts
+++ b/applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts
@@ -1,5 +1,11 @@
+///
+
import { BasePage } from "./BasePage";
+/**
+ * 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 = {
diff --git a/applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts b/applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts
new file mode 100644
index 0000000000..aae8eb72bb
--- /dev/null
+++ b/applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts
@@ -0,0 +1,574 @@
+///
+
+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 = {
+ 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",
+ 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 ============
+
+ /**
+ * 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 })
+ .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;
+ }
+}
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);
+ });
}
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
+})
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"]
}
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/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..1e4ab34f13 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;
@@ -15,6 +16,13 @@ namespace Unity.Flex.Reporting.Configuration
public static partial class WorksheetFieldSchemaParser
{
private const string UnknownSectionName = "unknown_section";
+ private const string ComponentsPropertyName = "components";
+
+ private static readonly JsonDocumentOptions LenientJsonOptions = new()
+ {
+ AllowTrailingCommas = true,
+ CommentHandling = JsonCommentHandling.Skip
+ };
///
/// Parses a custom field and returns component metadata items.
@@ -23,8 +31,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
/// 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 +47,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 +71,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 +84,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 +121,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 +203,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 +219,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, LenientJsonOptions);
+ 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 +388,172 @@ 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, LenientJsonOptions);
+ var root = document.RootElement;
+
+ // CHEFS form schemas have a "components" array at the root
+ if (root.TryGetProperty(ComponentsPropertyName, 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 and has a type
+ if (component.TryGetProperty("key", out var keyElement) &&
+ keyElement.GetString() == dataGridKey &&
+ component.TryGetProperty("type", out var typeElement))
+ {
+ var type = typeElement.GetString()?.ToLower();
+ if (type == "datagrid")
+ {
+ return ExtractColumnsFromDataGrid(component);
+ }
+ }
+
+ // Recursively search in nested components (for panels, columns, etc.)
+ if (component.TryGetProperty(ComponentsPropertyName, out var nestedComponents))
+ {
+ var result = FindDataGridInComponents(nestedComponents, dataGridKey);
+ if (result != null)
+ return result;
+ }
+
+ // Also check columns property (for layout components)
+ var layoutResult = FindDataGridInLayoutColumns(component, dataGridKey);
+ if (layoutResult != null)
+ return layoutResult;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Searches for a DataGrid component within layout column components.
+ ///
+ /// The JSON element representing a layout component
+ /// The key of the DataGrid to find
+ /// List of DataGrid column definitions, or null if not found
+ private static List? FindDataGridInLayoutColumns(JsonElement component, string dataGridKey)
+ {
+ if (!component.TryGetProperty("columns", out var columnsElement) ||
+ columnsElement.ValueKind != JsonValueKind.Array)
+ return null;
+
+ foreach (var column in columnsElement.EnumerateArray())
+ {
+ if (column.TryGetProperty(ComponentsPropertyName, 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(ComponentsPropertyName, 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.Flex/test/Unity.Flex.Application.Tests/Reporting/WorksheetFieldSchemaParserTests.cs b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Application.Tests/Reporting/WorksheetFieldSchemaParserTests.cs
index 2be221dcf2..72ca6ade85 100644
--- a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Application.Tests/Reporting/WorksheetFieldSchemaParserTests.cs
+++ b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Application.Tests/Reporting/WorksheetFieldSchemaParserTests.cs
@@ -141,19 +141,19 @@ public async Task ParseField_WithNonDataGridType_ShouldReturnSimpleComponent()
{
// Arrange
using var uow = _unitOfWorkManager.Begin();
-
+
var worksheet = new Worksheet(Guid.NewGuid(), "TestWorksheet", "Test Worksheet");
var section = new WorksheetSection(Guid.NewGuid(), "TestSection");
worksheet.Sections.Add(section);
-
+
await _worksheetRepository.InsertAsync(worksheet, true);
await uow.SaveChangesAsync();
-
+
var field = new CustomField(Guid.NewGuid(), "testTextField", "TestWorksheet", "Test Text Field",
CustomFieldType.Text, "{}");
section.AddField(field);
await uow.SaveChangesAsync();
-
+
worksheet = await _worksheetRepository.GetAsync(worksheet.Id);
// Act
@@ -162,11 +162,346 @@ public async Task ParseField_WithNonDataGridType_ShouldReturnSimpleComponent()
// Assert
result.ShouldNotBeNull();
result.Count.ShouldBe(1);
-
+
var component = result.First();
component.Id.ShouldBe(field.Id.ToString());
component.Key.ShouldBe("testTextField");
component.Type.ShouldBe("Text");
}
+
+ [Fact]
+ public async Task ParseDataGridField_DynamicWithFormSchema_ShouldExtractColumnsFromChefsSchema()
+ {
+ // Arrange
+ using var uow = _unitOfWorkManager.Begin();
+
+ var worksheet = new Worksheet(Guid.NewGuid(), "TestWorksheet", "Test Worksheet");
+ var section = new WorksheetSection(Guid.NewGuid(), "TestSection");
+ worksheet.Sections.Add(section);
+
+ await _worksheetRepository.InsertAsync(worksheet, true);
+ await uow.SaveChangesAsync();
+
+ var field = new CustomField(Guid.NewGuid(), "testDataGrid", "TestWorksheet", "Test DataGrid",
+ CustomFieldType.DataGrid,
+ @"{""dynamic"": true, ""columns"": [], ""summaryOption"": ""None""}");
+ section.AddField(field);
+ await uow.SaveChangesAsync();
+
+ worksheet = await _worksheetRepository.GetAsync(worksheet.Id);
+
+ // The header mapping maps "field.Name.DataGrid" -> the CHEFS datagrid key
+ var submissionHeaderMapping = $@"{{""{field.Name}.DataGrid"": ""chefsDataGrid1""}}";
+
+ var formSchema = @"{
+ ""components"": [
+ {
+ ""key"": ""chefsDataGrid1"",
+ ""type"": ""datagrid"",
+ ""components"": [
+ { ""key"": ""firstName"", ""label"": ""First Name"", ""type"": ""textfield"" },
+ { ""key"": ""amount"", ""label"": ""Amount"", ""type"": ""number"" }
+ ]
+ }
+ ]
+ }";
+
+ // Act
+ var result = WorksheetFieldSchemaParser.ParseField(field, worksheet, formSchema, submissionHeaderMapping);
+
+ // Assert — should extract columns from CHEFS schema, no dynamic placeholder
+ result.ShouldNotBeNull();
+ result.Count.ShouldBe(2);
+ result.ShouldNotContain(c => c.Key == "dynamic_columns");
+
+ var firstNameCol = result.FirstOrDefault(c => c.Key == "firstName");
+ firstNameCol.ShouldNotBeNull();
+ firstNameCol.Label.ShouldBe("First Name");
+ firstNameCol.Type.ShouldBe("Text");
+
+ var amountCol = result.FirstOrDefault(c => c.Key == "amount");
+ amountCol.ShouldNotBeNull();
+ amountCol.Label.ShouldBe("Amount");
+ amountCol.Type.ShouldBe("Numeric");
+ }
+
+ [Fact]
+ public async Task ParseDataGridField_DynamicWithFormSchema_ShouldSkipDefinedColumnsWhenChefsExtracted()
+ {
+ // Arrange
+ using var uow = _unitOfWorkManager.Begin();
+
+ var worksheet = new Worksheet(Guid.NewGuid(), "TestWorksheet", "Test Worksheet");
+ var section = new WorksheetSection(Guid.NewGuid(), "TestSection");
+ worksheet.Sections.Add(section);
+
+ await _worksheetRepository.InsertAsync(worksheet, true);
+ await uow.SaveChangesAsync();
+
+ // Definition has both dynamic=true AND static columns defined
+ var field = new CustomField(Guid.NewGuid(), "testDataGrid", "TestWorksheet", "Test DataGrid",
+ CustomFieldType.DataGrid,
+ @"{""dynamic"": true, ""columns"": [{""name"": ""staticCol"", ""type"": ""Text""}], ""summaryOption"": ""None""}");
+ section.AddField(field);
+ await uow.SaveChangesAsync();
+
+ worksheet = await _worksheetRepository.GetAsync(worksheet.Id);
+
+ var submissionHeaderMapping = $@"{{""{field.Name}.DataGrid"": ""chefsGrid""}}";
+
+ var formSchema = @"{
+ ""components"": [
+ {
+ ""key"": ""chefsGrid"",
+ ""type"": ""datagrid"",
+ ""components"": [
+ { ""key"": ""dynamicCol"", ""label"": ""Dynamic Column"", ""type"": ""textfield"" }
+ ]
+ }
+ ]
+ }";
+
+ // Act
+ var result = WorksheetFieldSchemaParser.ParseField(field, worksheet, formSchema, submissionHeaderMapping);
+
+ // Assert — CHEFS extraction succeeded, so static columns should be skipped to avoid duplicates
+ result.ShouldNotBeNull();
+ result.Count.ShouldBe(1);
+ result.ShouldNotContain(c => c.Key == "staticCol");
+ result.ShouldNotContain(c => c.Key == "dynamic_columns");
+
+ var dynamicCol = result.First();
+ dynamicCol.Key.ShouldBe("dynamicCol");
+ dynamicCol.Label.ShouldBe("Dynamic Column");
+ dynamicCol.Type.ShouldBe("Text");
+ }
+
+ [Fact]
+ public async Task ParseDataGridField_DynamicWithNoHeaderMapping_ShouldFallBackToPlaceholder()
+ {
+ // Arrange
+ using var uow = _unitOfWorkManager.Begin();
+
+ var worksheet = new Worksheet(Guid.NewGuid(), "TestWorksheet", "Test Worksheet");
+ var section = new WorksheetSection(Guid.NewGuid(), "TestSection");
+ worksheet.Sections.Add(section);
+
+ await _worksheetRepository.InsertAsync(worksheet, true);
+ await uow.SaveChangesAsync();
+
+ var field = new CustomField(Guid.NewGuid(), "testDataGrid", "TestWorksheet", "Test DataGrid",
+ CustomFieldType.DataGrid,
+ @"{""dynamic"": true, ""columns"": [{""name"": ""col1"", ""type"": ""Text""}], ""summaryOption"": ""None""}");
+ section.AddField(field);
+ await uow.SaveChangesAsync();
+
+ worksheet = await _worksheetRepository.GetAsync(worksheet.Id);
+
+ // Header mapping does NOT contain an entry for this field
+ var submissionHeaderMapping = @"{""unrelated_key.DataGrid"": ""someGrid""}";
+ var formSchema = @"{ ""components"": [] }";
+
+ // Act
+ var result = WorksheetFieldSchemaParser.ParseField(field, worksheet, formSchema, submissionHeaderMapping);
+
+ // Assert — no matching header mapping, so dynamic placeholder + static columns
+ result.ShouldNotBeNull();
+ result.Count.ShouldBe(2); // 1 dynamic placeholder + 1 defined column
+
+ result.ShouldContain(c => c.Key == "dynamic_columns");
+ result.ShouldContain(c => c.Key == "col1");
+ }
+
+ [Fact]
+ public async Task ParseDataGridField_DynamicWithNestedFormSchema_ShouldFindDataGridInPanel()
+ {
+ // Arrange
+ using var uow = _unitOfWorkManager.Begin();
+
+ var worksheet = new Worksheet(Guid.NewGuid(), "TestWorksheet", "Test Worksheet");
+ var section = new WorksheetSection(Guid.NewGuid(), "TestSection");
+ worksheet.Sections.Add(section);
+
+ await _worksheetRepository.InsertAsync(worksheet, true);
+ await uow.SaveChangesAsync();
+
+ var field = new CustomField(Guid.NewGuid(), "testDataGrid", "TestWorksheet", "Test DataGrid",
+ CustomFieldType.DataGrid,
+ @"{""dynamic"": true, ""columns"": [], ""summaryOption"": ""None""}");
+ section.AddField(field);
+ await uow.SaveChangesAsync();
+
+ worksheet = await _worksheetRepository.GetAsync(worksheet.Id);
+
+ var submissionHeaderMapping = $@"{{""{field.Name}.DataGrid"": ""nestedGrid""}}";
+
+ // DataGrid is nested inside a panel component
+ var formSchema = @"{
+ ""components"": [
+ {
+ ""key"": ""panel1"",
+ ""type"": ""panel"",
+ ""components"": [
+ {
+ ""key"": ""nestedGrid"",
+ ""type"": ""datagrid"",
+ ""components"": [
+ { ""key"": ""nestedCol"", ""label"": ""Nested Column"", ""type"": ""textarea"" }
+ ]
+ }
+ ]
+ }
+ ]
+ }";
+
+ // Act
+ var result = WorksheetFieldSchemaParser.ParseField(field, worksheet, formSchema, submissionHeaderMapping);
+
+ // Assert — should find the datagrid inside the panel via recursive search
+ result.ShouldNotBeNull();
+ result.Count.ShouldBe(1);
+ result.ShouldNotContain(c => c.Key == "dynamic_columns");
+
+ var col = result.First();
+ col.Key.ShouldBe("nestedCol");
+ col.Label.ShouldBe("Nested Column");
+ col.Type.ShouldBe("Text"); // textarea maps to text
+ }
+
+ [Fact]
+ public async Task ParseDataGridField_DynamicWithLayoutColumns_ShouldFindDataGridInLayoutColumn()
+ {
+ // Arrange
+ using var uow = _unitOfWorkManager.Begin();
+
+ var worksheet = new Worksheet(Guid.NewGuid(), "TestWorksheet", "Test Worksheet");
+ var section = new WorksheetSection(Guid.NewGuid(), "TestSection");
+ worksheet.Sections.Add(section);
+
+ await _worksheetRepository.InsertAsync(worksheet, true);
+ await uow.SaveChangesAsync();
+
+ var field = new CustomField(Guid.NewGuid(), "testDataGrid", "TestWorksheet", "Test DataGrid",
+ CustomFieldType.DataGrid,
+ @"{""dynamic"": true, ""columns"": [], ""summaryOption"": ""None""}");
+ section.AddField(field);
+ await uow.SaveChangesAsync();
+
+ worksheet = await _worksheetRepository.GetAsync(worksheet.Id);
+
+ var submissionHeaderMapping = $@"{{""{field.Name}.DataGrid"": ""layoutGrid""}}";
+
+ // DataGrid is inside a layout "columns" element
+ var formSchema = @"{
+ ""components"": [
+ {
+ ""key"": ""layout1"",
+ ""type"": ""columns"",
+ ""columns"": [
+ {
+ ""components"": [
+ {
+ ""key"": ""layoutGrid"",
+ ""type"": ""datagrid"",
+ ""components"": [
+ { ""key"": ""layoutCol"", ""label"": ""Layout Column"", ""type"": ""currency"" }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }";
+
+ // Act
+ var result = WorksheetFieldSchemaParser.ParseField(field, worksheet, formSchema, submissionHeaderMapping);
+
+ // Assert — should find the datagrid inside layout columns
+ result.ShouldNotBeNull();
+ result.Count.ShouldBe(1);
+ result.ShouldNotContain(c => c.Key == "dynamic_columns");
+
+ var col = result.First();
+ col.Key.ShouldBe("layoutCol");
+ col.Label.ShouldBe("Layout Column");
+ col.Type.ShouldBe("Currency");
+ }
+
+ [Fact]
+ public async Task ParseDataGridField_DynamicWithInvalidFormSchema_ShouldFallBackToPlaceholder()
+ {
+ // Arrange
+ using var uow = _unitOfWorkManager.Begin();
+
+ var worksheet = new Worksheet(Guid.NewGuid(), "TestWorksheet", "Test Worksheet");
+ var section = new WorksheetSection(Guid.NewGuid(), "TestSection");
+ worksheet.Sections.Add(section);
+
+ await _worksheetRepository.InsertAsync(worksheet, true);
+ await uow.SaveChangesAsync();
+
+ var field = new CustomField(Guid.NewGuid(), "testDataGrid", "TestWorksheet", "Test DataGrid",
+ CustomFieldType.DataGrid,
+ @"{""dynamic"": true, ""columns"": [], ""summaryOption"": ""None""}");
+ section.AddField(field);
+ await uow.SaveChangesAsync();
+
+ worksheet = await _worksheetRepository.GetAsync(worksheet.Id);
+
+ var submissionHeaderMapping = $@"{{""{field.Name}.DataGrid"": ""someGrid""}}";
+ var formSchema = "not valid json {{{";
+
+ // Act
+ var result = WorksheetFieldSchemaParser.ParseField(field, worksheet, formSchema, submissionHeaderMapping);
+
+ // Assert — invalid form schema should fall back to dynamic placeholder
+ result.ShouldNotBeNull();
+ result.ShouldContain(c => c.Key == "dynamic_columns");
+ result.First(c => c.Key == "dynamic_columns").Type.ShouldBe("Dynamic");
+ }
+
+ [Fact]
+ public async Task ParseDataGridField_DynamicWithFormSchemaKeyMismatch_ShouldFallBackToPlaceholder()
+ {
+ // Arrange
+ using var uow = _unitOfWorkManager.Begin();
+
+ var worksheet = new Worksheet(Guid.NewGuid(), "TestWorksheet", "Test Worksheet");
+ var section = new WorksheetSection(Guid.NewGuid(), "TestSection");
+ worksheet.Sections.Add(section);
+
+ await _worksheetRepository.InsertAsync(worksheet, true);
+ await uow.SaveChangesAsync();
+
+ var field = new CustomField(Guid.NewGuid(), "testDataGrid", "TestWorksheet", "Test DataGrid",
+ CustomFieldType.DataGrid,
+ @"{""dynamic"": true, ""columns"": [], ""summaryOption"": ""None""}");
+ section.AddField(field);
+ await uow.SaveChangesAsync();
+
+ worksheet = await _worksheetRepository.GetAsync(worksheet.Id);
+
+ // Header mapping points to a key that does NOT exist in the form schema
+ var submissionHeaderMapping = $@"{{""{field.Name}.DataGrid"": ""nonExistentGrid""}}";
+ var formSchema = @"{
+ ""components"": [
+ {
+ ""key"": ""differentGrid"",
+ ""type"": ""datagrid"",
+ ""components"": [
+ { ""key"": ""col1"", ""label"": ""Col 1"", ""type"": ""textfield"" }
+ ]
+ }
+ ]
+ }";
+
+ // Act
+ var result = WorksheetFieldSchemaParser.ParseField(field, worksheet, formSchema, submissionHeaderMapping);
+
+ // Assert — key mismatch means no columns extracted, should fall back to placeholder
+ result.ShouldNotBeNull();
+ result.ShouldContain(c => c.Key == "dynamic_columns");
+ }
}
}
\ No newline at end of file
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..731cb41c9c
--- /dev/null
+++ b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Web.Tests/Components/ComponentTestFixture.cs
@@ -0,0 +1,45 @@
+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 (OperatingSystem.IsWindows())
+ {
+ services.RemoveAll();
+ }
+
+ 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/modules/Unity.Identity.Web/src/Pages/Identity/Roles/index.css b/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Roles/index.css
index 2abd758d1a..d465d3b2d9 100644
--- a/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Roles/index.css
+++ b/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Roles/index.css
@@ -19,8 +19,3 @@
#IdentityRolesWrapper {
background-color: transparent;
}
-
-#IdentityRolesWrapper .dt-scroll-body {
- max-height: calc(100vh - 370px);
- overflow-y: scroll;
-}
diff --git a/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Roles/index.js b/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Roles/index.js
index 74f5376cfb..d54abb1091 100644
--- a/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Roles/index.js
+++ b/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Roles/index.js
@@ -168,7 +168,8 @@ $(function () {
dataTableName: 'IdentityRolesTable',
dynamicButtonContainerId: 'dynamicButtonContainerId',
useNullPlaceholder: true,
- externalSearchId: 'search-roles'
+ externalSearchId: 'search-roles',
+ fixedHeaders: true
});
_createModal.onResult(function () {
diff --git a/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/index.css b/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/index.css
index 3e2e3dfa90..26f3c597ad 100644
--- a/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/index.css
+++ b/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/index.css
@@ -28,8 +28,3 @@
#UsersWrapper {
background-color: transparent;
}
-
-#UsersWrapper .dt-scroll-body {
- max-height: calc(100vh - 370px);
- overflow-y: scroll;
-}
diff --git a/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/index.js b/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/index.js
index 6cfa429cc7..ca1c07d2ed 100644
--- a/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/index.js
+++ b/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/index.js
@@ -266,7 +266,8 @@ $(function () {
dataTableName: 'UsersTable',
dynamicButtonContainerId: 'dynamicButtonContainerId',
useNullPlaceholder: true,
- externalSearchId: 'search-users'
+ externalSearchId: 'search-users',
+ fixedHeaders: true
});
_editModal.onResult(function () {
diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationManager.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationManager.cs
index 65eacb3356..eb031554d0 100644
--- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationManager.cs
+++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationManager.cs
@@ -117,7 +117,9 @@ public async Task SendEmailAsync(string emailTo, string bod
}
// Send the email using the CHES client service
- var emailObject = await GetEmailObjectAsync(emailTo, body, subject, emailFrom, emailBodyType, emailTemplateName, emailCC, emailBCC);
+ var emailObject = await GetEmailObjectAsync(
+ emailTo, body, subject, emailFrom, emailBodyType, emailTemplateName, emailCC, emailBCC, excludeTemplate: true);
+
var response = await chesClientService.SendAsync(emailObject);
// Assuming SendAsync returns a HttpResponseMessage or equivalent:
@@ -222,7 +224,8 @@ public async Task BuildEmailObjectWithAttachmentsAsync(EmailLog emailLo
emailLog.BodyType,
emailLog.TemplateName,
emailLog.CC,
- emailLog.BCC);
+ emailLog.BCC,
+ excludeTemplate: true);
// Retrieve attachments from S3
var attachments = await emailAttachmentService.GetAttachmentsAsync(emailLog.Id);
@@ -261,7 +264,8 @@ protected virtual async Task GetEmailObjectAsync(
string? emailBodyType,
string? emailTemplateName,
string? emailCC = null,
- string? emailBCC = null)
+ string? emailBCC = null,
+ bool excludeTemplate = false)
{
var toList = emailTo.ParseEmailList() ?? [];
var ccList = emailCC.ParseEmailList();
@@ -274,28 +278,47 @@ protected virtual async Task GetEmailObjectAsync(
emailObjectDictionary["body"] = body;
emailObjectDictionary["bodyType"] = emailBodyType ?? "text";
- emailObjectDictionary["cc"] = ccList;
- emailObjectDictionary["bcc"] = bccList;
emailObjectDictionary["encoding"] = "utf-8";
emailObjectDictionary["from"] = emailFrom ?? defaultFromAddress ?? "NoReply@gov.bc.ca";
emailObjectDictionary["priority"] = "normal";
emailObjectDictionary["subject"] = subject;
emailObjectDictionary["tag"] = "tag";
emailObjectDictionary["to"] = toList;
- emailObjectDictionary["templateName"] = emailTemplateName;
+
+ // Only include cc/bcc when provided CHES API expects arrays, not null.
+ if (ccList != null)
+ {
+ emailObjectDictionary["cc"] = ccList;
+ }
+ if (bccList != null)
+ {
+ emailObjectDictionary["bcc"] = bccList;
+ }
+
+ // templateName is not part of the CHES MessageObject schema
+ // store it on the EmailLog but don't send it to the API.
+ if (!excludeTemplate)
+ {
+ emailObjectDictionary["templateName"] = emailTemplateName;
+ }
return emailObject;
}
protected virtual EmailLog UpdateMappedEmailLog(EmailLog emailLog, dynamic emailDynamicObject)
{
+ var dict = (IDictionary)emailDynamicObject;
emailLog.Body = emailDynamicObject.body;
emailLog.Subject = emailDynamicObject.subject;
emailLog.BodyType = emailDynamicObject.bodyType;
emailLog.FromAddress = emailDynamicObject.from;
emailLog.ToAddress = string.Join(",", emailDynamicObject.to);
- emailLog.CC = emailDynamicObject.cc != null ? string.Join(",", (IEnumerable)emailDynamicObject.cc) : string.Empty;
- emailLog.BCC = emailDynamicObject.bcc != null ? string.Join(",", (IEnumerable)emailDynamicObject.bcc) : string.Empty;
+ emailLog.CC = dict.TryGetValue("cc", out var cc) && cc is IEnumerable ccList
+ ? string.Join(",", ccList)
+ : string.Empty;
+ emailLog.BCC = dict.TryGetValue("bcc", out var bcc) && bcc is IEnumerable bccList
+ ? string.Join(",", bccList)
+ : string.Empty;
emailLog.TemplateName = emailDynamicObject.templateName;
return emailLog;
}
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..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
@@ -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,
diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/Codes/CasPaymentRequestStatus.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/Codes/CasPaymentRequestStatus.cs
index e10b259c9c..3b292dda57 100644
--- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/Codes/CasPaymentRequestStatus.cs
+++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/Codes/CasPaymentRequestStatus.cs
@@ -4,6 +4,7 @@ public static class CasPaymentRequestStatus
{
// Unity Status
public const string SentToCas = "SentToCas";
+ public const string SentToAccountsPayable = "SentToAccountsPayable";
// CAS INVOICE STATUS
public const string ErrorFromCas = "Error";
diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/ApplicationPaymentRollupDto.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/ApplicationPaymentRollupDto.cs
new file mode 100644
index 0000000000..71053bd12f
--- /dev/null
+++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/ApplicationPaymentRollupDto.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace Unity.Payments.PaymentRequests;
+
+[Serializable]
+public class ApplicationPaymentRollupDto
+{
+ 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..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,5 +21,7 @@ public interface IPaymentRequestAppService : IApplicationService
Task GetUserPaymentThresholdAsync();
Task ManuallyAddPaymentRequestsToReconciliationQueue(List paymentRequestIds);
Task> GetPaymentPendingListByCorrelationIdAsync(Guid applicationId);
+ Task GetApplicationPaymentRollupAsync(Guid applicationId);
+ Task> GetApplicationPaymentRollupBatchAsync(List applicationIds);
}
}
diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/AccountCodings/AccountCoding.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/AccountCodings/AccountCoding.cs
index 03c04bb23e..f510bb458e 100644
--- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/AccountCodings/AccountCoding.cs
+++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/AccountCodings/AccountCoding.cs
@@ -4,6 +4,7 @@
using Volo.Abp.Domain.Entities.Auditing;
using System.Linq;
using Volo.Abp.MultiTenancy;
+using Unity.Payments.PaymentRequests;
namespace Unity.Payments.Domain.AccountCodings
{
@@ -16,6 +17,16 @@ public class AccountCoding : AuditedAggregateRoot, IMultiTenant
public string Stob { get; private set; }
public string ProjectNumber { get; private set; }
+ public string FullAccountCode()
+ {
+ return AccountCodingFormatter.Format(this);
+ }
+
+ public string FullAccountCodeWithDescription()
+ {
+ return AccountCodingFormatter.FormatWithDescription(this);
+ }
+
public string? Description { get; private set; }
public AccountCoding()
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..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
@@ -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> 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 222dc9f83d..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
@@ -35,5 +35,9 @@ public interface IPaymentRequestQueryManager
// Pending Payments
Task> GetPaymentPendingListByCorrelationIdAsync(Guid applicationId);
+
+ // 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 f5fe8e9728..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
@@ -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,98 @@ public async Task> GetPaymentPendingListByCorrelationIdA
var payments = await paymentRequestRepository.GetPaymentPendingListByCorrelationIdAsync(applicationId);
return objectMapper.Map, List>(payments);
}
+
+ ///
+ /// 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 batchRollup = await paymentRequestRepository.GetBatchPaymentRollupsByCorrelationIdsAsync(allCorrelationIds);
+
+ return new ApplicationPaymentRollupDto
+ {
+ ApplicationId = applicationId,
+ TotalPaid = batchRollup.Sum(s => s.TotalPaid),
+ TotalPending = batchRollup.Sum(s => s.TotalPending)
+ };
+ }
+
+ ///
+ /// 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)
+ {
+ // 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 paymentRollups = await paymentRequestRepository.GetBatchPaymentRollupsByCorrelationIdsAsync(allCorrelationIds.ToList());
+ var rollupLookup = paymentRollups.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 (rollupLookup.TryGetValue(applicationId, out var parentRollup))
+ {
+ totalPaid += parentRollup.TotalPaid;
+ totalPending += parentRollup.TotalPending;
+ }
+
+ // Add child application amounts
+ if (childApplicationIdsByParent.TryGetValue(applicationId, out var childIds))
+ {
+ foreach (var childId in childIds)
+ {
+ if (rollupLookup.TryGetValue(childId, out var childApplicationRollup))
+ {
+ totalPaid += childApplicationRollup.TotalPaid;
+ totalPending += childApplicationRollup.TotalPending;
+ }
+ }
+ }
+
+ result[applicationId] = new ApplicationPaymentRollupDto
+ {
+ ApplicationId = applicationId,
+ TotalPaid = totalPaid,
+ TotalPending = totalPending
+ };
+ }
+
+ return result;
+ }
}
}
diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/PaymentsManager.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/PaymentsManager.cs
index 73bd07d3c3..31ebc73262 100644
--- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/PaymentsManager.cs
+++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/PaymentsManager.cs
@@ -9,6 +9,7 @@
using Unity.Payments.Domain.Shared;
using Unity.Payments.Domain.Workflow;
using Unity.Payments.Enums;
+using Unity.Payments.Codes;
using Unity.Payments.PaymentRequests;
using Unity.Payments.Permissions;
using Volo.Abp.Authorization.Permissions;
@@ -150,6 +151,7 @@ public async Task TriggerAction(Guid paymentRequestsId, PaymentA
if (preventPayment)
{
statusChangedTo = PaymentRequestStatus.FSB;
+ paymentRequest.SetInvoiceStatus(CasPaymentRequestStatus.SentToAccountsPayable);
}
else
{
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..be9999cd12 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,13 +18,12 @@ 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);
ReCheckStatusList.Add(CasPaymentRequestStatus.SentToCas);
+ ReCheckStatusList.Add(CasPaymentRequestStatus.NotFound);
+ ReCheckStatusList.Add(CasPaymentRequestStatus.SentToAccountsPayable);
ReCheckStatusList.Add(CasPaymentRequestStatus.NeverValidated);
FailedStatusList.Add(CasPaymentRequestStatus.ServiceUnavailable);
@@ -32,25 +33,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 +62,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 +70,19 @@ 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()
{
+ // LastModificationTime is stored as UTC by ABP; use UtcNow for consistent 24-hour window
+ var cutoff = DateTime.UtcNow.AddDays(-1);
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 >= cutoff).IncludeDetails().ToListAsync();
}
public override async Task> WithDetailsAsync()
@@ -87,8 +94,52 @@ 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();
+ }
+
+ ///
+ /// 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> GetBatchPaymentRollupsByCorrelationIdsAsync(List correlationIds)
+ {
+ var dbSet = await GetDbSetAsync();
+
+ var results = await dbSet
+ .Where(p => correlationIds.Contains(p.CorrelationId))
+ .GroupBy(p => p.CorrelationId)
+ .Select(g => new ApplicationPaymentRollupDto
+ {
+ ApplicationId = g.Key,
+ TotalPaid = g
+ .Where(p => p.PaymentStatus != null
+ && p.PaymentStatus.Trim().ToUpper() == CasPaymentRequestStatus.FullyPaid.ToUpper())
+ .Sum(p => p.Amount),
+ TotalPending = g
+ .Where(p => p.Status == PaymentRequestStatus.L1Pending
+ || p.Status == PaymentRequestStatus.L2Pending
+ || p.Status == PaymentRequestStatus.L3Pending
+ || (p.Status == PaymentRequestStatus.Submitted
+ && string.IsNullOrEmpty(p.PaymentStatus)
+ && (string.IsNullOrEmpty(p.InvoiceStatus)
+ || !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/AccountCodingFormatter.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/AccountCodingFormatter.cs
index 5c208a912b..071d6e6b68 100644
--- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/AccountCodingFormatter.cs
+++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/AccountCodingFormatter.cs
@@ -43,5 +43,24 @@ public static string Format(AccountCoding? accountCoding)
return string.Empty;
}
+
+ public static string FormatWithDescription(AccountCoding? accountCoding)
+ {
+ if (accountCoding == null)
+ {
+ return string.Empty;
+ }
+
+ if (accountCoding.Responsibility != null
+ && accountCoding.ServiceLine != null
+ && accountCoding.Stob != null
+ && accountCoding.MinistryClient != null
+ && accountCoding.ProjectNumber != null)
+ {
+ return $"{accountCoding.MinistryClient}.{accountCoding.Responsibility}.{accountCoding.ServiceLine}.{accountCoding.Stob}.{accountCoding.ProjectNumber}.{AccountDistributionPostfix} {accountCoding.Description}";
+ }
+
+ return string.Empty;
+ }
}
}
diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/CasPaymentRequestCoordinator.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/CasPaymentRequestCoordinator.cs
index 84b8cc8faf..7e2e287bec 100644
--- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/CasPaymentRequestCoordinator.cs
+++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/CasPaymentRequestCoordinator.cs
@@ -1,6 +1,7 @@
using System.Threading.Tasks;
using Unity.Payments.Domain.PaymentRequests;
using System;
+using System.Linq;
using Volo.Abp.Application.Services;
using System.Collections.Generic;
using Volo.Abp.TenantManagement;
@@ -8,7 +9,7 @@
using Volo.Abp.Uow;
using Microsoft.Extensions.Logging;
using Unity.Payments.Integrations.Cas;
-using System.Linq;
+using Unity.Payments.Codes;
using Unity.Payments.RabbitMQ.QueueMessages;
using Unity.Notifications.Integrations.RabbitMQ;
@@ -138,6 +139,11 @@ public async Task AddPaymentRequestsToReconciliationQueue()
paymentReqeust = await _paymentRequestsRepository.GetAsync(PaymentRequestId);
if (paymentReqeust != null)
{
+ if(paymentReqeust.InvoiceStatus == CasPaymentRequestStatus.NotFound && result.InvoiceStatus == CasPaymentRequestStatus.NotFound)
+ {
+ result.InvoiceStatus = CasPaymentRequestStatus.NotFound+"2";
+ }
+
paymentReqeust.SetInvoiceStatus(result.InvoiceStatus ?? "");
paymentReqeust.SetPaymentStatus(result.PaymentStatus ?? "");
paymentReqeust.SetPaymentDate(result.PaymentDate ?? "");
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..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
@@ -4,19 +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;
namespace Unity.Payments.PaymentRequests
{
@@ -29,7 +30,8 @@ public class PaymentRequestAppService(
IPaymentsManager paymentsManager,
FsbPaymentNotifier fsbPaymentNotifier,
IPaymentRequestQueryManager paymentRequestQueryManager,
- IPaymentRequestConfigurationManager paymentRequestConfigurationManager) : PaymentsAppService, IPaymentRequestAppService
+ IPaymentRequestConfigurationManager paymentRequestConfigurationManager,
+ Lazy applicationLinksService) : PaymentsAppService, IPaymentRequestAppService
{
public async Task GetDefaultAccountCodingId()
@@ -328,5 +330,38 @@ public async Task> GetPaymentPendingListByCorrelationIdA
{
return await paymentRequestQueryManager.GetPaymentPendingListByCorrelationIdAsync(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.GetApplicationPaymentRollupAsync(applicationId, childApplicationIds);
+ }
+
+ ///
+ /// 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.GetApplicationPaymentRollupBatchAsync(applicationIds, childApplicationIdsByParent);
+ }
}
}
diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Permissions/PaymentsPermissionDefinitionProvider.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Permissions/PaymentsPermissionDefinitionProvider.cs
index 8f9610d1b9..d29c48009b 100644
--- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Permissions/PaymentsPermissionDefinitionProvider.cs
+++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Permissions/PaymentsPermissionDefinitionProvider.cs
@@ -18,6 +18,7 @@ public override void Define(IPermissionDefinitionContext context)
paymentsPermissions.AddChild(PaymentsPermissions.Payments.L2ApproveOrDecline, L("Permission:Payments.L2ApproveOrDecline"));
paymentsPermissions.AddChild(PaymentsPermissions.Payments.L3ApproveOrDecline, L("Permission:Payments.L3ApproveOrDecline"));
paymentsPermissions.AddChild(PaymentsPermissions.Payments.RequestPayment, L("Permission:Payments.RequestPayment"));
+ paymentsPermissions.AddChild(PaymentsPermissions.Payments.AccountCodingOverride, L("Permission:Payments.AccountCodingOverride"));
//-- PAYMENT INFO PERMISSIONS
grantApplicationPermissionsGroup.Add_PaymentInfo_Permissions();
diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Shared/Localization/Payments/en.json b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Shared/Localization/Payments/en.json
index 0a18cb85e3..79e9083674 100644
--- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Shared/Localization/Payments/en.json
+++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Shared/Localization/Payments/en.json
@@ -25,6 +25,8 @@
"ApplicationPaymentRequest:BatchNumberName": "Batch #",
"ApplicationPaymentRequest:NumberPayment": "Number of Payments",
"ApplicationPaymentRequest:TotalAmount": "Total Amount",
+ "ApplicationPaymentRequest:AccountCodingOverride": "Account Coding Override",
+ "ApplicationPaymentRequest:AccountCodingOverrideWarning": "An overridden account code is being used for this payment request.",
"ApplicationPaymentRequest:BatchNote": "Note",
"ApplicationPaymentRequest:Validations:RemainingAmountExceeded": "Cannot add a payment that exceeds the remaining amount of ",
@@ -120,6 +122,7 @@
"Permission:Payments.L2ApproveOrDecline": "Approve/Decline L2 Payments",
"Permission:Payments.L3ApproveOrDecline": "Approve/Decline L3 Payments",
"Permission:Payments.RequestPayment": "Request Payment",
+ "Permission:Payments.AccountCodingOverride": "Override Account Coding",
"Permission:Payments.EditFormPaymentConfiguration": "Edit Form Payment Configuration",
"Enum:PaymentRequestStatus.L1Pending": "L1 Pending",
"Enum:PaymentRequestStatus.L1Approved": "L1 Approved",
diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Shared/Permissions/PaymentsPermissions.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Shared/Permissions/PaymentsPermissions.cs
index fc28917033..0cda791d74 100644
--- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Shared/Permissions/PaymentsPermissions.cs
+++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Shared/Permissions/PaymentsPermissions.cs
@@ -14,6 +14,7 @@ public static class Payments
public const string L3ApproveOrDecline = Default + ".L3ApproveOrDecline";
public const string Decline = Default + ".Decline";
public const string RequestPayment = Default + ".RequestPayment";
+ public const string AccountCodingOverride = Default + ".AccountCodingOverride";
public const string EditSupplierInfo = Default + ".EditSupplierInfo";
public const string EditFormPaymentConfiguration = Default + ".EditFormPaymentConfiguration";
}
diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/CreatePaymentRequests.cshtml b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/CreatePaymentRequests.cshtml
index 7b0f73fdd3..7509ead8f8 100644
--- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/CreatePaymentRequests.cshtml
+++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/CreatePaymentRequests.cshtml
@@ -2,12 +2,15 @@
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
@using Unity.Payments.Localization
@using Microsoft.Extensions.Localization
-
+@using Unity.Payments.Permissions
+@using Volo.Abp.Authorization.Permissions;
+@inject IPermissionChecker PermissionChecker
@model Unity.Payments.Web.Pages.Payments.CreatePaymentRequestsModel
@inject IStringLocalizer L
@{
Layout = null;
+ var canOverride = await PermissionChecker.IsGrantedAsync(PaymentsPermissions.Payments.AccountCodingOverride);
}
').appendTo('head');
+ $('').appendTo('head');
}
}
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 {
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/IAIService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/IAIService.cs
index e4c3d26ac8..160f8ed233 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,10 +6,17 @@ namespace Unity.GrantManager.AI
public interface IAIService
{
Task IsAvailableAsync();
- Task GenerateSummaryAsync(string content, string? prompt = null, int maxTokens = 150);
+
+ 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 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.Contracts/AI/Models/AIAttachmentItem.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/AIAttachmentItem.cs
new file mode 100644
index 0000000000..fc4b31e2a9
--- /dev/null
+++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/AIAttachmentItem.cs
@@ -0,0 +1,13 @@
+using System.Text.Json.Serialization;
+
+namespace Unity.GrantManager.AI
+{
+ public class AIAttachmentItem
+ {
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+
+ [JsonPropertyName(AIJsonKeys.Summary)]
+ public string Summary { get; set; } = string.Empty;
+ }
+}
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/ApplicationAnalysisFinding.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/ApplicationAnalysisFinding.cs
new file mode 100644
index 0000000000..79785fee59
--- /dev/null
+++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/ApplicationAnalysisFinding.cs
@@ -0,0 +1,16 @@
+using System.Text.Json.Serialization;
+
+namespace Unity.GrantManager.AI
+{
+ 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/Models/ScoresheetSectionAnswer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/ScoresheetSectionAnswer.cs
new file mode 100644
index 0000000000..0a76cbb0e0
--- /dev/null
+++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/ScoresheetSectionAnswer.cs
@@ -0,0 +1,17 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Unity.GrantManager.AI
+{
+ public class ScoresheetSectionAnswer
+ {
+ [JsonPropertyName(AIJsonKeys.Answer)]
+ public JsonElement Answer { get; set; }
+
+ [JsonPropertyName(AIJsonKeys.Rationale)]
+ public string Rationale { get; set; } = string.Empty;
+
+ [JsonPropertyName(AIJsonKeys.Confidence)]
+ public int Confidence { get; set; }
+ }
+}
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/AICompletionRequest.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/AICompletionRequest.cs
new file mode 100644
index 0000000000..2ec1bf8d30
--- /dev/null
+++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/AICompletionRequest.cs
@@ -0,0 +1,19 @@
+using System.Text.Json.Serialization;
+
+namespace Unity.GrantManager.AI
+{
+ public class AICompletionRequest
+ {
+ [JsonPropertyName("userPrompt")]
+ public string UserPrompt { get; set; } = string.Empty;
+
+ [JsonPropertyName("systemPrompt")]
+ public string? SystemPrompt { get; set; }
+
+ [JsonPropertyName("maxTokens")]
+ public int MaxTokens { get; set; } = 150;
+
+ [JsonPropertyName("temperature")]
+ public double? Temperature { get; set; }
+ }
+}
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/ApplicationAnalysisRequest.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/ApplicationAnalysisRequest.cs
new file mode 100644
index 0000000000..fcd809b8b8
--- /dev/null
+++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/ApplicationAnalysisRequest.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Unity.GrantManager.AI
+{
+ public class ApplicationAnalysisRequest
+ {
+ [JsonPropertyName("schema")]
+ public JsonElement Schema { get; set; }
+
+ [JsonPropertyName("data")]
+ public JsonElement Data { get; set; }
+
+ [JsonPropertyName("attachments")]
+ public List Attachments { get; set; } = new();
+
+ [JsonPropertyName("rubric")]
+ public string? Rubric { get; set; }
+ }
+}
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/AttachmentSummaryRequest.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/AttachmentSummaryRequest.cs
new file mode 100644
index 0000000000..c0e1bfd1ee
--- /dev/null
+++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/AttachmentSummaryRequest.cs
@@ -0,0 +1,16 @@
+using System.Text.Json.Serialization;
+
+namespace Unity.GrantManager.AI
+{
+ public class AttachmentSummaryRequest
+ {
+ [JsonPropertyName("fileName")]
+ public string FileName { get; set; } = string.Empty;
+
+ [JsonPropertyName("fileContent")]
+ public byte[] FileContent { get; set; } = System.Array.Empty();
+
+ [JsonPropertyName("contentType")]
+ public string ContentType { get; set; } = "application/octet-stream";
+ }
+}
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/ScoresheetSectionRequest.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/ScoresheetSectionRequest.cs
new file mode 100644
index 0000000000..870412d079
--- /dev/null
+++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/ScoresheetSectionRequest.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Unity.GrantManager.AI
+{
+ public class ScoresheetSectionRequest
+ {
+ [JsonPropertyName("data")]
+ public JsonElement Data { get; set; }
+
+ [JsonPropertyName("attachments")]
+ public List Attachments { get; set; } = new();
+
+ [JsonPropertyName("sectionName")]
+ public string SectionName { get; set; } = string.Empty;
+
+ [JsonPropertyName("sectionSchema")]
+ public JsonElement SectionSchema { get; set; }
+ }
+}
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/AICompletionResponse.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/AICompletionResponse.cs
new file mode 100644
index 0000000000..316d2ef162
--- /dev/null
+++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/AICompletionResponse.cs
@@ -0,0 +1,10 @@
+using System.Text.Json.Serialization;
+
+namespace Unity.GrantManager.AI
+{
+ public class AICompletionResponse
+ {
+ [JsonPropertyName("content")]
+ public string Content { get; set; } = string.Empty;
+ }
+}
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/ApplicationAnalysisResponse.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/ApplicationAnalysisResponse.cs
new file mode 100644
index 0000000000..e8a5afa194
--- /dev/null
+++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/ApplicationAnalysisResponse.cs
@@ -0,0 +1,23 @@
+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();
+ }
+}
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/AttachmentSummaryResponse.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/AttachmentSummaryResponse.cs
new file mode 100644
index 0000000000..4f30b8c44a
--- /dev/null
+++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/AttachmentSummaryResponse.cs
@@ -0,0 +1,10 @@
+using System.Text.Json.Serialization;
+
+namespace Unity.GrantManager.AI
+{
+ public class AttachmentSummaryResponse
+ {
+ [JsonPropertyName(AIJsonKeys.Summary)]
+ public string Summary { get; set; } = string.Empty;
+ }
+}
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/ScoresheetSectionResponse.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/ScoresheetSectionResponse.cs
new file mode 100644
index 0000000000..cf4569dd07
--- /dev/null
+++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/ScoresheetSectionResponse.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Unity.GrantManager.AI
+{
+ public class ScoresheetSectionResponse
+ {
+ [JsonPropertyName("answers")]
+ public Dictionary Answers { get; set; } = new();
+ }
+}
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/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.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..3db1d7dcd6
--- /dev/null
+++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileContactService.cs
@@ -0,0 +1,39 @@
+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 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
+{
+ ///
+ /// 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);
+
+ ///
+ /// 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/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/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
new file mode 100644
index 0000000000..f7b956aba2
--- /dev/null
+++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantAddressInfoDto.cs
@@ -0,0 +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.Contracts/ApplicantProfile/ProfileData/ApplicantContactInfoDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantContactInfoDto.cs
new file mode 100644
index 0000000000..716f78928c
--- /dev/null
+++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantContactInfoDto.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+
+namespace Unity.GrantManager.ApplicantProfile.ProfileData
+{
+ public class ApplicantContactInfoDto : ApplicantProfileDataDto
+ {
+ public override string DataType => "CONTACTINFO";
+
+ 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/ApplicantProfile/ProfileData/ApplicantSubmissionInfoDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantSubmissionInfoDto.cs
new file mode 100644
index 0000000000..9c1fc36c73
--- /dev/null
+++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ApplicantSubmissionInfoDto.cs
@@ -0,0 +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/ContactInfoItemDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ContactInfoItemDto.cs
new file mode 100644
index 0000000000..112eed817b
--- /dev/null
+++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ContactInfoItemDto.cs
@@ -0,0 +1,22 @@
+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; }
+ public string? ReferenceNo { get; set; }
+ }
+}
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.Contracts/Applicants/ProfileData/ApplicantAddressInfoDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ProfileData/ApplicantAddressInfoDto.cs
deleted file mode 100644
index fde1734a05..0000000000
--- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ProfileData/ApplicantAddressInfoDto.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace Unity.GrantManager.Applicants.ProfileData
-{
- public class ApplicantAddressInfoDto : ApplicantProfileDataDto
- {
- public override string DataType => "ADDRESSINFO";
- }
-}
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/Applicants/ProfileData/ApplicantSubmissionInfoDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ProfileData/ApplicantSubmissionInfoDto.cs
deleted file mode 100644
index 4c0a0ba60b..0000000000
--- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ProfileData/ApplicantSubmissionInfoDto.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace Unity.GrantManager.Applicants.ProfileData
-{
- public class ApplicantSubmissionInfoDto : ApplicantProfileDataDto
- {
- public override string DataType => "SUBMISSIONINFO";
- }
-}
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Assessments/AssessmentDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Assessments/AssessmentDto.cs
index f11a22cc53..3dc9eb44c1 100644
--- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Assessments/AssessmentDto.cs
+++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Assessments/AssessmentDto.cs
@@ -15,6 +15,7 @@ public class AssessmentDto : EntityDto
public AssessmentState Status { get; set; }
public bool IsComplete { get; set; }
public bool? ApprovalRecommended { get; set; }
+ public bool IsAiAssessment { get; set; }
}
}
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Assessments/AssessmentListItemDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Assessments/AssessmentListItemDto.cs
index 9cb78cee74..f1e9606f65 100644
--- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Assessments/AssessmentListItemDto.cs
+++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Assessments/AssessmentListItemDto.cs
@@ -17,7 +17,8 @@ public class AssessmentListItemDto
public AssessmentState Status { get; set; }
public bool IsComplete { get; set; }
public bool? ApprovalRecommended { get; set; }
-
+ public bool IsAiAssessment { get; set; }
+
public double SubTotal { get; set; }
public int? FinancialAnalysis { get; set; }
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Assessments/IAssessmentAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Assessments/IAssessmentAppService.cs
index 0ed44b9737..6965da148b 100644
--- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Assessments/IAssessmentAppService.cs
+++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Assessments/IAssessmentAppService.cs
@@ -16,4 +16,6 @@ public interface IAssessmentAppService : IApplicationService
Task GetCurrentUserAssessmentId(Guid applicationId);
Task UpdateAssessmentScore(AssessmentScoresDto dto);
Task SaveScoresheetSectionAnswers(AssessmentScoreSectionDto dto);
+ Task CloneFromAiAsync(Guid aiAssessmentId);
+ Task GetDisplayList(Guid applicationId);
}
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/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; }
}
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.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());
}
}
}
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..710a9bfca0 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
@@ -48,6 +48,12 @@ public override void Define(IPermissionDefinitionContext context)
applicatPermissions.AddChild(GrantApplicationPermissions.Applicants.ViewList, L("Permission:GrantApplicationManagement.Applicants.ViewList"));
applicatPermissions.AddChild(GrantApplicationPermissions.Applicants.Edit, L("Permission:GrantApplicationManagement.Applicants.Edit"));
applicatPermissions.AddChild(GrantApplicationPermissions.Applicants.AssignApplicant, L("Permission:GrantApplicationManagement.Applicants.AssignApplicant"));
+ var applicantInfoPermissions = applicatPermissions.AddChild(
+ GrantApplicationPermissions.Applicants.ApplicantInfoDefault,
+ L("Permission:GrantApplicationManagement.Applicants.ApplicantInfo"));
+ applicantInfoPermissions.AddChild(
+ GrantApplicationPermissions.Applicants.EditRedStop,
+ L("Permission:GrantApplicationManagement.Applicants.ApplicantInfo.EditRedStop"));
// Assignment
var assignmentPermissions = grantApplicationPermissionsGroup.AddPermission(GrantApplicationPermissions.Assignments.Default, L("Permission:GrantApplicationManagement.Assignments.Default"));
@@ -130,6 +136,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.Application/AI/OpenAIService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs
index 1374af052b..418c31ebcf 100644
--- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs
+++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs
@@ -18,12 +18,32 @@ public class OpenAIService : IAIService, ITransientDependency
private readonly IConfiguration _configuration;
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 readonly string NoKeyError = "OpenAI API key is not configured";
-
- public OpenAIService(HttpClient httpClient, IConfiguration configuration, ILogger logger, ITextExtractionService textExtractionService)
+ private const string ApplicationAnalysisPromptType = "ApplicationAnalysis";
+ private const string AttachmentSummaryPromptType = "AttachmentSummary";
+ private const string ScoresheetAllPromptType = "ScoresheetAll";
+ private const string ScoresheetSectionPromptType = "ScoresheetSection";
+ private const string NoSummaryGeneratedMessage = "No summary generated.";
+ private const string ServiceNotConfiguredMessage = "AI analysis not available - service not configured.";
+ private const string ServiceTemporarilyUnavailableMessage = "AI analysis failed - service temporarily unavailable.";
+ private const string SummaryFailedRetryMessage = "AI analysis failed - please try again later.";
+
+ 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("Azure:Logging:EnablePromptFileLog") ?? false;
+ private const string PromptLogDirectoryName = "logs";
+ private static readonly string PromptLogFileName = $"ai-prompts-{DateTime.UtcNow:yyyyMMdd-HHmmss}-{Environment.ProcessId}.log";
+
+ private static readonly JsonSerializerOptions JsonLogOptions = new() { WriteIndented = true };
+
+ public OpenAIService(
+ HttpClient httpClient,
+ IConfiguration configuration,
+ ILogger logger,
+ ITextExtractionService textExtractionService)
{
_httpClient = httpClient;
_configuration = configuration;
@@ -35,33 +55,69 @@ public Task IsAvailableAsync()
{
if (string.IsNullOrEmpty(ApiKey))
{
- _logger.LogWarning("Error: {Message}", NoKeyError);
+ _logger.LogWarning("Error: {Message}", MissingApiKeyMessage);
return Task.FromResult(false);
}
return Task.FromResult(true);
}
+ public async Task GenerateCompletionAsync(AICompletionRequest request)
+ {
+ var content = await GenerateSummaryAsync(
+ request?.UserPrompt ?? string.Empty,
+ request?.SystemPrompt,
+ request?.MaxTokens ?? 150);
+ return new AICompletionResponse { Content = content };
+ }
+
+ public async Task GenerateApplicationAnalysisAsync(ApplicationAnalysisRequest request)
+ {
+ var dataJson = JsonSerializer.Serialize(request.Data, JsonLogOptions);
+ var schemaJson = JsonSerializer.Serialize(request.Schema, JsonLogOptions);
+
+ var attachmentsPayload = request.Attachments
+ .Select(a => new
+ {
+ name = string.IsNullOrWhiteSpace(a.Name) ? "attachment" : a.Name.Trim(),
+ summary = string.IsNullOrWhiteSpace(a.Summary) ? string.Empty : a.Summary.Trim()
+ })
+ .Cast