diff --git a/.github/workflows/docker-build-dev.yml b/.github/workflows/docker-build-dev.yml index e1968299ec..22096b09d9 100644 --- a/.github/workflows/docker-build-dev.yml +++ b/.github/workflows/docker-build-dev.yml @@ -125,10 +125,10 @@ jobs: echo "$JFROG_PASSWORD" | docker login -u "$JFROG_USERNAME" --password-stdin $JFROG_SERVICE - name: Push application images to Artifactory container registry run: | - docker tag unity-grantmanager-dbmigrator $JFROG_SERVICE/$JFROG_REPO_PATH/unity-grantmanager-dbmigrator - docker push $JFROG_SERVICE/$JFROG_REPO_PATH/unity-grantmanager-dbmigrator - docker tag unity-grantmanager-web $JFROG_SERVICE/$JFROG_REPO_PATH/unity-grantmanager-web - docker push $JFROG_SERVICE/$JFROG_REPO_PATH/unity-grantmanager-web + docker tag unity-grantmanager-dbmigrator $JFROG_SERVICE/$JFROG_REPO_PATH/unity-grantmanager-dbmigrator:latest + docker push $JFROG_SERVICE/$JFROG_REPO_PATH/unity-grantmanager-dbmigrator:latest + docker tag unity-grantmanager-web $JFROG_SERVICE/$JFROG_REPO_PATH/unity-grantmanager-web:latest + docker push $JFROG_SERVICE/$JFROG_REPO_PATH/unity-grantmanager-web:latest - name: Disconnect docker from JFrog Artifactory run: | docker logout diff --git a/.github/workflows/docker-build-test.yml b/.github/workflows/docker-build-test.yml index 96a43e594d..f6a35b804a 100644 --- a/.github/workflows/docker-build-test.yml +++ b/.github/workflows/docker-build-test.yml @@ -158,10 +158,10 @@ jobs: echo "$JFROG_PASSWORD" | docker login -u "$JFROG_USERNAME" --password-stdin $JFROG_SERVICE - name: Push application images to Artifactory container registry run: | - docker tag unity-grantmanager-dbmigrator $JFROG_SERVICE/$JFROG_REPO_PATH/unity-grantmanager-dbmigrator - docker push $JFROG_SERVICE/$JFROG_REPO_PATH/unity-grantmanager-dbmigrator - docker tag unity-grantmanager-web $JFROG_SERVICE/$JFROG_REPO_PATH/unity-grantmanager-web - docker push $JFROG_SERVICE/$JFROG_REPO_PATH/unity-grantmanager-web + docker tag unity-grantmanager-dbmigrator $JFROG_SERVICE/$JFROG_REPO_PATH/unity-grantmanager-dbmigrator:latest + docker push $JFROG_SERVICE/$JFROG_REPO_PATH/unity-grantmanager-dbmigrator:latest + docker tag unity-grantmanager-web $JFROG_SERVICE/$JFROG_REPO_PATH/unity-grantmanager-web:latest + docker push $JFROG_SERVICE/$JFROG_REPO_PATH/unity-grantmanager-web:latest - name: Disconnect docker from JFrog Artifactory run: | docker logout diff --git a/.github/workflows/pr-check-dev-branch.yml b/.github/workflows/pr-check-dev-branch.yml index 80f6731abd..6e5b709ff2 100644 --- a/.github/workflows/pr-check-dev-branch.yml +++ b/.github/workflows/pr-check-dev-branch.yml @@ -69,7 +69,7 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@v5 with: dotnet-version: "9.0.x" @@ -85,7 +85,7 @@ jobs: --logger "trx;LogFileName=${NAME}.trx" \ --results-directory TestResults - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v5 with: name: test-output-${{ strategy.job-index }} path: TestResults/ @@ -133,7 +133,7 @@ jobs: echo "failed=$FAILED" >> $GITHUB_OUTPUT echo "skipped=$SKIPPED" >> $GITHUB_OUTPUT - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v5 with: name: merged-test-results path: merged/ diff --git a/.github/workflows/pr-check-main-branch.yml b/.github/workflows/pr-check-main-branch.yml index cbb297ba79..d81966efe6 100644 --- a/.github/workflows/pr-check-main-branch.yml +++ b/.github/workflows/pr-check-main-branch.yml @@ -65,7 +65,7 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@v5 with: dotnet-version: "9.0.x" @@ -81,7 +81,7 @@ jobs: --logger "trx;LogFileName=${NAME}.trx" \ --results-directory TestResults - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v5 with: name: test-output-${{ strategy.job-index }} path: TestResults/ @@ -129,7 +129,7 @@ jobs: echo "failed=$FAILED" >> $GITHUB_OUTPUT echo "skipped=$SKIPPED" >> $GITHUB_OUTPUT - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v5 with: name: merged-test-results path: merged/ diff --git a/.github/workflows/pr-check-test-branch.yml b/.github/workflows/pr-check-test-branch.yml index 7deb972735..9fea720bbd 100644 --- a/.github/workflows/pr-check-test-branch.yml +++ b/.github/workflows/pr-check-test-branch.yml @@ -67,7 +67,7 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@v5 with: dotnet-version: "9.0.x" @@ -83,7 +83,7 @@ jobs: --logger "trx;LogFileName=${NAME}.trx" \ --results-directory TestResults - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v5 with: name: test-output-${{ strategy.job-index }} path: TestResults/ @@ -131,7 +131,7 @@ jobs: echo "failed=$FAILED" >> $GITHUB_OUTPUT echo "skipped=$SKIPPED" >> $GITHUB_OUTPUT - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v5 with: name: merged-test-results path: merged/ diff --git a/.github/workflows/sonarsource-scan.yml b/.github/workflows/sonarsource-scan.yml new file mode 100644 index 0000000000..defcb66827 --- /dev/null +++ b/.github/workflows/sonarsource-scan.yml @@ -0,0 +1,92 @@ +name: SonarCloud Analysis + +on: + push: + branches: + - dev2 +# - dev +# - test +# - main +# pull_request: +# types: [opened, synchronize, reopened] +# workflow_dispatch: + +permissions: + contents: read + pull-requests: write + checks: write + security-events: write + + +jobs: + sonarcloud: + name: SonarCloud + runs-on: ubuntu-latest + environment: ${{ github.ref_name == 'main' && 'main' || github.ref_name == 'test' && 'test' || 'dev' }} + steps: + - name: Set up JDK 17 + uses: actions/setup-java@v5 + with: + java-version: 17 + distribution: 'zulu' + + - uses: actions/checkout@v6 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: '9.0.x' + + - name: Cache SonarCloud packages + uses: actions/cache@v5 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + + - name: Cache SonarCloud scanner + id: cache-sonar-scanner + uses: actions/cache@v5 + with: + path: ./.sonar/scanner + key: ${{ runner.os }}-sonar-scanner + restore-keys: ${{ runner.os }}-sonar-scanner + + - name: Install SonarCloud scanner + if: steps.cache-sonar-scanner.outputs.cache-hit != 'true' + run: | + dotnet tool install --global dotnet-sonarscanner + + - name: Set version for SonarCloud + run: | + VERSION="${{ vars.UGM_BUILD_VERSION }}" + echo "Debug: UGM_BUILD_VERSION variable value: '$VERSION'" + if [ -z "$VERSION" ]; then + echo "BUILD_VERSION=1.0.0-dev" >> $GITHUB_ENV + echo "Using fallback version: 1.0.0-dev (UGM_BUILD_VERSION variable not set)" + else + echo "BUILD_VERSION=$VERSION" >> $GITHUB_ENV + echo "Using project version: $VERSION" + fi + + - name: Restore dependencies + working-directory: ./applications/Unity.GrantManager + run: dotnet restore Unity.GrantManager.sln + + - name: Build solution + working-directory: ./applications/Unity.GrantManager + run: dotnet build Unity.GrantManager.sln --no-restore + + - name: Run tests with coverage + working-directory: ./applications/Unity.GrantManager + run: dotnet test Unity.GrantManager.sln --no-build --verbosity normal --collect:"XPlat Code Coverage" --results-directory ./TestResults/ + + - name: SonarCloud Scan + uses: SonarSource/sonarqube-scan-action@v7 + with: + projectBaseDir: applications/Unity.GrantManager + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} \ No newline at end of file diff --git a/applications/Unity.AutoUI/cypress.config.ts b/applications/Unity.AutoUI/cypress.config.ts index 2d846e5c58..b1e381022d 100644 --- a/applications/Unity.AutoUI/cypress.config.ts +++ b/applications/Unity.AutoUI/cypress.config.ts @@ -8,6 +8,16 @@ export default defineConfig({ e2e: { setupNodeEvents(on) { on("task", { + readJsonIfExists(filePath: string): Record | null { + try { + // path.resolve handles both forward and back slashes on Windows + const content = fs.readFileSync(path.resolve(filePath), "utf-8"); + return JSON.parse(content); + } catch { + return null; + } + }, + async uploadChefsFile({ baseURL, authToken, diff --git a/applications/Unity.AutoUI/cypress/e2e/lists.cy.ts b/applications/Unity.AutoUI/cypress/e2e/lists.cy.ts index 7a3869f0ca..1774c123e3 100644 --- a/applications/Unity.AutoUI/cypress/e2e/lists.cy.ts +++ b/applications/Unity.AutoUI/cypress/e2e/lists.cy.ts @@ -75,7 +75,8 @@ describe('Grant Manager Login and List Navigation', () => { cy.get(listboxSel).within(() => { cy.get('a.dropdown-item[role="option"]').then(($opts) => { const match = $opts.filter((_, el) => { - const text = el.querySelector('span.text')?.textContent || '' + const textNode = el.querySelector('span.text') + const text = textNode ? textNode.textContent || '' : '' return text.trim() === 'Test' }) @@ -95,7 +96,8 @@ describe('Grant Manager Login and List Navigation', () => { }) }) - cy.get('body').click(0, 0) + cy.get(btnSel).first().click({ force: true }) + cy.get(btnSel).first().should('have.attr', 'aria-expanded', 'false') }) } diff --git a/applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts b/applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts index 22f0f2eee6..550c95ff22 100644 --- a/applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts +++ b/applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts @@ -182,7 +182,7 @@ export class ApplicationDetailsPage extends BasePage { | "projectInfo" | "applicantInfo" | "fundingAgreement" - | "paymentInfo" + | "paymentInfo", ): this { const tabSelectors: Record = { submission: this.tabs.submission, @@ -348,8 +348,10 @@ export class ApplicationDetailsPage extends BasePage { * Verify Site Info table is populated */ verifySiteInfoTablePopulated(): this { - cy.get("#SiteInfoTable tbody tr", { timeout: 20000 }) - .should("have.length.at.least", 1); + cy.get("#SiteInfoTable tbody tr", { timeout: 20000 }).should( + "have.length.at.least", + 1, + ); return this; } @@ -357,11 +359,13 @@ export class ApplicationDetailsPage extends BasePage { * Verify Site Info table has data in specific columns */ verifySiteInfoTableHasData(): this { - cy.get("#SiteInfoTable tbody tr", { timeout: 20000 }).first().within(() => { - cy.get("td").eq(0).should("not.be.empty"); // Site # - cy.get("td").eq(1).should("not.be.empty"); // Pay Group - cy.get("td").eq(2).should("not.be.empty"); // Mailing Address - }); + cy.get("#SiteInfoTable tbody tr", { timeout: 20000 }) + .first() + .within(() => { + cy.get("td").eq(0).should("not.be.empty"); // Site # + cy.get("td").eq(1).should("not.be.empty"); // Pay Group + cy.get("td").eq(2).should("not.be.empty"); // Mailing Address + }); return this; } @@ -392,8 +396,9 @@ export class ApplicationDetailsPage extends BasePage { * Select Payment Group in Edit Site modal */ selectPaymentGroup(paymentGroup: "EFT" | "Cheque"): this { - cy.get("#Site_PaymentGroup", { timeout: 20000 }) - .select(paymentGroup, { force: true }); + cy.get("#Site_PaymentGroup", { timeout: 20000 }).select(paymentGroup, { + force: true, + }); return this; } @@ -406,7 +411,9 @@ export class ApplicationDetailsPage extends BasePage { .click({ force: true }); cy.wait(2000); // Wait for save to process cy.get("body").type("{esc}"); - cy.get(".modal.show, .modal.fade.show", { timeout: 20000 }).should("not.exist"); + cy.get(".modal.show, .modal.fade.show", { timeout: 20000 }).should( + "not.exist", + ); cy.get(".modal-backdrop", { timeout: 20000 }).should("not.exist"); return this; } @@ -435,7 +442,9 @@ export class ApplicationDetailsPage extends BasePage { .click({ force: true }); } }); - cy.get(this.statusActions.dropdownMenu, { timeout: 10000 }).should("be.visible"); + cy.get(this.statusActions.dropdownMenu, { timeout: 10000 }).should( + "be.visible", + ); } /** @@ -472,26 +481,36 @@ export class ApplicationDetailsPage extends BasePage { /** * Click Approve action. - * If "Complete Assessment" is enabled in the dropdown, click it first, - * then reopen the dropdown before clicking Approve. + * If "Complete Assessment" is present and enabled in the dropdown, click it first + * (and confirm any resulting dialog), then reopen the dropdown before clicking Approve. */ clickApprove(): this { this.openStatusActionsDropdown(); - cy.get(this.statusActions.completeAssessment).then(($btn) => { - if (!$btn.is(":disabled")) { - cy.wrap($btn).click({ force: true }); - cy.get("body").then(($body) => { - if ($body.find(this.confirmModal.modal).filter(":visible").length > 0) { + + // Use body-check so we never timeout when the button is absent from the DOM + cy.get("body").then(($body) => { + const $completeBtn = $body.find(this.statusActions.completeAssessment); + if ($completeBtn.length > 0 && !$completeBtn.is(":disabled")) { + cy.wait(10000); // Wait for any potential UI updates before clicking + cy.wrap($completeBtn).click(); + cy.wait(2000); // Wait for any potential UI updates after clicking + cy.get("body").then(($b) => { + if ($b.find(this.confirmModal.modal).filter(":visible").length > 0) { cy.get(this.confirmModal.modal) .find(this.confirmModal.confirmButton) .click({ force: true }); } }); - // Wait for page to stabilize after status transition - cy.get(this.statusActions.dropdownToggle, { timeout: 20000 }).should("be.visible"); + this.dismissErrorModalIfPresent(); + + // Wait for the page to stabilize before reopening the dropdown + cy.get(this.statusActions.dropdownToggle, { timeout: 20000 }).should( + "be.visible", + ); cy.wait(2000); } }); + // Always reopen dropdown fresh before clicking Approve (dropdown may have closed) this.openStatusActionsDropdown(); cy.get(this.statusActions.approve, { timeout: 10000 }) @@ -604,7 +623,7 @@ export class ApplicationDetailsPage extends BasePage { | "close" | "withdraw" | "defer" - | "onHold" + | "onHold", ): void { const actionSelectors: Record = { startReview: this.statusActions.startReview, @@ -636,7 +655,7 @@ export class ApplicationDetailsPage extends BasePage { | "close" | "withdraw" | "defer" - | "onHold" + | "onHold", ): void { const actionSelectors: Record = { startReview: this.statusActions.startReview, diff --git a/applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts b/applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts index 4d27a480c0..b5f2636ef9 100644 --- a/applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts +++ b/applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts @@ -387,7 +387,11 @@ export class ReviewAssessmentPage extends BasePage { * Set decision date to today (format: YYYY-MM-DD) */ setDecisionDateToToday(): this { - const today = new Date().toISOString().split("T")[0]; + const now = new Date(); + const yyyy = now.getFullYear(); + const mm = String(now.getMonth() + 1).padStart(2, "0"); + const dd = String(now.getDate()).padStart(2, "0"); + const today = `${yyyy}-${mm}-${dd}`; cy.get(this.assessment.decisionDate, { timeout: this.STANDARD_TIMEOUT }) .clear() .type(today); diff --git a/applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts b/applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts index d34c424860..5b480b2aab 100644 --- a/applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts +++ b/applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts @@ -4,61 +4,117 @@ * Approval Flow Regression Test - Full Approval Workflow * * This test validates the complete application approval workflow including: - * - Dynamic submission ID fetching from API + * - Submission ID resolution (seeded file → static override → dynamic API fetch) * - Searching and opening a submission * - Review and assessment process * - Payment info configuration * - Adding comments and attachments - * - Approval action (confirmed via dialog) - * - * The submission ID is fetched dynamically from the API after login, - * ensuring tests always run against valid, available data. + * - Application approval (confirmed via dialog) + * - Payment request submission + * - L1 and L2 payment approvals (two separate users) + * - Post-approval status and date validation on the Payments table */ import { ApplicationsListPage } from "../pages/ApplicationsListPage"; import { ApplicationDetailsPage } from "../pages/ApplicationDetailsPage"; import { ReviewAssessmentPage } from "../pages/ReviewAssessmentPage"; import { ApplicationDetailsRightTabPage } from "../pages/ApplicationDetailsRightTabPage"; +import { NavigationPage } from "../pages/NavigationPage"; import { loginIfNeeded } from "../support/auth"; -const isProd = (Cypress.env("CHEFS_ENV") || Cypress.env("environment") || "").toLowerCase() === "prod"; +const isProd = + ( + Cypress.env("CHEFS_ENV") || + Cypress.env("environment") || + "" + ).toLowerCase() === "prod"; // ============ Test Configuration ============ -// Set submissionId to null for dynamic fetch, or provide a value to override const TEST_CONFIG = { - // Dynamic submission: set to null to fetch from API, or provide ID to use static value + // Set to null to resolve ID automatically, or provide a value to force a specific submission submissionId: null as string | null, grantProgram: "Default Grants Program", approvedAmount: "5000", supplierNumber: Cypress.env("environment") === "TEST" ? "2002712" : "2009366", paymentGroup: "Cheque" as const, testComment: "Test comment from automated regression test", - // Options for dynamic submission fetching (only used when submissionId is null) - // Results are sorted by submissionDate descending (latest first) by default + // Options used when fetching dynamically (ignored when submissionId is set or seeded file exists) fetchOptions: { - // Filter by category (required for this test) categoryFilter: "Data Seeder", - // Filter by status (uncomment to enable): - // Available: 'Submitted', 'Under Assessment', 'Approved', 'Closed', 'Deferred' + // Available statuses: 'Submitted', 'Under Assessment', 'Approved', 'Closed', 'Deferred' statusFilter: ["Submitted"], - // Limit to submissions within N days (uncomment to enable): - maxAge: 30, - // Which submission to use after sorting (0 = latest, 1 = second-latest, etc.) - // Use index > 0 to avoid picking the same submission as other concurrent tests - index: 0, + maxAge: 30, // Only consider submissions created within the last N days + index: 0, // 0 = latest; increment to avoid collision with concurrent tests }, }; (isProd ? describe.skip : describe)("Approval Flow Regression Test", () => { - // Page object instances (reused across all tests) + // Page object instances reused across all tests const listPage = new ApplicationsListPage(); const detailsPage = new ApplicationDetailsPage(); const reviewPage = new ReviewAssessmentPage(); const rightTabPage = new ApplicationDetailsRightTabPage(); + const navPage = new NavigationPage(); - // Dynamic submission ID - populated after login + // Resolved after "Fetch submission ID from API" — shared across all subsequent tests let submissionId: string; + // ============ Shared Helpers ============ + + /** Navigate to the Payments tab and filter the table by submissionId. */ + function navigateToPaymentsAndSearch(): void { + cy.reload(); + navPage.goToPayments(); + cy.location("pathname", { timeout: 20000 }).should("include", "Payment"); + cy.get("#search", { timeout: 20000 }) + .should("be.visible") + .clear() + .type(submissionId); + cy.contains("tr", submissionId, { timeout: 20000 }).should("exist"); + } + + /** Select the submissionId row and open the Approve Payments modal. */ + function selectRowAndOpenApproveModal(): void { + cy.contains("tr", submissionId, { timeout: 20000 }) + .find(".checkbox-select") + .click({ force: true }); + cy.contains("button", "Approve", { timeout: 20000 }) + .should("be.visible") + .click(); + cy.contains(".modal-title", "Approve Payments", { timeout: 20000 }).should( + "be.visible", + ); + } + + /** + * Validate the Approve Payments modal fields, enter an auto-generated note, + * submit the approval and assert the modal closes. + * @param notePrefix - Short prefix to distinguish L1 vs L2 notes (max length enforced at 50 chars total) + */ + function validateAndSubmitApproveModal(notePrefix: string): void { + cy.get("#ApplicationCount").should("have.value", "1"); + cy.get("#UpdateTotalAmount").should("not.have.value", "0"); + cy.get("#Note").should("be.visible"); + + const approvalNote = `${notePrefix}-${submissionId}-${Date.now()}`.slice( + 0, + 50, + ); + cy.get("#Note").clear().type(approvalNote); + + cy.get("#btnSubmitPayment") + .should("be.visible") + .and("not.be.disabled") + .click(); + + cy.contains(".modal-title", "Approve Payments", { timeout: 20000 }).should( + "not.exist", + ); + cy.log(`✅ Payment approved with note: ${approvalNote}`); + } + + // ============ Setup ============ + before(() => { Cypress.config("includeShadowDom", true); loginIfNeeded(); @@ -67,18 +123,35 @@ const TEST_CONFIG = { // ============ Dynamic Submission Fetch ============ it("Fetch submission ID from API", () => { - // Use static ID if provided, otherwise fetch dynamically - if (TEST_CONFIG.submissionId) { - submissionId = TEST_CONFIG.submissionId; - cy.log(`📌 Using static submission ID: ${submissionId}`); - return; - } - - // Fetch submission ID dynamically from API using session cookies - cy.fetchDynamicSubmission(TEST_CONFIG.fetchOptions).then((id) => { - submissionId = id; - cy.log(`✅ Fetched dynamic submission ID: ${submissionId}`); - }); + // Priority 1: ID written by the seed script when running test:approval-flow + cy.task("readJsonIfExists", "cypress/scripts/last-submission-id.json").then( + (result) => { + const seeded = result as { + submissionId?: string; + createdAt?: string; + } | null; + if (seeded?.submissionId) { + submissionId = seeded.submissionId; + cy.log(`📌 Using seeded submission ID: ${submissionId}`); + // Clear file to prevent reuse on the next standalone run + cy.writeFile("cypress/scripts/last-submission-id.json", {}); + return; + } + + // Priority 2: static override in TEST_CONFIG + if (TEST_CONFIG.submissionId) { + submissionId = TEST_CONFIG.submissionId; + cy.log(`📌 Using static submission ID: ${submissionId}`); + return; + } + + // Priority 3: fetch the latest matching submission from the Unity API + cy.fetchDynamicSubmission(TEST_CONFIG.fetchOptions).then((id) => { + submissionId = id; + cy.log(`✅ Fetched dynamic submission ID: ${submissionId}`); + }); + }, + ); }); // ============ Navigation & Search ============ @@ -88,7 +161,6 @@ const TEST_CONFIG = { }); it("Search for submission", () => { - // Ensure submissionId is available before searching expect(submissionId, "Submission ID should be set").to.exist; listPage .selectQuickDateRange("alltime") @@ -115,11 +187,9 @@ const TEST_CONFIG = { }); it("Create and complete assessment", () => { - // Wait for assessment section to load - cy.wait(2000); + cy.wait(2000); // Allow assessment section to fully load reviewPage.scrollToAssessmentList(); - // Check if Create button exists and click it cy.get("body").then(($body) => { if ($body.find("#CreateButton").length > 0) { cy.get("#CreateButton").click({ force: true }); @@ -129,7 +199,6 @@ const TEST_CONFIG = { } }); - // Check if Complete button exists and click it cy.get("body").then(($body) => { if ($body.find("#CompleteButton").length > 0) { cy.get("#CompleteButton").click({ force: true }); @@ -145,8 +214,7 @@ const TEST_CONFIG = { // ============ Payment Info ============ it("Configure payment info", () => { - // Reload page to get fresh data and avoid concurrency issues - cy.reload(); + cy.reload(); // Reload to get fresh data and avoid concurrency issues cy.wait(2000); detailsPage .goToPaymentInfoTab() @@ -168,7 +236,6 @@ const TEST_CONFIG = { // ============ Comments & Attachments ============ it("Add a comment", () => { - // Dismiss any error modals from previous steps detailsPage.dismissErrorModalIfPresent(); rightTabPage .goToCommentsTab() @@ -177,46 +244,155 @@ const TEST_CONFIG = { }); it("Add an attachment", () => { - // Dismiss any error modals from previous steps detailsPage.dismissErrorModalIfPresent(); rightTabPage.goToAttachmentsTab(); cy.wait(1000); // Allow tab content to load - // Store initial count to verify upload rightTabPage.getAttachmentsCount().then((initialCount) => { cy.log(`Initial attachment count: ${initialCount}`); - // Generate unique filename to ensure new file is added const timestamp = Date.now(); const uniqueFileName = `test-attachment-${timestamp}.txt`; - // Upload file with unique content rightTabPage.uploadUniqueAttachment(uniqueFileName, timestamp); - // Verify upload success cy.contains("Successful").should("be.visible"); - cy.wait(2000); // Allow UI to update + cy.wait(2000); // Allow UI to update after upload - // Verify count increased rightTabPage.getAttachmentsCount().then((newCount) => { cy.log(`New attachment count: ${newCount}`); expect(newCount).to.be.greaterThan(initialCount); }); - // Verify file appears in list rightTabPage.verifyAttachmentExists(uniqueFileName); cy.screenshot("attachment-upload-complete"); }); }); - // ============ Approval Action ============ + // ============ Application Approval ============ it("Test approval workflow (confirm)", () => { - // Dismiss any error modals from previous steps + cy.reload(); // Refresh to ensure all changes are reflected before approval detailsPage.dismissErrorModalIfPresent(); detailsPage.clickApprove().waitForConfirmModal().clickConfirm(); }); + // ============ Post-Approval Verification ============ + + it("Navigate back to applications list", () => { + cy.visit(`${Cypress.env("webapp.url")}GrantApplications`); + listPage.switchToGrantProgram(TEST_CONFIG.grantProgram); + }); + + it("Verify application status is Approved", () => { + expect(submissionId, "Submission ID should be set").to.exist; + listPage + .selectQuickDateRange("alltime") + .waitForTableRefresh() + .searchForSubmission(submissionId); + + cy.contains("tr", submissionId, { timeout: 20000 }).should( + "contain.text", + "Approved", + ); + }); + + // ============ Payment Request ============ + + it("Select approved application and submit payment request", () => { + listPage.selectRowByText(submissionId).clickPaymentButtonWithWait(); + listPage.waitForPaymentModalVisible(); + + // Description field has a max length of 40 chars + const paymentDescription = `AutoTest-${submissionId}`.slice(0, 40); + + // Modal uses divs (not a table) — target description input by id suffix + cy.get("#payment-modal input[id$='__Description']") + .should("be.visible") + .clear() + .type(paymentDescription); + + cy.contains("button", "Submit Payment Requests") + .should("be.visible") + .and("not.be.disabled") + .click(); + + cy.get("#payment-modal", { timeout: 20000 }).should("not.be.visible"); + cy.log(`✅ Payment request submitted: ${paymentDescription}`); + }); + + // ============ L1 Payment Approval (User 1) ============ + + it("Navigate to Payments tab and search for submission", () => { + navigateToPaymentsAndSearch(); + }); + + it("Select payment row and open Approve Payments modal", () => { + selectRowAndOpenApproveModal(); + }); + + it("L1 Approval - Validate modal details, enter note and approve", () => { + validateAndSubmitApproveModal("AutoApproval"); + }); + + // ============ L2 Payment Approval (User 2) ============ + + it("Logout user1 and login as user2 for L2 approval", () => { + cy.logout(); + loginIfNeeded({ + username: Cypress.env("test2username") as string, + password: Cypress.env("test2password") as string, + }); + }); + + it("Navigate to Payments tab and search for submission (L2)", () => { + listPage.switchToGrantProgram(TEST_CONFIG.grantProgram); + navigateToPaymentsAndSearch(); + }); + + it("Select payment row and open Approve Payments modal (L2)", () => { + selectRowAndOpenApproveModal(); + }); + + it("L2 Approval - Validate modal details, enter note and approve", () => { + validateAndSubmitApproveModal("L2Approval"); + }); + + // ============ Post-L2 Validation ============ + + it("Validate payment status and approval dates on Payments table", () => { + const today = listPage.getTodayIsoLocal(); + + // Re-search to ensure the table reflects the latest state after L2 approval + cy.get("#search", { timeout: 20000 }) + .should("be.visible") + .clear() + .type(submissionId); + + cy.contains("tr", submissionId, { timeout: 20000 }).should(($row) => { + const text = $row.text(); + expect( + text.includes("Sent to Accounts Payable") || + text.includes("Submitted to CAS"), + 'Expected row to contain "Sent to Accounts Payable" or "Submitted to CAS"', + ).to.be.true; + }); + + // Validate date columns by resolving each column's index from its header title + ["Updated On", "L1 Approval Date", "L2 Approval Date"].forEach((header) => { + cy.get(".dt-scroll-head span.dt-column-title") + .contains(header) + .closest("th") + .invoke("index") + .then((colIndex) => { + cy.contains("tr", submissionId) + .find("td") + .eq(colIndex) + .should("contain.text", today); + }); + }); + }); + // ============ Cleanup ============ it("Logout", () => { diff --git a/applications/Unity.AutoUI/cypress/scripts/README.md b/applications/Unity.AutoUI/cypress/scripts/README.md index 528f451a64..ce63c13c27 100644 --- a/applications/Unity.AutoUI/cypress/scripts/README.md +++ b/applications/Unity.AutoUI/cypress/scripts/README.md @@ -42,7 +42,7 @@ Add or update the token in `cypress.env.json`: ```json { - "CHEFS_AUTH_TOKEN": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." + "CHEFS_AUTH_TOKEN": "Bearer *..." } ``` diff --git a/applications/Unity.AutoUI/cypress/scripts/chefs-api-submission.cy.ts b/applications/Unity.AutoUI/cypress/scripts/chefs-api-submission.cy.ts index ff81ec0ffd..74902a5dd9 100644 --- a/applications/Unity.AutoUI/cypress/scripts/chefs-api-submission.cy.ts +++ b/applications/Unity.AutoUI/cypress/scripts/chefs-api-submission.cy.ts @@ -320,7 +320,13 @@ const isProd = if (response.body.id) { createdSubmissionId = response.body.id; + const confirmationId = response.body.confirmationId || response.body.id; cy.log(`✅ Submission created with ID: ${response.body.id}`); + cy.log(`✅ Confirmation ID: ${confirmationId}`); + cy.writeFile("cypress/scripts/last-submission-id.json", { + submissionId: confirmationId, + createdAt: new Date().toISOString(), + }); } expect(response.body).to.have.property("formVersionId", environment.versionId); diff --git a/applications/Unity.AutoUI/package.json b/applications/Unity.AutoUI/package.json index 76aa67a0f5..d275c5ea12 100644 --- a/applications/Unity.AutoUI/package.json +++ b/applications/Unity.AutoUI/package.json @@ -6,7 +6,8 @@ "test:regression-headless": "env -u ELECTRON_RUN_AS_NODE cypress run --spec 'cypress/regression/**/*.cy.ts' --headless --browser chrome", "test:open": "env -u ELECTRON_RUN_AS_NODE cypress open --browser chrome", "test:seed": "env -u ELECTRON_RUN_AS_NODE cypress run --spec 'cypress/scripts/chefs-api-submission.cy.ts' --browser chrome", - "test:approval-flow": "npm run test:seed && env -u ELECTRON_RUN_AS_NODE cypress run --spec 'cypress/regression/ApprovalFlow.cy.ts' --headless --browser chrome" + "test:approval-flow": "npm run test:seed && env -u ELECTRON_RUN_AS_NODE cypress run --spec 'cypress/regression/ApprovalFlow.cy.ts' --headed --browser chrome", + "test:approval-flow-headless": "npm run test:seed && env -u ELECTRON_RUN_AS_NODE cypress run --spec 'cypress/regression/ApprovalFlow.cy.ts' --browser chrome" }, "dependencies": { "form-data": "^4.0.5", @@ -16,4 +17,4 @@ "@types/node": "^25.4.0", "cypress": "15.12.0" } -} +} \ No newline at end of file diff --git a/applications/Unity.GrantManager/.env.example b/applications/Unity.GrantManager/.env.example index c2094e4c05..c937e86596 100644 --- a/applications/Unity.GrantManager/.env.example +++ b/applications/Unity.GrantManager/.env.example @@ -56,6 +56,8 @@ AuthServer__OidcSignoutCallback="http://localhost:44342/signout-callback-oidc" ##S3__AssessmentS3Folder="Unity/Adjudication" ##S3__DisallowedFileTypes="[ "exe" , "sh" , "ksh" , "bat" , "cmd" ]" ##S3__MaxFileSize="25" +##S3__EmailAttachmentMaxFileSize="20" +##S3__EmailAttachmentsTotalMaxFileSize="25" ## RABBIT MQ UNITY_RABBIT_MQ_HOST="rabbitmq" UNITY_RABBIT_MQ_PORT="5672" diff --git a/applications/Unity.GrantManager/.github/agents/feature-planner.agent.md b/applications/Unity.GrantManager/.github/agents/feature-planner.agent.md index adb12769d6..a52030b62f 100644 --- a/applications/Unity.GrantManager/.github/agents/feature-planner.agent.md +++ b/applications/Unity.GrantManager/.github/agents/feature-planner.agent.md @@ -1,16 +1,88 @@ --- name: feature-planner description: Plans feature implementation across Domain, Application, EF Core, Web, and tests. -tools: ['fetch', 'githubRepo', 'problems', 'usages', 'search', 'todos', 'runSubagent'] +argument-hint: Outline the goal or problem to research +target: vscode +disable-model-invocation: true +tools: ['search', 'read', 'web', 'vscode/memory', 'github/issue_read', 'github.vscode-pull-request-github/issue_fetch', 'github.vscode-pull-request-github/activePullRequest', 'execute/getTerminalOutput', 'execute/testFailure', 'agent', 'vscode/askQuestions'] +agents: ['Explore'] +handoffs: + - label: Start Implementation + agent: agent + prompt: 'Start implementation' + send: true + - label: Open in Editor + agent: agent + prompt: '#createFile the plan as is into an untitled file (`untitled:plan-${camelCaseName}.prompt.md` without frontmatter) for further refinement.' + send: true + showContinueOn: false --- # ABP Feature Planner Agent -You are the feature planning specialist for Unity Grant Manager. +You are the FEATURE PLANNING AGENT for Unity Grant Manager, pairing with the user to create a detailed, actionable plan. -## Mission +You research the codebase → clarify with the user → capture findings and decisions to convert a feature request into a comprehensive plan that respects ABP modular layering and delivery flow. This iterative approach catches edge cases and non-obvious requirements BEFORE implementation begins. -Convert a feature request into an implementation plan that respects ABP modular layering and delivery flow. +Your SOLE responsibility is planning. NEVER start implementation. + +**Current plan**: `/memories/session/plan.md` - update using #tool:vscode/memory. + + +- STOP if you consider running file editing tools — plans are for others to execute. The only write tool you have is #tool:vscode/memory for persisting plans. +- Use #tool:vscode/askQuestions freely to clarify requirements — don't make large assumptions +- Present a well-researched plan with loose ends tied BEFORE implementation + + + +Cycle through these phases based on user input. This is iterative, not linear. If the user task is highly ambiguous, do only *Discovery* to outline a draft plan, then move on to alignment before fleshing out the full plan. + +## 1. Discovery + +Run the *Explore* subagent to gather context, analogous existing features to use as implementation templates, and potential blockers or ambiguities. When the task spans multiple independent areas (e.g., frontend + backend, different features, separate modules), launch **2-3 *Explore* subagents in parallel** — one per area — to speed up discovery. + +Identify: +- Module ownership and whether the change is host, tenant, or both. +- Work split by ABP layer: Domain.Shared → Domain → Application.Contracts → Application → EntityFrameworkCore → HttpApi/Web → Tests. +- Dependencies and ordering constraints between layers. +- Cross-module impacts and permission/localization requirements. + +Update the plan with your findings. + +## 2. Alignment + +If research reveals major ambiguities or if you need to validate assumptions: +- Use #tool:vscode/askQuestions to clarify intent with the user. +- Surface discovered technical constraints or alternative approaches. +- If answers significantly change the scope, loop back to **Discovery**. + +## 3. Design + +Once context is clear, draft a comprehensive implementation plan structured around ABP layers. + +The plan should reflect: +- Structured concisely enough to be scannable and detailed enough for effective execution. +- Step-by-step implementation with explicit dependencies — mark which steps can run in parallel vs. which block on prior steps. +- For plans with many steps, group into named phases that are each independently verifiable. +- Verification steps for validating the implementation, both automated and manual. +- Critical architecture to reuse or use as reference — reference specific functions, types, or patterns, not just file names. +- Critical files to be modified (with full paths). +- Explicit scope boundaries — what's included and what's deliberately excluded. +- Reference decisions from the discussion. +- Leave no ambiguity. + +Save the comprehensive plan document to `/memories/session/plan.md` via #tool:vscode/memory, then show the scannable plan to the user for review. You MUST show the plan to the user, as the plan file is for persistence only, not a substitute for showing it to the user. + +## 4. Refinement + +On user input after showing the plan: +- Changes requested → revise and present updated plan. Update `/memories/session/plan.md` to keep the documented plan in sync. +- Questions asked → clarify, or use #tool:vscode/askQuestions for follow-ups. +- Alternatives wanted → loop back to **Discovery** with new subagent. +- Approval given → acknowledge, the user can now use handoff buttons. + +Keep iterating until explicit approval or handoff. + ## Inputs @@ -19,30 +91,54 @@ Convert a feature request into an implementation plan that respects ABP modular - Target module(s). - Any constraints (timeline, migration risk, tenant scope, security requirements). -## Process - -1. Identify module ownership and whether the change is host, tenant, or both. -2. Split work by layer: - - Domain.Shared - - Domain - - Application.Contracts - - Application - - EntityFrameworkCore - - HttpApi/Web - - Tests -3. List dependencies and ordering constraints. -4. Flag cross-module impacts and permission/localization requirements. - -## Output Format - -Return sections in this order: - -1. Scope summary. -2. Layer-by-layer implementation tasks. -3. Migration and data impact. -4. Test plan summary. -5. Risks and mitigations. -6. Definition of done checklist. + +```markdown +## Plan: {Title (2-10 words)} + +{TL;DR - what, why, and how (your recommended approach).} + +**Steps** + +### Phase 1 — Domain & Contracts +1. {Domain.Shared changes — enums, consts, error codes} +2. {Domain entity/aggregate changes — note dependency ("*depends on N*") or parallelism ("*parallel with step N*") when applicable} +3. {Application.Contracts — DTOs, IAppService interfaces, permissions} + +### Phase 2 — Application & Persistence +4. {Application service implementation} +5. {EntityFrameworkCore — DbContext, entity config, migration} + +### Phase 3 — API & Frontend +6. {HttpApi controller / AutoAPI} +7. {Web — Pages, JS, localization} + +### Phase 4 — Tests +8. {Unit and integration tests} + +**Relevant files** +- `{full/path/to/file}` — {what to modify or reuse, referencing specific functions/patterns} + +**Migration & Data Impact** +- {Host vs tenant migration scope, data backfill needs, breaking schema changes} + +**Verification** +1. {Verification steps for validating the implementation (**Specific** tasks, tests, commands, MCP tools, etc; not generic statements)} + +**Decisions** (if applicable) +- {Decision, assumptions, and includes/excluded scope} + +**Risks & Mitigations** (if applicable) +- {Risk and mitigation strategy} + +**Definition of Done** +- [ ] {Checklist item} +``` + +Rules: +- NO code blocks — describe changes, link to files and specific symbols/functions. +- NO blocking questions at the end — ask during workflow via #tool:vscode/askQuestions. +- The plan MUST be presented to the user, don't just mention the plan file. + ## Guardrails diff --git a/applications/Unity.GrantManager/.github/skills/abp-cli/SKILL.md b/applications/Unity.GrantManager/.github/skills/abp-cli/SKILL.md index df1fd7b055..f65cd83801 100644 --- a/applications/Unity.GrantManager/.github/skills/abp-cli/SKILL.md +++ b/applications/Unity.GrantManager/.github/skills/abp-cli/SKILL.md @@ -1,6 +1,6 @@ --- name: abp-cli -description: ABP CLI commands - generate-proxy, install-libs, add-package-ref, new-module, install-module, abp update, abp clean, abp suite generate. Use when the user asks how to run ABP CLI commands, generate proxies, install libraries, or use ABP Suite. +description: ABP CLI commands - generate-proxy, install-libs, add-package-ref, new-module, install-module, abp update, abp clean, abp suite generate. Use when the user asks how to run ABP CLI commands, generate proxies, install NPM libraries, or use ABP Suite. --- # ABP CLI Commands diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/ITextExtractionService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Extraction/ITextExtractionService.cs similarity index 85% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/ITextExtractionService.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Extraction/ITextExtractionService.cs index 22f34e292f..d1c4d9f992 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/ITextExtractionService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Extraction/ITextExtractionService.cs @@ -1,6 +1,6 @@ using System.Threading.Tasks; -namespace Unity.GrantManager.AI +namespace Unity.AI.Extraction { public interface ITextExtractionService { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/IAIService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/IAIService.cs similarity index 70% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/IAIService.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/IAIService.cs index d14438a2d2..84c1e1c08b 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/IAIService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/IAIService.cs @@ -1,6 +1,8 @@ using System.Threading.Tasks; +using Unity.AI.Requests; +using Unity.AI.Responses; -namespace Unity.GrantManager.AI +namespace Unity.AI { public interface IAIService { @@ -9,6 +11,6 @@ public interface IAIService Task GenerateCompletionAsync(AICompletionRequest request); Task GenerateAttachmentSummaryAsync(AttachmentSummaryRequest request); Task GenerateApplicationAnalysisAsync(ApplicationAnalysisRequest request); - Task GenerateScoresheetSectionAsync(ScoresheetSectionRequest request); + Task GenerateApplicationScoringAsync(ApplicationScoringRequest request); } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/AIAttachmentItem.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Models/AIAttachmentItem.cs similarity index 90% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/AIAttachmentItem.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Models/AIAttachmentItem.cs index fc4b31e2a9..550b032916 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/AIAttachmentItem.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Models/AIAttachmentItem.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Unity.GrantManager.AI +namespace Unity.AI.Models { public class AIAttachmentItem { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AIJsonKeys.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Models/AIJsonKeys.cs similarity index 96% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AIJsonKeys.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Models/AIJsonKeys.cs index 60a5b52441..501de0c856 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AIJsonKeys.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Models/AIJsonKeys.cs @@ -1,4 +1,4 @@ -namespace Unity.GrantManager.AI +namespace Unity.AI.Models { public static class AIJsonKeys { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/ApplicationAnalysisFinding.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Models/ApplicationAnalysisFinding.cs similarity index 93% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/ApplicationAnalysisFinding.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Models/ApplicationAnalysisFinding.cs index d441d29493..181168009e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/ApplicationAnalysisFinding.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Models/ApplicationAnalysisFinding.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Unity.GrantManager.AI +namespace Unity.AI.Models { public class ApplicationAnalysisFinding { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/ApplicationAnalysisRecommendation.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Models/ApplicationAnalysisRecommendation.cs similarity index 90% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/ApplicationAnalysisRecommendation.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Models/ApplicationAnalysisRecommendation.cs index 1a70d1a5f4..6b082f5295 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/ApplicationAnalysisRecommendation.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Models/ApplicationAnalysisRecommendation.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Unity.GrantManager.AI +namespace Unity.AI.Models { public class ApplicationAnalysisRecommendation { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/ScoresheetSectionAnswer.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Models/ApplicationScoringAnswer.cs similarity index 83% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/ScoresheetSectionAnswer.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Models/ApplicationScoringAnswer.cs index 0a76cbb0e0..48b348164f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/ScoresheetSectionAnswer.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Models/ApplicationScoringAnswer.cs @@ -1,9 +1,9 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Unity.GrantManager.AI +namespace Unity.AI.Models { - public class ScoresheetSectionAnswer + public class ApplicationScoringAnswer { [JsonPropertyName(AIJsonKeys.Answer)] public JsonElement Answer { get; set; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/AICompletionRequest.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Requests/AICompletionRequest.cs similarity index 92% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/AICompletionRequest.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Requests/AICompletionRequest.cs index 74b8aa5494..cc134ef988 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/AICompletionRequest.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Requests/AICompletionRequest.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Unity.GrantManager.AI +namespace Unity.AI.Requests { public class AICompletionRequest { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/ApplicationAnalysisRequest.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Requests/ApplicationAnalysisRequest.cs similarity index 69% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/ApplicationAnalysisRequest.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Requests/ApplicationAnalysisRequest.cs index 3d9aaf789f..4a4eca5de1 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/ApplicationAnalysisRequest.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Requests/ApplicationAnalysisRequest.cs @@ -1,8 +1,9 @@ using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; +using Unity.AI.Models; -namespace Unity.GrantManager.AI +namespace Unity.AI.Requests { public class ApplicationAnalysisRequest { @@ -17,11 +18,5 @@ public class ApplicationAnalysisRequest [JsonPropertyName("promptVersion")] public string? PromptVersion { get; set; } - - [JsonPropertyName("capturePromptIo")] - public bool CapturePromptIo { get; set; } - - [JsonPropertyName("captureContextId")] - public string? CaptureContextId { get; set; } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/ScoresheetSectionRequest.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Requests/ApplicationScoringRequest.cs similarity index 69% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/ScoresheetSectionRequest.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Requests/ApplicationScoringRequest.cs index 7f904ea77a..5a72460138 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/ScoresheetSectionRequest.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Requests/ApplicationScoringRequest.cs @@ -1,10 +1,11 @@ using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; +using Unity.AI.Models; -namespace Unity.GrantManager.AI +namespace Unity.AI.Requests { - public class ScoresheetSectionRequest + public class ApplicationScoringRequest { [JsonPropertyName("data")] public JsonElement Data { get; set; } @@ -20,11 +21,5 @@ public class ScoresheetSectionRequest [JsonPropertyName("promptVersion")] public string? PromptVersion { get; set; } - - [JsonPropertyName("capturePromptIo")] - public bool CapturePromptIo { get; set; } - - [JsonPropertyName("captureContextId")] - public string? CaptureContextId { get; set; } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/AttachmentSummaryRequest.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Requests/AttachmentSummaryRequest.cs similarity index 69% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/AttachmentSummaryRequest.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Requests/AttachmentSummaryRequest.cs index d3eb7fe217..e1703a01ff 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/AttachmentSummaryRequest.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Requests/AttachmentSummaryRequest.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Unity.GrantManager.AI +namespace Unity.AI.Requests { public class AttachmentSummaryRequest { @@ -15,11 +15,5 @@ public class AttachmentSummaryRequest [JsonPropertyName("promptVersion")] public string? PromptVersion { get; set; } - - [JsonPropertyName("capturePromptIo")] - public bool CapturePromptIo { get; set; } - - [JsonPropertyName("captureContextId")] - public string? CaptureContextId { get; set; } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/AICompletionResponse.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/AICompletionResponse.cs similarity index 85% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/AICompletionResponse.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/AICompletionResponse.cs index 316d2ef162..0f0c1c8efe 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/AICompletionResponse.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/AICompletionResponse.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Unity.GrantManager.AI +namespace Unity.AI.Responses { public class AICompletionResponse { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/ApplicationAnalysisResponse.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/ApplicationAnalysisResponse.cs similarity index 94% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/ApplicationAnalysisResponse.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/ApplicationAnalysisResponse.cs index 705b713c00..12f3532f4e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/ApplicationAnalysisResponse.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/ApplicationAnalysisResponse.cs @@ -1,7 +1,8 @@ using System.Collections.Generic; using System.Text.Json.Serialization; +using Unity.AI.Models; -namespace Unity.GrantManager.AI +namespace Unity.AI.Responses { public class ApplicationAnalysisResponse { diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/ApplicationScoringResponse.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/ApplicationScoringResponse.cs new file mode 100644 index 0000000000..d838e7667e --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/ApplicationScoringResponse.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Unity.AI.Models; + +namespace Unity.AI.Responses +{ + public class ApplicationScoringResponse + { + [JsonPropertyName("answers")] + public Dictionary Answers { get; set; } = new(); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/AttachmentSummaryResponse.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/AttachmentSummaryResponse.cs similarity index 79% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/AttachmentSummaryResponse.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/AttachmentSummaryResponse.cs index 4f30b8c44a..546e20bd0f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/AttachmentSummaryResponse.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/AttachmentSummaryResponse.cs @@ -1,6 +1,7 @@ using System.Text.Json.Serialization; +using Unity.AI.Models; -namespace Unity.GrantManager.AI +namespace Unity.AI.Responses { public class AttachmentSummaryResponse { diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AIApplicationContractsModule.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AIApplicationContractsModule.cs index f9e53089f8..c9c5d44334 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AIApplicationContractsModule.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AIApplicationContractsModule.cs @@ -1,13 +1,15 @@ using Volo.Abp.Application; using Volo.Abp.Modularity; using Volo.Abp.Authorization; +using Volo.Abp.SettingManagement; namespace Unity.AI; [DependsOn( typeof(AIDomainSharedModule), typeof(AbpDddApplicationContractsModule), - typeof(AbpAuthorizationModule) + typeof(AbpAuthorizationModule), + typeof(AbpSettingManagementApplicationContractsModule) )] public class AIApplicationContractsModule : AbpModule { diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Attachments/IAttachmentSummaryAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Attachments/IAttachmentSummaryAppService.cs new file mode 100644 index 0000000000..65589ed840 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Attachments/IAttachmentSummaryAppService.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace Unity.GrantManager.Attachments; + +public interface IAttachmentSummaryAppService : IApplicationService +{ + Task GenerateAttachmentSummaryAsync(Guid attachmentId, string? promptVersion = null); + Task> GenerateAttachmentSummariesAsync(List attachmentIds, string? promptVersion = null); +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Automation/IApplicationAIGenerationQueue.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Automation/IApplicationAIGenerationQueue.cs new file mode 100644 index 0000000000..b9d5db8a9a --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Automation/IApplicationAIGenerationQueue.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Unity.AI.Automation; + +public interface IApplicationAIGenerationQueue +{ + Task QueueAttachmentSummariesAsync(IReadOnlyList attachmentIds, Guid? tenantId, string? promptVersion = null); + Task QueueApplicationAnalysisAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null); + Task QueueApplicationScoringAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null); + Task QueueApplicationPipelineAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null); +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/IApplicationAnalysisAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/IApplicationAnalysisAppService.cs new file mode 100644 index 0000000000..493eb82c56 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/IApplicationAnalysisAppService.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace Unity.GrantManager.GrantApplications +{ + public interface IApplicationAnalysisAppService : IApplicationService + { + Task GenerateApplicationAnalysisAsync(Guid applicationId, string? promptVersion = null); + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/IApplicationContentAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/IApplicationContentAppService.cs new file mode 100644 index 0000000000..c2d27129a6 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/IApplicationContentAppService.cs @@ -0,0 +1,10 @@ +using System; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace Unity.GrantManager.GrantApplications; + +public interface IApplicationContentAppService : IApplicationService +{ + Task GenerateContentAsync(Guid applicationId, string? promptVersion = null); +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/IApplicationScoringAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/IApplicationScoringAppService.cs new file mode 100644 index 0000000000..dae0bc4fb7 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/IApplicationScoringAppService.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace Unity.GrantManager.GrantApplications +{ + public interface IApplicationScoringAppService : IApplicationService + { + Task GenerateApplicationScoringAsync(Guid applicationId, string? promptVersion = null); + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissionDefinitionProvider.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissionDefinitionProvider.cs index 05a8f98c81..fb7980f037 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissionDefinitionProvider.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissionDefinitionProvider.cs @@ -2,6 +2,7 @@ using Volo.Abp.Authorization.Permissions; using Volo.Abp.Localization; using Volo.Abp.Features; +using Volo.Abp.SettingManagement; namespace Unity.AI.Permissions; @@ -35,6 +36,12 @@ public override void Define(IPermissionDefinitionContext context) L("Permission:AI.ScoringAssistant")) .RequireFeatures("Unity.AI.Scoring"); + var settingManagement = context.GetGroup(SettingManagementPermissions.GroupName); + settingManagement.AddPermission( + AIPermissions.Configuration.ConfigureAI, + L("Permission:AI.ConfigureAI")) + .RequireFeatures("Unity.AI.Scoring"); + } private static LocalizableString L(string name) diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissions.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissions.cs index 844a8d8e1f..8caef403c4 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissions.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissions.cs @@ -26,8 +26,12 @@ public static class AttachmentSummary public static class ScoringAssistant { public const string ScoringAssistantDefault = GroupName + ".ScoringAssistant"; - } + } + public static class Configuration + { + public const string ConfigureAI = "SettingManagement.ConfigureAI"; + } public static string[] GetAll() { diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/AIPromptDto.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/AIPromptDto.cs new file mode 100644 index 0000000000..0bdff63a08 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/AIPromptDto.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using Volo.Abp.Application.Dtos; + +namespace Unity.AI.Prompts; + +public class AIPromptDto : AuditedEntityDto +{ + public string Name { get; set; } = string.Empty; + public string? Description { get; set; } + public PromptType Type { get; set; } + public bool IsActive { get; set; } + public List Versions { get; set; } = new(); +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/AIPromptVersionDto.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/AIPromptVersionDto.cs new file mode 100644 index 0000000000..a9ed4e50a8 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/AIPromptVersionDto.cs @@ -0,0 +1,20 @@ +using System; +using Volo.Abp.Application.Dtos; + +namespace Unity.AI.Prompts; + +public class AIPromptVersionDto : AuditedEntityDto +{ + public Guid PromptId { get; set; } + public int VersionNumber { get; set; } + public string SystemPrompt { get; set; } = string.Empty; + public string UserPromptTemplate { get; set; } = string.Empty; + public string? DeveloperNotes { get; set; } + public string? TargetModel { get; set; } + public string? TargetProvider { get; set; } + public double Temperature { get; set; } + public int? MaxTokens { get; set; } + public bool IsPublished { get; set; } + public bool IsDeprecated { get; set; } + public string? MetadataJson { get; set; } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/CreateUpdateAIPromptDto.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/CreateUpdateAIPromptDto.cs new file mode 100644 index 0000000000..6d361fd3ba --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/CreateUpdateAIPromptDto.cs @@ -0,0 +1,22 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace Unity.AI.Prompts; + +public class CreateUpdateAIPromptDto +{ + [Required] + [MaxLength(200)] + [DisplayName("PromptName")] + public string Name { get; set; } = string.Empty; + + [MaxLength(2000)] + [DisplayName("PromptDescription")] + public string? Description { get; set; } + + [DisplayName("PromptType")] + public PromptType Type { get; set; } + + [DisplayName("PromptIsActive")] + public bool IsActive { get; set; } = true; +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/CreateUpdateAIPromptVersionDto.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/CreateUpdateAIPromptVersionDto.cs new file mode 100644 index 0000000000..8e3414943f --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/CreateUpdateAIPromptVersionDto.cs @@ -0,0 +1,47 @@ +using System; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace Unity.AI.Prompts; + +public class CreateUpdateAIPromptVersionDto +{ + public Guid PromptId { get; set; } + + [DisplayName("VersionNumber")] + public int VersionNumber { get; set; } + + [Required] + [DisplayName("SystemPrompt")] + public string SystemPrompt { get; set; } = string.Empty; + + [Required] + [DisplayName("UserPromptTemplate")] + public string UserPromptTemplate { get; set; } = string.Empty; + + [DisplayName("DeveloperNotes")] + public string? DeveloperNotes { get; set; } + + [MaxLength(100)] + [DisplayName("TargetModel")] + public string? TargetModel { get; set; } + + [MaxLength(100)] + [DisplayName("TargetProvider")] + public string? TargetProvider { get; set; } + + [DisplayName("Temperature")] + public double Temperature { get; set; } = 0.2; + + [DisplayName("MaxTokens")] + public int? MaxTokens { get; set; } + + [DisplayName("IsPublished")] + public bool IsPublished { get; set; } + + [DisplayName("IsDeprecated")] + public bool IsDeprecated { get; set; } + + [DisplayName("MetadataJson")] + public string? MetadataJson { get; set; } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/IAIPromptAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/IAIPromptAppService.cs new file mode 100644 index 0000000000..c259996db0 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/IAIPromptAppService.cs @@ -0,0 +1,13 @@ +using System; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; + +namespace Unity.AI.Prompts; + +public interface IAIPromptAppService : ICrudAppService< + AIPromptDto, + Guid, + PagedAndSortedResultRequestDto, + CreateUpdateAIPromptDto> +{ +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/IAIPromptVersionAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/IAIPromptVersionAppService.cs new file mode 100644 index 0000000000..269029e5ec --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/IAIPromptVersionAppService.cs @@ -0,0 +1,14 @@ +using System; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; + +namespace Unity.AI.Prompts; + +public interface IAIPromptVersionAppService : ICrudAppService< + AIPromptVersionDto, + Guid, + PagedAndSortedResultRequestDto, + CreateUpdateAIPromptVersionDto> +{ + System.Threading.Tasks.Task> GetByPromptAsync(Guid promptId); +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Settings/AIScoringSettingsDto.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Settings/AIScoringSettingsDto.cs new file mode 100644 index 0000000000..14837f27b4 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Settings/AIScoringSettingsDto.cs @@ -0,0 +1,6 @@ +namespace Unity.AI.Settings; + +public class AIScoringSettingsDto +{ + public bool ScoringAssistantEnabled { get; set; } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Settings/IAIConfigurationAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Settings/IAIConfigurationAppService.cs new file mode 100644 index 0000000000..8a21259aa6 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Settings/IAIConfigurationAppService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace Unity.AI.Settings; + +public interface IAIConfigurationAppService : IApplicationService +{ + Task GetScoringSettingsAsync(); + Task UpdateScoringSettingsAsync(UpdateAIScoringSettingsDto input); +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Settings/UpdateAIScoringSettingsDto.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Settings/UpdateAIScoringSettingsDto.cs new file mode 100644 index 0000000000..a6323e4a34 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Settings/UpdateAIScoringSettingsDto.cs @@ -0,0 +1,6 @@ +namespace Unity.AI.Settings; + +public class UpdateAIScoringSettingsDto +{ + public bool ScoringAssistantEnabled { get; set; } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Unity.AI.Application.Contracts.csproj b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Unity.AI.Application.Contracts.csproj index cd71888469..a78c926a82 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Unity.AI.Application.Contracts.csproj +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Unity.AI.Application.Contracts.csproj @@ -1,20 +1,18 @@ - - net9.0 enable Unity.AI - - + + + - @@ -22,5 +20,4 @@ runtime; build; native; contentfiles; analyzers - - + \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/TextExtractionService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Extraction/TextExtractionService.cs similarity index 99% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/TextExtractionService.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Extraction/TextExtractionService.cs index c45eeb9d36..8d91759dce 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/TextExtractionService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Extraction/TextExtractionService.cs @@ -13,7 +13,7 @@ using UglyToad.PdfPig; using Volo.Abp.DependencyInjection; -namespace Unity.GrantManager.AI +namespace Unity.AI.Extraction { public partial class TextExtractionService : ITextExtractionService, ITransientDependency { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/ApplicationAnalysisService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationAnalysisService.cs similarity index 97% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/ApplicationAnalysisService.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationAnalysisService.cs index 4b633cfd82..473a50d5b6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/ApplicationAnalysisService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationAnalysisService.cs @@ -5,10 +5,13 @@ using System.Linq; using System.Text.Json; using System.Threading.Tasks; +using Unity.AI.Models; +using Unity.AI.Prompts; +using Unity.AI.Requests; using Unity.GrantManager.Applications; using Volo.Abp.DependencyInjection; -namespace Unity.GrantManager.AI +namespace Unity.AI.Operations { public class ApplicationAnalysisService( IApplicationRepository applicationRepository, @@ -29,7 +32,7 @@ public class ApplicationAnalysisService( "applicantAgent" }; - public async Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null, bool capturePromptIo = false) + public async Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null) { var application = await applicationRepository.GetAsync(applicationId); var formSubmission = await applicationFormSubmissionRepository.GetByApplicationAsync(applicationId); @@ -57,8 +60,6 @@ public async Task RegenerateAndSaveAsync(Guid applicationId, string? pro Data = PromptDataPayloadBuilder.BuildPromptDataPayload(application, formSubmission, formSchema, logger), Attachments = attachmentSummaries, PromptVersion = promptVersion, - CapturePromptIo = capturePromptIo, - CaptureContextId = applicationId.ToString() }); var analysisJson = JsonSerializer.Serialize(analysis, _jsonOptionsIndented); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/ApplicationScoresheetAnalysisService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationScoringService.cs similarity index 86% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/ApplicationScoresheetAnalysisService.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationScoringService.cs index 82b7c12ae4..bcbf240301 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/ApplicationScoresheetAnalysisService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationScoringService.cs @@ -5,12 +5,15 @@ using System.Text.Json; using System.Threading.Tasks; using Unity.Flex.Domain.Scoresheets; +using Unity.AI.Models; +using Unity.AI.Prompts; +using Unity.AI.Requests; using Unity.GrantManager.Applications; using Volo.Abp.DependencyInjection; -namespace Unity.GrantManager.AI +namespace Unity.AI.Operations { - public class ApplicationScoresheetAnalysisService( + public class ApplicationScoringService( IApplicationRepository applicationRepository, IApplicationFormRepository applicationFormRepository, IApplicationFormSubmissionRepository applicationFormSubmissionRepository, @@ -18,7 +21,7 @@ public class ApplicationScoresheetAnalysisService( IApplicationChefsFileAttachmentRepository applicationChefsFileAttachmentRepository, IScoresheetRepository scoresheetRepository, IAIService aiService, - ILogger logger) : IApplicationScoresheetAnalysisService, ITransientDependency + ILogger logger) : IApplicationScoringService, ITransientDependency { private readonly JsonSerializerOptions _jsonOptions = new() { @@ -31,7 +34,7 @@ public class ApplicationScoresheetAnalysisService( WriteIndented = true }; - public async Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null, bool capturePromptIo = false) + public async Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null) { var application = await applicationRepository.GetAsync(applicationId); var applicationForm = await applicationFormRepository.GetAsync(application.ApplicationFormId); @@ -80,21 +83,19 @@ public async Task RegenerateAndSaveAsync(Guid applicationId, string? pro }); } - var sectionRequest = new ScoresheetSectionRequest + var applicationScoringRequest = new ApplicationScoringRequest { Data = promptData, Attachments = attachmentSummaries, SectionName = section.Name, SectionSchema = JsonSerializer.SerializeToElement(sectionQuestionsData, _jsonOptions), PromptVersion = promptVersion, - CapturePromptIo = capturePromptIo, - CaptureContextId = applicationId.ToString() }; - var sectionAnswers = await aiService.GenerateScoresheetSectionAsync(sectionRequest); + var applicationScoringResponse = await aiService.GenerateApplicationScoringAsync(applicationScoringRequest); - if (sectionAnswers.Answers.Count > 0) + if (applicationScoringResponse.Answers.Count > 0) { - var sectionJson = JsonSerializer.Serialize(sectionAnswers.Answers, _jsonOptions); + var sectionJson = JsonSerializer.Serialize(applicationScoringResponse.Answers, _jsonOptions); using var sectionDoc = JsonDocument.Parse(sectionJson); foreach (var property in sectionDoc.RootElement.EnumerateObject()) { @@ -104,12 +105,12 @@ public async Task RegenerateAndSaveAsync(Guid applicationId, string? pro } catch (Exception ex) { - logger.LogError(ex, "Error processing AI scoresheet section {SectionName} for application {ApplicationId}", section.Name, applicationId); + logger.LogError(ex, "Error processing AI application scoring section {SectionName} for application {ApplicationId}", section.Name, applicationId); } } var combinedResults = JsonSerializer.Serialize(allSectionResults, _jsonOptionsIndented); - var validatedJson = ValidateScoresheetJson(combinedResults); + var validatedJson = ValidateApplicationScoringJson(combinedResults); application.AIScoresheetAnswers = validatedJson; await applicationRepository.UpdateAsync(application); return validatedJson; @@ -129,12 +130,12 @@ public async Task RegenerateAndSaveAsync(Guid applicationId, string? pro } catch (Exception ex) { - logger.LogWarning(ex, "Unable to load form schema for scoresheet prompt data generation for form version {FormVersionId}.", formVersionId); + logger.LogWarning(ex, "Unable to load form schema for application scoring prompt data generation for form version {FormVersionId}.", formVersionId); return null; } } - private static string ValidateScoresheetJson(string scoresheetAnswers) + private static string ValidateApplicationScoringJson(string scoresheetAnswers) { try { diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryService.cs new file mode 100644 index 0000000000..1225970df5 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryService.cs @@ -0,0 +1,104 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Unity.AI.Requests; +using Unity.GrantManager.Applications; +using Unity.GrantManager.Intakes; +using Volo.Abp.DependencyInjection; + +namespace Unity.AI.Operations; + +public class AttachmentSummaryService( + IApplicationChefsFileAttachmentRepository applicationChefsFileAttachmentRepository, + ISubmissionAppService submissionAppService, + IAIService aiService, + ILogger logger) : IAttachmentSummaryService, ITransientDependency +{ + private const string DefaultContentType = "application/octet-stream"; + private const string SummaryGenerationFailedMessage = "AI summary generation failed."; + + public async Task GenerateAndSaveAsync(Guid attachmentId, string? promptVersion = null) + { + var attachment = await applicationChefsFileAttachmentRepository.GetAsync(attachmentId); + var fileName = string.IsNullOrWhiteSpace(attachment.FileName) ? "unknown" : attachment.FileName; + var (fileContent, contentType) = await GetAttachmentContentForSummaryAsync(attachment, fileName); + + var summaryResponse = await aiService.GenerateAttachmentSummaryAsync(new AttachmentSummaryRequest + { + FileName = fileName, + FileContent = fileContent, + ContentType = contentType, + PromptVersion = promptVersion, + }); + + attachment.AISummary = summaryResponse.Summary; + await applicationChefsFileAttachmentRepository.UpdateAsync(attachment); + + return summaryResponse.Summary; + } + + public async Task> GenerateAndSaveAsync(IEnumerable attachmentIds, string? promptVersion = null) + { + var summaries = new List(); + + foreach (var attachmentId in attachmentIds) + { + try + { + summaries.Add(await GenerateAndSaveAsync(attachmentId, promptVersion)); + } + catch (Exception ex) + { + logger.LogError(ex, "Error generating AI summary for attachment {AttachmentId}", attachmentId); + summaries.Add(SummaryGenerationFailedMessage); + } + } + + return summaries; + } + + public async Task> GenerateForApplicationAsync(Guid applicationId, string? promptVersion = null) + { + var attachmentIds = (await applicationChefsFileAttachmentRepository.GetListAsync(a => a.ApplicationId == applicationId)) + .Select(a => a.Id) + .ToList(); + + return await GenerateAndSaveAsync(attachmentIds, promptVersion); + } + + private async Task<(byte[] Content, string ContentType)> GetAttachmentContentForSummaryAsync(ApplicationChefsFileAttachment attachment, string fileName) + { + if (!Guid.TryParse(attachment.ChefsSubmissionId, out var submissionId) || + !Guid.TryParse(attachment.ChefsFileId, out var fileId)) + { + logger.LogWarning( + "Attachment {AttachmentId} has invalid CHEFS IDs. Falling back to metadata-only summary generation.", + attachment.Id); + return (Array.Empty(), DefaultContentType); + } + + try + { + var fileDto = await submissionAppService.GetChefsFileAttachment(submissionId, fileId, fileName); + if (fileDto?.Content == null) + { + logger.LogWarning( + "Attachment {AttachmentId} has no retrievable content. Falling back to metadata-only summary generation.", + attachment.Id); + return (Array.Empty(), DefaultContentType); + } + + return (fileDto.Content, string.IsNullOrWhiteSpace(fileDto.ContentType) ? DefaultContentType : fileDto.ContentType); + } + catch (Exception ex) + { + logger.LogWarning( + ex, + "Failed retrieving CHEFS content for attachment {AttachmentId}. Falling back to metadata-only summary generation.", + attachment.Id); + return (Array.Empty(), DefaultContentType); + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/IApplicationAnalysisService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationAnalysisService.cs similarity index 65% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/IApplicationAnalysisService.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationAnalysisService.cs index 172a3b9c5a..991a9fe3c0 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/IApplicationAnalysisService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationAnalysisService.cs @@ -1,10 +1,10 @@ using System; using System.Threading.Tasks; -namespace Unity.GrantManager.AI +namespace Unity.AI.Operations { public interface IApplicationAnalysisService { - Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null, bool capturePromptIo = false); + Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null); } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationScoringService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationScoringService.cs new file mode 100644 index 0000000000..96fd5db6d1 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationScoringService.cs @@ -0,0 +1,10 @@ +using System; +using System.Threading.Tasks; + +namespace Unity.AI.Operations +{ + public interface IApplicationScoringService + { + Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null); + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IAttachmentSummaryService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IAttachmentSummaryService.cs new file mode 100644 index 0000000000..c14ea0e01b --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IAttachmentSummaryService.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Unity.AI.Operations; + +public interface IAttachmentSummaryService +{ + Task GenerateAndSaveAsync(Guid attachmentId, string? promptVersion = null); + Task> GenerateAndSaveAsync(IEnumerable attachmentIds, string? promptVersion = null); + Task> GenerateForApplicationAsync(Guid applicationId, string? promptVersion = null); +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/AIPromptTypes.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/AIPromptTypes.cs similarity index 63% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/AIPromptTypes.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/AIPromptTypes.cs index 41ce17e33e..f908e1a388 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/AIPromptTypes.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/AIPromptTypes.cs @@ -1,8 +1,8 @@ -namespace Unity.GrantManager.AI; +namespace Unity.AI.Prompts; public static class AIPromptTypes { public const string AttachmentSummary = "AttachmentSummary"; public const string ApplicationAnalysis = "ApplicationAnalysis"; - public const string ScoresheetSection = "ScoresheetSection"; + public const string ApplicationScoring = "ApplicationScoring"; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/PromptDataPayloadBuilder.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/PromptDataPayloadBuilder.cs similarity index 99% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/PromptDataPayloadBuilder.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/PromptDataPayloadBuilder.cs index ec60077961..315a26fc92 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/PromptDataPayloadBuilder.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/PromptDataPayloadBuilder.cs @@ -6,7 +6,7 @@ using System.Text.Json; using Unity.GrantManager.Applications; -namespace Unity.GrantManager.AI +namespace Unity.AI.Prompts { internal static class PromptDataPayloadBuilder { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Versions/README.md b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/README.md similarity index 57% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Versions/README.md rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/README.md index 0a2ae41b7b..3cc30965e8 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Versions/README.md +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/README.md @@ -1,27 +1,27 @@ # Runtime Prompt Templates These files are the source of truth for runtime prompts. -`OpenAIService` resolves templates from: +`OpenAIRuntimeService` resolves templates from: - `AI/Prompts/Versions//