diff --git a/.gitignore b/.gitignore index 8cae5022ff..587bfe8b5c 100644 --- a/.gitignore +++ b/.gitignore @@ -118,4 +118,9 @@ appsettings.json /applications/Orchestrator *.env -/applications/Unity.GrantManager/src/Unity.GrantManager.Web/package-lock.json \ No newline at end of file +/applications/Unity.GrantManager/src/Unity.GrantManager.Web/package-lock.json +/applications/Unity.AutoUI/cypress/config/dev.json +/applications/Unity.AutoUI/cypress/config/dev2.json +/applications/Unity.AutoUI/cypress/config/test.json +/applications/Unity.AutoUI/cypress/config/uat.json +/applications/Unity.AutoUI/cypress/config/prod.json diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..96ae0cbbf4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,63 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +> This is **Unity Portal**, a grant management system for the Province of British Columbia. It is NOT the Unity game engine — do not suggest UnityEngine APIs. + +## Repository Layout + +`applications/Unity.GrantManager/` is where almost all work happens — a self-contained ABP Framework solution with its own extensive AI-agent instructions. `applications/Unity.AutoUI/` holds Cypress E2E tests; `applications/Unity.Tools/`, `database/`, and `documentation/` round out the rest (see root `README.md` for the full layout). + +**Read these before making non-trivial changes in `applications/Unity.GrantManager/`** — note this solution has its own `.github/`, separate from the root `.github/` above it: + +- `applications/Unity.GrantManager/.github/copilot-instructions.md` — authoritative project overview, layering, and conventions (trust this first) +- `applications/Unity.GrantManager/.github/instructions/*.instructions.md` — path-scoped rules for C#, EF Core, JavaScript, security, testing +- `applications/Unity.GrantManager/.github/skills/*/SKILL.md` — deep-dive patterns: DDD, application layer, EF Core, testing, ABP CLI, module structure +- `applications/Unity.GrantManager/.github/agents/*.agent.md` — planning agents for features, DDD modeling, EF migrations, permissions/localization audits, test strategy, PR readiness + +Where those files and this one overlap, prefer the more specific ones under `applications/Unity.GrantManager/.github/`. + +## Build & Test + +All commands run from `applications/Unity.GrantManager/`: + +```bash +dotnet restore Unity.GrantManager.sln +dotnet build Unity.GrantManager.sln --no-restore # ~3 min, 81 projects +dotnet test Unity.GrantManager.sln --no-build # ~470 tests, ~1-2 min + +# Single test project +dotnet test test/Unity.GrantManager.Application.Tests/ --no-build +``` + +- No PostgreSQL setup needed for tests — SQLite in-memory (most projects) or `EFCore.InMemory` (`Unity.GrantManager.Web.Tests`). +- `Unity.GrantManager.Web/Pages/Dashboard/Index.cshtml.cs` has one expected `CS8604` warning — don't fix it unless asked. +- `Directory.Build.props` / `common.props` (repo-wide MSBuild props) already suppress `NU1701`, `MSB3277`, `CS1591` — don't re-suppress per-project. + +### Local dev environment + +`docker-compose.yml` + `.env.example` in `applications/Unity.GrantManager/` spin up the web app, PostgreSQL, a DB migrator, and Redis. Copy `.env.example` to `.env` and fill in secrets before running `docker compose up`. + +### EF Core migrations + +There are **two separate database contexts** — always specify which one: + +```bash +cd applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore + +dotnet ef migrations add --context GrantManagerDbContext --output-dir Migrations/HostMigrations # host/shared tables +dotnet ef migrations add --context GrantTenantDbContext --output-dir Migrations/TenantMigrations # per-tenant data +``` + +## Architecture + +ABP Framework modular monolith, DDD-layered — see `applications/Unity.GrantManager/.github/copilot-instructions.md` and `applications/Unity.GrantManager/.github/skills/unity-module-structure/SKILL.md` for the full module list and dependency-direction diagram. + +Business rules belong in Domain entities/managers, not controllers or app services. Don't call another app service within the same module — push shared logic into a domain service. + +### Key conventions + +C#, EF Core, JavaScript, security, and testing conventions are detailed in `.claude/rules/*.md` (loaded automatically for matching files) and `applications/Unity.GrantManager/.github/instructions/*.instructions.md`. + +- **Branching**: `dev` → `test` → `main` promotion. Feature branches `feature/*`, fixes `bugfix/*`, urgent `hotfix/*`. PRs to `dev` come from `feature/*`/`bugfix/*`/`hotfix/*`; PRs to `main` only from `test` or `hotfix/*`. +- **Commit messages**: prefix with `[AB#]` extracted from the branch name (e.g. `feature/AB#32037-...` → `AB#32037`), then a short description. diff --git a/applications/Unity.AutoUI/CypressTestLauncher.bat b/applications/Unity.AutoUI/CypressTestLauncher.bat index a88219b77b..035acfc7ef 100644 --- a/applications/Unity.AutoUI/CypressTestLauncher.bat +++ b/applications/Unity.AutoUI/CypressTestLauncher.bat @@ -1,5 +1,6 @@ @echo off +@echo off setlocal cd /d "%~dp0" -powershell -NoProfile -ExecutionPolicy Bypass -NoExit -Command "$ErrorActionPreference='Stop';$ProgressPreference='SilentlyContinue';Add-Type -AssemblyName System.Windows.Forms;Add-Type -AssemblyName System.Drawing;$projectPath=(Get-Location).Path;$form=New-Object System.Windows.Forms.Form;$form.Text='Cypress Test Launcher';$form.Size=New-Object System.Drawing.Size(420,240);$form.StartPosition='CenterScreen';$form.AutoScaleMode=[System.Windows.Forms.AutoScaleMode]::None;$envLabel=New-Object System.Windows.Forms.Label;$envLabel.Text='Select Environment:';$envLabel.AutoSize=$true;$envLabel.Location=New-Object System.Drawing.Point(15,15);$envBox=New-Object System.Windows.Forms.ComboBox;$envBox.Location=New-Object System.Drawing.Point(15,35);$envBox.Size=New-Object System.Drawing.Size(380,25);$envBox.DropDownStyle='DropDownList';$envBox.Items.Add('Please select an environment');$envBox.Items.Add('DEV');$envBox.Items.Add('DEV2');$envBox.Items.Add('TEST');$envBox.Items.Add('UAT');$envBox.Items.Add('PROD');$envBox.SelectedIndex=0;$modeLabel=New-Object System.Windows.Forms.Label;$modeLabel.Text='Select Mode:';$modeLabel.AutoSize=$true;$modeLabel.Location=New-Object System.Drawing.Point(15,70);$modeBox=New-Object System.Windows.Forms.ComboBox;$modeBox.Location=New-Object System.Drawing.Point(15,90);$modeBox.Size=New-Object System.Drawing.Size(380,25);$modeBox.DropDownStyle='DropDownList';$modeBox.Items.Add('Please select a mode');$modeBox.Items.Add('GUI');$modeBox.Items.Add('Headless');$modeBox.SelectedIndex=0;$run=New-Object System.Windows.Forms.Button;$run.Text='Launch Cypress';$run.Location=New-Object System.Drawing.Point(15,135);$run.Size=New-Object System.Drawing.Size(380,32);$run.Add_Click({try{if($envBox.SelectedIndex -eq 0 -or $modeBox.SelectedIndex -eq 0){[System.Windows.Forms.MessageBox]::Show('Please select both an environment and a mode.','Missing Selection');return};Set-Location $projectPath;$envName=$envBox.SelectedItem;$envFile='.\\cypress.'+$envName+'.env.json';if(Test-Path $envFile){Copy-Item $envFile '.\\cypress.env.json' -Force}else{[System.Windows.Forms.MessageBox]::Show('Environment file not found: '+$envFile,'Missing Env File');return};if($modeBox.SelectedItem -eq 'Headless'){Start-Process powershell -ArgumentList '-NoExit','-Command',\"cd '$projectPath'; npx cypress run\"}else{Start-Process powershell -ArgumentList '-NoExit','-Command',\"cd '$projectPath'; npx cypress open\"}}catch{[System.Windows.Forms.MessageBox]::Show($_.Exception.Message,'Cypress Launcher Error')}});$form.Controls.Add($envLabel);$form.Controls.Add($envBox);$form.Controls.Add($modeLabel);$form.Controls.Add($modeBox);$form.Controls.Add($run);$form.ShowDialog()" +powershell -NoProfile -ExecutionPolicy Bypass -NoExit -Command "$ErrorActionPreference='Stop';$ProgressPreference='SilentlyContinue';Add-Type -AssemblyName System.Windows.Forms;Add-Type -AssemblyName System.Drawing;$projectPath=(Get-Location).Path;$form=New-Object System.Windows.Forms.Form;$form.Text='Cypress Test Launcher';$form.Size=New-Object System.Drawing.Size(420,240);$form.StartPosition='CenterScreen';$form.AutoScaleMode=[System.Windows.Forms.AutoScaleMode]::None;$envLabel=New-Object System.Windows.Forms.Label;$envLabel.Text='Select Environment:';$envLabel.AutoSize=$true;$envLabel.Location=New-Object System.Drawing.Point(15,15);$envBox=New-Object System.Windows.Forms.ComboBox;$envBox.Location=New-Object System.Drawing.Point(15,35);$envBox.Size=New-Object System.Drawing.Size(380,25);$envBox.DropDownStyle='DropDownList';$envBox.Items.Add('Please select an environment');$envBox.Items.Add('DEV');$envBox.Items.Add('DEV2');$envBox.Items.Add('TEST');$envBox.Items.Add('UAT');$envBox.Items.Add('PROD');$envBox.SelectedIndex=0;$modeLabel=New-Object System.Windows.Forms.Label;$modeLabel.Text='Select Mode:';$modeLabel.AutoSize=$true;$modeLabel.Location=New-Object System.Drawing.Point(15,70);$modeBox=New-Object System.Windows.Forms.ComboBox;$modeBox.Location=New-Object System.Drawing.Point(15,90);$modeBox.Size=New-Object System.Drawing.Size(380,25);$modeBox.DropDownStyle='DropDownList';$modeBox.Items.Add('Please select a mode');$modeBox.Items.Add('GUI');$modeBox.Items.Add('Headless');$modeBox.SelectedIndex=0;$run=New-Object System.Windows.Forms.Button;$run.Text='Launch Cypress';$run.Location=New-Object System.Drawing.Point(15,135);$run.Size=New-Object System.Drawing.Size(380,32);$run.Add_Click({try{if($envBox.SelectedIndex -eq 0 -or $modeBox.SelectedIndex -eq 0){[System.Windows.Forms.MessageBox]::Show('Please select both an environment and a mode.','Missing Selection');return};Set-Location $projectPath;$envName=$envBox.SelectedItem.ToString().ToLowerInvariant();$envFile=Join-Path $projectPath ('cypress\config\'+$envName+'.json');if(-not (Test-Path $envFile)){[System.Windows.Forms.MessageBox]::Show('Environment file not found: '+$envFile,'Missing Env File');return};if($modeBox.SelectedItem -eq 'Headless'){Start-Process powershell -ArgumentList '-NoExit','-ExecutionPolicy','Bypass','-Command',\"`$env:UNITY_CYPRESS_ENV='$envName'; cd '$projectPath'; node .\\scripts\\run-cypress.js run --browser chrome\"}else{Start-Process powershell -ArgumentList '-NoExit','-ExecutionPolicy','Bypass','-Command',\"`$env:UNITY_CYPRESS_ENV='$envName'; cd '$projectPath'; node .\\scripts\\run-cypress.js open --browser chrome\"}}catch{[System.Windows.Forms.MessageBox]::Show($_.Exception.Message,'Cypress Launcher Error')}});$form.Controls.Add($envLabel);$form.Controls.Add($envBox);$form.Controls.Add($modeLabel);$form.Controls.Add($modeBox);$form.Controls.Add($run);$form.ShowDialog()" diff --git a/applications/Unity.AutoUI/cypress.config.ts b/applications/Unity.AutoUI/cypress.config.ts index 53936349c8..4a8fb6e4eb 100644 --- a/applications/Unity.AutoUI/cypress.config.ts +++ b/applications/Unity.AutoUI/cypress.config.ts @@ -3,10 +3,30 @@ import FormData from "form-data"; import fs from "fs"; import path from "path"; +function loadLocalEnvironmentConfig(): Record { + const environmentName = ( + process.env.UNITY_CYPRESS_ENV || "dev" + ).toLowerCase(); + const environmentFilePath = path.resolve( + "cypress", + "config", + `${environmentName}.json`, + ); + + try { + const content = fs.readFileSync(environmentFilePath, "utf-8"); + return JSON.parse(content) as Record; + } catch { + return {}; + } +} + // https://docs.cypress.io/guides/references/configuration export default defineConfig({ e2e: { - setupNodeEvents(on) { + setupNodeEvents(on, config) { + const environmentConfig = loadLocalEnvironmentConfig(); + on("task", { readJsonIfExists(filePath: string): Record | null { try { @@ -59,6 +79,17 @@ export default defineConfig({ return response.json(); }, }); + + return { + ...config, + baseUrl: + (environmentConfig["webapp.url"] as string | undefined) || + config.baseUrl, + env: { + ...config.env, + ...environmentConfig, + }, + }; }, specPattern: [ "cypress/e2e/**/*.cy.{js,jsx,ts,tsx}", diff --git a/applications/Unity.AutoUI/cypress/config/dev.json.example b/applications/Unity.AutoUI/cypress/config/dev.json.example index b34148f6e7..48035f0b27 100644 --- a/applications/Unity.AutoUI/cypress/config/dev.json.example +++ b/applications/Unity.AutoUI/cypress/config/dev.json.example @@ -7,5 +7,6 @@ "test2password": "", "TEST_EMAIL_TO": "", "TEST_EMAIL_CC": "", - "TEST_EMAIL_BCC": "" + "TEST_EMAIL_BCC": "", + "chefsApiKey": "" } diff --git a/applications/Unity.AutoUI/cypress/config/test.json.example b/applications/Unity.AutoUI/cypress/config/test.json.example index 0b012b37d0..5d9b2c47a1 100644 --- a/applications/Unity.AutoUI/cypress/config/test.json.example +++ b/applications/Unity.AutoUI/cypress/config/test.json.example @@ -7,5 +7,6 @@ "test2password": "", "TEST_EMAIL_TO": "", "TEST_EMAIL_CC": "", - "TEST_EMAIL_BCC": "" + "TEST_EMAIL_BCC": "", + "chefsApiKey": "" } diff --git a/applications/Unity.AutoUI/cypress/config/uat.json.example b/applications/Unity.AutoUI/cypress/config/uat.json.example index 2ab1fe261f..2510724582 100644 --- a/applications/Unity.AutoUI/cypress/config/uat.json.example +++ b/applications/Unity.AutoUI/cypress/config/uat.json.example @@ -7,5 +7,6 @@ "test2password": "", "TEST_EMAIL_TO": "", "TEST_EMAIL_CC": "", - "TEST_EMAIL_BCC": "" + "TEST_EMAIL_BCC": "", + "chefsApiKey": "" } diff --git a/applications/Unity.AutoUI/cypress/e2e/lists.cy.ts b/applications/Unity.AutoUI/cypress/e2e/lists.cy.ts index 3a73fef29b..35be3f99c9 100644 --- a/applications/Unity.AutoUI/cypress/e2e/lists.cy.ts +++ b/applications/Unity.AutoUI/cypress/e2e/lists.cy.ts @@ -10,31 +10,34 @@ describe('Grant Manager Login and List Navigation', () => { const appsPage = ApplicationsPageInstance() function setDashboardIntakeToTestIfAvailable() { - const btnSel = 'button[data-id="dashboardIntakeId"]' - const listboxSel = '#bs-select-1[role="listbox"]' - const searchSel = 'input[type="search"][aria-controls="bs-select-1"]' - - cy.get(btnSel, { timeout: 30000 }) + // The INTAKES filter is a Select2 (bootstrap-5 theme) multi-select, not + // bootstrap-select — the toggle is the [role="combobox"] wrapping the + // rendered-choices
    . + const renderedSel = '#select2-dashboardIntakeId-container' + const dropdownSel = '.select2-dropdown' + const optionSel = `${dropdownSel} li.select2-results__option` + + cy.get(renderedSel, { timeout: 30000 }) .should('be.visible') - .first() + .closest('[role="combobox"]') + .as('intakeCombobox') .click({ force: true }) - cy.get(listboxSel, { timeout: 30000 }).should('be.visible') + cy.get(dropdownSel, { timeout: 30000 }).should('be.visible') - cy.get(searchSel, { timeout: 30000 }) + cy.get(renderedSel) + .parent() + .find('textarea.select2-search__field', { timeout: 30000 }) .should('be.visible') .clear() .type('Test') - cy.contains(`${listboxSel} a.dropdown-item[role="option"] span.text`, /^Test$/, { timeout: 30000 }) - .closest('a.dropdown-item') + cy.contains(optionSel, /^Test$/, { timeout: 30000 }) .then(($opt) => { - const selected = - $opt.attr('aria-selected') === 'true' || - $opt.hasClass('selected') + const selected = $opt.attr('aria-selected') === 'true' if (!selected) { - cy.wrap($opt).scrollIntoView().click({ force: true }) + cy.wrap($opt).click({ force: true }) } }) @@ -43,8 +46,8 @@ describe('Grant Manager Login and List Navigation', () => { expect(texts).to.include('Test') }) - cy.get(btnSel).first().click({ force: true }) - cy.get(btnSel).first().should('have.attr', 'aria-expanded', 'false') + cy.get('@intakeCombobox').click({ force: true }) + cy.get('@intakeCombobox').should('have.attr', 'aria-expanded', 'false') } it('Verify Login', () => { diff --git a/applications/Unity.AutoUI/cypress/e2e/login.cy.ts b/applications/Unity.AutoUI/cypress/e2e/login.cy.ts index 3735041179..b88cb0bbf3 100644 --- a/applications/Unity.AutoUI/cypress/e2e/login.cy.ts +++ b/applications/Unity.AutoUI/cypress/e2e/login.cy.ts @@ -1,3 +1,4 @@ +/// import { LoginPageInstance, NavigationPageInstance } from "../utilities"; describe('Grant Manager Login and Logout', () => { @@ -8,10 +9,9 @@ describe('Grant Manager Login and Logout', () => { loginPage.login() loginPage.verifyOnGrantApplications() - // Verify Default Grant Program tenant is selected + navPage.switchToDefaultGrantsProgramIfAvailable() navPage.verifyCurrentTenant('Default Grants Program') - // Logout (terminal action) loginPage.quickLogout() }) }) diff --git a/applications/Unity.AutoUI/cypress/e2e/navigation.cy.ts b/applications/Unity.AutoUI/cypress/e2e/navigation.cy.ts index 6bcf9a65df..5549ae3f16 100644 --- a/applications/Unity.AutoUI/cypress/e2e/navigation.cy.ts +++ b/applications/Unity.AutoUI/cypress/e2e/navigation.cy.ts @@ -11,10 +11,9 @@ describe('Grant Manager Login and Top Navigation', () => { it('Verify navigation options in the top banner', () => { - // 3.) Verify Default Grant Program tenant is selected. + navPage.switchToDefaultGrantsProgramIfAvailable() navPage.verifyCurrentTenant('Default Grants Program') - // 4.) Ensure all expected headings are present. navPage.verifyAllNavItemsExist() // 5.) Applications diff --git a/applications/Unity.AutoUI/cypress/fixtures/metabase.json b/applications/Unity.AutoUI/cypress/fixtures/metabase.json index 051308782f..1ba2314590 100644 --- a/applications/Unity.AutoUI/cypress/fixtures/metabase.json +++ b/applications/Unity.AutoUI/cypress/fixtures/metabase.json @@ -18,7 +18,7 @@ }, { "unityEnv": "PROD", - "baseURL": "https://unity-reporting.apps.gold.devops.gov.bc.ca/" + "baseURL": "https://unity-reporting.apps.silver.devops.gov.bc.ca/" } ] } \ No newline at end of file diff --git a/applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts b/applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts index 9e58c6ddeb..0efc3c2bd7 100644 --- a/applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts +++ b/applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts @@ -506,10 +506,25 @@ const APPLICATIONS_PATH = "GrantApplications"; return; } - // Priority 3: fetch the latest matching submission from the Unity API + // Priority 3: fetch the latest matching submission from the Unity API, + // falling back to seeding a fresh one via CHEFS if none currently + // match (e.g. every existing seeded submission has already been + // approved by a prior run) — so this spec is self-sufficient and + // doesn't depend on a separate seed step running first. cy.fetchDynamicSubmission(TEST_CONFIG.fetchOptions).then((id) => { - submissionId = id; - cy.log(`✅ Fetched dynamic submission ID: ${submissionId}`); + if (id) { + submissionId = id; + cy.log(`✅ Fetched dynamic submission ID: ${submissionId}`); + return; + } + + cy.log( + "⚠️ No matching submission found — seeding a fresh one via CHEFS", + ); + cy.seedApprovalFlowSubmission().then((seededId) => { + submissionId = seededId; + cy.log(`✅ Seeded and using submission ID: ${submissionId}`); + }); }); }, ); @@ -619,6 +634,25 @@ const APPLICATIONS_PATH = "GrantApplications"; .enterSupplierNumber(TEST_CONFIG.supplierNumber) .clickElsewhere() .clickPaymentInfoSave(); + + // Saving the supplier number calls out to CAS to resolve it, which can + // transiently fail with "GetAuthTokenAsync: Error retrieving Token". + // When that happens the save silently doesn't attach a supplier, leaving + // SupplierId empty for the rest of the flow — dismiss and retry once. + cy.get("body").then(($body) => { + const hasTokenError = + $body.text().includes("GetAuthTokenAsync") || + $body.text().includes("Error retrieving Token"); + + if (hasTokenError) { + cy.log("⚠️ Transient CAS token error on payment save — retrying once"); + detailsPage.dismissErrorModalIfPresent(); + detailsPage + .enterSupplierNumber(TEST_CONFIG.supplierNumber) + .clickElsewhere() + .clickPaymentInfoSave(); + } + }); }); // Must use function() (not arrow) so this.skip() is accessible diff --git a/applications/Unity.AutoUI/cypress/scripts/chefs-api-config.json b/applications/Unity.AutoUI/cypress/scripts/chefs-api-config.json index 0bf9000101..50d724e8d8 100644 --- a/applications/Unity.AutoUI/cypress/scripts/chefs-api-config.json +++ b/applications/Unity.AutoUI/cypress/scripts/chefs-api-config.json @@ -1,21 +1,31 @@ { - "environments": { - "test": { - "baseURL": "https://chefs-test.apps.silver.devops.gov.bc.ca", - "formId": "46e25863-0ead-4aa8-897f-51e45f79e137", - "versionId": "4ef52ead-2cc3-4bdb-a7b7-73be983a7838" - }, - "dev": { - "baseURL": "https://chefs-dev.apps.silver.devops.gov.bc.ca", - "formId": "233f47f9-b566-46c3-926a-73d565bf710f", - "versionId": "1e209d6b-46f5-4ddb-bc79-6e04033231cb" - }, - "uat": { - "baseURL": "https://chefs-test.apps.silver.devops.gov.bc.ca", - "formId": "f2f45aa7-62c5-49ca-8846-b214e02adb46", - "versionId": "1d4d73ec-00e7-4b57-98c9-49d1e0c7d15b" - } + "environments": { + "dev": { + "baseURL": "https://chefs-dev.apps.silver.devops.gov.bc.ca", + "formId": "233f47f9-b566-46c3-926a-73d565bf710f", + "versionId": "1e209d6b-46f5-4ddb-bc79-6e04033231cb" }, + "dev2": { + "baseURL": "https://chefs-dev.apps.silver.devops.gov.bc.ca", + "formId": "92c0df4c-34f9-4a9a-a1b4-61c12495749c", + "versionId": "8218c285-7fa3-42df-b445-7e8ae835fad0" + }, + "test": { + "baseURL": "https://chefs-test.apps.silver.devops.gov.bc.ca", + "formId": "46e25863-0ead-4aa8-897f-51e45f79e137", + "versionId": "4ef52ead-2cc3-4bdb-a7b7-73be983a7838" + }, + "uat": { + "baseURL": "https://chefs-test.apps.silver.devops.gov.bc.ca", + "formId": "f2f45aa7-62c5-49ca-8846-b214e02adb46", + "versionId": "1d4d73ec-00e7-4b57-98c9-49d1e0c7d15b" + }, + "prod": { + "baseURL": "https://submit.digital.gov.bc.ca", + "formId": "4defd3bf-f57c-4969-94a1-ced745a99ccf", + "versionId": "d8736cde-a11a-41be-8800-084f2dfcb825" + } + }, "headers": { "Accept": "application/json", "Content-Type": "application/json" 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 0b7124aaf0..a309bdd85e 100644 --- a/applications/Unity.AutoUI/cypress/scripts/chefs-api-submission.cy.ts +++ b/applications/Unity.AutoUI/cypress/scripts/chefs-api-submission.cy.ts @@ -3,337 +3,26 @@ export {}; /** - * CHEFS Form Submission Seeder + * CHEFS Form Submission Seeder (standalone entry point) * - * Creates exactly one submitted form entry in CHEFS via API and writes its - * confirmation ID to cypress/scripts/last-submission-id.json so that - * ApprovalFlow.cy.ts can pick it up without a dynamic API lookup. + * Thin wrapper around cy.seedApprovalFlowSubmission() so `npm run test:seed` + * / `npm run test:approval-flow` can still seed a submission as a separate + * step before ApprovalFlow.cy.ts runs. The actual seeding logic lives in + * cypress/support/commands.ts so ApprovalFlow.cy.ts can also call it + * directly as a fallback when no existing submission matches its search + * criteria, without requiring this spec to run first. * * Configuration files: * - cypress/scripts/chefs-submission-payload.json — form submission data * - cypress/scripts/chefs-api-config.json — API config and headers */ -interface ChefsEnvironment { - baseURL: string; - formId: string; - versionId: string; -} - -interface ChefsApiConfig { - environments: Record; - headers: Record; -} - -interface ChefsSubmissionPayload { - draft?: boolean; - submission: { - state: string; - metadata: { - origin: string; - referrer: string; - }; - data: Record; - }; -} - -const TOKEN_PROPERTY_KEYS = [ - "access_token", - "accessToken", - "token", - "id_token", - "idToken", -]; - -function isJwtLike(value: string): boolean { - return /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/.test(value); -} - -function extractTokenFromString(value: string): string { - const trimmed = value.trim(); - - if (trimmed.toLowerCase().startsWith("bearer ")) { - const bearerToken = trimmed.replace(/^Bearer\s+/i, "").trim(); - if (isJwtLike(bearerToken)) { - return bearerToken; - } - } - - if (isJwtLike(trimmed)) { - return trimmed; - } - - try { - return extractTokenFromValue(JSON.parse(trimmed)); - } catch { - return ""; - } -} - -function extractTokenFromArray(values: unknown[]): string { - for (const value of values) { - const token = extractTokenFromValue(value); - if (token) { - return token; - } - } - - return ""; -} - -function extractTokenFromObject(value: Record): string { - for (const key of TOKEN_PROPERTY_KEYS) { - const token = extractTokenFromValue(value[key]); - if (token) { - return token; - } - } - - return extractTokenFromArray(Object.values(value)); -} - -function extractTokenFromValue(value: unknown): string { - if (typeof value === "string") { - return extractTokenFromString(value); - } - - if (Array.isArray(value)) { - return extractTokenFromArray(value); - } - - if (value && typeof value === "object") { - return extractTokenFromObject(value as Record); - } - - return ""; -} - -function extractTokenFromStorage(win: Window): string { - const storages = [win.localStorage, win.sessionStorage]; - - for (const storage of storages) { - for (let index = 0; index < storage.length; index += 1) { - const key = storage.key(index); - if (!key) { - continue; - } - - const value = storage.getItem(key); - if (!value) { - continue; - } - - const token = extractTokenFromValue(value); - if (token) { - return token; - } - } - } - - return ""; -} - -function getChefsHostname(baseURL: string): string { - return new URL(baseURL).hostname; -} - -function waitForIdentityRedirectOrAuthenticatedChefsPage( - baseURL: string, - timeout: number, -): void { - const chefsHostname = getChefsHostname(baseURL); - - cy.location("hostname", { timeout }).should((hostname) => { - const onChefs = hostname === chefsHostname; - const onBcGovIdentity = hostname.endsWith("gov.bc.ca"); - - expect( - onChefs || onBcGovIdentity, - `Expected CHEFS or BC Gov identity host, got '${hostname}'`, - ).to.eq(true); - }); -} - -function completeChefsLogin(environment: ChefsEnvironment, timeout: number): void { - const chefsHostname = getChefsHostname(environment.baseURL); - - cy.visit(`${environment.baseURL}/app`); - - // The header auth button hydrates asynchronously after an auth-state check; - // wait for its label rather than just its (empty) wrapper to exist. - cy.get("#loginButton", { timeout }).should("be.visible").click(); - - // CHEFS shows an identity-provider picker (IDIR / IDIR MFA / BC Services - // Card / BCeID); each button carries a stable data-test attribute. - cy.get('[data-test="idir"]', { timeout }).should("be.visible").click(); - - waitForIdentityRedirectOrAuthenticatedChefsPage(environment.baseURL, timeout); - - cy.location("hostname", { timeout }).then((hostname) => { - if (hostname === chefsHostname) { - cy.log("Already logged in to CHEFS"); - return; - } - - cy.get("#user", { timeout }) - .should("be.visible") - .clear() - .type(Cypress.env("test1username"), { log: false }); - - cy.get("#password", { timeout }) - .should("be.visible") - .clear() - .type(Cypress.env("test1password"), { log: false }); - - cy.contains("Continue", { timeout }).should("be.visible").click(); - - cy.location("hostname", { timeout }).should("eq", chefsHostname); - }); -} - -function visitChefsForm(environment: ChefsEnvironment, timeout: number): void { - cy.visit(`${environment.baseURL}/app/form/submit?f=${environment.formId}`); - cy.location("hostname", { timeout }).should( - "eq", - getChefsHostname(environment.baseURL), - ); - cy.location("pathname", { timeout }).should("include", "/app"); -} - const isProd = (Cypress.env("CHEFS_ENV") || Cypress.env("environment") || "").toLowerCase() === "prod"; (isProd ? describe.skip : describe)("CHEFS Approval Flow Seeder", () => { - let apiConfig: ChefsApiConfig; - let submissionPayload: ChefsSubmissionPayload; - let environment: ChefsEnvironment; - let authToken: string; - - before(() => { - const authTimeout = 60000; - - cy.readFile("cypress/scripts/chefs-api-config.json").then((config) => { - apiConfig = config; - - const envKey = ( - Cypress.env("CHEFS_ENV") || - Cypress.env("environment") || - "test" - ).toLowerCase(); - - environment = config.environments[envKey]; - - expect( - environment, - `Missing CHEFS environment configuration for '${envKey}'`, - ).to.exist; - - cy.log(`Using environment: ${envKey}`); - cy.log(`Base URL: ${environment.baseURL}`); - cy.log(`Form ID: ${environment.formId}`); - cy.log(`Version ID: ${environment.versionId}`); - - cy.readFile("cypress/scripts/chefs-submission-payload.json").then( - (payload) => { - submissionPayload = payload; - submissionPayload.submission.metadata.origin = environment.baseURL; - submissionPayload.submission.metadata.referrer = `${environment.baseURL}/app/form/submit?f=${environment.formId}`; - - cy.log( - `Payload loaded with ${ - Object.keys(payload.submission.data).length - } data fields`, - ); - cy.log(`Metadata origin set to: ${environment.baseURL}`); - }, - ); - - let capturedToken = ""; - - cy.intercept("**/app/api/v1/**", (req) => { - const authHeader = req.headers["authorization"] as string; - if (authHeader && !capturedToken) { - capturedToken = authHeader.replace(/^Bearer\s+/i, ""); - } - }).as("chefsApiCalls"); - - completeChefsLogin(environment, authTimeout); - visitChefsForm(environment, authTimeout); - - cy.window({ timeout: authTimeout }) - .should((win) => { - const tokenFromStorage = extractTokenFromStorage(win); - const resolvedToken = capturedToken || tokenFromStorage; - - expect( - resolvedToken, - "Waiting for authenticated CHEFS API token from request or browser storage", - ).to.not.equal(""); - - if (!capturedToken && tokenFromStorage) { - capturedToken = tokenFromStorage; - } - }) - .then(() => { - authToken = capturedToken; - cy.log("✅ Auth token captured from CHEFS login"); - }); - }); - }); - - // Creates the single submission that ApprovalFlow.cy.ts will process. - // The confirmation ID is written to last-submission-id.json and consumed - // by the "Fetch submission ID from API" step in ApprovalFlow.cy.ts. it("Create approval flow submission", () => { - const submissionUrl = `${environment.baseURL}/app/api/v1/forms/${environment.formId}/versions/${environment.versionId}/submissions`; - - cy.log(`Submitting to: ${submissionUrl}`); - - cy.request({ - method: "POST", - url: submissionUrl, - headers: { - ...apiConfig.headers, - Authorization: `Bearer ${authToken}`, - Origin: environment.baseURL, - Referer: `${environment.baseURL}/app/form/submit?f=${environment.formId}`, - }, - body: submissionPayload, - failOnStatusCode: false, - }).then((response) => { - cy.log(`Response Status: ${response.status}`); - cy.log( - `Response Body: ${JSON.stringify(response.body).substring(0, 200)}...`, - ); - - if (response.status === 401) { - cy.log("❌ 401 Unauthorized - Token is expired or invalid"); - cy.log("📖 See cypress/scripts/README.md for token refresh instructions"); - throw new Error( - "Authentication failed (401). Check that test1username/test1password credentials in cypress.env.json are valid and that the CHEFS UI login succeeded during test setup.", - ); - } - - expect(response.status).to.be.oneOf([200, 201]); - expect(response.body).to.have.property("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); - - if (response.body.formId) { - expect(response.body.formId).to.eq(environment.formId); - } else { - cy.log("⚠️ Response doesn't include formId (CHEFS version-dependent)"); - } - }); + cy.seedApprovalFlowSubmission(); }); }); diff --git a/applications/Unity.AutoUI/cypress/support/auth.ts b/applications/Unity.AutoUI/cypress/support/auth.ts index aa47482b8a..8e5be6aeea 100644 --- a/applications/Unity.AutoUI/cypress/support/auth.ts +++ b/applications/Unity.AutoUI/cypress/support/auth.ts @@ -191,7 +191,10 @@ function ensureGrantApplicationsPage(timeout: number): void { * Performs the actual login flow */ function performLogin(options: LoginOptions = {}): void { - const baseUrl = options.baseUrl || (Cypress.env("webapp.url") as string); + const baseUrl = + options.baseUrl || + (Cypress.env("webapp.url") as string | undefined) || + Cypress.config("baseUrl"); const useMfa = options.useMfa || false; const timeout = options.timeout || 20000; @@ -237,7 +240,10 @@ export function loginIfNeeded( options: LoginOptions = {}, ): void { const username = options.username || (Cypress.env("test1username") as string); - const baseUrl = options.baseUrl || (Cypress.env("webapp.url") as string); + const baseUrl = + options.baseUrl || + (Cypress.env("webapp.url") as string | undefined) || + Cypress.config("baseUrl"); const sessionId = `unity-${baseUrl}-${username}`; cy.session( diff --git a/applications/Unity.AutoUI/cypress/support/commands.ts b/applications/Unity.AutoUI/cypress/support/commands.ts index 07edc350ba..9d17908f68 100644 --- a/applications/Unity.AutoUI/cypress/support/commands.ts +++ b/applications/Unity.AutoUI/cypress/support/commands.ts @@ -331,9 +331,11 @@ Cypress.Commands.add( } if (applications.length === 0) { - throw new Error( - "No applications found matching the specified criteria", - ); + Cypress.log({ + name: "fetch", + message: "⚠️ No applications found matching the specified criteria", + }); + return ""; } // Sort applications (default: by submissionDate descending for latest first) @@ -386,3 +388,132 @@ Cypress.Commands.add( Cypress.Commands.add("fetchAllSubmissions", () => { return fetchGrantApplications(); }); + +// ============ CHEFS Submission Seeding ============ + +interface ChefsSeedEnvironment { + baseURL: string; + formId: string; + versionId: string; +} + +interface ChefsSeedApiConfig { + environments: Record; + headers: Record; +} + +interface ChefsSeedSubmissionPayload { + draft?: boolean; + submission: { + state: string; + metadata: { + origin: string; + referrer: string; + }; + data: Record; + }; +} + +/** + * Seeds exactly one submission in CHEFS via its form-level Basic Auth API + * key (formId:apiKey) — a single direct POST, no browser/IDIR login + * required — and writes the confirmation ID to + * cypress/scripts/last-submission-id.json. + * + * This is the same auth approach used by cypress- seeder/support/apiCalls.ts, + * applied here so ApprovalFlow.cy.ts (or any other spec) can call it + * directly as a fallback when no existing submission matches its search + * criteria, without needing a separate seed step — and without the + * IDIR/MFA-picker flakiness a UI-driven login carries. + * + * Requires a `chefsApiKey` value for the current environment, set in + * cypress/config/{env}.json (gitignored) — the form's API key from CHEFS + * form management settings. + * + * @returns Chainable containing the confirmation ID of the created submission + */ +Cypress.Commands.add("seedApprovalFlowSubmission", () => { + const envKey = ( + Cypress.env("CHEFS_ENV") || + Cypress.env("environment") || + "test" + ).toLowerCase(); + + if (envKey === "prod") { + throw new Error( + "seedApprovalFlowSubmission() is disabled for PROD — seeding real submissions in production is not supported.", + ); + } + + const apiKey = Cypress.env("chefsApiKey") as string | undefined; + + expect( + apiKey, + `Missing chefsApiKey for '${envKey}' — set it in cypress/config/${envKey}.json`, + ).to.exist; + + return cy + .readFile("cypress/scripts/chefs-api-config.json") + .then((apiConfig) => { + const environment = apiConfig.environments[envKey]; + + expect( + environment, + `Missing CHEFS environment configuration for '${envKey}'`, + ).to.exist; + + cy.log(`🌱 Seeding submission via CHEFS API key — environment: ${envKey}`); + + return cy + .readFile( + "cypress/scripts/chefs-submission-payload.json", + ) + .then((submissionPayload) => { + submissionPayload.submission.metadata.origin = environment.baseURL; + submissionPayload.submission.metadata.referrer = `${environment.baseURL}/app/form/submit?f=${environment.formId}`; + + const basicCredentials = btoa(`${environment.formId}:${apiKey}`); + const submissionUrl = `${environment.baseURL}/app/api/v1/forms/${environment.formId}/versions/${environment.versionId}/submissions`; + + return cy + .request({ + method: "POST", + url: submissionUrl, + headers: { + ...apiConfig.headers, + Authorization: `Basic ${basicCredentials}`, + }, + body: { + ...submissionPayload, + createdBy: `${Cypress.env("test1username")}@idir`, + updatedBy: `${Cypress.env("test1username")}@idir`, + }, + failOnStatusCode: false, + }) + .then((response) => { + if (response.status === 401) { + throw new Error( + "Authentication failed (401) while seeding a submission via cy.seedApprovalFlowSubmission(). Check that chefsApiKey is valid for this environment's form.", + ); + } + + expect(response.status).to.be.oneOf([200, 201]); + expect(response.body).to.have.property("id"); + + const confirmationId = + response.body.confirmationId || response.body.id; + Cypress.log({ + name: "seed", + message: `✅ Seeded submission with confirmation ID: ${confirmationId}`, + }); + + return cy + .writeFile("cypress/scripts/last-submission-id.json", { + submissionId: confirmationId, + createdAt: new Date().toISOString(), + }) + .then(() => confirmationId as string); + }); + }); + }); +}); diff --git a/applications/Unity.AutoUI/cypress/support/index.d.ts b/applications/Unity.AutoUI/cypress/support/index.d.ts index 979f84c4a5..194e6a9216 100644 --- a/applications/Unity.AutoUI/cypress/support/index.d.ts +++ b/applications/Unity.AutoUI/cypress/support/index.d.ts @@ -89,7 +89,8 @@ declare namespace Cypress { * Uses session cookies automatically from Cypress. * * @param options - Optional filters for selecting submissions - * @returns Chainable containing the confirmation ID + * @returns Chainable containing the confirmation ID, or an empty string + * if no application matches the given filters (does not throw) * * @example * // Get first available submission @@ -108,5 +109,17 @@ declare namespace Cypress { * @returns Chainable containing array of grant applications */ fetchAllSubmissions(): Chainable; + + /** + * Seeds exactly one submission in CHEFS via its form-level Basic Auth + * API key (formId:apiKey) — a single direct POST, no browser/IDIR login + * required — and writes the confirmation ID to + * cypress/scripts/last-submission-id.json. Disabled for PROD. Requires + * a `chefsApiKey` value for the current environment in + * cypress/config/{env}.json. + * + * @returns Chainable containing the confirmation ID of the created submission + */ + seedApprovalFlowSubmission(): Chainable; } } diff --git a/applications/Unity.AutoUI/package.json b/applications/Unity.AutoUI/package.json index cbdade5aeb..9ec62d2b54 100644 --- a/applications/Unity.AutoUI/package.json +++ b/applications/Unity.AutoUI/package.json @@ -1,13 +1,13 @@ { "scripts": { - "test": "env -u ELECTRON_RUN_AS_NODE cypress run --spec 'cypress/e2e/**/*.cy.ts' --browser chrome", - "test:e2e": "env -u ELECTRON_RUN_AS_NODE cypress run --spec 'cypress/e2e/**/*.cy.ts' --browser chrome", - "test:regression-headed": "env -u ELECTRON_RUN_AS_NODE cypress run --spec 'cypress/regression/**/*.cy.ts' --headed --browser chrome", - "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' --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" + "test": "node ./scripts/run-cypress.js run --spec \"cypress/e2e/**/*.cy.ts\" --browser chrome", + "test:e2e": "node ./scripts/run-cypress.js run --spec \"cypress/e2e/**/*.cy.ts\" --browser chrome", + "test:regression-headed": "node ./scripts/run-cypress.js run --spec \"cypress/regression/**/*.cy.ts\" --headed --browser chrome", + "test:regression-headless": "node ./scripts/run-cypress.js run --spec \"cypress/regression/**/*.cy.ts\" --headless --browser chrome", + "test:open": "node ./scripts/run-cypress.js open --browser chrome", + "test:seed": "node ./scripts/run-cypress.js run --spec \"cypress/scripts/chefs-api-submission.cy.ts\" --browser chrome", + "test:approval-flow": "npm run test:seed && node ./scripts/run-cypress.js run --spec \"cypress/regression/ApprovalFlow.cy.ts\" --headed --browser chrome", + "test:approval-flow-headless": "npm run test:seed && node ./scripts/run-cypress.js run --spec \"cypress/regression/ApprovalFlow.cy.ts\" --browser chrome" }, "dependencies": { "form-data": "^4.0.5", diff --git a/applications/Unity.AutoUI/scripts/run-cypress.js b/applications/Unity.AutoUI/scripts/run-cypress.js new file mode 100644 index 0000000000..cbe1f4801e --- /dev/null +++ b/applications/Unity.AutoUI/scripts/run-cypress.js @@ -0,0 +1,25 @@ +const path = require("path"); +const { spawnSync } = require("child_process"); + +delete process.env.ELECTRON_RUN_AS_NODE; + +const cypressCli = path.resolve( + __dirname, + "..", + "node_modules", + "cypress", + "bin", + "cypress", +); + +const result = spawnSync(process.execPath, [cypressCli, ...process.argv.slice(2)], { + stdio: "inherit", + env: process.env, + shell: false, +}); + +if (result.error) { + throw result.error; +} + +process.exit(result.status ?? 1); diff --git a/applications/Unity.GrantManager/.claude/agents/application-service-designer.md b/applications/Unity.GrantManager/.claude/agents/application-service-designer.md new file mode 100644 index 0000000000..c1ccceafe1 --- /dev/null +++ b/applications/Unity.GrantManager/.claude/agents/application-service-designer.md @@ -0,0 +1,46 @@ +--- +name: application-service-designer +description: Designs ABP application service contracts, DTOs, authorization, and Mapperly mapping plans for Unity Grant Manager. Use when adding or changing app services, DTOs, or mapping profiles. +tools: Read, Grep, Glob, Bash +model: inherit +--- + +# ABP Application Service Designer Agent + +You are the application-layer design specialist for Unity Grant Manager. + +## Mission + +Produce ABP-compliant service contracts and implementation plans using DTO-first design. + +## Inputs + +- Use cases and API behavior. +- Existing service interfaces and DTOs. +- Target module and permissions. + +## Process + +1. Propose or update `I*AppService` method signatures. +2. Define DTOs per method intent (create, update, get, list). +3. Identify authorization requirements and permission constants. +4. Define Mapperly mapper changes. +5. Define validation and business-exception boundaries. + +## Output Format + +1. Contract changes. +2. DTO matrix. +3. Authorization matrix. +4. Mapping profile changes. +5. Service implementation checklist. +6. Test targets. + +## Guardrails + +- Apply the `unity-application-layer` skill's patterns. +- Follow `applications/Unity.GrantManager/.github/instructions/csharp.instructions.md`. +- Methods must be async and end with `Async`. +- Accept/return DTOs only, never entities. +- Use Mapperly with `ObjectMapper.Map<>()`, never AutoMapper. Mapper classes inherit `MapperBase` or `TwoWayMapperBase` and are decorated with `[Mapper]`. +- This agent designs only — it does not edit files. diff --git a/applications/Unity.GrantManager/.claude/agents/ddd-modeler.md b/applications/Unity.GrantManager/.claude/agents/ddd-modeler.md new file mode 100644 index 0000000000..9e400fe9cf --- /dev/null +++ b/applications/Unity.GrantManager/.claude/agents/ddd-modeler.md @@ -0,0 +1,47 @@ +--- +name: ddd-modeler +description: Designs and reviews ABP DDD models, aggregates, repositories, and domain managers for Unity Grant Manager. Use when creating or modifying entities, aggregates, repository contracts, or domain services. +tools: Read, Grep, Glob, Bash +model: inherit +--- + +# ABP DDD Modeler Agent + +You are the DDD modeling specialist for Unity Grant Manager. + +## Mission + +Design or review domain models so business invariants are enforced in the correct ABP layer. + +## Inputs + +- Business rules and scenarios. +- Existing entities and repository interfaces. +- Target module. + +## Process + +1. Define aggregate boundaries and ownership rules. +2. Identify entity/value object responsibilities. +3. Propose behavior methods that enforce invariants. +4. Define repository contract additions only for aggregate roots. +5. Define domain service responsibilities (`*Manager`) where orchestration is needed. +6. Propose business error codes and exception points. + +## Output Format + +1. Aggregate model proposal. +2. Invariants and rule enforcement table. +3. Repository contract changes. +4. Domain manager methods. +5. Error code list. +6. Anti-pattern checks. + +## Guardrails + +- Apply the `unity-domain-driven-design` skill's patterns. +- Follow `applications/Unity.GrantManager/.github/instructions/csharp.instructions.md`. +- Do not generate GUIDs in entity constructors. +- Reference external aggregates by Id only. +- Keep app-service logic out of the domain model design. +- This agent designs/reviews only — it does not edit files. diff --git a/applications/Unity.GrantManager/.claude/agents/efcore-migration-planner.md b/applications/Unity.GrantManager/.claude/agents/efcore-migration-planner.md new file mode 100644 index 0000000000..64e91f6487 --- /dev/null +++ b/applications/Unity.GrantManager/.claude/agents/efcore-migration-planner.md @@ -0,0 +1,46 @@ +--- +name: efcore-migration-planner +description: Plans EF Core model updates and host versus tenant migrations safely for Unity Grant Manager. Use when a change touches entity mapping or requires a database migration. +tools: Read, Grep, Glob, Bash +model: inherit +--- + +# ABP EF Core Migration Planner Agent + +You are the EF Core migration planning specialist for Unity Grant Manager. + +## Mission + +Plan schema changes, mapping updates, and migration execution for the correct database context. + +## Inputs + +- Proposed entity/model changes. +- Whether data is host-wide or tenant-scoped. +- Existing migrations and repository code. + +## Process + +1. Classify each change as host, tenant, or both. +2. Propose `ModelBuilder` mapping updates. +3. Verify repository impact and query behavior. +4. Produce migration commands and ordering. +5. Identify rollback and data backfill considerations. + +## Output Format + +1. Context classification. +2. Mapping change checklist. +3. Migration command plan. +4. Data safety notes. +5. Repository update checklist. +6. Validation tests. + +## Guardrails + +- Apply the `unity-ef-core` skill's patterns. +- Follow `applications/Unity.GrantManager/.github/instructions/efcore.instructions.md`. +- Always call `ConfigureByConvention()` for mapped entities. +- Default repositories are currently registered with `includeAllEntities: true`; remove it only when you intentionally want aggregate-roots-only repositories and have verified no callers rely on entity repositories. +- Always specify context (`GrantManagerDbContext` or `GrantTenantDbContext`) for migration commands. +- This agent plans only — it does not run `dotnet ef migrations add` or edit files itself. diff --git a/applications/Unity.GrantManager/.claude/agents/feature-planner.md b/applications/Unity.GrantManager/.claude/agents/feature-planner.md new file mode 100644 index 0000000000..73a21bab90 --- /dev/null +++ b/applications/Unity.GrantManager/.claude/agents/feature-planner.md @@ -0,0 +1,131 @@ +--- +name: feature-planner +description: Plans feature implementation across Domain, Application, EF Core, Web, and tests for Unity Grant Manager, respecting ABP layering. Use when a feature or bug needs a structured implementation plan before coding starts. +tools: Read, Grep, Glob, Bash, AskUserQuestion +model: inherit +--- + +# ABP Feature Planner Agent + +You are the FEATURE PLANNING AGENT for Unity Grant Manager, pairing with the user to create a detailed, actionable plan. + +You research the codebase → clarify with the user → produce a comprehensive plan that respects ABP modular layering and delivery flow. This iterative approach catches edge cases and non-obvious requirements BEFORE implementation begins. + +Your SOLE responsibility is planning. NEVER start implementation — you have no `Edit`/`Write` tools for that reason. + + +- Do not attempt to edit or write files — plans are for the user (or a follow-up implementation turn) to execute. +- Use `AskUserQuestion` freely to clarify requirements — don't make large assumptions. +- Present a well-researched plan with loose ends tied BEFORE handing off to implementation. + + + +Cycle through these phases based on user input. This is iterative, not linear. If the task is highly ambiguous, do only *Discovery* to outline a draft plan, then move to alignment before fleshing out the full plan. + +## 1. Discovery + +Read and search the codebase to gather context: analogous existing features to use as implementation templates, and potential blockers or ambiguities. + +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. + +## 2. Alignment + +If research reveals major ambiguities or if you need to validate assumptions: +- Use `AskUserQuestion` 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. + +Present the plan directly in your response — this agent has no persistent scratch file, so the plan you return IS the deliverable. + +## 4. Refinement + +On user input after showing the plan: +- Changes requested → revise and present updated plan. +- Questions asked → clarify, or use `AskUserQuestion` for follow-ups. +- Alternatives wanted → loop back to **Discovery**. +- Approval given → acknowledge; implementation is a separate turn/agent from here. + +Keep iterating until explicit approval. + + +## Inputs + +- Feature or bug statement. +- Acceptance criteria. +- Target module(s). +- Any constraints (timeline, migration risk, tenant scope, security requirements). + + +```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, 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 `AskUserQuestion`. +- The plan MUST be presented in full to the user, not just summarized. + + +## Guardrails + +- Enforce module dependency direction from the `unity-module-structure` skill. +- Enforce ABP app/domain rules from `applications/Unity.GrantManager/.github/instructions/csharp.instructions.md`. +- Do not use AutoMapper. Use Mapperly (`[Mapper]` attribute, `MapperBase`). +- Do not place business rules in controllers or app services. diff --git a/applications/Unity.GrantManager/.claude/agents/permissions-localization-auditor.md b/applications/Unity.GrantManager/.claude/agents/permissions-localization-auditor.md new file mode 100644 index 0000000000..1e124f4d7a --- /dev/null +++ b/applications/Unity.GrantManager/.claude/agents/permissions-localization-auditor.md @@ -0,0 +1,43 @@ +--- +name: permissions-localization-auditor +description: Audits ABP changes in Unity Grant Manager for permission coverage, localization correctness, and policy compliance. Use before a PR to check for missing permissions or hardcoded user-facing strings. +tools: Read, Grep, Glob, Bash +model: inherit +--- + +# ABP Permissions and Localization Auditor Agent + +You are the ABP compliance auditing specialist for Unity Grant Manager. + +## Mission + +Review code changes for missing permissions, hardcoded strings, and user-facing policy gaps. + +## Inputs + +- Diff or list of changed files. +- Affected user flows and roles. + +## Process + +1. Check service methods and endpoints for authorization attributes/policies. +2. Verify permission constants and definition provider coverage. +3. Scan for hardcoded user-facing text. +4. Verify localization key usage and resource updates. +5. Identify likely regressions and required tests. + +## Output Format + +1. Findings by severity. +2. Missing permissions list. +3. Localization findings list. +4. Required code changes. +5. Validation checklist. + +## Guardrails + +- Follow `applications/Unity.GrantManager/.github/copilot-instructions.md` and `applications/Unity.GrantManager/.github/instructions/csharp.instructions.md`. +- All user-facing text must be localized. +- Permissions must be defined in Application.Contracts permission providers. +- Do not propose hardcoded strings in services, controllers, or UI code. +- This agent audits only — it reports findings rather than editing files. diff --git a/applications/Unity.GrantManager/.claude/agents/pr-readiness-deep.md b/applications/Unity.GrantManager/.claude/agents/pr-readiness-deep.md new file mode 100644 index 0000000000..a8178f1d05 --- /dev/null +++ b/applications/Unity.GrantManager/.claude/agents/pr-readiness-deep.md @@ -0,0 +1,102 @@ +--- +name: pr-readiness-deep +description: Deep PR quality gate for Unity Grant Manager that checks ABP architecture, runs backend and Cypress E2E tests. Use for a more thorough pre-PR check than pr-readiness, when you specifically need Cypress E2E coverage included. +tools: Read, Grep, Glob, Bash +model: inherit +--- + +# PR Readiness Agent (Deep Scan) + +Final quality gate for Unity Grant Manager PRs, covering ABP architecture, backend tests, and Cypress E2E. + +> **Scope note**: the original Copilot version of this agent also drove SonarQube (`sonarqube_analyze_file`, `sonarqube_list_potential_security_issues`) and CodeQL scanning/auto-fix. Those depend on VS Code extension tooling that isn't wired into this Claude Code setup (no SonarQube/CodeQL MCP server is configured in this project). This version keeps the parts that work standalone — ABP architecture review, build/test, and Cypress E2E — and applies the security-pattern checks below via code review instead of a scanner. If SonarQube/CodeQL MCP tools are added to this project later, re-introduce the scanning steps. + +## Inputs +- Branch diff, build/test status, target branch + +## Quality Checks Workflow + +### Step 1: ABP Architecture Review +- Layer boundaries (Domain → Application → Web) — see the `unity-module-structure` skill. +- Repository/DTO/Mapperly conventions — see the `unity-application-layer` skill. +- Permissions and localization keys present for all new user-facing behavior. +- EF migrations correct (host vs tenant context) if schema changes exist — see the `unity-ef-core` skill. + +### Step 2: Security Pattern Review (manual, in lieu of SonarQube/CodeQL) +Review changed files for the patterns in **Common Fixes** below. Flag any match as a blocking issue. + +### Step 3: Build & Backend Tests +```bash +dotnet build Unity.GrantManager.sln --no-restore +dotnet test Unity.GrantManager.sln --no-build +``` + +### Step 4: Cypress E2E Testing +```bash +cd applications/Unity.AutoUI +npm install +npx cypress run # headless +# npx cypress open # interactive, for debugging failures +``` + +Check for: all specs passing, no failed assertions, no unexpected console errors. On failure, review `cypress/screenshots/` and `cypress/videos/`, determine whether it's a stale selector (UI changed) or a real regression, then report which. + +## Common Fixes (patterns to flag during Step 2) + +```csharp +// ❌ SQL Injection +var sql = $"SELECT * FROM Users WHERE Email = '{email}'"; + +// ✅ Use EF LINQ +var users = await _dbContext.Users.Where(u => u.Email == email).ToListAsync(); + +// ❌ Missing authorization +public async Task DeleteAsync(Guid id) + +// ✅ Add attribute +[Authorize(GrantManagerPermissions.Applications.Delete)] +public async Task DeleteAsync(Guid id) + +// ❌ Return entity +public async Task GetAsync(Guid id) + +// ✅ Return DTO +public async Task GetAsync(Guid id) +{ + var entity = await _repository.GetAsync(id); + return ObjectMapper.Map(entity); +} + +// ❌ Path traversal +public async Task GetDocumentAsync(string fileName) +{ + var path = Path.Combine(root, "Documents", fileName); + return await File.ReadAllBytesAsync(path); +} + +// ✅ Validate path +public async Task GetDocumentAsync(Guid documentId) +{ + var doc = await _repository.GetAsync(documentId); + var safeFileName = Path.GetFileName(doc.FileName); + var fullPath = Path.GetFullPath(Path.Combine(root, "Documents", safeFileName)); + var allowedPath = Path.GetFullPath(Path.Combine(root, "Documents")); + + if (!fullPath.StartsWith(allowedPath)) + throw new BusinessException("Invalid path"); + + return await File.ReadAllBytesAsync(fullPath); +} +``` + +Also flag: hardcoded credentials/secrets, resource leaks (missing `using`/disposal), empty catch blocks, and logging of sensitive data. + +## Output + +1. **Summary**: files reviewed, issues found by severity, backend test result (X passed / Y failed), Cypress result (X passed / Y failed, with screenshot/video paths for failures). +2. **Go/No-Go**: + - ✅ GO — no blocking issues, all tests pass. + - ❌ NO-GO — blocking issues, test failures, or flagged security patterns need resolution. + - ⚠️ CONDITIONAL — minor issues present but mergeable with a follow-up task. +3. **Detailed findings**: file:line for each issue, with the specific fix. +4. **Validation commands run**, so the user can reproduce. diff --git a/applications/Unity.GrantManager/.claude/agents/pr-readiness.md b/applications/Unity.GrantManager/.claude/agents/pr-readiness.md new file mode 100644 index 0000000000..0294a5be73 --- /dev/null +++ b/applications/Unity.GrantManager/.claude/agents/pr-readiness.md @@ -0,0 +1,43 @@ +--- +name: pr-readiness +description: Performs a pre-PR quality gate for Unity Grant Manager - build, tests, ABP layering, and policy compliance. Use before opening a PR to get a go/no-go readiness check. +tools: Read, Grep, Glob, Bash +model: inherit +--- + +# ABP PR Readiness Agent + +You are the final quality gate specialist for Unity Grant Manager pull requests. + +## Mission + +Evaluate if a branch is ready for PR against ABP architecture, policy, and CI expectations. + +## Inputs + +- Branch diff. +- Build and test status. +- Target branch. + +## Process + +1. Verify branch policy and PR source/target compatibility (`dev` from `feature/*`/`bugfix/*`/`hotfix/*`; `main` only from `test` or `hotfix/*`). +2. Check layering boundaries and module dependency direction. +3. Check mapping, DTO boundaries, localization, and permissions. +4. Check migration context correctness when EF changes exist. +5. Confirm test coverage and CI command readiness. + +## Output Format + +1. Go/No-go recommendation. +2. Blocking issues. +3. Non-blocking improvements. +4. Required validation commands. +5. PR description checklist. + +## Guardrails + +- Follow `applications/Unity.GrantManager/.github/copilot-instructions.md`. +- Require `dotnet build Unity.GrantManager.sln --no-restore` and `dotnet test Unity.GrantManager.sln --no-build` readiness — run them if not already confirmed clean. +- Enforce ABP module layering rules from the `unity-module-structure` skill. +- Enforce Mapperly, localization, and permissions conventions. diff --git a/applications/Unity.GrantManager/.claude/agents/test-strategy.md b/applications/Unity.GrantManager/.claude/agents/test-strategy.md new file mode 100644 index 0000000000..42e943fb18 --- /dev/null +++ b/applications/Unity.GrantManager/.claude/agents/test-strategy.md @@ -0,0 +1,45 @@ +--- +name: test-strategy +description: Builds a risk-based test strategy for Unity Grant Manager using xUnit, Shouldly, NSubstitute, and layered coverage. Use when planning test coverage for a new feature or bug fix. +tools: Read, Grep, Glob, Bash +model: inherit +--- + +# ABP Test Strategy Agent + +You are the testing strategy specialist for Unity Grant Manager. + +## Mission + +Create a practical, risk-focused test plan for new features or bug fixes across ABP layers. + +## Inputs + +- Feature scope or code diff. +- Changed modules and layers. +- Known edge cases. + +## Process + +1. Identify impacted behavior per layer. +2. Split test coverage into unit, integration, and optional web tests. +3. Propose fixtures and test data setup. +4. Map scenarios to concrete test cases. +5. Prioritize tests for fastest feedback. + +## Output Format + +1. Coverage scope summary. +2. Unit test cases. +3. Integration test cases. +4. Test data and fixture requirements. +5. Execution order and commands. + +## Guardrails + +- Apply the `unity-testing` skill's patterns. +- Follow `applications/Unity.GrantManager/.github/instructions/testing.instructions.md`. +- Use xUnit with Shouldly and NSubstitute. +- Avoid `Assert.*` and Moq patterns. +- Keep tests deterministic and isolated. +- This agent plans only — it does not write test code itself. diff --git a/applications/Unity.GrantManager/.claude/agents/test-triage.md b/applications/Unity.GrantManager/.claude/agents/test-triage.md new file mode 100644 index 0000000000..f73f49b8e9 --- /dev/null +++ b/applications/Unity.GrantManager/.claude/agents/test-triage.md @@ -0,0 +1,45 @@ +--- +name: test-triage +description: Diagnoses failing Unity Grant Manager tests, isolates root cause, and proposes minimal-risk fixes. Use when tests are failing and you need to find the smallest reliable fix. +tools: Read, Grep, Glob, Bash +model: inherit +--- + +# ABP Test Triage Agent + +You are the failure triage specialist for Unity Grant Manager tests. + +## Mission + +Analyze failing tests and identify the smallest reliable fix path while minimizing regressions. + +## Inputs + +- Test output logs. +- Recent code diff. +- Affected project/module. + +## Process + +1. Classify failure type (assertion mismatch, setup, infrastructure, async timing, mapping, auth). +2. Correlate failing tests with changed code paths. +3. Identify probable root cause and confidence level. +4. Propose minimum fix sequence with verification steps. +5. Identify regression tests that must be added or updated. + +## Output Format + +1. Failure summary. +2. Root-cause hypotheses ranked by probability. +3. Recommended fix path. +4. Verification command checklist. +5. Regression prevention tests. + +## Guardrails + +- Use module/layer rules from the `unity-module-structure` skill. +- Use testing conventions from the `unity-testing` skill. +- You may run `dotnet test` (e.g. `dotnet test Unity.GrantManager.sln --no-build`) to reproduce failures and verify hypotheses. +- Prefer minimal changes over broad refactors during triage. +- Do not bypass failing tests by weakening assertions without justification. +- This agent diagnoses and proposes fixes — it does not apply code edits itself. diff --git a/applications/Unity.GrantManager/.claude/rules/csharp.md b/applications/Unity.GrantManager/.claude/rules/csharp.md new file mode 100644 index 0000000000..8ec10b8ffc --- /dev/null +++ b/applications/Unity.GrantManager/.claude/rules/csharp.md @@ -0,0 +1,120 @@ +--- +globs: "**/*.cs" +--- + +# C# Conventions for Unity Grant Manager + +> C# and .NET 10 development standards for ABP Framework 10.5. + +- Target framework: .NET 10.0 with `latest`. +- Nullable reference types are enabled in most projects. +- This is an ABP Framework project. Use ABP base classes, not raw ASP.NET Core. +- This is NOT the Unity game engine. Do not suggest UnityEngine APIs. + +## ABP Base Classes + +- Application Services: Inherit `ApplicationService`, implement interface from Application.Contracts +- Domain Services: Inherit `DomainService`, use `Manager` suffix +- Entities: Inherit `FullAuditedAggregateRoot` or `AuditedAggregateRoot` +- API Controllers: Inherit `AbpController` +- Repositories: Use `IRepository` by default; custom only when needed + +### Injected Properties Available in Base Classes + +These properties are pre-injected in `ApplicationService`, `DomainService`, and `AbpController`: + +| Property | Purpose | +|---|---| +| `GuidGenerator` | Create new entity IDs — never use `Guid.NewGuid()` | +| `Clock` | Use `Clock.Now` — never use `DateTime.Now` or `DateTime.UtcNow` | +| `CurrentUser` | Access authenticated user (Id, Name, Email, Roles) | +| `CurrentTenant` | Access current tenant context (Id, Name) | +| `L` / `L["Key"]` | Localization shortcut | +| `ObjectMapper` | Mapperly-based mapping | +| `Logger` | Structured logging via `ILogger` | +| `AuthorizationService` | Programmatic authorization checks | +| `UnitOfWorkManager` | Manual unit-of-work control | + +## Dependency Injection + +- ABP auto-registers services using marker interfaces — do NOT manually call `services.AddScoped<>()` +- `ITransientDependency` — new instance per injection +- `ISingletonDependency` — single shared instance +- `IScopedDependency` — one per request +- Application services, domain services, and repositories are auto-registered by ABP + +## Entities & Domain + +- Entities use rich domain model: private/protected setters, behaviour via methods. +- Include `protected` parameterless constructor for EF Core deserialization. +- Do not generate `Guid` keys inside constructors; accept `id` from `IGuidGenerator`. +- Reference other aggregate roots by Id only, not navigation properties. +- Domain services use `*Manager` suffix. +- Throw `BusinessException` with namespaced error codes for rule violations. + +## Application Services + +- Interface naming: `I*AppService` inheriting `IApplicationService`. +- All methods `async`, name ends with `Async`. +- Accept/return DTOs only, never entities. Define DTOs in `*.Application.Contracts`. +- Make all public methods `virtual`. +- Use **Mapperly** (`ObjectMapper.Map<>()`) for DTO mapping. Do NOT use AutoMapper. +- Mapper classes: `*MapperlyProfile.cs` decorated with `[Mapper]`, inheriting `MapperBase` or `TwoWayMapperBase`. + +## Code Style + +- 4 spaces indentation, no tabs +- No emojis in comments +- Always use braces, even for single-line statements +- Use `nameof` instead of string literals when referring to member names +- Prefer pattern matching and switch expressions where appropriate +- All user-facing text must be localized via `L["Key"]`. No hardcoded English strings. +- Permissions defined in `*PermissionDefinitionProvider` in Application.Contracts. +- Do not call other application services within the same module; push shared logic to domain services. + +## Naming Conventions + +- Follow PascalCase for public members, types, and methods +- Use camelCase for private fields and local variables +- Prefix interface names with `I` +- Domain Services: `*Manager` suffix (e.g., `AssessmentManager`) +- Application Services: `*AppService` suffix (e.g., `ApplicationAppService`) +- DTOs: Descriptive suffixes (`CreateApplicationDto`, `UpdateApplicationDto`, `ApplicationDto`) +- Event Transfer Objects: `*Eto` suffix for distributed events + +## DTOs vs Entities + +- Application services MUST accept and return DTOs only, never entities +- Use `ObjectMapper` (Mapperly) to map between entities and DTOs +- Define mappers in `*MapperlyProfile` class in Application project + +## Authorization + +- Apply `[Authorize(PermissionName)]` attributes on application service methods +- Define permissions in `*Permissions` static class in Domain.Shared project + +## Multi-Tenancy + +- Tenant entities MUST implement `IMultiTenant` interface +- NEVER manually filter by `TenantId` — ABP handles this automatically +- Use `GrantTenantDbContext` for tenant data, `GrantManagerDbContext` for host data + +## Error Handling + +- Use `BusinessException` for domain-level errors with namespaced error codes (e.g., `"GrantManager:ApplicationNotFound"`) +- Map error codes to localization keys for user-friendly messages +- Use `.WithData("key", value)` for localized message interpolation +- Catch specific exception types, not generic `Exception` + +## Common Mistakes to Avoid + +- Don't expose entities from application services — always return DTOs +- Don't put business logic in application services — use domain services +- Don't create custom repositories unnecessarily — use generic `IRepository` first +- Don't mix host and tenant data in same DbContext +- Don't ignore nullable warnings — fix them properly +- Don't use `DateTime.Now` — use `Clock.Now` or inject `IClock` +- Don't use `Guid.NewGuid()` — use `GuidGenerator.Create()` +- Don't use `services.AddScoped<>()` for ABP services — use marker interfaces +- Don't call application services from within the same module — extract shared logic to a domain service +- Don't embed entity name in app service methods — use `GetAsync`, not `GetApplicationAsync` diff --git a/applications/Unity.GrantManager/.claude/rules/efcore.md b/applications/Unity.GrantManager/.claude/rules/efcore.md new file mode 100644 index 0000000000..07bd97e41a --- /dev/null +++ b/applications/Unity.GrantManager/.claude/rules/efcore.md @@ -0,0 +1,25 @@ +--- +globs: "**/EntityFrameworkCore/**/*.cs" +--- + +# EF Core Conventions for Unity Grant Manager + +- Provider: **Npgsql** (PostgreSQL 17). +- Two database contexts: `GrantManagerDbContext` (host) and `GrantTenantDbContext` (tenant). +- Entity configuration is done inline in `OnModelCreating` of `GrantManagerDbContext` and `GrantTenantDbContext`. +- When configuring entities, follow ABP conventions (e.g., table naming, key configuration) consistently. +- Use `options.AddDefaultRepositories(includeAllEntities: true)` in `GrantManagerEntityFrameworkCoreModule`. +- Prefer ABP's generated default repositories; add custom repositories only when additional behavior is required. +- Tests use **SQLite in-memory** databases, not PostgreSQL. + +## Migrations + +Always specify the context when adding migrations: + +```bash +# Host migrations +dotnet ef migrations add --context GrantManagerDbContext --output-dir Migrations/HostMigrations + +# Tenant migrations +dotnet ef migrations add --context GrantTenantDbContext --output-dir Migrations/TenantMigrations +``` diff --git a/applications/Unity.GrantManager/.claude/rules/javascript.md b/applications/Unity.GrantManager/.claude/rules/javascript.md new file mode 100644 index 0000000000..5885faa808 --- /dev/null +++ b/applications/Unity.GrantManager/.claude/rules/javascript.md @@ -0,0 +1,57 @@ +--- +globs: "**/*.js" +--- + +# JavaScript Development Standards + +> JavaScript development standards for ABP Framework frontend patterns. + +- Variables should be declared with "let" or "const" instead of "var" + +## General Patterns + +- Wrap all page scripts in IIFE: `(function ($) { ... })(jQuery);` +- Never create global JavaScript variables +- Use `var l = abp.localization.getResource('GrantManager');` for all user-facing text +- Use ABP's dynamic JavaScript API client proxies instead of manual AJAX + +## ABP JavaScript Utilities + +- Notifications: `abp.notify.success()`, `.error()`, `.warn()`, `.info()` +- Confirmation: `abp.message.confirm()` for destructive actions +- Authorization: `abp.auth.isGranted()` for permission checks +- Busy indicators: `abp.ui.setBusy()` / `abp.ui.clearBusy()` +- Localization: `l('LocalizationKey')` — never hardcode user-facing strings + +## DataTables Integration + +- Use DataTables.net 2.x with Bootstrap 5 integration (`datatables.net-bs5`) +- Always wrap configuration with `abp.libs.datatables.normalizeConfiguration()` +- Use `abp.libs.datatables.createAjax()` for server-side pagination +- Use `rowAction` for action buttons with `abp.auth.isGranted()` visibility checks +- Use `dataFormat` property for automatic date/boolean formatting +- Always call `dataTable.ajax.reload()` after CRUD operations + +## Modal Manager + +- Use `abp.ModalManager` for all modal dialogs +- Configure with `viewUrl`, `scriptUrl`, and `modalClass` +- Implement `onResult()` callback to reload DataTable after save +- Modal script classes: register in `abp.modals.*` namespace +- Return `NoContent()` from Razor Page handler to close modal + +## DOM Auto-Initialization + +- ABP auto-initializes: tooltips, popovers, datepickers, AJAX forms, autocomplete selects +- Use `data-bs-toggle="tooltip"` for tooltips +- Use `class="auto-complete-select"` with `data-autocomplete-*` attributes for lookups +- Use `data-ajaxForm="true"` for AJAX form submission + +## Client-Side Package Management + +- Add NPM packages to `package.json`, prefer `@abp/*` packages +- Configure `abp.resourcemapping.js` to map from `node_modules` to `wwwroot/libs` +- Run `abp install-libs` to copy resources +- Add to bundle contributor in `Unity.Theme.UX2` module + + diff --git a/applications/Unity.GrantManager/.claude/rules/security.md b/applications/Unity.GrantManager/.claude/rules/security.md new file mode 100644 index 0000000000..73f0eecac5 --- /dev/null +++ b/applications/Unity.GrantManager/.claude/rules/security.md @@ -0,0 +1,49 @@ +--- +globs: "**/*.cs, **/*.cshtml, **/*.js" +--- + +# Security Standards + +> Security best practices for Unity Grant Manager. + +## Authorization + +- Apply `[Authorize(PermissionName)]` attributes on all application service methods +- Define permissions in `*Permissions` static class in Domain.Shared project +- Use `abp.auth.isGranted()` in JavaScript for UI permission checks +- Never rely solely on UI-level permission hiding — always enforce server-side + +## Multi-Tenancy Security + +- Never manually filter by `TenantId` — ABP handles tenant isolation automatically +- Ensure tenant-scoped entities implement `IMultiTenant` +- Test cross-tenant data isolation explicitly +- Use `GrantTenantDbContext` for tenant data, `GrantManagerDbContext` for host data +- Be cautious with `[IgnoreMultiTenancy]` — understand the security implications + +## Input Validation + +- Validate all inputs at the application service boundary using data annotations or FluentValidation +- Use ABP's `Check.*` methods for domain-level validation (e.g., `Check.NotNullOrWhiteSpace`) +- Sanitize user inputs before storage — prevent XSS and injection attacks +- Use parameterized queries — never concatenate user input into SQL + +## Secrets Management + +- Never commit secrets, connection strings, or API keys to source code +- Use environment variables or secure configuration providers +- Reference `.env.example` for required environment variables +- Sensitive configuration is stored in OpenShift secrets and HashiCorp Vault when deployed + +## Authentication + +- Authentication is handled via Keycloak (OpenID Connect) +- Do not implement custom authentication — use ABP's identity infrastructure +- Ensure all API endpoints require authentication unless explicitly public + +## Data Protection + +- Use Redis-backed data protection for key storage in distributed deployments +- Encrypt sensitive data at rest when required by compliance +- Follow government security standards (BC Government policies) +- Audit logging is enabled via ABP — ensure sensitive operations are captured diff --git a/applications/Unity.GrantManager/.claude/rules/testing.md b/applications/Unity.GrantManager/.claude/rules/testing.md new file mode 100644 index 0000000000..538fd25278 --- /dev/null +++ b/applications/Unity.GrantManager/.claude/rules/testing.md @@ -0,0 +1,30 @@ +--- +globs: "**/test/**/*.cs" +--- + +# Testing Conventions for Unity Grant Manager + +- Framework: **xUnit 2.9.3** with **Shouldly 4.3.0** assertions and **NSubstitute 5.3.0** mocks. +- Tests use in-memory database providers (SQLite in-memory for most test projects; `Unity.GrantManager.Web.Tests` uses `Microsoft.EntityFrameworkCore.InMemory`). No external PostgreSQL/database setup is required. +- Test class naming: `*Tests.cs`. +- Base class hierarchy: `AbpIntegratedTest` → `GrantManagerTestBase` → domain-specific bases. +- Use `[Fact]` for single tests, `[Theory]` with `[InlineData]` for parameterized. +- Assertions: Shouldly (`result.ShouldBe(expected)`, `result.ShouldNotBeNull()`). Do NOT use `Assert.*`. +- Mocking: NSubstitute (`Substitute.For()`). Do NOT use Moq. +- JSON test fixtures loaded from `AppDomain.CurrentDomain.BaseDirectory`. +- Run all tests: `dotnet test Unity.GrantManager.sln --no-build` +- Test method naming: `Should_[Expected]_[Scenario]` +- Follow Arrange-Act-Assert pattern consistently +- Do not emit "Arrange", "Act", or "Assert" comments in generated tests + +## Multi-Tenancy Testing + +- Test tenant data isolation using `CurrentTenant.Change(tenantId)` +- Verify that data created in one tenant is not visible in another +- Test both host-level and tenant-level operations + +## Test Data Management + +- Use helper methods for test data creation (e.g., `CreateTestApplicationAsync()`) +- Use static test data constants for well-known IDs +- Keep test data self-contained — each test should set up its own state diff --git a/applications/Unity.GrantManager/.claude/skills/abp-cli/SKILL.md b/applications/Unity.GrantManager/.claude/skills/abp-cli/SKILL.md new file mode 100644 index 0000000000..8ef9306fc6 --- /dev/null +++ b/applications/Unity.GrantManager/.claude/skills/abp-cli/SKILL.md @@ -0,0 +1,78 @@ +--- +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 NPM libraries, or use ABP Suite. +--- + +# ABP CLI Commands + +> **Full documentation**: https://abp.io/docs/latest/cli +> Use `abp help [command]` for detailed options. + +## Generate Client Proxies + +```bash +# URL flag: `-u` (short) or `--url` (long). Use whichever your team prefers, but keep it consistent. +# +# Angular (host must be running) +abp generate-proxy -t ng + +# C# client proxies +abp generate-proxy -t csharp -u https://localhost:44300 + +# Integration services only (microservices) +abp generate-proxy -t csharp -u https://localhost:44300 -st integration + +# JavaScript +abp generate-proxy -t js -u https://localhost:44300 +``` + +## Install Client-Side Libraries + +```bash +# Install NPM packages for MVC/Blazor Server +abp install-libs +``` + +## Add Package Reference + +```bash +# Add project reference with module dependency +abp add-package-ref Acme.BookStore.Domain +abp add-package-ref Acme.BookStore.Domain -t Acme.BookStore.Application +``` + +## Module Operations + +```bash +# Create new module in solution +abp new-module Acme.OrderManagement -t module:ddd + +# Install published module +abp install-module Volo.Blogging + +# Add ABP NuGet package +abp add-package Volo.Abp.Caching.StackExchangeRedis +``` + +## Update & Clean + +```bash +abp update # Update all ABP packages +abp update --version 8.0.0 # Specific version +abp clean # Delete bin/obj folders +``` + +## Quick Reference + +| Task | Command | +|------|---------| +| Angular proxies | `abp generate-proxy -t ng` | +| C# proxies | `abp generate-proxy -t csharp -u URL` | +| Install JS libs | `abp install-libs` | +| Add reference | `abp add-package-ref PackageName` | +| Create module | `abp new-module ModuleName` | +| Install module | `abp install-module ModuleName` | +| Update packages | `abp update` | +| Clean solution | `abp clean` | +| Suite CRUD | `abp suite generate -e entity.json -s solution.sln` | +| Get help | `abp help [command]` | diff --git a/applications/Unity.GrantManager/.claude/skills/unity-application-layer/SKILL.md b/applications/Unity.GrantManager/.claude/skills/unity-application-layer/SKILL.md new file mode 100644 index 0000000000..ea2111c494 --- /dev/null +++ b/applications/Unity.GrantManager/.claude/skills/unity-application-layer/SKILL.md @@ -0,0 +1,102 @@ +--- +name: unity-application-layer +description: ABP Application Services, DTOs, Mapperly mapping, validation, and error handling for Unity. Use when creating or modifying app services, DTOs, or mapping profiles in Application or Application.Contracts projects. +--- + +# Unity Application Layer Patterns + +## Application Service Contracts (Application.Contracts) + +- Interface naming: `I*AppService` inheriting `IApplicationService`. +- Define DTOs in `*.Application.Contracts` — never in Domain or Web. +- All methods async, end with `Async`. +- Do NOT repeat entity name in method names: use `GetAsync`, not `GetGrantAsync`. + +```csharp +public interface IGrantAppService : IApplicationService +{ + Task GetAsync(Guid id); + Task> GetListAsync(GetGrantListInput input); + Task CreateAsync(CreateGrantDto input); + Task UpdateAsync(Guid id, UpdateGrantDto input); // ID separate from DTO + Task DeleteAsync(Guid id); +} +``` + +## DTO Conventions + +| Purpose | Convention | Example | +|---------|------------|---------| +| Query input | `Get{Entity}Input` | `GetGrantInput` | +| List query | `Get{Entity}ListInput` | `GetGrantListInput` | +| Create input | `Create{Entity}Dto` | `CreateGrantDto` | +| Update input | `Update{Entity}Dto` | `UpdateGrantDto` | +| Output | `{Entity}Dto` | `GrantDto` | + +- Use data annotations for validation; reuse constants from Domain.Shared. +- Do NOT share input DTOs between methods. +- Do NOT put logic in DTOs (except `IValidatableObject` when necessary). + +## Implementation (Application) + +- Inherit from `ApplicationService`. +- Make all public methods `virtual`. +- Prefer `protected virtual` over `private` for helper methods. +- Use dedicated repositories, not inline LINQ in app services. +- Call `repository.UpdateAsync()` explicitly after mutations (don't assume change tracking). +- Do NOT use web types (`IFormFile`, `Stream`) — accept `byte[]` from controllers. +- Do NOT call other app services in the same module. Use domain services or repositories. + +## Object Mapping (Mapperly) + +This project uses **Mapperly** (not AutoMapper). Mapper classes are defined using `Riok.Mapperly.Abstractions` and `Volo.Abp.Mapperly`: + +```csharp +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; + +[Mapper] +public partial class GrantToGrantDtoMapper : MapperBase +{ + public override partial GrantDto Map(Grant source); + public override partial void Map(Grant source, GrantDto destination); +} + +// For bidirectional mapping: +[Mapper] +public partial class ZoneGroupDefinitionMapper : TwoWayMapperBase +{ + public override partial ZoneGroupDefinitionDto Map(ZoneGroupDefinition source); + public override partial void Map(ZoneGroupDefinition source, ZoneGroupDefinitionDto destination); + public override partial ZoneGroupDefinition ReverseMap(ZoneGroupDefinitionDto source); + public override partial void ReverseMap(ZoneGroupDefinitionDto source, ZoneGroupDefinition destination); +} +``` + +- Mapper files follow `*MapperlyProfile.cs` naming; each Application and Web project has its own file. +- Use `[MapperIgnoreTarget(nameof(...))]` to skip properties, `[MapProperty]` to rename, and `[MapPropertyFromSource]` for custom resolver methods. +- Call sites still use `ObjectMapper.Map(source)` — Mapperly provides the source-generated implementation. + +## Error Handling + +```csharp +// Business rule violation — use namespaced error code +throw new BusinessException("GrantManager:DuplicateName") + .WithData("Name", name); + +// Entity not found +throw new EntityNotFoundException(typeof(Grant), id); + +// User-facing message (use localized string) +throw new UserFriendlyException(L["GrantNotAvailable"]); +``` + +## Authorization + +- Use `[Authorize(PermissionName)]` on service methods. +- Permission names defined as constants in `*Permissions` classes in Application.Contracts. + +## Cross-Module Calls + +- You MAY call other modules' app services via their Application.Contracts interfaces. +- Do NOT call app services within the same module — use domain services or repositories. diff --git a/applications/Unity.GrantManager/.claude/skills/unity-domain-driven-design/SKILL.md b/applications/Unity.GrantManager/.claude/skills/unity-domain-driven-design/SKILL.md new file mode 100644 index 0000000000..f33f45ffc8 --- /dev/null +++ b/applications/Unity.GrantManager/.claude/skills/unity-domain-driven-design/SKILL.md @@ -0,0 +1,105 @@ +--- +name: unity-domain-driven-design +description: DDD patterns for Unity - Entities, Aggregate Roots, Repositories, Domain Services, Domain Events. Use when creating or modifying entities, repositories, or domain services in Domain or Domain.Shared projects. +--- + +# Unity ABP DDD Patterns + +> Based on ABP Framework DDD conventions. This project uses ABP 10.5 with PostgreSQL 17 and EF Core 10. + +## Entities + +- Define entities in `*.Domain` projects. +- Use **rich domain model**: private/protected setters with methods that enforce invariants. +- Always provide a `protected` parameterless constructor for EF Core. +- Accept `Guid id` in the primary constructor; do NOT generate GUIDs inside constructors. Use `IGuidGenerator` from calling code. +- Make members `virtual` for ORM proxy compatibility. +- Initialize sub-collections in the primary constructor. + +```csharp +public class Grant : AuditedAggregateRoot +{ + public string Name { get; private set; } + public GrantStatus Status { get; private set; } + public ICollection Applications { get; private set; } + + protected Grant() { } // For EF Core + + public Grant(Guid id, string name) : base(id) + { + Name = Check.NotNullOrWhiteSpace(name, nameof(name)); + Status = GrantStatus.Draft; + Applications = new List(); + } + + public void SetName(string name) + { + Name = Check.NotNullOrWhiteSpace(name, nameof(name)); + } +} +``` + +## Aggregate Roots + +- Use a single `Id` property, prefer `Guid` keys. +- Inherit from `AggregateRoot` or audited base classes (`AuditedAggregateRoot`, `FullAuditedAggregateRoot`). +- Reference other aggregate roots **by Id only** — no cross-aggregate navigation properties. +- Keep aggregates small. + +## Repositories + +- Define repository interfaces in the Domain layer. +- One repository per aggregate root only. Never create repositories for child entities. +- Custom repository interface should inherit `IRepository`. +- All methods async with `CancellationToken cancellationToken = default`. +- Single-entity methods: `includeDetails = true` by default. +- List methods: `includeDetails = false` by default. + +```csharp +public interface IGrantRepository : IRepository +{ + Task FindByNameAsync(string name, bool includeDetails = true, CancellationToken cancellationToken = default); + Task> GetListByStatusAsync(GrantStatus status, bool includeDetails = false, CancellationToken cancellationToken = default); +} +``` + +## Domain Services + +- Naming: `*Manager` suffix (e.g., `GrantManager`). +- No interface by default unless multiple implementations are needed. +- Accept/return domain objects, not DTOs. +- Do NOT depend on authenticated user; accept required values from application layer. +- Use `GuidGenerator`, `Clock` from base class properties. + +```csharp +public class GrantManager : DomainService +{ + private readonly IGrantRepository _grantRepository; + + public GrantManager(IGrantRepository grantRepository) + { + _grantRepository = grantRepository; + } + + public async Task CreateAsync(string name) + { + var existing = await _grantRepository.FindByNameAsync(name); + if (existing != null) + throw new BusinessException("GrantManager:NameAlreadyExists").WithData("Name", name); + + return new Grant(GuidGenerator.Create(), name); + } +} +``` + +## Domain Events + +- `AddLocalEvent()` — same transaction, can access full entity state. +- `AddDistributedEvent()` — async, use ETOs defined in Domain.Shared. +- This project uses **RabbitMQ** for distributed events via `IDistributedEventBus`. + +## Shared Constants + +- Define constants, enums, and error codes in `*.Domain.Shared`. +- Localization resources (JSON) live under `Domain.Shared/Localization/*/en.json`. +- Error codes: namespaced as `ModuleName:ErrorCode`. diff --git a/applications/Unity.GrantManager/.claude/skills/unity-ef-core/SKILL.md b/applications/Unity.GrantManager/.claude/skills/unity-ef-core/SKILL.md new file mode 100644 index 0000000000..6ed9e666a4 --- /dev/null +++ b/applications/Unity.GrantManager/.claude/skills/unity-ef-core/SKILL.md @@ -0,0 +1,111 @@ +--- +name: unity-ef-core +description: ABP Entity Framework Core for Unity - DbContext configuration, entity mapping, repository implementation, EF migrations. Use when working in EntityFrameworkCore projects, adding migrations, or implementing repositories. +--- + +# Unity EF Core Patterns + +> This project uses EF Core 10 with PostgreSQL 17 (Npgsql). Tests use SQLite in-memory. + +## Database Contexts + +This project has **two distinct database contexts**: + +| Context | Purpose | Migrations Directory | +|---------|---------|---------------------| +| `GrantManagerDbContext` | Host/shared system tables | `Migrations/HostMigrations` | +| `GrantTenantDbContext` | Per-tenant isolated data | `Migrations/TenantMigrations` | + +Always specify the context when adding migrations: + +```bash +cd src/Unity.GrantManager.EntityFrameworkCore + +# Host migration +dotnet ef migrations add --context GrantManagerDbContext --output-dir Migrations/HostMigrations + +# Tenant migration +dotnet ef migrations add --context GrantTenantDbContext --output-dir Migrations/TenantMigrations +``` + +## Entity Configuration + +Entity mapping is primarily configured inline in `OnModelCreating` in `GrantManagerDbContext` / `GrantTenantDbContext`. Extension methods on `ModelBuilder` are also used for shared/module-specific configuration (e.g., `modelBuilder.ConfigureAI()`). +```csharp +public static class GrantManagerDbContextModelCreatingExtensions +{ + public static void ConfigureGrantManager(this ModelBuilder builder) + { + Check.NotNull(builder, nameof(builder)); + + builder.Entity(b => + { + b.ToTable(GrantManagerConsts.DbTablePrefix + "Grants", GrantManagerConsts.DbSchema); + b.ConfigureByConvention(); // Always call this first + + b.Property(x => x.Name) + .IsRequired() + .HasMaxLength(GrantConsts.MaxNameLength); + + b.HasIndex(x => x.Name); + }); + } +} +``` + +**Rules:** +- Always call `b.ConfigureByConvention()` for every entity. +- Use table prefix from constants (not hardcoded). +- Default schema should be `null`. + +## Repository Implementation + +```csharp +public class GrantRepository : EfCoreRepository, IGrantRepository +{ + public GrantRepository(IDbContextProvider dbContextProvider) + : base(dbContextProvider) { } + + public async Task FindByNameAsync( + string name, + bool includeDetails = true, + CancellationToken cancellationToken = default) + { + var dbSet = await GetDbSetAsync(); + return await dbSet + .IncludeDetails(includeDetails) + .FirstOrDefaultAsync(g => g.Name == name, GetCancellationToken(cancellationToken)); + } +} +``` + +- Use DbContext interface as generic parameter. +- Pass cancellation tokens via `GetCancellationToken(cancellationToken)`. +- Use `IncludeDetails()` extensions per aggregate root. + +## Module Registration + +```csharp +context.Services.AddAbpDbContext(options => +{ + options.AddDefaultRepositories(); // Aggregate roots only, NOT includeAllEntities: true +}); + +Configure(options => +{ + options.UseNpgsql(); // PostgreSQL +}); +``` + +## Never Do + +| Don't | Do Instead | +|-------|-----------| +| `AddDefaultRepositories(includeAllEntities: true)` | `AddDefaultRepositories()` — aggregate roots only | +| Skip `ConfigureByConvention()` | Always call it first in entity config | +| Inject DbContext in app/domain services | Use `IRepository` or custom repository interface | +| Use lazy loading | Explicit `.Include()` via `IncludeDetails()` | + +## Migrations .editorconfig + +The `Migrations/` folder has its own `.editorconfig` suppressing analyzer warnings (S1128, S1192, CS8981, CA1861, IDE naming rules). This is intentional — do not modify migration files for style. diff --git a/applications/Unity.GrantManager/.claude/skills/unity-module-structure/SKILL.md b/applications/Unity.GrantManager/.claude/skills/unity-module-structure/SKILL.md new file mode 100644 index 0000000000..851e3db082 --- /dev/null +++ b/applications/Unity.GrantManager/.claude/skills/unity-module-structure/SKILL.md @@ -0,0 +1,113 @@ +--- +name: unity-module-structure +description: ABP module architecture and layering rules for Unity. Use when creating new modules, adding cross-module dependencies, or understanding project organization and dependency direction. +--- + +# Unity Module Architecture + +## Module Layout + +Each ABP module follows a standard layered structure under `modules/`: + +``` +Unity.{ModuleName}/ + src/ + Unity.{ModuleName}.Domain.Shared/ ← Enums, constants, localization, ETOs + Unity.{ModuleName}.Domain/ ← Entities, repository interfaces, domain services + Unity.{ModuleName}.Application.Contracts/ ← DTOs, app service interfaces + Unity.{ModuleName}.Application/ ← App service implementations, Mapperly mappers + Unity.{ModuleName}.EntityFrameworkCore/ ← DbContext, migrations (if module has own DB tables) + Unity.{ModuleName}.HttpApi/ ← REST controllers + Unity.{ModuleName}.HttpApi.Client/ ← Remote client proxies + Unity.{ModuleName}.Web/ ← Razor Pages, view components + test/ + Unity.{ModuleName}.TestBase/ + Unity.{ModuleName}.Application.Tests/ + Unity.{ModuleName}.Domain.Tests/ + Unity.{ModuleName}.EntityFrameworkCore.Tests/ +``` + +Not all modules have every layer. Simpler modules may only have `Application`, `Application.Contracts`, `Shared`, and `Web`. + +## Current Modules + +| Module | Layers Present | Purpose | +|--------|---------------|---------| +| **Unity.Flex** | Shared, App.Contracts, App, Web, Tests | Dynamic forms/worksheets | +| **Unity.Notifications** | Full stack (Domain→Web, HttpApi, EF) | Email/messaging | +| **Unity.Payments** | Shared, App.Contracts, App, Web, Tests | Financial transactions | +| **Unity.Reporting** | Shared, App.Contracts, App, Web, Tests | Analytics & reports | +| **Unity.AI** | Shared, App.Contracts, App, Web | AI analysis (OpenAI) | +| **Unity.TenantManagement** | App.Contracts, App, HttpApi, Web, Tests | Multi-tenant admin | +| **Unity.Identity.Web** | Web, Tests | OIDC authentication UI | +| **Unity.Theme.UX2** | Theme package, Tests | Custom Razor Pages theme | +| **Unity.SharedKernel** | Single project | Cross-cutting utilities | + +## Dependency Direction (Strict) + +``` +Web → HttpApi → Application.Contracts +Application → Domain + Application.Contracts +Domain → Domain.Shared +EntityFrameworkCore → Domain only +``` + +### Rules + +- Web/HttpApi must NEVER depend on Application (only Application.Contracts). +- Application must NEVER depend on Web or EF Core. +- Domain must NEVER depend on Application, Web, or EF Core. +- Domain.Shared must have NO dependencies on other layers. +- EF Core must ONLY depend on Domain. + +## ABP Module Classes + +Every package has exactly one `AbpModule` class with `[DependsOn]` attributes. + +```csharp +[DependsOn( + typeof(GrantManagerDomainModule), + typeof(AbpEntityFrameworkCoreModule) +)] +public class GrantManagerEntityFrameworkCoreModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddAbpDbContext(options => + { + options.AddDefaultRepositories(includeAllEntities: true); + }); + } +} +``` + +## Multi-Tenancy + +- The system uses ABP multi-tenancy with separate database per tenant. +- `GrantManagerDbContext` = host context, `GrantTenantDbContext` = tenant context. +- Tenant-scoped data is accessed via `ICurrentTenant` / tenant switching. +- The `Unity.TenantManagement` module handles tenant administration. + +## Adding a New Feature + +1. Identify which module the feature belongs to. +2. Add entities/repositories in Domain layer. +3. Add DTOs/interfaces in Application.Contracts. +4. Implement app services in Application. +5. Add EF Core configuration if new tables are needed. +6. Add UI in Web layer. +7. Add tests in the module's test projects. +8. Register the module class with `[DependsOn]`. +9. Run `dotnet build Unity.GrantManager.sln` and `dotnet test Unity.GrantManager.sln` to verify. + +## Localization + +Each module with Domain.Shared has its own localization under: +`src/Unity.{ModuleName}.Domain.Shared/Localization/{ModuleName}/en.json` + +Use `L["Key"]` in application services and pages. All user-facing text must be localized. + +## Permissions + +Define in `*PermissionDefinitionProvider` in Application.Contracts. +Permission names follow `{ModuleName}.{Resource}.{Action}` convention. diff --git a/applications/Unity.GrantManager/.claude/skills/unity-testing/SKILL.md b/applications/Unity.GrantManager/.claude/skills/unity-testing/SKILL.md new file mode 100644 index 0000000000..4ddd9b1d0c --- /dev/null +++ b/applications/Unity.GrantManager/.claude/skills/unity-testing/SKILL.md @@ -0,0 +1,155 @@ +--- +name: unity-testing +description: Testing patterns for Unity - xUnit, Shouldly assertions, NSubstitute mocks, ABP test infrastructure. Use when writing or modifying unit tests or integration tests. +--- + +# Unity Testing Patterns + +## Test Infrastructure + +| Aspect | Value | +|--------|-------| +| Framework | xUnit 2.9.3 | +| Assertions | Shouldly 4.3.0 | +| Mocking | NSubstitute 5.3.0 | +| Database | In-memory (SQLite for most projects; EFCore.InMemory for Web tests – no PostgreSQL required) | +| Base Classes | ABP `AbpIntegratedTest` | +| Target | .NET 10 | + +## Test Project Locations + +``` +test/ + Unity.GrantManager.TestBase/ ← Shared fixtures & test data + Unity.GrantManager.Application.Tests/ ← App service tests + Unity.GrantManager.Domain.Tests/ ← Domain logic tests + Unity.GrantManager.EntityFrameworkCore.Tests/ + Unity.GrantManager.Web.Tests/ +modules/Unity.*/test/ ← Each module has its own test projects +``` + +## Running Tests + +```bash +# All tests (~470 tests, ~2 min) +dotnet test Unity.GrantManager.sln + +# Single project +dotnet test test/Unity.GrantManager.Application.Tests/ + +# After build (faster) +dotnet test Unity.GrantManager.sln --no-build +``` + +## Base Class Hierarchy + +``` +AbpIntegratedTest (Volo.Abp.Testing) +└── GrantManagerTestBase (shared UoW helpers) + ├── GrantManagerDomainTestBase (domain tests) + ├── GrantManagerEntityFrameworkCoreTestBase + └── Module-specific bases: + ├── FlexTestBaseModule + ├── TenantManagementTestBase + └── ReportingTestBase +``` + +## Writing Tests + +### Unit Test Example (with mocking) + +```csharp +public class MyServiceTests +{ + private readonly IMyRepository _repository; + private readonly MyService _sut; + + public MyServiceTests() + { + _repository = Substitute.For(); + _sut = new MyService(_repository); + } + + [Fact] + public async Task CreateAsync_WithValidInput_ShouldSucceed() + { + // Arrange + _repository.FindByNameAsync(Arg.Any()).Returns((MyEntity?)null); + + // Act + var result = await _sut.CreateAsync("test"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("test"); + } +} +``` + +### Integration Test Example (ABP) + +```csharp +public class GrantAppServiceTests : GrantManagerApplicationTestBase +{ + private readonly IGrantAppService _grantAppService; + + public GrantAppServiceTests() + { + _grantAppService = GetRequiredService(); + } + + [Fact] + public async Task Should_Get_Grant_By_Id() + { + var result = await _grantAppService.GetAsync(GrantManagerTestData.GrantId); + result.ShouldNotBeNull(); + result.Id.ShouldBe(GrantManagerTestData.GrantId); + } +} +``` + +### Parameterized Tests + +```csharp +[Theory] +[InlineData("schema1.json", 128)] +[InlineData("schema2.json", 10)] +public void TestMapping(string filename, int expectedCount) +{ + var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "TestData", filename); + var json = File.ReadAllText(path); + var result = Parse(json); + result.Count.ShouldBe(expectedCount); +} +``` + +## Test Data + +- JSON fixtures are loaded from `AppDomain.CurrentDomain.BaseDirectory` subdirectories. +- Domain tests include JSON files in `Intake/Files/*.json` and `Intake/Mapping/*.json` (copied to output via `.csproj`). +- Shared test data constants live in `*TestData.cs` classes within TestBase projects. + +## Web Tests + +Web tests use `[Collection]` fixture pattern: + +```csharp +[Collection(WebTestCollection.Name)] +public class MyWidgetTests +{ + private readonly IAbpLazyServiceProvider _lazyServiceProvider; + + public MyWidgetTests(WebTestFixture fixture) + { + _lazyServiceProvider = fixture.Services.GetRequiredService(); + } +} +``` + +## Conventions + +- Test class naming: `*Tests.cs` +- Method naming: `Should_ExpectedBehavior_When_Condition` or `MethodName_Scenario_ExpectedResult` +- Always use `Shouldly` for assertions (not `Assert.Equal`) +- Always use `NSubstitute` for mocking (not Moq) +- Test runner config: `xunit.runner.json` with `"shadowCopy": false` diff --git a/applications/Unity.GrantManager/modules/Unity.AI/docs/README.md b/applications/Unity.GrantManager/modules/Unity.AI/docs/README.md index 8b8537d6b0..9baa5e28ea 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/docs/README.md +++ b/applications/Unity.GrantManager/modules/Unity.AI/docs/README.md @@ -3,7 +3,9 @@ ## Architecture - [`index.md`](./index.md) - [`flow-map.md`](./flow-map.md) +- [`operation-pipeline.md`](./operation-pipeline.md) - [`prompt-map.md`](./prompt-map.md) +- [`configuration.md`](./configuration.md) - [`implementation-playbook.md`](./implementation-playbook.md) ## Operations @@ -12,3 +14,4 @@ - [`operations/application-scoring.md`](./operations/application-scoring.md) - [`operations/form-mapping.md`](./operations/form-mapping.md) - [`operations/form-worksheet.md`](./operations/form-worksheet.md) +- [`operations/form-scoresheet.md`](./operations/form-scoresheet.md) diff --git a/applications/Unity.GrantManager/modules/Unity.AI/docs/configuration.md b/applications/Unity.GrantManager/modules/Unity.AI/docs/configuration.md new file mode 100644 index 0000000000..aecbcd34e9 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/docs/configuration.md @@ -0,0 +1,35 @@ +# Runtime Configuration + +AI behavior is split between database-owned configuration and deployment +configuration. The database is the source of truth for which model, operation, and +prompt are used; appsettings holds deployment connectivity and operational settings. + +## Database configuration + +| Record | Owns | +| --- | --- | +| `AIModel` | Provider, deployment name (`Name`), active state, and model settings JSON | +| `AIOperation` | Prompt family (`Name`), selected model, execution mode, completion-token limit, and active state | +| `AIPrompt` | Versioned system/user templates, metadata, active state, and optional tenant ownership | + +Host seeders create the built-in models, operations, and global prompts. Operations +select models by ID; `AIModel.Name` is the provider deployment identifier. The runtime +rejects inactive or unsupported configuration rather than choosing a fallback model. + +## Prompt selection + +For a prompt family, host requests use the newest active global prompt. Tenant requests +use the newest active tenant prompt, then fall back to the newest active global prompt. +Operations do not store a prompt ID or version. + +## External configuration + +Provider endpoint, API key, and authenticated-user cooldown remain deployment configuration: + +```text +Azure:OpenAI:Endpoint +Azure:OpenAI:ApiKey +Azure:Generation:CooldownSeconds +``` + +Do not add operation defaults, profile maps, or prompt versions to appsettings. diff --git a/applications/Unity.GrantManager/modules/Unity.AI/docs/flow-map.md b/applications/Unity.GrantManager/modules/Unity.AI/docs/flow-map.md index 42bd018836..0401643211 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/docs/flow-map.md +++ b/applications/Unity.GrantManager/modules/Unity.AI/docs/flow-map.md @@ -1,14 +1,21 @@ # Flow Map -## Standard path -UI -> API app service -> queue -> background job -> AI runtime -> persisted result +```text +UI -> AIGenerationAppService -> IApplicationGenerationQueue +automation -------------------> IApplicationGenerationQueue +IApplicationGenerationQueue -> AIGenerationRequest + background job + -> operation executor + -> Unity.AI runtime + -> operation-specific persisted result +``` -## Operation families -- Application Analysis: submission -> analysis -- Attachment Summary: attachment ids -> summaries -- Application Scoring: application + scoresheet -> scoring -- Form Mapping: form version -> mapping -- Form Worksheet: form version -> worksheet +The app service authorizes and feature-gates UI requests. Automatic intake checks its +own tenant, form, and feature preconditions before entering the queue. The Grant Manager +queue resolves the active database operation, prevents duplicate active requests, +validates prerequisites, and enqueues work. The background job establishes tenant scope +and records request state; its executor owns operation-specific input and persistence. +The runtime resolves the prompt and model configuration, renders the request, calls the +provider, and parses the response. -## Build Rule -See [`implementation-playbook.md`](./implementation-playbook.md) for the canonical add-a-new-operation sequence. +The form mapping, worksheet, and scoresheet operations require an application form +version. See [operation pipeline](./operation-pipeline.md) for ownership rules. diff --git a/applications/Unity.GrantManager/modules/Unity.AI/docs/implementation-playbook.md b/applications/Unity.GrantManager/modules/Unity.AI/docs/implementation-playbook.md index 9a9a50a207..4f6e9f1530 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/docs/implementation-playbook.md +++ b/applications/Unity.GrantManager/modules/Unity.AI/docs/implementation-playbook.md @@ -10,64 +10,47 @@ Use these existing operations as the canonical references: 3. `AttachmentSummary` 4. `FormMapping` 5. `FormWorksheet` +6. `FormScoresheet` ## Base Pattern -1. Define the prompt type. -2. Add the v2 prompt seed. -3. Add the operation seed. -4. Add the runtime contract method. -5. Add the runtime implementation. -6. Add the app service or queue entry. -7. Add the background job only if the result must be applied or persisted. -8. Add the UI button and status polling only if users trigger the operation from the web app. -9. Add tests for the prompt, runtime parsing, and job or service path. - -## Bare Minimum -For the first pass, only add what is required for a working operation: - -- prompt type -- prompt seed -- operation seed -- runtime method -- queue/app service entry -- job or direct apply path, if needed - -## Optional Pieces -Add these only when the operation needs them: - -- feature flag -- permissions -- permission definition provider entries -- menu entry -- UI button -- status polling -- refresh-after-complete behavior -- persistence/import/publish/assign behavior +1. Add the catalog definition, prompt family, model/operation seed, feature, and permissions. +2. Add supported prompt versions; do not assume a specific version number. +3. Add the runtime request/response contract and implementation. +4. Add an executor when Grant Manager must load input or persist a result. +5. Register the executor through the existing transient DI convention. +6. Expose a generate surface and UI only when users need one. +7. Add focused catalog, runtime, executor, and persistence tests. + +## Staged form mapping + +`FormMapping` and `FormWorksheet` remain independent operations. The Mapping +tab can guide an administrator through them as a staged form-configuration flow: + +1. Generate mapping suggestions using the form fields, core fields, assigned + custom fields, and current saved mapping. +2. Persist suggestions for review; accept them individually so existing + non-empty mappings always win. +3. Generate and review `FormWorksheet` suggestions, either after mapping review + or directly from the idle state. +4. Publish and assign created worksheet drafts through the normal worksheet + configuration UI. +5. Invoke the same `FormMapping` operation again after publication and + assignment. + +Keep mapping review state scoped to the form version. Do not auto-link unpublished +AI worksheet drafts to a UI anchor just to make their fields visible to mapping. ## Rules -- Keep the prompt as the source of truth. -- Reuse the existing async generation pattern. +- Keep prompt content and operation/model configuration in the database. +- Reuse the shared generation pipeline. - Do not hardcode field buckets or response shapes in UI code. - Do not invent new plumbing if an existing operation already does the same job. -- Do not add tenant feature seeding. -- Do not add write-back UI behavior unless the operation already persists output. - -## Expected Flow -1. User clicks Generate. -2. UI disables the button and shows generating state, if the operation has UI. -3. API checks permission and feature flag, if the operation uses them. -4. API queues the generation request. -5. Background job loads the operation context. -6. Job builds the prompt payload from existing data. -7. AI runtime renders v2 prompts and logs input/output. -8. Job parses the AI response. -9. Job applies the result if needed. -10. Job stamps status and rate limit state. -11. UI polls status and refreshes after completion, if applicable. +- Keep operation-specific input and persistence in the executor. +- Do not add UI write-back behavior unless the operation persists its output. ## Validation -- Confirm the prompt version is v2. -- Confirm the operation exists in the AI operation seed. +- Confirm the operation exists in the catalog and host seed. +- Confirm every supported prompt version resolves correctly. - Confirm any required feature flag exists in the host feature definitions. - Confirm any required permission is wired in the permission definition provider. - Confirm the UI button uses the same generating/status flow as the other operations, if it is user-triggered. diff --git a/applications/Unity.GrantManager/modules/Unity.AI/docs/index.md b/applications/Unity.GrantManager/modules/Unity.AI/docs/index.md index bc108e3310..bc5832aab7 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/docs/index.md +++ b/applications/Unity.GrantManager/modules/Unity.AI/docs/index.md @@ -1,80 +1,37 @@ -# Unity.AI Index +# Unity.AI -## Domain.Shared -AI constants: -- feature flags -- permission names -- localization keys -- prompt type names +`Unity.AI` owns provider-neutral AI contracts, prompt/model/operation configuration, +runtime execution, and the generation API. Grant Manager owns the application data, +queue implementation, operation executors, and persistence of generated results. -## Application.Contracts -Public AI surface: -- app service interfaces -- queue interfaces -- DTOs -- permission definitions +## Boundaries -## Application -AI implementation: -- runtime -- prompt seeding -- generation app services -- validators -- prompt logging +| Area | Responsibility | +| --- | --- | +| `Domain.Shared` | Features, permissions, localization, and prompt family names | +| `Application.Contracts` | Runtime, generation, queue, and DTO contracts | +| `Application` | Prompt/model/operation seeds, provider runtime, API, and status reads | +| `Runtime/Execution` | Prompt rendering, provider calls, response parsing, and prompt logging | +| Grant Manager | Request locking, background jobs, operation executors, and result persistence | +| `Web` | Menus, generate actions, and status polling | -## Web -UI-facing AI bits: -- menus -- generation buttons -- status polling +## Operation catalog -## Files -### Application -- `Operations` - validators and helpers -- `Runtime/Execution` - rendering, parsing, logging, provider calls -- `Runtime/Prompts` - prompt types and template plumbing -- `DataSeed` - seeded prompt and operation data -- `Generation/AIGenerationAppService.cs` - generation API +`AIGenerationOperations` is the single catalog for operation type, prompt family, +feature, permissions, and form-version requirement. -### Application.Contracts -- `IAIService.cs` - runtime contract -- `Generation/IAIGenerationAppService.cs` - generation app service contract -- `Generation/*ResultDto.cs` - queued result DTOs -- `Operations/IAIGenerationPrerequisiteValidator.cs` - queue prerequisites -- `Automation/IApplicationAIGenerationQueue.cs` - queue contract -- `Permissions/*` - permissions - -### Domain.Shared -- `Features/AIFeatures.cs` - feature flags -- `Localization/AILocalizationKeys.cs` - messages -- `PromptTypes/AIPromptTypes.cs` - prompt family names - -### Web -- `Menus/AIMenuContributor.cs` - menu entries -- `Menus/AIMenus.cs` - menu item names - -## Access -| Operation | View | Generate | +| Operation | Type | Requires form version | | --- | --- | --- | -| Application Analysis | `ViewApplicationAnalysis` | `GenerateApplicationAnalysis` | -| Attachment Summary | `ViewAttachmentSummary` | `GenerateAttachmentSummaries` | -| Application Scoring | `ViewScoringResult` | `GenerateScoring` | -| Form Mapping | `ViewFormMapping` | `GenerateFormMapping` | -| Form Worksheet | `ViewFormWorksheet` | `GenerateFormWorksheet` | - -- Features: - - `Unity.AI.ApplicationAnalysis` - - `Unity.AI.AttachmentSummaries` - - `Unity.AI.Scoring` - - `Unity.AI.FormMapping` - - `Unity.AI.FormWorksheet` - -- Rule: - - Both permission and feature gate must allow generation. - -## AI Notes -- Prompt logging: logs rendered system/user prompts and provider output. -- Response parsing: parses provider output into stable app-facing results. -- Feature gating: disabled features fail early at the API boundary. -- Background jobs: mark failures, then re-throw. -- New operation playbook: see `implementation-playbook.md`. +| Application Analysis | `application-analysis` | No | +| Attachment Summary | `attachment-summary` | No | +| Application Scoring | `application-scoring` | No | +| Form Mapping | `form-mapping` | Yes | +| Form Worksheet | `form-worksheet` | Yes | +| Form Scoresheet | `form-scoresheet` | Yes | + +User-triggered generation requires both the catalogued feature and generate permission. +Automatic intake enforces its tenant, form, feature, and generation prerequisites without +user permission authorization. Status reads require the corresponding view permission. + +See [configuration](./configuration.md), [pipeline](./operation-pipeline.md), and the +[implementation playbook](./implementation-playbook.md). diff --git a/applications/Unity.GrantManager/modules/Unity.AI/docs/operation-pipeline.md b/applications/Unity.GrantManager/modules/Unity.AI/docs/operation-pipeline.md index e31433e284..bfd5d555ac 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/docs/operation-pipeline.md +++ b/applications/Unity.GrantManager/modules/Unity.AI/docs/operation-pipeline.md @@ -1,6 +1,6 @@ # AI operation pipeline -AI generation uses a shared operation catalog and queued lifecycle. The catalog owns the operation key, seeded operation name, feature gate, permissions, and whether a form version is required. Submission enters `IAIGenerationAppService.SubmitAsync`, and the Grant Manager queue preserves the existing duplicate-request lock and operation-specific validation. +AI generation uses a shared operation catalog and queued lifecycle. The catalog owns the operation key, seeded operation name, feature gate, permissions, and whether a form version is required. UI submission enters `IAIGenerationAppService.SubmitAsync`; automatic intake checks its own preconditions and enters the Grant Manager queue directly. The queue preserves the duplicate-request lock and operation-specific validation. The generic background-job base owns tenant scope, structured logging, request state transitions, failure handling, and cooldown stamping. Operation-specific executors remain responsible for loading input, calling the AI contract, validating the response, and persisting the result. @@ -14,3 +14,6 @@ The generic background-job base owns tenant scope, structured logging, request s 6. Add focused catalog, lifecycle, executor, and persistence tests. Do not add another queue branch for shared lifecycle concerns. New operation behavior belongs in its executor; request locking, status transitions, tenant scope, logging, and cooldown behavior stay in the common pipeline. + +For Grant Manager queue and executor ownership, see the +[generation hand-off](../../../src/Unity.GrantManager.Application/GrantApplications/Automation/Generation/README.md). diff --git a/applications/Unity.GrantManager/modules/Unity.AI/docs/operations/application-analysis.md b/applications/Unity.GrantManager/modules/Unity.AI/docs/operations/application-analysis.md index 1b2bfdb194..d1c87ae7b1 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/docs/operations/application-analysis.md +++ b/applications/Unity.GrantManager/modules/Unity.AI/docs/operations/application-analysis.md @@ -13,7 +13,7 @@ Generate an AI analysis of an application submission. - `GET /api/app/ai/generation/status` ## Contract -- Structured analysis output. Returns an immediate queued result via API app service, queue, background job, and AI runtime. +- Structured analysis output. The POST request enqueues generation and returns without the generated payload; clients use the shared status endpoint while the background executor persists the result. ## Notes - This is a reviewer-oriented summary and recommendation flow. diff --git a/applications/Unity.GrantManager/modules/Unity.AI/docs/operations/application-scoring.md b/applications/Unity.GrantManager/modules/Unity.AI/docs/operations/application-scoring.md index a19e1f8bb1..0fa174c45c 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/docs/operations/application-scoring.md +++ b/applications/Unity.GrantManager/modules/Unity.AI/docs/operations/application-scoring.md @@ -13,7 +13,7 @@ Generate scored answers for a submitted application against an assigned scoreshe - `GET /api/app/ai/generation/status` ## Contract -- Structured scoring output. Returns an immediate queued result via API app service, queue, background job, and AI runtime. +- Structured scoring output. The POST request enqueues generation and returns without the generated payload; clients use the shared status endpoint while the background executor persists the result. ## Notes - The prompt asks for answers only for the configured section or scoresheet context. diff --git a/applications/Unity.GrantManager/modules/Unity.AI/docs/operations/attachment-summary.md b/applications/Unity.GrantManager/modules/Unity.AI/docs/operations/attachment-summary.md index 5735c3996f..27e7386037 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/docs/operations/attachment-summary.md +++ b/applications/Unity.GrantManager/modules/Unity.AI/docs/operations/attachment-summary.md @@ -12,7 +12,7 @@ Generate summaries for selected application attachments. - `GET /api/app/ai/generation/status` ## Contract -- Structured attachment summary output. Returns an immediate queued result via API app service, queue, background job, and AI runtime. +- Structured attachment summary output. The POST request enqueues generation and returns without the generated payload; clients use the shared status endpoint while the background executor persists the result. ## Notes - Each attachment is processed as part of the generation request. diff --git a/applications/Unity.GrantManager/modules/Unity.AI/docs/operations/form-mapping.md b/applications/Unity.GrantManager/modules/Unity.AI/docs/operations/form-mapping.md index 2881cefe9f..d7b66db156 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/docs/operations/form-mapping.md +++ b/applications/Unity.GrantManager/modules/Unity.AI/docs/operations/form-mapping.md @@ -18,7 +18,7 @@ Generate recommended CHEFS-to-Unity field mapping for a form version. - `GET /api/app/application-form-version/{id}` ## Contract -- Structured mapping recommendation JSON output. Returns an immediate queued result via API app service, queue, background job, and AI runtime. +- Structured mapping recommendation JSON output. The POST request enqueues generation and returns without the generated payload; clients use the shared status endpoint while the background executor persists the result. ## Output Shape - Core field matches. diff --git a/applications/Unity.GrantManager/modules/Unity.AI/docs/operations/form-scoresheet.md b/applications/Unity.GrantManager/modules/Unity.AI/docs/operations/form-scoresheet.md new file mode 100644 index 0000000000..dde122e67a --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/docs/operations/form-scoresheet.md @@ -0,0 +1,21 @@ +# Form Scoresheet + +## Goal + +Generate and publish a scoresheet definition for a form version. + +## Inputs + +- Form version and form context +- Existing linked scoresheet, when present +- Existing scoresheet sections and fields + +## Surface + +- `POST /api/app/ai/generation/form-scoresheet` +- `GET /api/app/ai/generation/status` + +## Result + +The executor validates the generated scoresheet JSON, creates or replaces the form's +scoresheet, publishes it, and links it to the application form. diff --git a/applications/Unity.GrantManager/modules/Unity.AI/docs/operations/form-worksheet.md b/applications/Unity.GrantManager/modules/Unity.AI/docs/operations/form-worksheet.md index 49d76dd9df..ee9ddf6a6e 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/docs/operations/form-worksheet.md +++ b/applications/Unity.GrantManager/modules/Unity.AI/docs/operations/form-worksheet.md @@ -18,12 +18,14 @@ Generate a recommended worksheet definition for a form version. - `GET /api/app/ai/generation/status` ## Contract -- Structured Flex worksheet JSON output. Returns an immediate queued result via API app service, queue, background job, and AI runtime. +- Structured worksheet field-suggestion JSON. The POST request enqueues generation and returns without the generated payload; clients use the shared status endpoint while the background executor validates the suggestions and creates an unpublished worksheet for review. ## Output Shape -- Full worksheet definition JSON. -- Include only additional worksheet fields that the form needs beyond core Unity fields. -- Keep the result valid JSON and compatible with Flex import. +- A `fields` collection containing the suggested additional worksheet fields. +- Include all applicable additional fields in the collection; do not limit the response to one suggestion. +- Each suggestion supplies the field key, label, and supported custom-field type. +- The executor builds the worksheet and its `Suggested Fields` section from the validated suggestions. +- Keep the result valid JSON and include only fields that the form needs beyond core Unity fields. ## Notes - The AI output should stay valid JSON. diff --git a/applications/Unity.GrantManager/modules/Unity.AI/docs/prompt-map.md b/applications/Unity.GrantManager/modules/Unity.AI/docs/prompt-map.md index f686766ffc..cc212da0de 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/docs/prompt-map.md +++ b/applications/Unity.GrantManager/modules/Unity.AI/docs/prompt-map.md @@ -6,16 +6,24 @@ - `ApplicationScoring` - question scoring - `FormMapping` - CHEFS to Unity mapping - `FormWorksheet` - worksheet generation +- `FormScoresheet` - scoresheet generation + +`FormMapping` and `FormWorksheet` are independent operations with complete +context inputs. The Mapping tab can guide them through mapping review, worksheet +review, publish and assignment, and another mapping run. Review suggestions are +persisted until accepted or discarded. Initial-review suggestions preserve existing +non-empty mappings; accepted final-review suggestions may replace conflicting mappings. ## Versions -- `v0`, `v1`, `v2` live under `Runtime/Prompts/Versions` -- The seeder loads built-in prompt rows from those versions -- Runtime selects the newest active prompt by family. +- Built-in prompt rows are defined and seeded by `AIPromptDataSeeder`. +- Families may have `v0`, `v1`, and `v2` rows; a new operation only needs the versions it supports. +- Without an explicit request version, runtime selects the newest active prompt by family. ## Tenant selection - `AIOperation.Name` is the prompt family; operations do not pin a prompt row or version. -- Host requests use the newest active global prompt in the family. -- Tenant requests use the newest active prompt owned by that tenant, falling back to the newest active global prompt. +- An explicit request version selects that active version, with the same tenant/global fallback. +- Otherwise, host requests use the newest active global prompt in the family. +- Otherwise, tenant requests use the newest active prompt owned by that tenant, falling back to the newest active global prompt. - To roll back a tenant or global prompt, deactivate the active version and leave the prior version active. - Tenant prompt rows are administrator-created; deployments seed only global prompts and operations. @@ -23,7 +31,7 @@ - Versioned prompts are the source of truth. - Prompt templates define the request shape. - Structured outputs should stay JSON-shaped. -- New versions should not silently change behavior. +- A new version should be additive and must not silently change an active prompt's behavior. ## Build Rule Use [`implementation-playbook.md`](./implementation-playbook.md) when adding a new prompt-backed operation. 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 195bedf97b..73f5943f02 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 @@ -95,7 +95,8 @@ public override void Define(IPermissionDefinitionContext context) "Unity.AI.ApplicationAnalysis", "Unity.AI.FormMapping", "Unity.AI.FormWorksheet", - "Unity.AI.FormScoresheet")); + "Unity.AI.FormScoresheet", + "Unity.AIReporting")); } private static LocalizableString L(string name) diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/UnityPromptAssetManifest.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/UnityPromptAssetManifest.cs deleted file mode 100644 index 93bdb236cf..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/UnityPromptAssetManifest.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Unity.AI.Prompts; - -public sealed record UnityPromptAssetManifest( - [property: JsonPropertyName("operationName")] string OperationName, - [property: JsonPropertyName("promptVersion")] string PromptVersion, - [property: JsonPropertyName("inputContractName")] string InputContractName, - [property: JsonPropertyName("outputContractName")] string OutputContractName, - [property: JsonPropertyName("modelHint")] string? ModelHint = null, - [property: JsonPropertyName("profileHint")] string? ProfileHint = null); diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/FormMappingResponse.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/FormMappingResponse.cs index 4015265699..215f66224c 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/FormMappingResponse.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/FormMappingResponse.cs @@ -7,6 +7,8 @@ public class FormMappingResponse { public string Mapping { get; set; } = string.Empty; + public string? FailureReason { get; set; } + [JsonPropertyName("coreFieldMatches")] public List CoreFieldMatches { get; set; } = []; diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/FormScoresheetResponse.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/FormScoresheetResponse.cs index 3ed8321221..827762c9a9 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/FormScoresheetResponse.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/FormScoresheetResponse.cs @@ -7,6 +7,8 @@ public class FormScoresheetResponse { public string Scoresheet { get; set; } = string.Empty; + public string? FailureReason { get; set; } + [JsonPropertyName("title")] public string Title { get; set; } = string.Empty; diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/FormWorksheetResponse.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/FormWorksheetResponse.cs index 1ee1d837bc..6490ce3660 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/FormWorksheetResponse.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/FormWorksheetResponse.cs @@ -3,4 +3,6 @@ namespace Unity.AI.Responses; public class FormWorksheetResponse { public string Worksheet { get; set; } = string.Empty; + + public string? FailureReason { get; set; } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Settings/AITenantConfigurationDto.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Settings/AITenantConfigurationDto.cs index 10a0d84b3c..f1d60c7c42 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Settings/AITenantConfigurationDto.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Settings/AITenantConfigurationDto.cs @@ -4,4 +4,5 @@ public class AITenantConfigurationDto { public bool AutomaticGenerationEnabled { get; set; } public bool ManualGenerationEnabled { get; set; } + public bool ReportingEnabled { get; set; } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Settings/UpdateAITenantConfigurationDto.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Settings/UpdateAITenantConfigurationDto.cs index bbbed5c2f0..cbe1979f56 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Settings/UpdateAITenantConfigurationDto.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Settings/UpdateAITenantConfigurationDto.cs @@ -4,4 +4,5 @@ public class UpdateAITenantConfigurationDto { public bool AutomaticGenerationEnabled { get; set; } public bool ManualGenerationEnabled { get; set; } + public bool ReportingEnabled { get; set; } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/DataSeed/AIModelDataSeeder.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/DataSeed/AIModelDataSeeder.cs index 7839db8e01..269ba47299 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/DataSeed/AIModelDataSeeder.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/DataSeed/AIModelDataSeeder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text.Json; using System.Threading.Tasks; using Unity.AI.Domain; @@ -14,9 +15,9 @@ public class AIModelDataSeeder( { private static readonly BuiltInModelDefinition[] BuiltInModels = [ - new("Gpt4oMini", true, 0.3d), - new("Gpt5Mini", false, null), - new("Gpt5Nano", false, null) + new("gpt-4o-mini", "OpenAI", true, 0.3d), + new("gpt-5-mini", "OpenAI", false, null), + new("gpt-5-nano", "OpenAI", false, null) ]; public async Task SeedAsync(DataSeedContext context) @@ -40,9 +41,11 @@ private async Task EnsureModelAsync(BuiltInModelDefinition definition) Temperature = definition.Temperature }; - var existing = await modelRepository.FirstOrDefaultAsync(model => model.Name == definition.Name); + var existing = (await modelRepository.GetListAsync(model => + model.Name == definition.Name)).SingleOrDefault(); if (existing != null) { + existing.Provider = definition.Provider; existing.IsActive = true; existing.SettingsJson = JsonSerializer.Serialize(settings); await modelRepository.UpdateAsync(existing, autoSave: true); @@ -50,7 +53,7 @@ private async Task EnsureModelAsync(BuiltInModelDefinition definition) } await modelRepository.InsertAsync( - new AIModel(Guid.CreateVersion7(), definition.Name) + new AIModel(Guid.CreateVersion7(), definition.Name, definition.Provider) { IsActive = true, SettingsJson = JsonSerializer.Serialize(settings) @@ -60,6 +63,7 @@ await modelRepository.InsertAsync( private sealed record BuiltInModelDefinition( string Name, + string Provider, bool MaxOutputTokenCountSupported, double? Temperature); } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/DataSeed/AIOperationDataSeeder.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/DataSeed/AIOperationDataSeeder.cs index d4504d69c7..4bf798fd0f 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/DataSeed/AIOperationDataSeeder.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/DataSeed/AIOperationDataSeeder.cs @@ -19,7 +19,7 @@ public class AIOperationDataSeeder( ICurrentTenant currentTenant, ILogger logger) : ITransientDependency { - private const string DefaultModelName = "Gpt5Mini"; + private const string DefaultModelName = "gpt-5-mini"; private static readonly BuiltInOperationDefinition[] BuiltInOperations = [ @@ -28,7 +28,7 @@ public class AIOperationDataSeeder( new(AIPromptTypes.ApplicationScoring, 8000), new(AIPromptTypes.FormMapping, 2000), new(AIPromptTypes.FormWorksheet, 4000), - new(AIPromptTypes.FormScoresheet, 4000) + new(AIPromptTypes.FormScoresheet, 8000) ]; public async Task SeedAsync(DataSeedContext context) diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/DataSeed/AIPromptDataSeeder.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/DataSeed/AIPromptDataSeeder.cs index 0d1af31c57..c94284bd3f 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/DataSeed/AIPromptDataSeeder.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/DataSeed/AIPromptDataSeeder.cs @@ -12,8 +12,7 @@ namespace Unity.AI.DataSeed; /// -/// Seeds the built-in AI prompts (application analysis, attachment summary, application scoring) into the host database. -/// Each prompt family is represented as versioned rows in AIPrompts. +/// Seeds host-owned, versioned built-in AI prompts. /// public class AIPromptDataSeeder( IRepository promptRepository, @@ -21,7 +20,10 @@ public class AIPromptDataSeeder( { public async Task SeedAsync(DataSeedContext context) { - if (context.TenantId != null) return; // host database only + if (context.TenantId != null) + { + return; + } using (currentTenant.Change(null)) { @@ -177,7 +179,7 @@ await promptRepository.InsertAsync(new AIPrompt( } // ═════════════════════════════════════════════════════════════════════════ - // PROMPT CONTENT — mirrors Runtime/Prompts/Versions/ text files verbatim + // PROMPT CONTENT — authoritative built-in database seed definitions // ═════════════════════════════════════════════════════════════════════════ // ── v0/analysis.system.txt ─────────────────────────────────────────────── @@ -866,7 +868,8 @@ Return only valid JSON. } Rules: - - Return one field-suggestion JSON object only. + - Return one JSON object containing all applicable field suggestions. Review the full form schema and every unmapped CHEFS field before responding; include every additional custom field genuinely needed, not just the first match. + - Return an empty fields array when no additional custom fields are needed. - chefsFields contains the available CHEFS source fields. - unityCoreFields contains existing Unity core fields. Do not create a custom field when one of these already fits. - existingMapping contains the current saved Unity-to-CHEFS mappings. Do not duplicate those mappings with a custom field. @@ -930,6 +933,8 @@ Return only valid JSON. Rules: - Return one scoresheet definition JSON object only. + - Title and Name must be non-empty strings; Sections must contain at least one section and every section must contain at least one field. + - Every field Name and Label must be non-empty, Order and Type must be non-negative integers, and Definition must be a valid JSON object encoded as a string. - The context contains CHEFS form fields, allowed Unity Flex question types, and a scoresheet template. - Fill out the scoresheet template to generate the rubric assessors use to score submitted applications. - Use CHEFS form fields as evidence for assessment criteria, but do not create one question per form field. diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Domain/AIModel.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Domain/AIModel.cs index 511ba071ec..4fd8d90936 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Domain/AIModel.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Domain/AIModel.cs @@ -7,6 +7,8 @@ public class AIModel : AuditedAggregateRoot { public string Name { get; set; } = default!; + public string Provider { get; set; } = default!; + public bool IsActive { get; set; } = true; /// Free-form model settings stored as JSON for dynamic runtime options. @@ -16,9 +18,10 @@ protected AIModel() { } - public AIModel(Guid id, string name) + public AIModel(Guid id, string name, string provider) { Id = id; Name = name; + Provider = provider; } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIDbContextModelCreatingExtensions.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIDbContextModelCreatingExtensions.cs index 99633a9030..421939ef02 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIDbContextModelCreatingExtensions.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIDbContextModelCreatingExtensions.cs @@ -54,6 +54,10 @@ public static void ConfigureAI(this ModelBuilder modelBuilder) .IsRequired() .HasMaxLength(200); + b.Property(x => x.Provider) + .IsRequired() + .HasMaxLength(100); + b.Property(x => x.IsActive) .IsRequired(); diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Generation/AIGenerationAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Generation/AIGenerationAppService.cs index 582c7d1cfb..c436d59f06 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Generation/AIGenerationAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Generation/AIGenerationAppService.cs @@ -25,6 +25,7 @@ public class AIGenerationAppService( [HttpPost("submit")] public virtual async Task SubmitAsync(string operationType, AIGenerationSubmissionDto request) { + // All generation routes converge here so authorization, feature, and form-version rules stay consistent. var operation = AIGenerationOperations.Get(operationType); await featureGuard.EnsureEnabledAsync(operation.FeatureName, operation.DisabledLocalizationKey); diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/AIExecutionModeResolver.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/AIExecutionModeResolver.cs index e988120367..5b111136b8 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/AIExecutionModeResolver.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/AIExecutionModeResolver.cs @@ -1,17 +1,19 @@ -using Microsoft.Extensions.Configuration; using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Unity.AI.Domain; using Unity.AI.Runtime.Prompts; using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; namespace Unity.AI.Operations; /// -/// Resolves the configured for an AI operation. -/// Configuration keys: -/// Azure:Operations:{operationName}:ExecutionMode - "Sequential" | "Parallel" | "Batch" (case-insensitive) -/// Azure:Operations:Defaults:ExecutionMode - required default when operation override is absent +/// Resolves the persisted for an active AI operation. /// -public class AIExecutionModeResolver(IConfiguration configuration) : ITransientDependency +public class AIExecutionModeResolver( + IRepository operationRepository) : ITransientDependency { public const string AttachmentSummaryOperation = AIPromptTypes.AttachmentSummary; public const string ApplicationScoringOperation = AIPromptTypes.ApplicationScoring; @@ -19,20 +21,20 @@ public class AIExecutionModeResolver(IConfiguration configuration) : ITransientD public const string FormWorksheetOperation = AIPromptTypes.FormWorksheet; public const string FormScoresheetOperation = AIPromptTypes.FormScoresheet; - public AIExecutionMode ResolveMode(string operationName) + public async Task ResolveModeAsync( + string operationName, + CancellationToken cancellationToken = default) { - var configured = configuration[$"Azure:Operations:{operationName}:ExecutionMode"]; - if (string.IsNullOrWhiteSpace(configured)) + var operations = await operationRepository.GetListAsync( + candidate => candidate.IsActive, + cancellationToken: cancellationToken); + var operation = operations.FirstOrDefault(candidate => + string.Equals(candidate.Name, operationName, StringComparison.OrdinalIgnoreCase)); + if (operation == null) { - configured = configuration["Azure:Operations:Defaults:ExecutionMode"]; + throw new InvalidOperationException($"AI operation '{operationName}' is not configured."); } - return configured?.Trim().ToLowerInvariant() switch - { - "sequential" => AIExecutionMode.Sequential, - "parallel" => AIExecutionMode.Parallel, - "batch" => AIExecutionMode.Batch, - _ => throw new InvalidOperationException($"AI execution mode is not configured or is invalid for operation '{operationName}'.") - }; + return operation.ExecutionMode; } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/ApplicationScoringService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/ApplicationScoringService.cs index aeb4dadb1e..af4385a329 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/ApplicationScoringService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/ApplicationScoringService.cs @@ -21,7 +21,9 @@ public class ApplicationScoringService( public async Task RegenerateAsync(ApplicationScoringOperationInputDto input, CancellationToken cancellationToken = default) { var sections = input.Sections; - var mode = executionModeResolver.ResolveMode(AIExecutionModeResolver.ApplicationScoringOperation); + var mode = await executionModeResolver.ResolveModeAsync( + AIExecutionModeResolver.ApplicationScoringOperation, + cancellationToken); var perSectionResults = await AIExecutionStrategy.RunAsync( sections, diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/RateLimit/AIRateLimiter.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/RateLimit/AIRateLimiter.cs index 6b6f1dc8a1..162efc619e 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/RateLimit/AIRateLimiter.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/RateLimit/AIRateLimiter.cs @@ -12,10 +12,8 @@ namespace Unity.AI.RateLimit; /// -/// Per-user cooldown for AI generate calls. KISS: a single cache entry per user -/// holds the cooldown end ticks; the cache TTL matches the cooldown so a missing -/// entry means the user can generate again. Anonymous/system callers are not -/// rate-limited (background event handlers also flow through the AI queue). +/// Per-user AI cooldown. Anonymous and system callers bypass it; activity providers +/// augment the state returned to authenticated users. /// public class AIRateLimiter( IDistributedCache cache, diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIOperationResult.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIOperationResult.cs index 41b3989649..024a4a214a 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIOperationResult.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIOperationResult.cs @@ -20,7 +20,8 @@ public enum AIFailureCategory public sealed record AIOperationResult( AIOperationOutcome Outcome, AIProviderResult Response, - AIFailureCategory FailureCategory = AIFailureCategory.None) + AIFailureCategory FailureCategory = AIFailureCategory.None, + string? FailureReason = null) { public string Content => Response.Content; @@ -41,8 +42,8 @@ public static AIOperationResult ProviderUnavailable(AIProviderResult? response = public static AIOperationResult InvalidOutput(AIProviderResult? response = null) => new(AIOperationOutcome.InvalidOutput, response ?? AIProviderResult.Empty, AIFailureCategory.InvalidOutput); - public AIOperationResult WithOutcome(AIOperationOutcome outcome, AIFailureCategory? failureCategory = null) => - new(outcome, Response, failureCategory ?? ResolveFailureCategory(outcome)); + public AIOperationResult WithOutcome(AIOperationOutcome outcome, AIFailureCategory? failureCategory = null, string? failureReason = null) => + new(outcome, Response, failureCategory ?? ResolveFailureCategory(outcome), failureReason ?? FailureReason); private static AIFailureCategory ResolveFailureCategory(AIOperationOutcome outcome) { diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIProviderPayloadValidator.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIProviderPayloadValidator.cs index 5dc14e9603..76b0c0fa03 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIProviderPayloadValidator.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIProviderPayloadValidator.cs @@ -206,7 +206,8 @@ private static AIResponseValidationResult ValidateSections(JsonElement root, str return sectionNameResult; } - if (!sectionNames.Add(section.GetProperty("Name").GetString()!)) + if (!TryGetProperty(section, "Name", out var sectionName) + || !sectionNames.Add(sectionName.GetString()!)) { return AIResponseValidationResult.Invalid($"{responseName} response contains duplicate section names."); } @@ -235,7 +236,8 @@ private static AIResponseValidationResult ValidateSections(JsonElement root, str return result; } - if (!fieldNames.Add(field.GetProperty("Name").GetString()!)) + if (!TryGetProperty(field, "Name", out var fieldName) + || !fieldNames.Add(fieldName.GetString()!)) { return AIResponseValidationResult.Invalid($"{responseName} response contains duplicate field names."); } @@ -355,6 +357,18 @@ private static bool TryGetProperty(JsonElement element, string propertyName, out return true; } + if (element.ValueKind == JsonValueKind.Object) + { + foreach (var candidate in element.EnumerateObject()) + { + if (string.Equals(candidate.Name, propertyName, StringComparison.OrdinalIgnoreCase)) + { + property = candidate.Value; + return true; + } + } + } + property = default; return false; } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/OpenAIConfigurationResolver.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/OpenAIConfigurationResolver.cs index e88fb4e731..8f536e9e40 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/OpenAIConfigurationResolver.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/OpenAIConfigurationResolver.cs @@ -23,6 +23,7 @@ public class OpenAIConfigurationResolver( IDataFilter multiTenantDataFilter, ICurrentTenant currentTenant) : ITransientDependency { + private const string DefaultProviderName = "OpenAI"; private static readonly TimeSpan SettingsCacheDuration = TimeSpan.FromMinutes(5); private static readonly JsonSerializerOptions JsonOptions = new() { @@ -37,12 +38,11 @@ public class OpenAIConfigurationResolver( private readonly IDataFilter _multiTenantDataFilter = multiTenantDataFilter; private readonly ICurrentTenant _currentTenant = currentTenant; - public string ResolveProviderName() => Required("Azure:Operations:Defaults:Provider"); + public string ResolveProviderName() => DefaultProviderName; public Task ResolveApiKeyAsync(string? modelName = null, CancellationToken cancellationToken = default) { - var providerName = Required("Azure:Operations:Defaults:Provider"); - return Task.FromResult(Required($"Azure:{providerName}:ApiKey")); + return Task.FromResult(Required($"Azure:{DefaultProviderName}:ApiKey")); } public async Task ResolveOperationSettingsAsync( @@ -68,7 +68,12 @@ public async Task ResolveOperationSettingsAsync( } var modelSettings = ResolveModelSettings(model); - var providerName = Required("Azure:Operations:Defaults:Provider"); + var providerName = model.Provider; + if (!string.Equals(providerName, DefaultProviderName, StringComparison.Ordinal)) + { + throw new InvalidOperationException($"AI provider '{providerName}' is not supported."); + } + var endpoint = Required($"Azure:{providerName}:Endpoint"); if (!Uri.TryCreate(endpoint, UriKind.Absolute, out _)) { @@ -91,7 +96,7 @@ public async Task ResolveOperationSettingsAsync( model.Name, apiKey, new Uri(endpoint), - Required($"Azure:{providerName}:Profiles:{model.Name}:DeploymentName"), + model.Name, modelSettings.MaxOutputTokenCountSupported, modelSettings.Temperature, operation.CompletionTokens, @@ -117,19 +122,6 @@ public async Task ResolveMaxOutputTokenCountSupportedAsync(string? modelNa var modelConfiguration = await ResolveModelConfigurationAsync(modelName, cancellationToken); if (modelConfiguration != null) { - var providerName = Required("Azure:Operations:Defaults:Provider"); - var profileName = modelConfiguration.Value.Model.Name; - var configuredValue = Optional($"Azure:{providerName}:Profiles:{profileName}:MaxOutputTokenCountSupported"); - if (configuredValue != null) - { - if (bool.TryParse(configuredValue, out var parsedValue)) - { - return parsedValue; - } - - throw new InvalidOperationException($"Azure:{providerName}:Profiles:{profileName}:MaxOutputTokenCountSupported is not a valid boolean."); - } - return modelConfiguration.Value.Settings.MaxOutputTokenCountSupported; } @@ -168,8 +160,7 @@ public async Task ResolveEndpointAsync(string? modelName = null, Cancellati var modelConfiguration = await ResolveModelConfigurationAsync(modelName, cancellationToken); if (modelConfiguration != null) { - var providerName = Required("Azure:Operations:Defaults:Provider"); - return new Uri(Required($"Azure:{providerName}:Endpoint")); + return new Uri(Required($"Azure:{modelConfiguration.Value.Model.Provider}:Endpoint")); } throw new InvalidOperationException("AI model is not configured."); @@ -180,8 +171,7 @@ public async Task ResolveDeploymentNameAsync(string? modelName = null, C var modelConfiguration = await ResolveModelConfigurationAsync(modelName, cancellationToken); if (modelConfiguration != null) { - var providerName = Required("Azure:Operations:Defaults:Provider"); - return Required($"Azure:{providerName}:Profiles:{modelConfiguration.Value.Model.Name}:DeploymentName"); + return modelConfiguration.Value.Model.Name; } throw new InvalidOperationException("AI model is not configured."); @@ -242,17 +232,6 @@ private static AIModelSettings ResolveModelSettings(AIModel model) string.Equals(model.Name, modelName, StringComparison.OrdinalIgnoreCase)); } - var configuredDefaultProfile = Optional("Azure:Operations:Defaults:Profile"); - if (!string.IsNullOrWhiteSpace(configuredDefaultProfile)) - { - var configuredDefaultModel = activeModels.FirstOrDefault(model => - string.Equals(model.Name, configuredDefaultProfile, StringComparison.OrdinalIgnoreCase)); - if (configuredDefaultModel != null) - { - return configuredDefaultModel; - } - } - return null; } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/OpenAIRuntimeService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/OpenAIRuntimeService.cs index 95eebffec6..fabef95333 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/OpenAIRuntimeService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/OpenAIRuntimeService.cs @@ -310,7 +310,10 @@ public async Task GenerateFormWorksheetAsync(FormWorkshee { Worksheet = result.Outcome == AIOperationOutcome.Success ? AIResponseJson.CleanJsonResponse(result.Content) - : "{}" + : "{}", + FailureReason = result.Outcome == AIOperationOutcome.Success + ? null + : result.FailureReason ?? $"Worksheet generation failed with outcome {result.Outcome}." }; } catch (OperationCanceledException) @@ -320,7 +323,7 @@ public async Task GenerateFormWorksheetAsync(FormWorkshee catch (Exception ex) { _logger.LogError(ex, "Form worksheet generation failed."); - return new FormWorksheetResponse(); + return new FormWorksheetResponse { FailureReason = ex.Message }; } } @@ -357,9 +360,8 @@ public async Task GenerateFormScoresheetAsync(FormScores return new FormScoresheetResponse { - Scoresheet = result.Outcome == AIOperationOutcome.Success - ? AIResponseJson.CleanJsonResponse(result.Content) - : "{}" + Scoresheet = AIResponseJson.CleanJsonResponse(result.Content), + FailureReason = result.FailureReason }; } catch (OperationCanceledException) @@ -406,7 +408,10 @@ private async Task GenerateFormMappingCoreAsync(FormMapping if (result.Outcome != AIOperationOutcome.Success) { - return new FormMappingResponse(); + return new FormMappingResponse + { + FailureReason = result.FailureReason ?? $"Mapping generation failed with outcome {result.Outcome}." + }; } return new FormMappingResponse @@ -421,7 +426,7 @@ private async Task GenerateFormMappingCoreAsync(FormMapping catch (Exception ex) { _logger.LogError(ex, "Mapping suggestion generation failed."); - return new FormMappingResponse(); + return new FormMappingResponse { FailureReason = ex.Message }; } } @@ -449,7 +454,10 @@ private async Task GenerateWithRetryAsync( return lastResult; } - lastResult = lastResult.WithOutcome(AIOperationOutcome.InvalidOutput, validationResult.FailureCategory); + lastResult = lastResult.WithOutcome( + AIOperationOutcome.InvalidOutput, + validationResult.FailureCategory, + validationResult.Reason); _logger.LogWarning( "AI {OperationName} attempt {Attempt}/{MaxAttempts} returned invalid response shape ({FailureCategory}): {Reason}; will retry if attempts remain", diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v0/application-analysis.system.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v0/application-analysis.system.txt deleted file mode 100644 index f7d3be3bb3..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v0/application-analysis.system.txt +++ /dev/null @@ -1,12 +0,0 @@ -You are an expert grant application reviewer for the BC Government. - -Conduct a thorough, comprehensive analysis across all rubric areas. Identify substantive issues, concerns, and opportunities for improvement. - -Classify findings by their effect on the application's quality and fundability: -- ERRORS: important missing information, significant gaps, compliance issues, or major concerns affecting eligibility -- WARNINGS: areas needing clarification, moderate issues, or concerns that should be addressed -- SUMMARIES: concise reviewer-facing recommendations or follow-up considerations - -Evaluate content quality, clarity, and appropriateness. Be thorough but fair and avoid nitpicking. - -Respond only with valid JSON in the exact format requested. diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v0/application-analysis.user.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v0/application-analysis.user.txt deleted file mode 100644 index 8d7c84c90e..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v0/application-analysis.user.txt +++ /dev/null @@ -1,93 +0,0 @@ -APPLICATION CONTENT: -{{DATA}} - -ATTACHMENT SUMMARIES: -{{ATTACHMENTS}} - -FORM FIELD CONFIGURATION: -{{SCHEMA}} - -MANDATORY FIELDS: -- Determine mandatory fields from FORM FIELD CONFIGURATION. -- Report missing mandatory fields as findings when they materially affect review quality. - -OPTIONAL FIELDS (may be left blank): -- Determine optional fields from FORM FIELD CONFIGURATION. -- Do not flag optional fields when blank unless they materially weaken rubric evidence. - -EVALUATION RUBRIC: -BC GOVERNMENT GRANT EVALUATION RUBRIC: - -1. ELIGIBILITY REQUIREMENTS: - - Project must align with program objectives - - Applicant must be eligible entity type - - Budget must be reasonable and well-justified - - Project timeline must be realistic - -2. COMPLETENESS CHECKS: - - All required fields completed - - Necessary supporting documents provided - - Budget breakdown detailed and accurate - - Project description clear and comprehensive - -3. FINANCIAL REVIEW: - - Requested amount is within program limits - - Budget is reasonable for scope of work - - Matching funds or in-kind contributions identified - - Cost per outcome/beneficiary is reasonable - -4. RISK ASSESSMENT: - - Applicant capacity to deliver project - - Technical feasibility of proposed work - - Environmental or regulatory compliance - - Potential for cost overruns or delays - -5. QUALITY INDICATORS: - - Clear project objectives and outcomes - - Well-defined target audience/beneficiaries - - Appropriate project methodology - - Sustainability plan for long-term impact - -EVALUATION CRITERIA: -- HIGH: Meets all requirements, well-prepared application, low risk -- MEDIUM: Meets most requirements, minor issues or missing elements -- LOW: Missing key requirements, significant concerns, high risk - -Analyze this grant application comprehensively across all five rubric categories (Eligibility, Completeness, Financial Review, Risk Assessment, and Quality Indicators). Identify issues, concerns, and areas for improvement. - -OUTPUT -{ - "decision": "", - "warnings": [ - { - "title": "", - "detail": "" - } - ], - "errors": [ - { - "title": "", - "detail": "" - } - ], - "summaries": [ - { - "title": "", - "detail": "" - } - ], - "recommendations": [ - { - "title": "", - "detail": "" - } - ] -} - -Important: -- Use only APPLICATION CONTENT, ATTACHMENT SUMMARIES, FORM FIELD CONFIGURATION, and EVALUATION RUBRIC as evidence. -- decision must be PROCEED or HOLD. -- Use summaries for overall application quality/readiness synthesis. -- Use recommendations for reviewer-facing follow-up actions or considerations before scoring or decision-making. -- Use "title" and "detail" keys for all finding objects. -- Return valid plain JSON only in the exact OUTPUT shape. diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v0/application-scoring.system.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v0/application-scoring.system.txt deleted file mode 100644 index 0a629a6a97..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v0/application-scoring.system.txt +++ /dev/null @@ -1,5 +0,0 @@ -You are an expert grant application reviewer for the BC Government. -Analyze the provided application and answer only the questions in the specified scoresheet section. -Be thorough, objective, and fair. Base answers strictly on provided evidence. -Always provide evidence-grounded rationale and an honest confidence score. -Respond only with valid JSON in the exact format requested. diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v0/application-scoring.user.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v0/application-scoring.user.txt deleted file mode 100644 index 6230fe0ea3..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v0/application-scoring.user.txt +++ /dev/null @@ -1,38 +0,0 @@ -APPLICATION CONTENT: -{{DATA}} - -ATTACHMENT SUMMARIES: -{{ATTACHMENTS}} - -SCORESHEET SECTION: -{{SECTION}} - -RESPONSE TEMPLATE: -{{RESPONSE}} - -Please analyze this grant application and provide answers for each question in the specified section only. - -For each question, provide: -1. The answer based on the application evidence -2. A brief rationale (1-2 complete sentences) citing concrete supporting evidence -3. A confidence score from 0-100 (integer) indicating certainty in the selected answer - -OUTPUT -{ - "": { - "answer": "", - "rationale": "", - "confidence": - } -} - -Important: -- Use only APPLICATION CONTENT and ATTACHMENT SUMMARIES as evidence. -- Answer only the question IDs in the specified section. -- Every question must include answer, rationale, and confidence. -- Use RESPONSE TEMPLATE as the contract and fill every placeholder value. -- answer type must match the question type. -- For select list questions, return only the option number as a string, never label text. -- rationale must be 1-2 complete sentences grounded in evidence. -- confidence must be an integer from 0 to 100 in increments of 5. -- Return valid plain JSON only in the exact OUTPUT shape. diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v0/attachment-summary.system.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v0/attachment-summary.system.txt deleted file mode 100644 index 6e2775e346..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v0/attachment-summary.system.txt +++ /dev/null @@ -1,10 +0,0 @@ -You are a professional grant analyst for the BC Government. - -Please analyze this attachment and provide a concise reviewer-facing summary of its content, purpose, and key information. - -OUTPUT -{ - "summary": "" -} - -Use only ATTACHMENT as evidence. If ATTACHMENT.text is present, summarize actual content; otherwise provide a conservative file-level summary. Write 1-2 complete sentences and return valid plain JSON only in the exact OUTPUT shape. diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v0/attachment-summary.user.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v0/attachment-summary.user.txt deleted file mode 100644 index 98da3a726e..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v0/attachment-summary.user.txt +++ /dev/null @@ -1,2 +0,0 @@ -ATTACHMENTS -{{ATTACHMENTS}} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-analysis.output.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-analysis.output.txt deleted file mode 100644 index 44093f50be..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-analysis.output.txt +++ /dev/null @@ -1,27 +0,0 @@ -{ - "decision": "", - "errors": [ - { - "title": "", - "detail": "" - } - ], - "warnings": [ - { - "title": "", - "detail": "" - } - ], - "summaries": [ - { - "title": "", - "detail": "" - } - ], - "recommendations": [ - { - "title": "", - "detail": "" - } - ] -} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-analysis.rubric.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-analysis.rubric.txt deleted file mode 100644 index 71ef4cb126..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-analysis.rubric.txt +++ /dev/null @@ -1,5 +0,0 @@ -ELIGIBILITY REQUIREMENTS: Project aligns with program objectives; Applicant is an eligible entity; Budget is reasonable and justified; Timeline is realistic. -COMPLETENESS CHECKS: Required information is present; Supporting materials are provided where applicable; Description is clear. -FINANCIAL REVIEW: Requested amount is within limits; Budget matches scope; Matching funds or contributions are identified. -RISK ASSESSMENT: Applicant capacity; Feasibility; Compliance considerations; Delivery risks. -QUALITY INDICATORS: Clear objectives; Defined beneficiaries; Appropriate approach; Long-term sustainability. diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-analysis.rules.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-analysis.rules.txt deleted file mode 100644 index f1627fb26f..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-analysis.rules.txt +++ /dev/null @@ -1,32 +0,0 @@ -- Use only provided input sections as evidence. -- Do not invent fields, documents, requirements, or facts. -- Prefer, in order: direct evidence from DATA, specific supporting evidence from ATTACHMENTS, then broader context only when necessary. -- Treat missing or empty values as findings only when they weaken rubric evidence. -- Prefer material findings; avoid nitpicking. -- Do not restate basic application facts as findings unless they support a specific reviewer conclusion about readiness, feasibility, budget credibility, eligibility, or confidence in proceeding. -- Prefer direct evidence from DATA over derivative statements in ATTACHMENTS when both address the same point. -- If ATTACHMENTS evidence is used, cite the attachment by name in detail. -- Each detail must cite concrete evidence from DATA or ATTACHMENTS. -- Write reviewer-facing natural language. Do not refer to prompt section names, internal field keys, or schema labels such as DATA, ATTACHMENTS, ProjectSummary, CustomField1, or OrganizationType. -- Refer to evidence by its plain-language meaning, quoted text, or attachment name rather than internal key names. -- Only include warnings when the evidence shows a specific, concrete risk, inconsistency, or meaningful uncertainty; a stated risk label alone is not enough. -- Do not state that one amount exceeds, matches, or conflicts with another unless the comparison is directly supported by the provided values. -- Do not treat ordinary lack of detailed supporting explanation as a material gap unless the provided evidence creates real uncertainty about feasibility, eligibility, or budget credibility. -- Prefer neutral evidence descriptions over evaluative adjectives unless the evidence directly supports a strong conclusion. -- Do not describe capacity, feasibility, or justification as strong, detailed, or well-supported unless the evidence shows more than the existence of basic organizational, budget, or timeline information. -- Do not infer community support, established partnerships, or delivery capacity from a single partner reference, staff count, or basic organizational status alone. -- Do not describe a timeline as realistic or feasible based only on start and end dates unless additional evidence supports deliverability. -- Use 3-6 words for title. -- Summary titles should name the specific substantive reviewer conclusion, strength, or risk, not a generic evaluation label or abstract category. -- Each detail must be 1-2 complete sentences. -- Summaries and recommendations must be concrete, distinct, reviewer-relevant, and specific to this application's evidence. -- Avoid generic praise, checklist language, and repeated conclusions across lists. -- Do not use a summary merely to say that supporting documents were provided; summarize the specific substantive evidence they add, or omit the finding. -- Errors and warnings may be empty. -- Summaries and recommendations must each include at least one item. -- Decision must be PROCEED or HOLD. -- Use summaries for overall application quality/readiness synthesis. -- Use recommendations for concrete reviewer-facing next actions based on the provided evidence. -- Recommendations may include proceeding with the normal review process when the application appears ready for that step. -- When evidence shows a meaningful gap, inconsistency, or uncertainty, use recommendations for specific follow-up or verification actions. -- Return an empty array only when no concrete next action would help the reviewer. diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-analysis.score.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-analysis.score.txt deleted file mode 100644 index 0ce56f3e81..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-analysis.score.txt +++ /dev/null @@ -1,3 +0,0 @@ -HIGH: Application demonstrates strong evidence across most rubric areas with few or no issues. -MEDIUM: Application has some gaps or weaknesses that require reviewer attention. -LOW: Application has significant gaps or risks across key rubric areas. diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-analysis.system.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-analysis.system.txt deleted file mode 100644 index b05196b8ab..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-analysis.system.txt +++ /dev/null @@ -1,9 +0,0 @@ -ROLE -You are a careful grant review assistant for human reviewers. Do not fill gaps, assume compliance, or treat relevance as proof. - -TASK -Using SCHEMA, DATA, ATTACHMENTS, RUBRIC, SCORE, OUTPUT, and RULES: -1. Review the application and any provided attachments for the strongest reviewer-relevant evidence. -2. Determine which conclusions are directly supported by that evidence. -3. Exclude weak, repetitive, or loosely supported conclusions. -4. Return only the strongest evidence-backed reviewer conclusions. diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-analysis.user.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-analysis.user.txt deleted file mode 100644 index b450089268..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-analysis.user.txt +++ /dev/null @@ -1,21 +0,0 @@ -SCHEMA -{{SCHEMA}} - -DATA -{{DATA}} - -ATTACHMENTS -{{ATTACHMENTS}} - -RUBRIC -{{RUBRIC}} - -SCORE -{{SCORE}} - -RESPONSE -{{RESPONSE}} - -RULES -{{RULES}} -{{COMMON_RULES}} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-scoring.output.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-scoring.output.txt deleted file mode 100644 index 8af7a3f2fd..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-scoring.output.txt +++ /dev/null @@ -1,7 +0,0 @@ -{ - "": { - "answer": "", - "rationale": "", - "confidence": - } -} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-scoring.rules.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-scoring.rules.txt deleted file mode 100644 index 81a4132069..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-scoring.rules.txt +++ /dev/null @@ -1,51 +0,0 @@ -- Use only DATA and ATTACHMENTS as evidence. -- Do not invent missing application details. -- Ignore fields or details that are not relevant to the specific question being answered. -- Prefer, in order: direct evidence of the exact condition asked, closely related supporting evidence, then general context only when necessary. -- If evidence is insufficient, partial, indirect, missing, or non-specific, choose the most conservative valid answer and explain the uncertainty. -- Do not convert general project descriptions into evidence for a specific scored condition unless that condition is directly supported. -- Treat prefilled labels, ratings, rankings, or statuses as background context only unless the question explicitly asks for that same item. -- Do not treat related concepts as equivalent; answer the specific question asked, not a nearby concept. -- Do not infer unsupported claims about requirements, conditions, relationships, compliance elements, mitigations, supports, or outcomes. -- Answer a specific condition positively only when that exact condition is directly evidenced in DATA or ATTACHMENTS. -- For eligibility, completeness, ownership, location, or compliance questions, do not answer positively unless the exact condition is directly confirmed in the provided evidence. -- If the evidence shows only involvement, presence, relevance, or association, do not treat that alone as proof that a requirement or condition is satisfied. -- Return exactly one answer object per question ID in SECTION.questions. -- Do not omit any question IDs from SECTION.questions. -- Do not add keys that are not question IDs from SECTION.questions. -- Use the exact question IDs from RESPONSE and SECTION.questions without alteration; never rewrite, normalize, or regenerate a question ID. -- Use RESPONSE as the output contract and fill every placeholder value. -- Each answer object must include: "answer", "rationale", and "confidence". -- Never omit "answer", "rationale", or "confidence" for any question type. -- The "answer" value type must match question type: Number => numeric; YesNo/SelectList/Text/TextArea => string. -- The "rationale" field must be 1-2 complete sentences grounded in concrete DATA/ATTACHMENTS evidence. -- In rationale, cite concrete source evidence from the provided input content in plain language rather than prompt section headers or internal field names. -- Write reviewer-facing natural language. Do not refer to prompt section names, internal field keys, or schema labels such as DATA, ATTACHMENTS, ProjectSummary, CustomField1, or OrganizationType. -- Refer to evidence by its plain-language meaning, quoted text, or attachment name rather than internal key names. -- For every question, rationale must justify both the selected answer and the selected confidence level based on evidence strength. -- The "confidence" field must be an integer from 0 to 100 in increments of 5 and represents confidence in the selected answer. -- Set confidence by certainty of the selected answer based on available evidence, regardless of which option is selected. -- Do not use maximum or near-maximum confidence when the answer depends on inference rather than an explicit statement of the exact condition. -- For yes/no questions, the "answer" field must be exactly "Yes" or "No". -- For numeric questions, answer must be a numeric value within the allowed range. -- For numeric questions, answer must never be blank. -- If evidence is insufficient for a numeric question, return the minimum allowed numeric value and explain uncertainty in rationale. -- If a required value is explicitly missing in DATA/ATTACHMENTS, set confidence high (80-100) when selecting the conservative minimum. -- For select list questions, use the matching SECTION.questions[].options entries and return only the selected options[].number as a string. -- For select list questions, the "answer" value must be one of the matching question.allowed_answers values exactly. -- For select list questions, return only the option number string, never the option label text such as "Yes", "No", or "N/A". -- Never return 0 for select list answers unless 0 exists as an explicit option number. -- For select list questions, choose the lowest option fully supported by the evidence; use a higher option only when the specific condition and required strength are directly supported. -- If evidence supports the existence of a topic but not the required strength, completeness, or specificity, choose the lowest option consistent with that evidence. -- If evidence is insufficient for a select list question, choose the lowest allowed answer value from question.allowed_answers and explain the uncertainty. -- Do not treat broad project descriptions, general goals, high-level timelines, budget presence, or a single indirect reference as sufficient evidence for a higher-scored select-list answer. -- For text and text area questions, answer must be concise, evidence-based, non-empty, and avoid boilerplate placeholders. -- For text and text area questions, answer is the reviewer comment, and rationale must explain the evidence basis and certainty for that comment. -- If no concerns are identified for a text or text area question, return a short non-empty evidence-based comment rather than leaving answer blank. -- For comment fields, summarize only the evidence-based conclusions supported by the scored answers, including uncertainty where applicable, and do not introduce stronger claims. -- For comment fields, describe the evidence and resulting answer without elevating it into an overall assessment unless the question explicitly asks for one. -- Do not add recommendations or stronger conclusions unless the question explicitly asks for them. -- For comment fields, do not leave answer empty even when all other answers are positive. -- Do not leave rationale empty when answer is populated. -- Final self-check before responding: every question ID in RESPONSE must have a non-empty "answer", non-empty "rationale", and "confidence". -- If any answer object is incomplete, regenerate the full JSON response before returning it. diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-scoring.system.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-scoring.system.txt deleted file mode 100644 index aefebf06d5..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-scoring.system.txt +++ /dev/null @@ -1,11 +0,0 @@ -ROLE -You are a careful grant review assistant for human reviewers. Do not fill gaps, assume compliance, or treat relevance as proof. - -TASK -Using DATA, ATTACHMENTS, SECTION, RESPONSE, OUTPUT, and RULES: -1. Review each question in SECTION one at a time. -2. Identify the exact condition the question asks about. -3. Consider only the most relevant evidence in DATA and any provided ATTACHMENTS for that condition. -4. Choose the most conservative valid answer supported by that evidence. -5. If evidence is incomplete or indirect, explain the uncertainty in the rationale. -6. Repeat for every question in SECTION. diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-scoring.user.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-scoring.user.txt deleted file mode 100644 index a8f64bc65f..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-scoring.user.txt +++ /dev/null @@ -1,15 +0,0 @@ -DATA -{{DATA}} - -ATTACHMENTS -{{ATTACHMENTS}} - -SECTION -{{SECTION}} - -RESPONSE -{{RESPONSE}} - -RULES -{{RULES}} -{{COMMON_RULES}} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/attachment-summary.output.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/attachment-summary.output.txt deleted file mode 100644 index b9d5a880a0..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/attachment-summary.output.txt +++ /dev/null @@ -1,3 +0,0 @@ -{ - "summary": "" -} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/attachment-summary.rules.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/attachment-summary.rules.txt deleted file mode 100644 index 2230e39228..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/attachment-summary.rules.txt +++ /dev/null @@ -1,13 +0,0 @@ -- Use only ATTACHMENT as evidence. -- Summarize actual content when ATTACHMENT.text is present; otherwise provide a conservative file-level summary. -- Describe the attachment itself rather than summarizing the overall project. -- Ensure the summary describes the attachment itself, not the overall project. -- If ATTACHMENT.text is primarily structured application, contact, organization, budget, or date fields, summarize it as a metadata-style attachment rather than rewriting it as a generic project summary. -- Begin with what the attachment contains or provides, not the file name or file type, unless that metadata is necessary to describe the evidence. -- Do not invent missing details. -- Do not calculate or restate totals, sums, or aggregates unless they are explicitly present in ATTACHMENT.text. -- Write reviewer-facing natural language. Do not refer to prompt section names, internal field keys, or schema labels such as ATTACHMENT or ATTACHMENT.text. -- Refer to evidence by its plain-language meaning, quoted text, or file name rather than internal key names. -- Write 1-2 complete sentences. -- Summary must be grounded in concrete ATTACHMENT evidence. -- Return exactly one object with only the key: summary. diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/attachment-summary.system.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/attachment-summary.system.txt deleted file mode 100644 index 50f0d6a6f3..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/attachment-summary.system.txt +++ /dev/null @@ -1,8 +0,0 @@ -ROLE -You are a careful grant review assistant for human reviewers. Do not fill gaps, assume compliance, or treat relevance as proof. - -TASK -Using ATTACHMENT, OUTPUT, and RULES: -1. Review the attachment to identify what it contains. -2. Summarize the attachment itself, not the overall project. -3. Return a concise reviewer-facing summary. \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/attachment-summary.user.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/attachment-summary.user.txt deleted file mode 100644 index 13cf1e6773..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/attachment-summary.user.txt +++ /dev/null @@ -1,9 +0,0 @@ -ATTACHMENTS -{{ATTACHMENTS}} - -RESPONSE -{{RESPONSE}} - -RULES -{{RULES}} -{{COMMON_RULES}} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/common.rules.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/common.rules.txt deleted file mode 100644 index 790f34fb73..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/common.rules.txt +++ /dev/null @@ -1,6 +0,0 @@ -- Any narrative text response must be at least 12 words. -- If ATTACHMENTS is empty, use DATA only and do not mention missing attachments unless their absence is material to the specific conclusion or question. -- Return values exactly as specified in OUTPUT. -- Do not return keys outside OUTPUT. -- Return valid JSON only. -- Return plain JSON only (no markdown). diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v2/form-worksheet.user.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v2/form-worksheet.user.txt deleted file mode 100644 index 1cccb19f32..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v2/form-worksheet.user.txt +++ /dev/null @@ -1,37 +0,0 @@ -WORKSHEET CONTEXT: -{{DATA}} - -OUTPUT -{ - "Name": "", - "Title": "", - "Version": , - "Published": true, - "Sections": [ - { - "Name": "", - "Order": 1, - "Fields": [ - { - "Name": "", - "Key": "", - "Label": "", - "Type": , - "Definition": "" - } - ] - } - ], - "ReportColumns": "", - "ReportKeys": "", - "ReportViewName": "" -} - -Rules: -- Return one worksheet definition JSON object only. -- The context includes CHEFS fields, Unity core fields, and existing worksheet-derived custom fields. -- Use the provided form context to decide which custom fields are genuinely needed. -- Prefer existing Unity core fields when they already satisfy the need. -- Only create additional worksheet custom fields when the form genuinely needs them. -- Keep the worksheet structure valid for Flex. -- Return valid plain JSON only. diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v2/onboarding-mapping.system.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v2/onboarding-mapping.system.txt deleted file mode 100644 index 3c791b606b..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v2/onboarding-mapping.system.txt +++ /dev/null @@ -1,4 +0,0 @@ -You are a careful mapping assistant for human reviewers. -Compare CHEFS fields, Unity core fields, and worksheet fields to suggest likely mappings. -Do not invent fields, persist changes, or assume a worksheet should exist if one is not clearly justified. -Return only valid JSON in the exact format requested. diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v2/onboarding-mapping.user.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v2/onboarding-mapping.user.txt deleted file mode 100644 index 3237f3c5ca..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v2/onboarding-mapping.user.txt +++ /dev/null @@ -1,26 +0,0 @@ -FORM MAPPING CONTEXT: -{{DATA}} - -OUTPUT -{ - "": "", - "": "" -} - -Rules: -- Return one flat JSON object only. -- The context is grouped as `chefsData` and `unityData`. -- `chefsData.fields` contains the CHEFS source fields. -- `unityData.coreFields` contains Unity target fields. -- `unityData.customFields` contains worksheet-derived Unity target fields. -- Each property name must be a Unity core or worksheet-derived target field name from the provided context. -- Each property value must be the CHEFS source field name that best matches that Unity target field. -- Only include mappings that are clearly semantically equivalent or strongly related by label, name, type, and purpose. -- Do not force one-to-one coverage. Omit Unity fields when no CHEFS field is a sensible match. -- Omit CHEFS fields that do not clearly map to a Unity target field. -- Do not map platform/system identifiers such as SubmissionId, SubmissionDate, or ConfirmationId; they are managed by Unity and should be omitted if present. -- If no fields clearly match, return `{}`. -- The mapping is dynamic; do not hardcode or assume a fixed list of fields. -- Prefer existing Unity core intake fields when they already fit the source field. -- Only use worksheet custom field targets when the form genuinely needs them. -- Return valid plain JSON only. diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Settings/AIConfigurationAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Settings/AIConfigurationAppService.cs index f7bca0af75..0873375fa7 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Settings/AIConfigurationAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Settings/AIConfigurationAppService.cs @@ -27,7 +27,9 @@ public virtual async Task GetTenantConfigurationAsync( AutomaticGenerationEnabled = await _settingProvider.GetAsync( AISettings.AutomaticGenerationEnabled, defaultValue: false), ManualGenerationEnabled = await _settingProvider.GetAsync( - AISettings.ManualGenerationEnabled, defaultValue: false) + AISettings.ManualGenerationEnabled, defaultValue: false), + ReportingEnabled = await _settingProvider.GetAsync( + AISettings.ReportingEnabled, defaultValue: false) }; } @@ -46,5 +48,11 @@ await _settingManager.SetAsync( input.ManualGenerationEnabled.ToString().ToLowerInvariant(), TenantSettingValueProvider.ProviderName, _currentTenant.Id?.ToString()); + + await _settingManager.SetAsync( + AISettings.ReportingEnabled, + input.ReportingEnabled.ToString().ToLowerInvariant(), + TenantSettingValueProvider.ProviderName, + _currentTenant.Id?.ToString()); } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Settings/AISettingDefinitionProvider.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Settings/AISettingDefinitionProvider.cs index 36a1914729..f7ff46d8dd 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Settings/AISettingDefinitionProvider.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Settings/AISettingDefinitionProvider.cs @@ -29,6 +29,17 @@ public override void Define(ISettingDefinitionContext context) isEncrypted: false) .WithProviders(TenantSettingValueProvider.ProviderName) ); + + context.Add( + new SettingDefinition( + AISettings.ReportingEnabled, + "false", + L("Setting:AI.ReportingEnabled"), + isVisibleToClients: false, + isInherited: false, + isEncrypted: false) + .WithProviders(TenantSettingValueProvider.ProviderName) + ); } private static LocalizableString L(string name) diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Unity.AI.Application.csproj b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Unity.AI.Application.csproj index 976f38baa3..e9f95639ec 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Unity.AI.Application.csproj +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Unity.AI.Application.csproj @@ -32,10 +32,4 @@ runtime; build; native; contentfiles; analyzers - - - PreserveNewest - PreserveNewest - - diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AI/en.json b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AI/en.json index 88d3d02728..26fd2bba24 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AI/en.json +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AI/en.json @@ -23,8 +23,21 @@ "Permission:AI.Prompts.Delete": "Delete Prompts", "Menu:AIReporting": "AI Reporting", + "AISettingGroup:Title": "AI Configuration", + "AISettingGroup:SaveChanges": "Save Changes", + "AISettingGroup:DiscardChanges": "Discard Changes", + "Setting:AI.AutomaticGenerationEnabled": "Automatically Generate AI Analysis", + "Setting:AI.AutomaticGenerationEnabled:Description": "When enabled, AI analysis runs automatically on new application intake (subject to form-level AI configuration).", "Setting:AI.ManualGenerationEnabled": "Manually Initiate AI Analysis", + "Setting:AI.ManualGenerationEnabled:Description": "When enabled, users with appropriate permissions can manually generate or regenerate AI analysis.", + "Setting:AI.ReportingEnabled": "AI Reporting", + "Setting:AI.ReportingEnabled:Description": "When enabled, users with the AI Reporting permission can access the AI Reporting dashboard.", + + "LegalDisclaimer:Title": "Legal Disclaimer", + "LegalDisclaimer:Body": "The Platform does not provide legal, financial, or policy advice, and may not be relied on to determine program eligibility or make final decisions in place of the user, who will be solely responsible for the use of the Platform, including the review and validation of Unity AI outputs before use.", + "LegalDisclaimer:Confirm": "I Understand and Agree", + "LegalDisclaimer:Confirming": "Confirming...", "AI:AttachmentSummariesDisabled": "AI attachment summaries are not enabled.", "AI:ApplicationAnalysisDisabled": "AI application analysis is not enabled.", @@ -42,6 +55,45 @@ "AI:ScoringRequiresScoresheetFields": "AI scoring requires a scoresheet with scoring fields.", "AI:AttachmentNotFound": "Attachment not found.", "AI:SelectAttachmentForSummaries": "Select at least one attachment to generate summaries.", + "AI:FormWorksheetDeleteProtected": "This AI worksheet cannot be deleted because it is published, linked to another form, or has existing instances.", + "AI:FormWorksheetUnavailable": "The AI worksheet is no longer available for review.", + "AI:FormWorksheetSelectionInvalid": "The AI worksheet selection is invalid.", + "AI:FormScoresheetDeleteProtected": "This AI scoresheet cannot be deleted because it is published or archived.", + "AI:FormScoresheetHasInstances": "This AI scoresheet cannot be deleted because it has existing instances.", + "AI:FormScoresheetUnavailable": "The AI scoresheet is no longer available for review.", + "AI:FormScoresheetTitleRequired": "A scoresheet title is required.", + "AI:FormScoresheetSelectionRequired": "Select at least one suggested question.", + "AI:FormScoresheetSelectionInvalid": "The AI scoresheet selection is invalid.", + "AI:GenerateFormScoresheetPermissionRequired": "You do not have permission to generate scoresheets.", + "AI:SavedMappingApplyFailed": "Unable to apply the saved mapping.", + "AI:FormGenerationReviewActive": "Review or discard the current AI suggestions before generating again.", + "AI:MappingReviewPending": "No mapping review is pending.", + "AI:MappingReviewInactive": "The mapping review is no longer active.", + "AI:MappingSelectionRequired": "Select at least one mapping suggestion.", + "AI:MappingSelectionInvalid": "One or more mapping suggestions are no longer available.", + "AI:MappingReviewTransitionInvalid": "That AI mapping workflow transition is not valid.", + "AI:MappingReviewPendingSuggestions": "Review or discard the pending mapping suggestions first.", + "AI:WorksheetDraftsMustBePublished": "Publish and assign the AI worksheet drafts before generating mapping.", + "AI:WorksheetTitleRequired": "A worksheet title is required.", + "AI:WorksheetSelectionRequired": "Select at least one suggested field.", + "AI:WorkflowGenerateInitialMapping": "Generate Initial Mapping", + "AI:WorkflowReviewInitialMapping": "Review Initial Mapping", + "AI:WorkflowGenerateWorksheets": "Generate Worksheets", + "AI:WorkflowReviewWorksheets": "Review Worksheets", + "AI:WorkflowPublishAssignWorksheets": "Publish & Assign Worksheets", + "AI:WorkflowGenerateFinalMapping": "Generate Final Mapping", + "AI:WorkflowReviewFinalMapping": "Review Final Mapping", + "AI:WorkflowGenerateMapping": "Generate Mapping", + "AI:WorkflowCompleted": "Completed", + "AI:ScoresheetGenerationRequiresFormVersion": "Form scoresheet generation requires an application form version.", + "AI:ScoresheetGenerationProtected": "The canonical AI scoresheet is published, archived, or has instances and cannot be regenerated.", + "AI:ScoresheetGenerationInvalidOutput": "Scoresheet generation returned invalid output: {0}", + "AI:ScoresheetGenerationEmpty": "Scoresheet generation returned empty content.", + "AI:ScoresheetGenerationUnusable": "Scoresheet generation returned an unusable scoresheet definition.", + "AI:ScoresheetGenerationNoVersion": "Scoresheet generation returned a definition without a valid Version.", + "AI:ScoresheetGenerationNoSections": "Scoresheet generation returned a definition without Sections.", + "AI:ScoresheetGenerationSectionNoFields": "Scoresheet generation returned section {0} without Fields.", + "AI:ScoresheetGenerationPropertyInvalid": "Scoresheet generation returned a {0} without a valid {1}.", "AIPrompts": "AI Prompts", "AIPrompt": "AI Prompt", diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AILocalizationKeys.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AILocalizationKeys.cs index 3af858cf44..925ec5dc3e 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AILocalizationKeys.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AILocalizationKeys.cs @@ -18,4 +18,43 @@ public static class AILocalizationKeys public const string ScoringRequiresScoresheetFields = "AI:ScoringRequiresScoresheetFields"; public const string AttachmentNotFound = "AI:AttachmentNotFound"; public const string SelectAttachmentForSummaries = "AI:SelectAttachmentForSummaries"; + public const string FormWorksheetUnavailable = "AI:FormWorksheetUnavailable"; + public const string FormWorksheetDeleteProtected = "AI:FormWorksheetDeleteProtected"; + public const string FormWorksheetSelectionInvalid = "AI:FormWorksheetSelectionInvalid"; + public const string FormScoresheetDeleteProtected = "AI:FormScoresheetDeleteProtected"; + public const string FormScoresheetHasInstances = "AI:FormScoresheetHasInstances"; + public const string FormScoresheetUnavailable = "AI:FormScoresheetUnavailable"; + public const string FormScoresheetTitleRequired = "AI:FormScoresheetTitleRequired"; + public const string FormScoresheetSelectionRequired = "AI:FormScoresheetSelectionRequired"; + public const string FormScoresheetSelectionInvalid = "AI:FormScoresheetSelectionInvalid"; + public const string GenerateFormScoresheetPermissionRequired = "AI:GenerateFormScoresheetPermissionRequired"; + public const string SavedMappingApplyFailed = "AI:SavedMappingApplyFailed"; + public const string FormGenerationReviewActive = "AI:FormGenerationReviewActive"; + public const string MappingReviewPending = "AI:MappingReviewPending"; + public const string MappingReviewInactive = "AI:MappingReviewInactive"; + public const string MappingSelectionRequired = "AI:MappingSelectionRequired"; + public const string MappingSelectionInvalid = "AI:MappingSelectionInvalid"; + public const string MappingReviewTransitionInvalid = "AI:MappingReviewTransitionInvalid"; + public const string MappingReviewPendingSuggestions = "AI:MappingReviewPendingSuggestions"; + public const string WorksheetDraftsMustBePublished = "AI:WorksheetDraftsMustBePublished"; + public const string WorksheetTitleRequired = "AI:WorksheetTitleRequired"; + public const string WorksheetSelectionRequired = "AI:WorksheetSelectionRequired"; + public const string WorkflowGenerateInitialMapping = "AI:WorkflowGenerateInitialMapping"; + public const string WorkflowReviewInitialMapping = "AI:WorkflowReviewInitialMapping"; + public const string WorkflowGenerateWorksheets = "AI:WorkflowGenerateWorksheets"; + public const string WorkflowReviewWorksheets = "AI:WorkflowReviewWorksheets"; + public const string WorkflowPublishAssignWorksheets = "AI:WorkflowPublishAssignWorksheets"; + public const string WorkflowGenerateFinalMapping = "AI:WorkflowGenerateFinalMapping"; + public const string WorkflowReviewFinalMapping = "AI:WorkflowReviewFinalMapping"; + public const string WorkflowGenerateMapping = "AI:WorkflowGenerateMapping"; + public const string WorkflowCompleted = "AI:WorkflowCompleted"; + public const string ScoresheetGenerationRequiresFormVersion = "AI:ScoresheetGenerationRequiresFormVersion"; + public const string ScoresheetGenerationProtected = "AI:ScoresheetGenerationProtected"; + public const string ScoresheetGenerationInvalidOutput = "AI:ScoresheetGenerationInvalidOutput"; + public const string ScoresheetGenerationEmpty = "AI:ScoresheetGenerationEmpty"; + public const string ScoresheetGenerationUnusable = "AI:ScoresheetGenerationUnusable"; + public const string ScoresheetGenerationNoVersion = "AI:ScoresheetGenerationNoVersion"; + public const string ScoresheetGenerationNoSections = "AI:ScoresheetGenerationNoSections"; + public const string ScoresheetGenerationSectionNoFields = "AI:ScoresheetGenerationSectionNoFields"; + public const string ScoresheetGenerationPropertyInvalid = "AI:ScoresheetGenerationPropertyInvalid"; } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Settings/AISettings.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Settings/AISettings.cs index d589a02c80..e7fdf2e0da 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Settings/AISettings.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Settings/AISettings.cs @@ -4,4 +4,5 @@ public static class AISettings { public const string AutomaticGenerationEnabled = "GrantManager.AI.AutomaticGenerationEnabled"; public const string ManualGenerationEnabled = "GrantManager.AI.ManualGenerationEnabled"; + public const string ReportingEnabled = "GrantManager.AI.ReportingEnabled"; } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Menus/AIMenuContributor.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Menus/AIMenuContributor.cs index deba87c054..8da7b54593 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Menus/AIMenuContributor.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Menus/AIMenuContributor.cs @@ -2,11 +2,14 @@ using Microsoft.Extensions.DependencyInjection; using Unity.AI.Localization; using Unity.AI.Permissions; +using Unity.AI.Settings; using Unity.Modules.Shared.Navigation; using Unity.Modules.Shared.Specializations; using Unity.Modules.Shared.Permissions; using Volo.Abp.Features; +using Volo.Abp.Settings; using Volo.Abp.UI.Navigation; +using Volo.Abp.Users; namespace Unity.AI.Web.Menus; @@ -24,6 +27,7 @@ private static async Task ConfigureMainMenuAsync(MenuConfigurationContext contex { var l = context.GetLocalizer(); var featureChecker = context.ServiceProvider.GetRequiredService(); + var settingProvider = context.ServiceProvider.GetRequiredService(); var specializationChecker = context.ServiceProvider.GetRequiredService(); if (!await specializationChecker.IsEnabledAsync(SpecializationConsts.Onboarding)) @@ -37,7 +41,13 @@ await context.AddItemAsync(new ApplicationMenuItem( ).OnlyWhenInRole(IdentityConsts.ITOperationsRoleName)); } - if (await featureChecker.IsEnabledAsync("Unity.AIReporting")) + var currentUser = context.ServiceProvider.GetRequiredService(); + var isItAdmin = currentUser.IsInRole(IdentityConsts.ITAdminRoleName); + + var reportingEnabled = await featureChecker.IsEnabledAsync("Unity.AIReporting") + && await settingProvider.GetAsync(AISettings.ReportingEnabled, defaultValue: false); + + if (reportingEnabled || isItAdmin) { context.Menu.AddItem(new ApplicationMenuItem( name: AIMenus.Reporting, diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/AIReporting/Index.cshtml.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/AIReporting/Index.cshtml.cs index 6d8259c9f6..3815419584 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/AIReporting/Index.cshtml.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/AIReporting/Index.cshtml.cs @@ -2,16 +2,19 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.Extensions.Logging; +using Unity.AI.Settings; using Unity.GrantManager.Integrations; using Unity.Modules.Shared.Permissions; using Volo.Abp; using Volo.Abp.Features; +using Volo.Abp.Settings; namespace Unity.AI.Web.Pages.AIReporting { public class IndexModel( IEndpointManagementAppService endpointManagementAppService, IFeatureChecker featureChecker, + ISettingProvider settingProvider, IAuthorizationService authorizationService, ILogger logger) : PageModel { @@ -20,8 +23,11 @@ public class IndexModel( public async Task OnGetAsync() { - CanViewAiReporting = await featureChecker.IsEnabledAsync("Unity.AIReporting") - || (await authorizationService.AuthorizeAsync(User, IdentityConsts.ITAdminPolicyName)).Succeeded; + var isItAdmin = (await authorizationService.AuthorizeAsync(User, IdentityConsts.ITAdminPolicyName)).Succeeded; + var featureAndSettingEnabled = await featureChecker.IsEnabledAsync("Unity.AIReporting") + && await settingProvider.GetAsync(AISettings.ReportingEnabled, defaultValue: false); + + CanViewAiReporting = featureAndSettingEnabled || isItAdmin; if (!CanViewAiReporting) { diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Settings/LegalDisclaimerModal.cshtml b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Settings/LegalDisclaimerModal.cshtml new file mode 100644 index 0000000000..9c31c63ff6 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Settings/LegalDisclaimerModal.cshtml @@ -0,0 +1,22 @@ +@page +@using Unity.AI.Localization +@using Microsoft.Extensions.Localization +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@model Unity.AI.Web.Pages.Settings.LegalDisclaimerModalModel +@inject IStringLocalizer L +@{ + Layout = null; +} + +
    + + + + @L["LegalDisclaimer:Body"].Value + + + + + + +
    diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Settings/LegalDisclaimerModal.cshtml.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Settings/LegalDisclaimerModal.cshtml.cs new file mode 100644 index 0000000000..5c4b9dfa2e --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Settings/LegalDisclaimerModal.cshtml.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; + +namespace Unity.AI.Web.Pages.Settings; + +public class LegalDisclaimerModalModel : AbpPageModel +{ + public IActionResult OnPost() + { + return NoContent(); + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Unity.AI.Web.csproj b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Unity.AI.Web.csproj index 4b5d57bd20..dc792dbe61 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Unity.AI.Web.csproj +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Unity.AI.Web.csproj @@ -35,6 +35,7 @@ + diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Settings/AISettingGroup/AISettingViewComponent.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Settings/AISettingGroup/AISettingViewComponent.cs index 9400f61c24..efa1854d6f 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Settings/AISettingGroup/AISettingViewComponent.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Settings/AISettingGroup/AISettingViewComponent.cs @@ -21,7 +21,9 @@ public virtual async Task InvokeAsync() AutomaticGenerationEnabled = await settingProvider.GetAsync( AISettings.AutomaticGenerationEnabled, defaultValue: false), ManualGenerationEnabled = await settingProvider.GetAsync( - AISettings.ManualGenerationEnabled, defaultValue: false) + AISettings.ManualGenerationEnabled, defaultValue: false), + ReportingEnabled = await settingProvider.GetAsync( + AISettings.ReportingEnabled, defaultValue: false) }; return View("~/Views/Settings/AISettingGroup/Default.cshtml", model); @@ -31,6 +33,7 @@ public class AISettingScriptBundleContributor : BundleContributor { public override void ConfigureBundle(BundleConfigurationContext context) { + context.Files.Add("/Views/Shared/Scripts/AiLegalDisclaimer.js"); context.Files.Add("/Views/Settings/AISettingGroup/Default.js"); } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Settings/AISettingGroup/AISettingViewModel.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Settings/AISettingGroup/AISettingViewModel.cs index f7dfa675c4..e8fb44056a 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Settings/AISettingGroup/AISettingViewModel.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Settings/AISettingGroup/AISettingViewModel.cs @@ -4,4 +4,5 @@ public class AISettingViewModel { public bool AutomaticGenerationEnabled { get; set; } public bool ManualGenerationEnabled { get; set; } + public bool ReportingEnabled { get; set; } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Settings/AISettingGroup/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Settings/AISettingGroup/Default.cshtml index 0a83cf001b..0db5e022dd 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Settings/AISettingGroup/Default.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Settings/AISettingGroup/Default.cshtml @@ -1,8 +1,11 @@ +@using Microsoft.Extensions.Localization +@using Unity.AI.Localization @model Unity.AI.Web.Views.Settings.AISettingGroup.AISettingViewModel +@inject IStringLocalizer L
    -

    AI Configuration

    +

    @L["AISettingGroup:Title"]

    @@ -18,14 +21,14 @@ value="true" @(Model.AutomaticGenerationEnabled ? "checked" : "") />
    - When enabled, AI analysis runs automatically on new application intake (subject to form-level AI configuration). + @L["Setting:AI.AutomaticGenerationEnabled:Description"]
    -
    +
    +
    + @L["Setting:AI.ManualGenerationEnabled:Description"] +
    +
    + +
    + +
    - When enabled, users with appropriate permissions can manually generate or regenerate AI analysis. + @L["Setting:AI.ReportingEnabled:Description"]
    @@ -46,13 +64,13 @@
    diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Settings/AISettingGroup/Default.js b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Settings/AISettingGroup/Default.js index b956d5bfe6..9427a6fad3 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Settings/AISettingGroup/Default.js +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Settings/AISettingGroup/Default.js @@ -7,12 +7,35 @@ $(function () { let initialFormState = uiElements.settingForm.serialize(); + let lastSavedValues = { + automaticGenerationEnabled: $('#AutomaticGenerationEnabled').is(':checked'), + manualGenerationEnabled: $('#ManualGenerationEnabled').is(':checked'), + reportingEnabled: $('#ReportingEnabled').is(':checked') + }; + function checkFormChanges() { let isFormChanged = uiElements.settingForm.serialize() !== initialFormState; uiElements.saveButton.prop('disabled', !isFormChanged); uiElements.discardButton.prop('disabled', !isFormChanged); } + function saveSettings(automaticEnabled, manualEnabled, reportingEnabled) { + unity.aI.settings.aIConfiguration.updateTenantConfiguration({ + automaticGenerationEnabled: automaticEnabled, + manualGenerationEnabled: manualEnabled, + reportingEnabled: reportingEnabled + }).then(function () { + lastSavedValues = { + automaticGenerationEnabled: automaticEnabled, + manualGenerationEnabled: manualEnabled, + reportingEnabled: reportingEnabled + }; + $(document).trigger('AbpSettingSaved'); + initialFormState = uiElements.settingForm.serialize(); + checkFormChanges(); + }); + } + uiElements.settingForm.on('change', function () { checkFormChanges(); }); @@ -22,14 +45,13 @@ $(function () { const automaticEnabled = $('#AutomaticGenerationEnabled').is(':checked'); const manualEnabled = $('#ManualGenerationEnabled').is(':checked'); + const reportingEnabled = $('#ReportingEnabled').is(':checked'); + const turningOn = (automaticEnabled && !lastSavedValues.automaticGenerationEnabled) || + (manualEnabled && !lastSavedValues.manualGenerationEnabled) || + (reportingEnabled && !lastSavedValues.reportingEnabled); - unity.aI.settings.aIConfiguration.updateTenantConfiguration({ - automaticGenerationEnabled: automaticEnabled, - manualGenerationEnabled: manualEnabled - }).then(function () { - $(document).trigger('AbpSettingSaved'); - initialFormState = uiElements.settingForm.serialize(); - checkFormChanges(); + unity.aI.legalDisclaimer.confirmIfNeeded(turningOn, function () { + saveSettings(automaticEnabled, manualEnabled, reportingEnabled); }); }); diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Shared/Components/AIConfiguration/AIConfigurationViewComponent.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Shared/Components/AIConfiguration/AIConfigurationViewComponent.cs new file mode 100644 index 0000000000..87e6120012 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Shared/Components/AIConfiguration/AIConfigurationViewComponent.cs @@ -0,0 +1,46 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Unity.AI.Settings; +using Unity.GrantManager.ApplicationForms; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; +using Volo.Abp.Settings; + +namespace Unity.AI.Web.Views.Shared.Components.AIConfiguration; + +[Widget( + ScriptTypes = new[] { typeof(AIConfigurationScriptBundleContributor) }, + AutoInitialize = true)] +public class AIConfigurationViewComponent( + IApplicationFormAppService applicationFormAppService, + ISettingProvider settingProvider) : AbpViewComponent +{ + public async Task InvokeAsync(Guid formId) + { + var applicationForm = await applicationFormAppService.GetAsync(formId); + + var model = new AIConfigurationViewModel + { + ApplicationFormId = formId, + ShowAutomatic = await settingProvider.GetAsync(AISettings.AutomaticGenerationEnabled, defaultValue: false), + ShowManual = await settingProvider.GetAsync(AISettings.ManualGenerationEnabled, defaultValue: false), + AutomaticallyGenerateAIAnalysis = applicationForm.AutomaticallyGenerateAIAnalysis, + ManuallyInitiateAIAnalysis = applicationForm.ManuallyInitiateAIAnalysis + }; + + return View(model); + } + + public class AIConfigurationScriptBundleContributor : BundleContributor + { + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files + .Add("/Views/Shared/Scripts/AiLegalDisclaimer.js"); + context.Files + .Add("/Views/Shared/Components/AIConfiguration/Default.js"); + } + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Shared/Components/AIConfiguration/AIConfigurationViewModel.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Shared/Components/AIConfiguration/AIConfigurationViewModel.cs new file mode 100644 index 0000000000..03067f5d83 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Shared/Components/AIConfiguration/AIConfigurationViewModel.cs @@ -0,0 +1,16 @@ +using System; + +namespace Unity.AI.Web.Views.Shared.Components.AIConfiguration; + +public class AIConfigurationViewModel +{ + public Guid ApplicationFormId { get; set; } + + public bool ShowAutomatic { get; set; } + + public bool ShowManual { get; set; } + + public bool AutomaticallyGenerateAIAnalysis { get; set; } + + public bool ManuallyInitiateAIAnalysis { get; set; } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Shared/Components/AIConfiguration/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Shared/Components/AIConfiguration/Default.cshtml new file mode 100644 index 0000000000..82acc15baa --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Shared/Components/AIConfiguration/Default.cshtml @@ -0,0 +1,71 @@ +@using Unity.AI.Web.Views.Shared.Components.AIConfiguration +@model AIConfigurationViewModel + +@{ + Layout = null; +} + + + +
    +
    +
    +
    AI Configuration
    +
    +
    + + + +
    +
    +
    +
    + @if (Model.ShowAutomatic) + { +
    + + +
    + When enabled, all AI analysis features run automatically when a new application is submitted through this form. + Requires tenant-level Automatic AI Generation to also be enabled. +
    +
    + } + + @if (Model.ShowManual) + { +
    + + +
    + When enabled, users with appropriate permissions can manually generate or regenerate AI analysis for applications submitted through this form. + Includes: AI Attachment Summary, Application Analysis, AI Scoring. +
    +
    + } +
    +
    +
    diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Shared/Components/AIConfiguration/Default.js b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Shared/Components/AIConfiguration/Default.js new file mode 100644 index 0000000000..fd9fbde05b --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Shared/Components/AIConfiguration/Default.js @@ -0,0 +1,72 @@ +$(function () { + const UIElements = { + btnSave: $('#btn-save-ai-config'), + btnCancel: $('#btn-cancel-ai-config'), + btnBack: $('#btn-back-ai-config'), + formId: $('#aiConfigFormId'), + automaticCheckbox: $('#AutomaticallyGenerateAIAnalysis'), + manualCheckbox: $('#ManuallyInitiateAIAnalysis') + }; + + let lastSavedAIValues = { + automaticallyGenerateAIAnalysis: UIElements.automaticCheckbox.is(':checked'), + manuallyInitiateAIAnalysis: UIElements.manualCheckbox.is(':checked') + }; + + init(); + + function init() { + bindUIEvents(); + } + + function bindUIEvents() { + UIElements.btnSave.on('click', handleSave); + UIElements.btnCancel.on('click', handleCancel); + UIElements.btnBack.on('click', function () { + location.href = '/ApplicationForms'; + }); + } + + function handleSave() { + const automaticEnabled = UIElements.automaticCheckbox.is(':checked'); + const manualEnabled = UIElements.manualCheckbox.is(':checked'); + const turningOn = (automaticEnabled && !lastSavedAIValues.automaticallyGenerateAIAnalysis) || + (manualEnabled && !lastSavedAIValues.manuallyInitiateAIAnalysis); + + unity.aI.legalDisclaimer.confirmIfNeeded(turningOn, function () { + saveAiConfig(automaticEnabled, manualEnabled); + }); + } + + function saveAiConfig(automaticEnabled, manualEnabled) { + UIElements.btnSave.prop('disabled', true); + + abp.ajax({ + url: `/api/app/application-form/${UIElements.formId.val()}/ai-config`, + type: 'PATCH', + data: JSON.stringify({ + automaticallyGenerateAIAnalysis: automaticEnabled, + manuallyInitiateAIAnalysis: manualEnabled + }), + contentType: 'application/json' + }) + .done(function () { + lastSavedAIValues = { + automaticallyGenerateAIAnalysis: automaticEnabled, + manuallyInitiateAIAnalysis: manualEnabled + }; + abp.notify.success('AI configuration saved successfully.'); + }) + .fail(function () { + abp.notify.error('Failed to save AI configuration.'); + }) + .always(function () { + UIElements.btnSave.prop('disabled', false); + }); + } + + function handleCancel() { + UIElements.automaticCheckbox.prop('checked', lastSavedAIValues.automaticallyGenerateAIAnalysis); + UIElements.manualCheckbox.prop('checked', lastSavedAIValues.manuallyInitiateAIAnalysis); + } +}); diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Shared/Scripts/AiLegalDisclaimer.js b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Shared/Scripts/AiLegalDisclaimer.js new file mode 100644 index 0000000000..6bdbe283ce --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Shared/Scripts/AiLegalDisclaimer.js @@ -0,0 +1,23 @@ +(function () { + window.unity = window.unity || {}; + unity.aI = unity.aI || {}; + + unity.aI.legalDisclaimer = { + confirmIfNeeded: function (turningOn, onConfirmed) { + if (!turningOn) { + onConfirmed(); + return; + } + + const modal = new abp.ModalManager({ + viewUrl: abp.appPath + 'Settings/LegalDisclaimerModal' + }); + + modal.onResult(function () { + onConfirmed(); + }); + + modal.open(); + } + }; +})(); diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Reporting/Configuration/WorksheetComponentMetaDataDto.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Reporting/Configuration/WorksheetComponentMetaDataDto.cs index f8dfbd7206..274cca8159 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Reporting/Configuration/WorksheetComponentMetaDataDto.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Reporting/Configuration/WorksheetComponentMetaDataDto.cs @@ -46,5 +46,11 @@ public class WorksheetComponentMetaDataItemDto /// The path to reach the data, this is a datacentric version of the Path, and could be the same /// public string DataPath { get; set; } = string.Empty; + + /// + /// The name of the worksheet this component belongs to (includes the version suffix, e.g. "grant_application-v2"). + /// Used to group and default-sort reporting configuration fields by worksheet. + /// + public string WorksheetName { get; set; } = string.Empty; } } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/ScoresheetInstances/IScoresheetInstanceRepository.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/ScoresheetInstances/IScoresheetInstanceRepository.cs index 6e7a0f5e67..f0132c7eba 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/ScoresheetInstances/IScoresheetInstanceRepository.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/ScoresheetInstances/IScoresheetInstanceRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; using Volo.Abp.Domain.Repositories; @@ -8,5 +8,6 @@ public interface IScoresheetInstanceRepository : IBasicRepository GetByCorrelationAsync(Guid correlationId); Task GetWithAnswersAsync(Guid scoresheetInstanceId); + Task AnyByScoresheetAsync(Guid scoresheetId); } } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Services/WorksheetsManager.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Services/WorksheetsManager.cs index 9fb9ec6ee4..307d7801fa 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Services/WorksheetsManager.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Services/WorksheetsManager.cs @@ -82,9 +82,16 @@ public async Task UpdateWorksheetInstanceValueAsync(WorksheetInstance instance) foreach (var field in instance.Values) { var fieldDefinition = fieldDefinitions.Find(s => s.Id == field.CustomFieldId); - if (fieldDefinition != null) - instanceCurrentValue.Values.Add(new FieldInstanceValue(fieldDefinition.Key, - JsonNode.Parse(field.CurrentValue)?["value"]?.ToString() ?? string.Empty)); + if (fieldDefinition == null) continue; + + // A missing key and an explicit JSON null both parse to a null JsonNode here; + // omit the entry rather than coercing it to "" so downstream reporting views + // (which treat a missing key as NULL) don't have to parse "" as JSON. + var value = JsonNode.Parse(field.CurrentValue)?["value"]; + if (value != null) + { + instanceCurrentValue.Values.Add(new FieldInstanceValue(fieldDefinition.Key, value.ToString())); + } } instance.SetValue(JsonSerializer.Serialize(instanceCurrentValue)); diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Services/WorksheetsManagerExtensions.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Services/WorksheetsManagerExtensions.cs index 2733995c25..0181a34098 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Services/WorksheetsManagerExtensions.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Services/WorksheetsManagerExtensions.cs @@ -38,11 +38,14 @@ public static List GroupAndTransformFieldSets(this List s.FieldId) .ToList(); - if (groups.Count == valueFields.Count) return valueFields; // no grouping required - var list = new List(); - // collect the value fields if known and store and transform to required values + // Always route through JoinFieldValues, even for a single posted value. A checkbox-group + // field with exactly one box checked still needs JSON-array encoding (JoinFieldValues + // dispatches to ConvertCheckboxGroupMultiValues for that type); skipping it here previously + // left the raw single checkbox value (e.g. "true") stored instead of a JSON array, which + // the reporting views can't jsonb_array_elements() over. JoinFieldValues already returns the + // raw single value unchanged for every other field type. foreach (var group in groups) { var fieldId = group.First().FieldId; @@ -56,11 +59,7 @@ public static List GroupAndTransformFieldSets(this List 1 ? - group.Select(s => s.Value) - .ToList() - .JoinFieldValues(fieldId, worksheet, additionalIdentifiers) - : values[0] + Value = values.JoinFieldValues(fieldId, worksheet, additionalIdentifiers) }); } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/EntityFrameworkCore/Repositories/ScoresheetInstanceRepository.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/EntityFrameworkCore/Repositories/ScoresheetInstanceRepository.cs index 92145196d3..1a38264d08 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/EntityFrameworkCore/Repositories/ScoresheetInstanceRepository.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/EntityFrameworkCore/Repositories/ScoresheetInstanceRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; using Volo.Abp.Domain.Repositories.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore; @@ -17,6 +17,12 @@ public class ScoresheetInstanceRepository(IDbContextProvider dbCo .FirstOrDefaultAsync(s => s.CorrelationId == correlationId); } + public async Task AnyByScoresheetAsync(Guid scoresheetId) + { + var dbContext = await GetDbContextAsync(); + return await dbContext.ScoresheetInstances.AnyAsync(instance => instance.ScoresheetId == scoresheetId); + } + public async Task GetWithAnswersAsync(Guid scoresheetInstanceId) { var dbSet = await GetDbSetAsync(); 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 011b97353d..2bcbbde2d9 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 @@ -83,8 +83,9 @@ public static List ParseWorksheet(Worksheet w return [..worksheet.Sections .Where(section => section.Fields != null) - .SelectMany(section => section.Fields) - .SelectMany(field => ParseField(field, worksheet, formSchema, submissionHeaderMapping))]; + .OrderBy(section => section.Order) + .SelectMany(section => section.Fields.OrderBy(field => field.Order)) + .SelectMany(field => ParseField(field, worksheet, formSchema, submissionHeaderMapping))]; } /// @@ -134,7 +135,8 @@ private static List ParseDataGridField(Custom if (dynamicColumns != null && dynamicColumns.Count > 0) { - // We found dynamic columns in the CHEFS form schema; emit them first. + // We found dynamic columns in the CHEFS form schema; emit them first, in the + // order CHEFS returns them (which mirrors the form builder's column order). // Any statically-defined columns on the DataGrid are merged in below. foreach (var column in dynamicColumns) { @@ -150,9 +152,10 @@ private static List ParseDataGridField(Custom Type = MapDataGridColumnType(column.Type), Path = $"{worksheetName}->{sectionName}->{dataGridName}->{columnKey}", TypePath = $"worksheet->section->datagrid->{MapDataGridColumnType(column.Type)}", - DataPath = $"({worksheetName}){dataGridName}->{columnKey}" + DataPath = $"({worksheetName}){dataGridName}->{columnKey}", + WorksheetName = worksheet.Name }; - + components.Add(component); } } @@ -167,7 +170,8 @@ private static List ParseDataGridField(Custom Type = "Dynamic", Path = $"{worksheetName}->{sectionName}->{dataGridName}->dynamic_columns", TypePath = $"worksheet->section->datagrid->Dynamic", - DataPath = $"({worksheetName}){dataGridName}->dynamic_columns" + DataPath = $"({worksheetName}){dataGridName}->dynamic_columns", + WorksheetName = worksheet.Name }; components.Add(dynamicComponent); @@ -183,6 +187,10 @@ private static List ParseDataGridField(Custom components.Select(c => c.Key ?? string.Empty), StringComparer.OrdinalIgnoreCase); + // DataGridDefinition.Columns is deserialized straight from the JSON array, so list + // order == author-defined column order. Combined with the dynamic columns above + // (which are always emitted first), this becomes the tie-break row order when the + // field expands into multiple rows. foreach (var column in dataGridDefinition.Columns) { // Skip columns that were already emitted from the CHEFS extraction @@ -201,7 +209,8 @@ private static List ParseDataGridField(Custom Type = MapDataGridColumnType(column.Type), Path = $"{worksheetName}->{sectionName}->{dataGridName}->{column.Name}", TypePath = $"worksheet->section->datagrid->{MapDataGridColumnType(column.Type)}", - DataPath = $"({worksheetName}){dataGridName}->{column.Name}" + DataPath = $"({worksheetName}){dataGridName}->{column.Name}", + WorksheetName = worksheet.Name }; components.Add(component); @@ -275,7 +284,10 @@ private static List ParseCheckboxGroupField(C var worksheetName = SanitizeName(worksheet.Name); var checkboxGroupName = SanitizeName(field.Key); - // Create a component for each option in the CheckboxGroup + // Create a component for each option in the CheckboxGroup, in the order the options + // are defined (CheckboxGroupDefinition.Options is deserialized straight from the JSON + // array, so list order == author-defined display order). Downstream sorting is stable, + // so this becomes the tie-break row order when the field expands into multiple rows. foreach (var option in checkboxGroupDefinition.Options) { var optionKey = SanitizeName(option.Key); @@ -288,7 +300,8 @@ private static List ParseCheckboxGroupField(C Type = "Checkbox", // Each option is essentially a checkbox Path = $"{worksheetName}->{sectionName}->{checkboxGroupName}->{option.Key}", TypePath = $"worksheet->section->checkboxgroup->Checkbox", - DataPath = $"({worksheetName}){checkboxGroupName}->{option.Key}" + DataPath = $"({worksheetName}){checkboxGroupName}->{option.Key}", + WorksheetName = worksheet.Name }; components.Add(component); @@ -355,7 +368,8 @@ private static WorksheetComponentMetaDataItemDto CreateSimpleComponent(CustomFie Type = field.Type.ToString(), Path = $"{worksheetName}->{sectionName}->{fieldName}", TypePath = $"worksheet->section->{field.Type.ToString().ToLowerInvariant()}", - DataPath = $"({worksheetName}){fieldName}" + DataPath = $"({worksheetName}){fieldName}", + WorksheetName = worksheet.Name }; } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/ValueConverter.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/ValueConverter.cs index 21356fec6f..e1ffa4c602 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/ValueConverter.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/ValueConverter.cs @@ -22,7 +22,7 @@ public static string Convert(string currentValue, CustomFieldType type, string? CustomFieldType.Email => JsonSerializer.Serialize(new EmailValue(currentValue)), CustomFieldType.Radio => JsonSerializer.Serialize(new RadioValue(currentValue)), CustomFieldType.Checkbox => JsonSerializer.Serialize(new CheckboxValue(ValueConverterHelpers.ConvertCheckbox(currentValue))), - CustomFieldType.CheckboxGroup => JsonSerializer.Serialize(new CheckboxGroupValue(currentValue)), + CustomFieldType.CheckboxGroup => JsonSerializer.Serialize(new CheckboxGroupValue(string.IsNullOrWhiteSpace(currentValue) ? "[]" : currentValue)), CustomFieldType.SelectList => JsonSerializer.Serialize(new SelectListValue(currentValue)), CustomFieldType.BCAddress => JsonSerializer.Serialize(new BCAddressValue(currentValue)), CustomFieldType.TextArea => JsonSerializer.Serialize(new TextAreaValue(currentValue)), 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 c6a31f58d4..de01939db3 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 @@ -544,5 +544,220 @@ public async Task ParseDataGridField_DynamicWithFormSchemaKeyMismatch_ShouldFall result.ShouldNotBeNull(); result.ShouldContain(c => c.Key == "dynamic_columns"); } + + [Fact] + public async Task ParseDataGridField_WithMultipleStaticColumns_ShouldPreserveDefinitionOrder() + { + // 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(); + + // Columns defined in a deliberately non-alphabetical order + var field = new CustomField(Guid.NewGuid(), "testDataGrid", "TestWorksheet", "Test DataGrid", + CustomFieldType.DataGrid, + @"{""dynamic"": false, ""columns"": [ + {""name"": ""zebra"", ""type"": ""Text""}, + {""name"": ""apple"", ""type"": ""Text""}, + {""name"": ""mango"", ""type"": ""Text""} + ], ""summaryOption"": ""None""}"); + section.AddField(field); + await uow.SaveChangesAsync(); + + worksheet = await _worksheetRepository.GetAsync(worksheet.Id); + + // Act + var result = WorksheetFieldSchemaParser.ParseField(field, worksheet); + + // Assert — rows must appear in the order the columns are defined on the grid, not sorted + result.ShouldNotBeNull(); + result.Select(c => c.Key).ShouldBe(["zebra", "apple", "mango"]); + } + + [Fact] + public async Task ParseDataGridField_WithDynamicAndStaticColumns_ShouldEmitDynamicColumnsBeforeStaticColumns() + { + // 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"": ""staticB"", ""type"": ""Text""}, + {""name"": ""staticA"", ""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"": ""dynamicB"", ""label"": ""Dynamic B"", ""type"": ""textfield"" }, + { ""key"": ""dynamicA"", ""label"": ""Dynamic A"", ""type"": ""textfield"" } + ] + } + ] + }"; + + // Act + var result = WorksheetFieldSchemaParser.ParseField(field, worksheet, formSchema, submissionHeaderMapping); + + // Assert — CHEFS-extracted dynamic columns keep their own order and come first, followed by + // the statically-defined columns (not covered by the dynamic extraction) in their own order + result.ShouldNotBeNull(); + result.Select(c => c.Key).ShouldBe(["dynamicB", "dynamicA", "staticB", "staticA"]); + } + + [Fact] + public async Task ParseCheckboxGroupField_WithMultipleOptions_ShouldPreserveDefinitionOrder() + { + // 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(); + + // Options defined in a deliberately non-alphabetical order + var field = new CustomField(Guid.NewGuid(), "testCheckboxGroup", "TestWorksheet", "Test Checkbox Group", + CustomFieldType.CheckboxGroup, + @"{""options"": [ + {""key"": ""rural"", ""value"": false, ""label"": ""Rural""}, + {""key"": ""urban"", ""value"": false, ""label"": ""Urban""}, + {""key"": ""remote"", ""value"": false, ""label"": ""Remote""} + ]}"); + section.AddField(field); + await uow.SaveChangesAsync(); + + worksheet = await _worksheetRepository.GetAsync(worksheet.Id); + + // Act + var result = WorksheetFieldSchemaParser.ParseField(field, worksheet); + + // Assert — rows must appear in the order the options are defined on the field, not sorted + result.ShouldNotBeNull(); + result.Select(c => c.Key).ShouldBe(["rural", "urban", "remote"]); + } + + [Fact] + public async Task ParseCheckboxGroupField_WithNoOptions_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(), "testCheckboxGroup", "TestWorksheet", "Test Checkbox Group", + CustomFieldType.CheckboxGroup, + @"{""options"": []}"); + section.AddField(field); + await uow.SaveChangesAsync(); + + worksheet = await _worksheetRepository.GetAsync(worksheet.Id); + + // Act + var result = WorksheetFieldSchemaParser.ParseField(field, worksheet); + + // Assert + result.ShouldNotBeNull(); + result.Count.ShouldBe(1); + + var component = result.First(); + component.Id.ShouldBe(field.Id.ToString()); + component.Key.ShouldBe("testCheckboxGroup"); + component.Type.ShouldBe("CheckboxGroup"); + } + + [Fact] + public async Task ParseWorksheet_WithFieldsOutOfInsertionOrder_ShouldEmitFieldsByOrderProperty() + { + // 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(); + + // Added to the section in A, B, C order, but Order is deliberately assigned out of sequence + // (B=1, C=2, A=3) so the test fails if ParseWorksheet ever falls back to collection/insertion + // order instead of sorting by the field's Order property. + section.Fields.Add(new CustomField(Guid.NewGuid(), "fieldA", "TestWorksheet", "Field A", CustomFieldType.Text, (string?)null).SetOrder(3)); + section.Fields.Add(new CustomField(Guid.NewGuid(), "fieldB", "TestWorksheet", "Field B", CustomFieldType.Text, (string?)null).SetOrder(1)); + section.Fields.Add(new CustomField(Guid.NewGuid(), "fieldC", "TestWorksheet", "Field C", CustomFieldType.Text, (string?)null).SetOrder(2)); + await uow.SaveChangesAsync(); + + worksheet = await _worksheetRepository.GetAsync(worksheet.Id); + + // Act + var result = WorksheetFieldSchemaParser.ParseWorksheet(worksheet); + + // Assert — fields must appear in Order order (B, C, A), not the order they were added + result.ShouldNotBeNull(); + result.Select(c => c.Key).ShouldBe(["fieldB", "fieldC", "fieldA"]); + } + + [Fact] + public async Task ParseWorksheet_WithSectionsOutOfInsertionOrder_ShouldEmitSectionsByOrderProperty() + { + // Arrange + using var uow = _unitOfWorkManager.Begin(); + + var worksheet = new Worksheet(Guid.NewGuid(), "TestWorksheet", "Test Worksheet"); + + var sectionA = new WorksheetSection(Guid.NewGuid(), "SectionA"); + var sectionB = new WorksheetSection(Guid.NewGuid(), "SectionB"); + + // Added to the worksheet in A, B order, but Order is deliberately reversed (A=2, B=1) so the + // test fails if ParseWorksheet ever falls back to collection/insertion order instead of + // sorting sections by their Order property. + worksheet.Sections.Add(sectionA.SetOrder(2)); + worksheet.Sections.Add(sectionB.SetOrder(1)); + + sectionA.Fields.Add(new CustomField(Guid.NewGuid(), "fieldInA", "TestWorksheet", "Field In A", CustomFieldType.Text, (string?)null).SetOrder(1)); + sectionB.Fields.Add(new CustomField(Guid.NewGuid(), "fieldInB", "TestWorksheet", "Field In B", CustomFieldType.Text, (string?)null).SetOrder(1)); + + await _worksheetRepository.InsertAsync(worksheet, true); + await uow.SaveChangesAsync(); + + worksheet = await _worksheetRepository.GetAsync(worksheet.Id); + + // Act + var result = WorksheetFieldSchemaParser.ParseWorksheet(worksheet); + + // Assert — Section B's field (Order 1) must come before Section A's field (Order 2), even + // though Section A was added to the worksheet first + result.ShouldNotBeNull(); + result.Select(c => c.Key).ShouldBe(["fieldInB", "fieldInA"]); + } } } \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationService.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationService.cs index f350797571..f5f7b3224d 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationService.cs @@ -110,9 +110,17 @@ public async Task SendCommentNotification(EmailCommentDto i string commentLink = input.CommentType switch { Comments.CommentType.ApplicationComment or Comments.CommentType.AssessmentComment => - QueryHelpers.AddQueryString($"{baseUrl}/GrantApplications/Details", "ApplicationId", input.OwnerId), + QueryHelpers.AddQueryString($"{baseUrl}/GrantApplications/Details", new Dictionary + { + ["ApplicationId"] = input.OwnerId, + ["TenantId"] = CurrentTenant.Id?.ToString() + }), Comments.CommentType.ApplicantComment => - QueryHelpers.AddQueryString($"{baseUrl}/GrantApplicants/Details", "ApplicantId", input.OwnerId), + QueryHelpers.AddQueryString($"{baseUrl}/GrantApplicants/Details", new Dictionary + { + ["ApplicantId"] = input.OwnerId, + ["TenantId"] = CurrentTenant.Id?.ToString() + }), _ => throw new InvalidOperationException("Invalid comment type.") }; diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Emails/EmailAttachmentService.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Emails/EmailAttachmentService.cs index a68c3109cc..0eadcb2dfb 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Emails/EmailAttachmentService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Emails/EmailAttachmentService.cs @@ -178,6 +178,63 @@ public async Task> GetAttachmentsAsync(Guid emailLogId) return await _emailLogAttachmentRepository.GetByEmailLogIdAsync(emailLogId); } + public async Task CopyTemplateAttachmentsAsync(Guid templateId, Guid emailLogId, Guid? tenantId) + { + var templateAttachments = await _emailLogAttachmentRepository.GetByTemplateIdAsync(templateId); + var existingAttachments = await _emailLogAttachmentRepository.GetByEmailLogIdAsync(emailLogId); + // Dedup by (FileName, FileSize, ContentType) rather than S3ObjectKey: each copy gets its own + // S3 object (see below), so a re-run of this method for the same emailLogId/templateId would + // never see a matching key even though the attachment was already copied. + var alreadyCopied = existingAttachments + .Where(a => a.OriginTemplateId == templateId) + .Select(a => (a.FileName, a.FileSize, a.ContentType)) + .ToHashSet(); + + var bucket = _configuration[S3BucketConfigKey]; + var copiedAttachmentCount = 0; + foreach (var templateAttachment in templateAttachments) + { + var identity = (templateAttachment.FileName, templateAttachment.FileSize, templateAttachment.ContentType); + if (!alreadyCopied.Add(identity)) + { + continue; + } + + // Physically duplicate the S3 object under a new key instead of pointing at the + // template attachment's own key. EmailLogAttachmentAppService.DeleteAsync deletes the + // underlying S3 object whenever a template attachment (TemplateId.HasValue) is removed; + // sharing the key would silently break the attachment on every scheduled email that had + // already copied it. + var copiedS3Key = BuildUserAttachmentS3Key( + tenantId, emailLogId, Guid.NewGuid(), templateAttachment.FileName ?? templateAttachment.DisplayName ?? "attachment"); + await _amazonS3Client.CopyObjectAsync(new CopyObjectRequest + { + SourceBucket = bucket, + SourceKey = templateAttachment.S3ObjectKey, + DestinationBucket = bucket, + DestinationKey = copiedS3Key + }); + + await _emailLogAttachmentRepository.InsertAsync(new EmailLogAttachment + { + EmailLogId = emailLogId, + TemplateId = null, + OriginTemplateId = templateId, + S3ObjectKey = copiedS3Key, + FileName = templateAttachment.FileName, + DisplayName = templateAttachment.DisplayName, + ContentType = templateAttachment.ContentType, + FileSize = templateAttachment.FileSize, + Time = DateTime.UtcNow, + UserId = Guid.Empty, + TenantId = tenantId + }); + copiedAttachmentCount++; + } + + return copiedAttachmentCount; + } + public async Task GetTotalFileSizeAsync(Guid? emailLogId, Guid? templateId) { if(emailLogId != null) diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Events/EmailNotificationHandler.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Events/EmailNotificationHandler.cs index 580deb3c88..05be5543b9 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Events/EmailNotificationHandler.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Events/EmailNotificationHandler.cs @@ -229,6 +229,26 @@ private async Task InitializeEmail(EmailInitParams p, string status) emailLog.ScheduledNotificationId = eventData.ScheduledNotificationId.Value; await emailLogsRepository.UpdateAsync(emailLog, autoSave: true); } + + if (eventData.ScheduledNotificationId.HasValue && eventData.TemplateId != Guid.Empty) + { + try + { + var copiedAttachmentCount = await emailAttachmentService.CopyTemplateAttachmentsAsync( + eventData.TemplateId, emailLog.Id, emailLog.TenantId); + _logger.LogInformation( + "Copied {AttachmentCount} template attachments for scheduled notification {ScheduledNotificationId}.", + copiedAttachmentCount, eventData.ScheduledNotificationId.Value); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Failed to copy template attachments for scheduled notification {ScheduledNotificationId}. Email will be sent WITHOUT attachments.", + eventData.ScheduledNotificationId.Value); + // DO NOT THROW - matches InitializeEmailAndUploadAttachments: an attachment + // failure should not block the email from being created/sent. + } + } await StampClassificationAsync(emailLog); return emailLog; diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.css b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.css index 14ef7a32e3..a08da5ac7f 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.css +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.css @@ -1,4 +1,50 @@ -body { +.notification-tooltip { + background: transparent; + border: 0; + cursor: help; + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 20px; + height: 20px; + margin-left: 0.35rem; + padding: 0; + position: relative; + z-index: 2; + margin-top: -7px; +} + +.notification-tooltip-icon { + border: 2px solid rgb(46, 93, 215); + border-radius: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + background-color: rgb(255, 255, 255); + color: rgb(46, 93, 215); + font-family: Georgia, serif; + font-size: 0.8rem; + font-weight: 700; + font-style: italic; + line-height: 22px; + transform: translateY(4px); +} + +.notification-tooltip:focus-visible { + outline: 2px solid #2e5dd7; + outline-offset: 2px; +} + +.notification-tooltip-popover .tooltip-inner { + font-size: 0.75rem; + line-height: 1.35; + max-width: 280px; + padding: 0.35rem 0.5rem; +} + +body { overflow-y:auto!important; } @@ -48,7 +94,7 @@ white-space: nowrap; transition: all 0.15s ease-in-out; border-radius: 4px; - font-size: 0.875rem; + font-size: 1rem; } .btn-add-user:hover:not(:disabled) { @@ -77,9 +123,23 @@ display: flex; flex-direction: row; align-items: flex-start; - min-height: 400px; + height: calc(100vh - 230px); + min-height: 0; gap: 0; - /* No fixed height — lets the DataTable footer and page footer remain visible */ +} + +#nav-tabContent.unt-tab-content { + display: flex; + flex-direction: column; + height: calc(100vh - 230px); + min-height: 0; +} + +#nav-tabContent.unt-tab-content > .tab-pane.show.active { + display: flex; + flex: 1 1 auto; + flex-direction: column; + min-height: 0; } /* ── Left pane ────────────────────────────────────────────────────────────── */ @@ -101,7 +161,7 @@ border-right: 1px solid #ced4da; position: sticky; top: 10px; - height: calc(100vh - 190px); + height: calc(100vh - 230px); z-index: 10; transition: background 0.15s; } @@ -129,6 +189,10 @@ justify-content: center; } +.split-container.split-active .left-pane { + flex: 0 0 48%; +} + /* ── Right pane ───────────────────────────────────────────────────────────── */ .right-pane { flex-shrink: 0; @@ -142,7 +206,20 @@ position: sticky; top: 10px; /* Explicit height (not max-height) so h-100 on the inner card resolves correctly */ - height: calc(100vh - 190px); + height: calc(100vh - 230px); +} + +.right-pane > .card { + min-height: 0; +} + +.right-pane .editor-body { + overflow-x: hidden; + overflow-y: auto; +} + +#email-attachments-section { + flex-shrink: 0; } @@ -162,6 +239,15 @@ padding: 2px; } +#email-attachments-section .d-flex.justify-content-end.mt-2.mb-1 { + padding-bottom: 50px !important; + display: flex !important; +} + +.right-pane .card-body.editor-body.overflow-auto { + padding-bottom: 20px !important; +} + /* ── Resizable textareas ──────────────────────────────────────────────────── */ .textarea { resize: vertical; @@ -183,10 +269,16 @@ span.tooltip-wrapper { } .template-field { - flex: 0 0 135px !important; - min-width: 140px !important; + align-items: center; + box-sizing: border-box; + display: flex; + flex: 0 0 180px !important; + font-size: 0.975rem; + gap: 0.15rem; + min-width: 180px !important; white-space: nowrap; - margin: 0.5rem; + margin: 0.5rem 0.25rem 0.5rem 0.5rem; + width: 180px; } /* ── Drag ghost (prevent text selection while dragging) ───────────────────── */ 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 54e1360715..80346cc479 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 @@ -1,4 +1,13 @@ - +function initializeTooltips() { + if (typeof bootstrap === 'undefined') return; + + document.querySelectorAll('#nav-template [data-bs-toggle="tooltip"]').forEach((tooltipElement) => { + bootstrap.Tooltip.getOrCreateInstance(tooltipElement, { + customClass: 'notification-tooltip-popover' + }); + }); +} + $(function () { const UiElements = { saveButton: $("#saveTemplateBtn"), @@ -20,9 +29,11 @@ $(function () { let emailAttachmentsTable = null; let templatesDataTable = null; let originalFormValues = {}; + let attachmentChangesPending = false; function init() { $('#email-attachments-section').hide(); + initializeTooltips(); initializeTemplateDataTables(); initializeDivider(); initializeTabPersistence(); @@ -88,6 +99,7 @@ $(function () { if (templatesDataTable) { setTimeout(() => { templatesDataTable.columns.adjust().draw(); + resizeTemplatesScrollBody(); }, 0); } }); @@ -106,6 +118,21 @@ $(function () { } } + function resizeTemplatesScrollBody() { + if (!templatesDataTable) return; + const scrollResize = templatesDataTable.settings?.()[0]?._scrollResize; + if (scrollResize && typeof scrollResize._size === 'function') { + scrollResize._size(); + return; + } + + try { + templatesDataTable.columns.adjust(); + } catch (e) { + console.debug('Templates table column adjust failed:', e.message); + } + } + // ── Select2 Initialization ───────────────────────────────────────────── function initializeRecipientSelect() { $('#templateRecipientSelect').select2({ @@ -270,6 +297,7 @@ $(function () { UiElements.deleteButton.show(); $('#email-attachments-section').show(); + attachmentChangesPending = false; initEmailAttachmentsTable(data.id); // Recalculate table columns after initialization @@ -302,6 +330,7 @@ $(function () { $('#templateRecipientSelect').empty().val([]).trigger('change'); UiElements.deleteButton.hide(); $('#email-attachments-section').hide(); + attachmentChangesPending = false; // Don't load attachments for new templates - they have no ID yet } @@ -398,6 +427,7 @@ $(function () { }; const isNewTemplate = !templateId || templateId.trim() === ''; + const templateChangesPending = hasTemplateChanges(templateData) || attachmentChangesPending; // Check template name uniqueness before saving checkTemplateNameUnique(templateName.trim(), templateId, function (isUnique) { @@ -405,10 +435,56 @@ $(function () { markFieldError('templateName', 'Template name must be unique.'); return; } - performSave(isNewTemplate, templateId, templateData, templateName, sendFrom, subject, bodyHTML); + confirmTemplateAttachmentImpact(templateId, isNewTemplate, templateChangesPending) + .then(function (confirmed) { + if (confirmed) { + performSave(isNewTemplate, templateId, templateData, templateName, sendFrom, subject, bodyHTML); + } + }); }); }); + function hasTemplateChanges(templateData) { + const original = originalFormValues; + const fields = ['name', 'description', 'sendFrom', 'subject', 'bodyText', 'bodyHTML', 'recipientCategory', 'recipientIdentifier']; + + return fields.some(field => String(templateData[field] ?? '') !== String(original[field] ?? '')); + } + + function confirmTemplateAttachmentImpact(templateId, isNewTemplate, templateChangesPending) { + if (isNewTemplate || !templateChangesPending) { + return Promise.resolve(true); + } + + return $.ajax({ + url: `/api/form-notifications/template-notification-plans/${encodeURIComponent(templateId)}`, + type: 'GET', + dataType: 'json' + }).then(function (response) { + const planNames = response.notificationPlanNames || []; + if (planNames.length === 0) { + return true; + } + + return Swal.fire({ + icon: 'warning', + title: 'Template changes', + html: '

    Warning: This template is currently associated with ' + planNames.length + ' notification plan' + (planNames.length === 1 ? '' : 's') + '. Any changes made to this template may impact these notification plan' + (planNames.length === 1 ? '' : 's') + '.

    ', + showCancelButton: true, + confirmButtonText: 'OK', + cancelButtonText: 'Cancel', + customClass: { + confirmButton: 'btn btn-primary', + cancelButton: 'btn btn-secondary' + } + }).then(result => result.isConfirmed); + }).catch(function (e) { + console.warn('Failed to check template notification plans:', e); + abp.notify.error('Unable to verify whether this template is used by a notification plan. The template was not saved.'); + return false; + }); + } + function performSave(isNewTemplate, templateId, templateData, templateName, sendFrom, subject, bodyHTML) { if (isNewTemplate) { // Create new template @@ -446,6 +522,7 @@ $(function () { unity.notifications.templates.template .updateTemplate(templateId, templateData) .then(function () { + attachmentChangesPending = false; abp.notify.success('Template updated successfully.'); // Update original values after successful save originalFormValues = { @@ -455,7 +532,9 @@ $(function () { sendFrom: sendFrom, subject: subject, bodyText: '', - bodyHTML: bodyHTML + bodyHTML: bodyHTML, + recipientCategory: templateData.recipientCategory || '', + recipientIdentifier: templateData.recipientIdentifier || '' }; PubSub.publish('reload_templates_table_no_close'); }) @@ -943,6 +1022,7 @@ $(function () { $('#attachment-upload-progress').show(); }, success: function () { + attachmentChangesPending = true; PubSub.publish('reload_email_attachments_table'); }, error: function (xhr) { @@ -1021,6 +1101,10 @@ $(function () { reloadEmailAttachmentsTable(); }); + PubSub.subscribe('template_attachment_changed', () => { + attachmentChangesPending = true; + }); + function reloadEmailAttachmentsTable() { if (emailAttachmentsTable) { emailAttachmentsTable.ajax.reload(); @@ -1136,14 +1220,24 @@ function generateEmailAttachmentButtonContent(attachmentId) { * @param {string} attachmentId - Attachment ID to delete */ function deleteEmailAttachment(attachmentId) { - abp.message.confirm( - 'Are you sure you want to delete this attachment?', - 'Delete Attachment', - function (confirmed) { - if (confirmed) { + const templateId = $('#templateId').val(); + const planImpactCheck = isConfigurationManagementTemplateEditor() + ? checkScheduledPlanImpactForAttachmentDelete(templateId) + : Promise.resolve(true); + + planImpactCheck.then(function (confirmed) { + if (!confirmed) return; + + abp.message.confirm( + 'Are you sure you want to delete this attachment?', + 'Delete Attachment', + function (deleteConfirmed) { + if (!deleteConfirmed) return; + unity.notifications.emails.emailLogAttachment .delete(attachmentId) .then(function () { + PubSub.publish('template_attachment_changed'); abp.notify.success('Attachment deleted successfully.'); PubSub.publish('reload_email_attachments_table'); }) @@ -1152,8 +1246,45 @@ function deleteEmailAttachment(attachmentId) { abp.notify.error('Failed to delete attachment.'); }); } - } - ); + ); + }); +} + +function isConfigurationManagementTemplateEditor() { + return window.location.pathname.toLowerCase() === '/configurationmanagement' && + $('#nav-template').length > 0 && + $('#TemplatesTable').length > 0 && + $('#templateId').length > 0; +} + +function checkScheduledPlanImpactForAttachmentDelete(templateId) { + if (!templateId) return Promise.resolve(true); + + return $.ajax({ + url: `/api/form-notifications/template-notification-plans/${encodeURIComponent(templateId)}`, + type: 'GET', + dataType: 'json' + }).then(function (response) { + const planNames = response.notificationPlanNames || []; + if (planNames.length === 0) return true; + + return Swal.fire({ + icon: 'warning', + title: 'Scheduled notification impact', + html: '

    Warning: This template is currently associated with ' + planNames.length + ' notification plan' + (planNames.length === 1 ? '' : 's') + '. Any changes made to this template may impact these notification plan' + (planNames.length === 1 ? '' : 's') + '.

    ', + showCancelButton: true, + confirmButtonText: 'OK', + cancelButtonText: 'Cancel', + customClass: { + confirmButton: 'btn btn-primary', + cancelButton: 'btn btn-secondary' + } + }).then(result => result.isConfirmed); + }).catch(function (e) { + console.warn('Failed to check template notification plans:', e); + abp.notify.error('Unable to verify whether this template is used by a scheduled notification plan. The attachment was not deleted.'); + return false; + }); } /** diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/_TemplateDetails.cshtml b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/_TemplateDetails.cshtml index e09b8ab96b..377a55840a 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/_TemplateDetails.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/_TemplateDetails.cshtml @@ -29,7 +29,13 @@
    - +
    diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Events/PaymentStatusChangedEvent.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Events/PaymentStatusChangedEvent.cs new file mode 100644 index 0000000000..b1e5f0de6b --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Events/PaymentStatusChangedEvent.cs @@ -0,0 +1,16 @@ +using System; +using Unity.Payments.Enums; + +namespace Unity.Payments.Events +{ + public class PaymentStatusChangedEvent + { + public Guid PaymentRequestId { get; set; } + + public Guid ApplicationId { get; set; } + + public PaymentRequestStatus Status { get; set; } + + public Guid? TenantId { get; set; } + } +} \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/SupplierService.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/SupplierService.cs index d9e7a06529..d70f466e54 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/SupplierService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/SupplierService.cs @@ -27,7 +27,7 @@ public class SupplierService : ApplicationService, ISupplierService { protected new ILogger Logger => LazyServiceProvider.LazyGetService(provider => LoggerFactory?.CreateLogger(GetType().FullName!) ?? NullLogger.Instance); private const string CFS_SUPPLIER = "cfs/supplier"; - private readonly Task casBaseApiTask; + private readonly Lazy> casBaseApiTask; private readonly ILocalEventBus localEventBus; private readonly IResilientHttpRequest resilientHttpRequest; private readonly ICasTokenService iTokenService; @@ -42,8 +42,8 @@ public SupplierService(ILocalEventBus localEventBus, this.resilientHttpRequest = resilientHttpRequest; this.iTokenService = iTokenService; - // Initialize the base API URL once during construction - casBaseApiTask = InitializeBaseApiAsync(endpointManagementAppService); + // Defer the database-backed lookup until the service is actually used. + casBaseApiTask = new(() => InitializeBaseApiAsync(endpointManagementAppService)); } private static async Task InitializeBaseApiAsync(IEndpointManagementAppService endpointManagementAppService) @@ -218,7 +218,7 @@ public async Task GetCasSupplierInformationAsync(string? supplierNumber { if (!string.IsNullOrEmpty(supplierNumber)) { - var casBaseApi = await casBaseApiTask; + var casBaseApi = await casBaseApiTask.Value; var resource = $"{casBaseApi}/{CFS_SUPPLIER}/{supplierNumber}"; return await GetCasSupplierInformationByResourceAsync(resource); } @@ -232,7 +232,7 @@ public async Task GetCasSupplierInformationByBn9Async(string? bn9) { if (!string.IsNullOrEmpty(bn9)) { - var casBaseApi = await casBaseApiTask; + var casBaseApi = await casBaseApiTask.Value; var resource = $"{casBaseApi}/{CFS_SUPPLIER}/{bn9}/businessnumber"; return await GetCasSupplierInformationByResourceAsync(resource); } 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 6b3670d4cb..0e8b49e9a4 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 @@ -10,12 +10,14 @@ using Unity.Payments.Domain.Services; using Unity.Payments.Domain.Shared; using Unity.Payments.Enums; +using Unity.Payments.Events; 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.EventBus.Local; using Volo.Abp.Features; using Volo.Abp.Users; @@ -31,7 +33,8 @@ public class PaymentRequestAppService( FsbPaymentNotifier fsbPaymentNotifier, IPaymentRequestQueryManager paymentRequestQueryManager, IPaymentRequestConfigurationManager paymentRequestConfigurationManager, - Lazy applicationLinksService) : PaymentsAppService, IPaymentRequestAppService + Lazy applicationLinksService, + ILocalEventBus localEventBus) : PaymentsAppService, IPaymentRequestAppService { public async Task GetDefaultAccountCodingId() @@ -60,6 +63,7 @@ public virtual async Task> CreateAsync(List> CreateHistoricalAsync(List GetNextBatchInfoAsync() { return await paymentRequestConfigurationManager.GetNextBatchInfoAsync(); @@ -212,6 +228,17 @@ public virtual async Task> UpdateStatusAsync(List CancelAsync(Guid paymentRequestId) .WithData("Status", payment.Status.ToString()); var result = await paymentsManager.CancelPaymentAsync(paymentRequestId); + + await localEventBus.PublishAsync(new PaymentStatusChangedEvent + { + PaymentRequestId = result.Id, + ApplicationId = result.CorrelationId, + Status = result.Status, + TenantId = CurrentTenant.Id + }); + return MapToPaymentRequestDto(result); } } diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application.Contracts/Configuration/FieldPathTypeDto.cs b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application.Contracts/Configuration/FieldPathTypeDto.cs index e50335bf1c..ddd73d90d1 100644 --- a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application.Contracts/Configuration/FieldPathTypeDto.cs +++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application.Contracts/Configuration/FieldPathTypeDto.cs @@ -47,5 +47,18 @@ public class FieldPathTypeDto /// A comma-separated value (e.g., "v1, v2") means the field appears in those versions but not all. /// public string? VersionLabel { get; set; } = null; + + /// + /// The name of the worksheet this field belongs to (includes the version suffix, e.g. "grant_application-v2"). + /// Populated only by the worksheet-based providers; null for scoresheet/formversion providers. + /// + public string? WorksheetName { get; set; } = null; + + /// + /// The field's 1-based position in the provider's calculated overall field order (e.g., for worksheet + /// providers: Worksheet Name A-Z, then section order, then field layout order, then checkbox group + /// option / data grid column order). Assigned by the provider after all ordering is applied. + /// + public int SourceOrder { get; set; } } } diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application.Contracts/Configuration/ReportColumnsMapDto.cs b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application.Contracts/Configuration/ReportColumnsMapDto.cs index 6583e3cf69..93d44c2600 100644 --- a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application.Contracts/Configuration/ReportColumnsMapDto.cs +++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application.Contracts/Configuration/ReportColumnsMapDto.cs @@ -140,5 +140,17 @@ public class MapRowDto /// a non-null value (e.g., "v1", "v2") means the column is specific to that form version. /// public string? VersionLabel { get; set; } = null; + + /// + /// Gets or sets the name of the worksheet this field belongs to (includes the version suffix, e.g. "grant_application-v2"). + /// Populated only for worksheet-based providers; null for scoresheet/formversion providers. + /// + public string? WorksheetName { get; set; } = null; + + /// + /// Gets or sets the field's 1-based position in the provider's calculated overall field order. + /// Drives the report-config-table's default sort (Source Order 1 -> n); not shown by default. + /// + public int SourceOrder { get; set; } } } diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/ConsolidatedFormVersionFieldsProvider.cs b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/ConsolidatedFormVersionFieldsProvider.cs index 72bad667f8..c9d2c578cc 100644 --- a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/ConsolidatedFormVersionFieldsProvider.cs +++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/ConsolidatedFormVersionFieldsProvider.cs @@ -54,6 +54,11 @@ public async Task GetFieldsMetadataAsync(Guid correlationId } var mergedFields = MergeFields(versionsWithFields); + + // Stamp each field with its 1-based position in the merged order as SourceOrder, which drives + // the report-config-table's default sort. + FieldOrderingUtils.AssignSourceOrder(mergedFields); + var mapMetadata = new MapMetadataDto { Info = metadataInfo }; return new FieldPathMetaMapDto { Fields = [.. mergedFields], Metadata = mapMetadata }; diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/ConsolidatedWorksheetFieldsProvider.cs b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/ConsolidatedWorksheetFieldsProvider.cs index bb8d055188..80cd9df5c8 100644 --- a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/ConsolidatedWorksheetFieldsProvider.cs +++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/ConsolidatedWorksheetFieldsProvider.cs @@ -61,16 +61,33 @@ public async Task GetFieldsMetadataAsync(Guid correlationId metadataInfo[$"ws_{version.Id}_{link.WorksheetId}"] = $"{worksheetTitle} ({worksheetName})"; } + // Default sort order within a version: Worksheet Name (A-Z), then section order, then field + // layout order within each section. Each worksheet's components are already emitted in + // section/field order by WorksheetFieldSchemaParser.ParseWorksheet, so a stable sort on + // WorksheetName alone preserves that ordering as the tie-breaker. + var orderedComponents = allComponents + .OrderBy(f => f.WorksheetName, StringComparer.OrdinalIgnoreCase) + .ToArray(); + // Stamp within-version duplicate DataPaths with (DK1), (DK2), … before merging, // so MergeFields() treats them as distinct paths and preserves both rather than // silently dropping the second occurrence. - var versionComponents = allComponents.ToArray(); + var versionComponents = orderedComponents; WorksheetFieldsUtils.UniqueifyDataPaths(versionComponents); versionsWithFields.Add((version.Id, versionLabel, versionComponents)); metadataInfo[$"formversion_{version.Id}"] = versionLabel; } - var mergedFields = MergeFields(versionsWithFields); + // Re-apply the Worksheet Name ordering across the merged result (stable sort preserves the + // section/field tie-break order already established per-version above). + var mergedFields = MergeFields(versionsWithFields) + .OrderBy(f => f.WorksheetName, StringComparer.OrdinalIgnoreCase) + .ToList(); + + // Stamp each field with its 1-based position in the final order as SourceOrder, which drives + // the report-config-table's default sort. + FieldOrderingUtils.AssignSourceOrder(mergedFields); + var mapMetadata = new MapMetadataDto { Info = metadataInfo }; return new FieldPathMetaMapDto { Fields = [.. mergedFields], Metadata = mapMetadata }; @@ -122,7 +139,8 @@ public async Task GetFieldsMetadataAsync(Guid correlationId Key = item.Key, Label = item.Label, TypePath = item.TypePath, - DataPath = item.DataPath + DataPath = item.DataPath, + WorksheetName = item.WorksheetName }; } @@ -197,7 +215,8 @@ private static List MergeFields( Type = field.Type ?? string.Empty, TypePath = field.TypePath, DataPath = field.DataPath, - VersionLabel = string.Join(", ", exactGroup.Select(e => e.VersionLabel)) + VersionLabel = string.Join(", ", exactGroup.Select(e => e.VersionLabel)), + WorksheetName = field.WorksheetName }); } else if (versionsWithFields.Count > 1 && versionsHavingThisExact.Count == versionsWithFields.Count) @@ -212,7 +231,8 @@ private static List MergeFields( Type = field.Type ?? string.Empty, TypePath = field.TypePath, DataPath = field.DataPath, - VersionLabel = null + VersionLabel = null, + WorksheetName = field.WorksheetName }); } else @@ -227,7 +247,8 @@ private static List MergeFields( Type = field.Type ?? string.Empty, TypePath = field.TypePath, DataPath = field.DataPath, - VersionLabel = string.Join(", ", exactGroup.Select(e => e.VersionLabel)) + VersionLabel = string.Join(", ", exactGroup.Select(e => e.VersionLabel)), + WorksheetName = field.WorksheetName }); } } diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/FieldOrderingUtils.cs b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/FieldOrderingUtils.cs new file mode 100644 index 0000000000..cb58dac675 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/FieldOrderingUtils.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Unity.Reporting.Domain.Configuration; + +namespace Unity.Reporting.Configuration.FieldsProviders +{ + /// + /// Shared utility for stamping fields with their calculated overall Source Order. + /// + internal static class FieldOrderingUtils + { + /// + /// Stamps each field with its 1-based position in the list as . + /// Must be called after a provider has applied its final field ordering (e.g., worksheet name, + /// section, field layout, checkbox option, or data grid column order), since this simply reflects + /// list position. Mutates the fields in place. + /// + internal static void AssignSourceOrder(IReadOnlyList fields) + { + for (var i = 0; i < fields.Count; i++) + { + fields[i].SourceOrder = i + 1; + } + } + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/FormVersionFieldsProvider.cs b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/FormVersionFieldsProvider.cs index 8b2e3ebd6d..58a01cda76 100644 --- a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/FormVersionFieldsProvider.cs +++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/FormVersionFieldsProvider.cs @@ -37,6 +37,10 @@ public async Task GetFieldsMetadataAsync(Guid correlationId .Where(x => x != null) .Select(x => x!)]; + // Stamp each field with its 1-based position in the source order as SourceOrder, which drives + // the report-config-table's default sort. + FieldOrderingUtils.AssignSourceOrder(convertedMetadata); + return new FieldPathMetaMapDto() { Fields = convertedMetadata }; } diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/ScoresheetFieldsProvider.cs b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/ScoresheetFieldsProvider.cs index 7372d461f9..a8af6b276b 100644 --- a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/ScoresheetFieldsProvider.cs +++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/ScoresheetFieldsProvider.cs @@ -52,7 +52,11 @@ public async Task GetFieldsMetadataAsync(Guid correlationId .Select(ConvertToFieldPathType) .Where(x => x != null) .Select(x => x!)]; - + + // Stamp each field with its 1-based position in the source order as SourceOrder, which drives + // the report-config-table's default sort. + FieldOrderingUtils.AssignSourceOrder(convertedMetadata); + // Create metadata information about the scoresheet used var mapMetadata = new MapMetadataDto(); var scoresheetKey = $"scoresheet_{scoresheetId.Value}"; diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/WorksheetFieldsProvider.cs b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/WorksheetFieldsProvider.cs index 4e984e35a7..7c72d16014 100644 --- a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/WorksheetFieldsProvider.cs +++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/WorksheetFieldsProvider.cs @@ -46,15 +46,24 @@ public async Task GetFieldsMetadataAsync(Guid correlationId mapMetadata.Info[worksheetKey] = $"{worksheetTitle} ({worksheetName}) - ID: {link.WorksheetId}"; } + // Default sort order: Worksheet Name (A-Z), then section order, then field layout order within + // each section. Each worksheet's components are already emitted in section/field order by + // WorksheetFieldSchemaParser.ParseWorksheet, so a stable sort on WorksheetName alone preserves + // that ordering as the tie-breaker. FieldPathTypeDto[] convertedMetadata = [.. worksheetMetadata.SelectMany(s => s.Components) .Select(ConvertToFieldPathType) .Where(x => x != null) - .Select(x => x!)]; + .Select(x => x!) + .OrderBy(x => x.WorksheetName, StringComparer.OrdinalIgnoreCase)]; // Mirror submission behaviour: stamp within-version duplicate DataPaths with (DK1), (DK2), … // so that each row is distinguishable and the Duplicate Keys warning is triggered. WorksheetFieldsUtils.UniqueifyDataPaths(convertedMetadata); + // Stamp each field with its 1-based position in the final order as SourceOrder, which drives + // the report-config-table's default sort. + FieldOrderingUtils.AssignSourceOrder(convertedMetadata); + return new FieldPathMetaMapDto() { Fields = convertedMetadata, Metadata = mapMetadata }; } @@ -76,7 +85,8 @@ public async Task GetFieldsMetadataAsync(Guid correlationId Key = metadataItem.Key, Label = metadataItem.Label, TypePath = metadataItem.TypePath, - DataPath = metadataItem.DataPath + DataPath = metadataItem.DataPath, + WorksheetName = metadataItem.WorksheetName }; } diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/ReportMappingService.cs b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/ReportMappingService.cs index e5a8f62729..0b4675b16e 100644 --- a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/ReportMappingService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/ReportMappingService.cs @@ -136,6 +136,48 @@ public async Task GetByCorrelationAsync(Guid correlationId, ? throw new EntityNotFoundException(typeof(ReportColumnsMap), $"CorrelationId: {correlationId}, CorrelationProvider: {correlationProvider}") : ObjectMapper.Map(reportColumnsMap); + // Mappings saved before the Worksheet Name / Source Order columns existed have neither value in + // their persisted JSON rows, so those columns would otherwise render blank/zero for a saved view + // even though a brand-new (unsaved) load of the same source shows them correctly. Backfill both + // from the live provider metadata by matching on Path, without requiring the user to re-save + // the configuration. SourceOrder of 0 is otherwise never assigned (providers always number from 1), + // so it doubles as the "missing" sentinel here. + var needsWorksheetNameBackfill = (providerKey == Providers.Worksheet || providerKey == Providers.WorksheetConsolidated) && + map.Mapping.Rows.Any(row => string.IsNullOrEmpty(row.WorksheetName)); + var needsSourceOrderBackfill = map.Mapping.Rows.Any(row => row.SourceOrder == 0); + + if (needsWorksheetNameBackfill || needsSourceOrderBackfill) + { + var liveFields = await provider.GetFieldsMetadataAsync(correlationId); + var worksheetNameByPath = new Dictionary(StringComparer.OrdinalIgnoreCase); + var sourceOrderByPath = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var field in liveFields.Fields) + { + if (!string.IsNullOrEmpty(field.WorksheetName) && !worksheetNameByPath.ContainsKey(field.Path)) + { + worksheetNameByPath[field.Path] = field.WorksheetName; + } + if (!string.IsNullOrEmpty(field.Path) && !sourceOrderByPath.ContainsKey(field.Path)) + { + sourceOrderByPath[field.Path] = field.SourceOrder; + } + } + + foreach (var row in map.Mapping.Rows) + { + if (needsWorksheetNameBackfill && string.IsNullOrEmpty(row.WorksheetName) && + worksheetNameByPath.TryGetValue(row.Path, out var worksheetName)) + { + row.WorksheetName = worksheetName; + } + + if (needsSourceOrderBackfill && row.SourceOrder == 0 && + sourceOrderByPath.TryGetValue(row.Path, out var sourceOrder)) + { + row.SourceOrder = sourceOrder; + } + } + } // If we have an existing reportColumnsMap - let check if changes have occured that could effect the mapping map.DetectedChanges = await provider.DetectChangesAsync(correlationId, reportColumnsMap); diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/ReportMappingUtils.cs b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/ReportMappingUtils.cs index 956a7aff06..e8716bae4b 100644 --- a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/ReportMappingUtils.cs +++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/ReportMappingUtils.cs @@ -344,7 +344,9 @@ internal static ReportColumnsMap CreateNewMap(UpsertReportColumnsMapDto upsertRe DataPath = field.DataPath, TypePath = field.TypePath, Id = field.Id, - VersionLabel = field.VersionLabel + VersionLabel = field.VersionLabel, + WorksheetName = field.WorksheetName, + SourceOrder = field.SourceOrder }; }).ToList(); @@ -453,7 +455,9 @@ internal static ReportColumnsMap UpdateExistingMap(UpsertReportColumnsMapDto upd DataPath = field.DataPath, TypePath = field.TypePath, Id = field.Id, - VersionLabel = field.VersionLabel + VersionLabel = field.VersionLabel, + WorksheetName = field.WorksheetName, + SourceOrder = field.SourceOrder }; }).ToList(); diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Domain/Configuration/ReportColumnsMap.cs b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Domain/Configuration/ReportColumnsMap.cs index 7bd059bb13..ed06422e82 100644 --- a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Domain/Configuration/ReportColumnsMap.cs +++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Domain/Configuration/ReportColumnsMap.cs @@ -141,6 +141,18 @@ public class MapRow /// a comma-separated value (e.g., "v1, v2") means the column appears in those versions but not all. /// public string? VersionLabel { get; set; } = null; + + /// + /// Gets or sets the name of the worksheet this field belongs to (includes the version suffix, e.g. "grant_application-v2"). + /// Populated only for worksheet-based providers; null for scoresheet/formversion providers. + /// + public string? WorksheetName { get; set; } = null; + + /// + /// Gets or sets the field's 1-based position in the provider's calculated overall field order. + /// Drives the report-config-table's default sort (Source Order 1 -> n); not shown by default. + /// + public int SourceOrder { get; set; } } /// diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfiguration/Default.js b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfiguration/Default.js index 0b74fb8a41..e0507a28a2 100644 --- a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfiguration/Default.js +++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfiguration/Default.js @@ -559,7 +559,9 @@ $(function () { dataPath: row.dataPath, columnName: row.columnName || '', typePath: row.typePath, - versionLabel: row.versionLabel || null + versionLabel: row.versionLabel || null, + worksheetName: row.worksheetName || null, + sourceOrder: row.sourceOrder || 0 })); return { @@ -593,7 +595,9 @@ $(function () { dataPath: field.dataPath, columnName: getDefaultColumnNameSource(field), typePath: field.typePath, - versionLabel: field.versionLabel || null + versionLabel: field.versionLabel || null, + worksheetName: field.worksheetName || null, + sourceOrder: field.sourceOrder || 0 })); return { @@ -656,12 +660,28 @@ $(function () { const providerConfig = getCurrentProviderConfig(); const listColumns = [ + { + title: 'Worksheet Name', + data: 'worksheetName', + name: 'worksheetName', + className: 'data-table-header', + width: '220px', + index: 0, + orderable: true, + visible: currentProvider === 'worksheet' || currentProvider === 'worksheet_consolidated', + render: function (data, type, _) { + if (type === 'display') { + return data || ''; + } + return data || ''; + } + }, { title: providerConfig.columns.label, data: 'label', name: 'label', className: 'data-table-header', - index: 0, + index: 1, orderable: true }, { @@ -669,7 +689,7 @@ $(function () { data: 'key', name: 'key', className: 'data-table-header', - index: 1, + index: 2, orderable: true }, { @@ -677,7 +697,7 @@ $(function () { data: 'type', name: 'type', className: 'data-table-header', - index: 2, + index: 3, orderable: true }, { @@ -685,7 +705,7 @@ $(function () { data: 'dataPath', // We use the dataPath explicitly here name: 'path', className: 'data-table-header', - index: 3, + index: 4, orderable: true, render: function (data, type, row) { if (type === 'display') { @@ -699,7 +719,7 @@ $(function () { data: 'columnName', name: 'columnName', className: 'data-table-header', - index: 4, + index: 5, orderable: false, render: function (data, type, row) { if (type === 'display') { @@ -714,7 +734,7 @@ $(function () { data: 'typePath', name: 'typePath', className: 'data-table-header', - index: 5, + index: 6, orderable: false, render: function (data, type, _) { if (type === 'display') { @@ -729,7 +749,7 @@ $(function () { name: 'versionLabel', className: 'data-table-header', width: '90px', - index: 6, + index: 7, orderable: true, visible: currentProvider === 'worksheet_consolidated' || currentProvider === 'formversion_consolidated', render: function (data, type, _) { @@ -738,6 +758,24 @@ $(function () { } return data || ''; } + }, + { + title: 'Source Order', + data: 'sourceOrder', + name: 'sourceOrder', + className: 'data-table-header', + width: '90px', + index: 8, + orderable: true, + // Hidden by default (not part of defaultVisibleColumns below) — it exists so users can + // opt in via the column picker and re-sort back to the calculated natural order after + // sorting by another column. + render: function (data, type, _) { + if (type === 'display') { + return data || ''; + } + return data; + } } ]; @@ -790,12 +828,26 @@ $(function () { if (currentProvider === 'worksheet_consolidated' || currentProvider === 'formversion_consolidated') { defaultVisibleColumns.push('versionLabel'); } + if (currentProvider === 'worksheet' || currentProvider === 'worksheet_consolidated') { + defaultVisibleColumns.push('worksheetName'); + } + + // Default sort: for worksheet-based providers, sort by the hidden Source Order column (1 -> n), + // the server's calculated overall field order (Worksheet Name A-Z, then section order, then field + // layout order, then checkbox group option / data grid column order). Other providers default-sort + // by Label, unchanged. Resolved here to the column's actual numeric index — DataTables' initial + // `order` option only accepts column indices, not names, so passing a name straight through would + // silently fail to apply any sort. + const defaultSortColumnName = (currentProvider === 'worksheet' || currentProvider === 'worksheet_consolidated') + ? 'sourceOrder' + : 'label'; + const defaultSortColumn = listColumns.find(c => c.name === defaultSortColumnName)?.index ?? 1; dataTable = initializeDataTable({ dt, defaultVisibleColumns, listColumns, - defaultSortColumn: 0, + defaultSortColumn, dataEndpoint: dataEndpoint, data: {}, responseCallback: responseCallback, @@ -811,6 +863,16 @@ $(function () { fixedHeaders: true }); + // table-utils' getVisibleColumnIndexes() always forces the column at position 0 to be + // visible (so the table never renders with an empty leftmost column), regardless of + // defaultVisibleColumns. Worksheet Name now occupies position 0 so it can be the leftmost + // column, which means that force-visible rule would otherwise show it even for providers + // where it should stay hidden. Re-apply the correct provider-based visibility here so it's + // not stuck "on" before any saved preference (below) has a chance to override it. + _suppressColvisSave = true; + dataTable.column('worksheetName:name').visible(currentProvider === 'worksheet' || currentProvider === 'worksheet_consolidated'); + _suppressColvisSave = false; + // Persist column visibility per provider dataTable.on('column-visibility.dt', function () { if (!_suppressColvisSave) saveColvisState(currentProvider); 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 93e869148d..870d90080c 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 @@ -367,11 +367,6 @@ thead input { background-color: var(--bc-colors-blue-background, #38598A); } -.abp-widget-wrapper { - top: 0; - z-index: 2; -} - td.dt-editable { cursor: pointer; } @@ -914,4 +909,11 @@ div.dt-container div.dt-search { .filter-search-action-bar_search-wrapper { flex: 1; +} + +table.dataTable th.dt-type-numeric div.dt-column-header, +table.dataTable th.dt-type-date div.dt-column-header, +table.dataTable td.dt-type-numeric div.dt-column-header, +table.dataTable td.dt-type-date div.dt-column-header { + flex-direction: row !important; } \ No newline at end of file diff --git a/applications/Unity.GrantManager/ocUnityDbConnect.ps1 b/applications/Unity.GrantManager/ocUnityDbConnect.ps1 index a575ec6c4b..68097f1f84 100644 --- a/applications/Unity.GrantManager/ocUnityDbConnect.ps1 +++ b/applications/Unity.GrantManager/ocUnityDbConnect.ps1 @@ -1,9 +1,30 @@ +# Prompt user for environment selection +$validEnvironments = @("dev", "test", "prod") +do { + Write-Host "Enter environment (dev, test, prod)" -ForegroundColor Green + $environment = Read-Host +} while (-not ($validEnvironments -contains $environment)) + +# Prompt user for cluster selection +$validPlatforms = @("gold", "silver") +do { + Write-Host "Enter OpenShift cluster (gold, silver)" -ForegroundColor Green + $platform = Read-Host +} while (-not ($validPlatforms -contains $platform.ToLowerInvariant())) + +$platform = $platform.ToLowerInvariant() +$server = if ($platform -eq "gold") { + "https://api.gold.devops.gov.bc.ca:6443" +} else { + "https://api.silver.devops.gov.bc.ca:6443" +} + # Prompt the user to optionally login to OpenShift Write-Host "Do you want to log in to OpenShift now? (y/n)" -ForegroundColor Green $loginResponse = Read-Host if ($loginResponse -match '^(y|yes)$') { try { - oc login --web --server=https://api.silver.devops.gov.bc.ca:6443 + oc login --web --server=$server } catch { Write-Host "Login failed. Please check your connection and credentials." -ForegroundColor Red @@ -11,13 +32,6 @@ if ($loginResponse -match '^(y|yes)$') { } } -# Prompt user for environment selection -$validEnvironments = @("dev", "test", "prod") -do { - Write-Host "Enter environment (dev, test, prod)" -ForegroundColor Green - $environment = Read-Host -} while (-not ($validEnvironments -contains $environment)) - # Define cluster mappings $clusterMappings = @{ @@ -37,8 +51,9 @@ if ($environment -eq 'prod') { } -# Configuration parameters (dynamically updated based on environment) -$NameSpace = "d18498-$environment" # OpenShift project namespace +# Configuration parameters (dynamically updated based on environment and cluster) +$namespacePrefix = if ($platform -eq "gold") { "ce395f" } else { "d18498" } +$NameSpace = "$namespacePrefix-$environment" # OpenShift project namespace $ClusterName = "$cluster-crunchy-postgres" $LocalPort = 5436 $RemotePort = 5432 diff --git a/applications/Unity.GrantManager/scripts/Get-SonarIssues.ps1 b/applications/Unity.GrantManager/scripts/Get-SonarIssues.ps1 index 736c658310..994a1ece4a 100644 --- a/applications/Unity.GrantManager/scripts/Get-SonarIssues.ps1 +++ b/applications/Unity.GrantManager/scripts/Get-SonarIssues.ps1 @@ -3,12 +3,16 @@ Pulls open SonarCloud issues for a branch via the public API and writes them to a Markdown report. .DESCRIPTION - Calls the SonarCloud /api/issues/search endpoint (paginating past its 500-per-page limit), + Calls the SonarQube /api/issues/search endpoint (paginating past its 500-per-page limit), then groups the results by severity into a Markdown file - handy for pasting into Copilot/Claude - or attaching to a PR instead of screen-scraping the SonarCloud UI. + or attaching to a PR instead of screen-scraping the SonarQube UI. + +.PARAMETER ServerUrl + SonarQube server URL. Default: https://sonarqube.econ.gov.bc.ca/sonar. Set this to + https://sonarcloud.io when querying SonarCloud. .PARAMETER ProjectKey - SonarCloud project (component) key. Default: bcgov_Unity. + SonarQube project (component) key. Default: UnityScanKey. .PARAMETER Branch Branch name to query. If omitted, you'll be prompted to pick the current git branch, one of @@ -28,6 +32,10 @@ .PARAMETER Types Optional filter, e.g. -Types BUG,VULNERABILITY. Valid values: BUG, VULNERABILITY, CODE_SMELL. +.PARAMETER FileType + Source file type to include. Valid values: js, css, cshtml, cs, all. If omitted, you'll be + prompted to choose one. + .PARAMETER IncludeResolved Include resolved/closed issues too. By default only unresolved (open) issues are fetched. @@ -41,7 +49,10 @@ this for unattended/CI runs where nothing should launch afterwards. .EXAMPLE - .\Get-SonarIssues.ps1 -Branch main -Token $env:SONAR_TOKEN + .\Get-SonarIssues.ps1 -Branch dev -Token $env:SONAR_TOKEN + +.EXAMPLE + .\Get-SonarIssues.ps1 -ServerUrl https://sonarcloud.io -ProjectKey bcgov_Unity -Branch main -Token $env:SONAR_TOKEN .EXAMPLE .\Get-SonarIssues.ps1 -ProjectKey bcgov_Unity -Branch feature/AB-12345 -Severities BLOCKER,CRITICAL -OutputPath .\sonar-report.md @@ -59,7 +70,9 @@ .\Get-SonarIssues.ps1 -Branch main -FixLevel Quick #> param( - [string]$ProjectKey = "bcgov_Unity", + [string]$ServerUrl = "https://sonarqube.econ.gov.bc.ca/sonar", + + [string]$ProjectKey = "UnityScanKey", [string]$Branch = "", @@ -73,6 +86,9 @@ param( [ValidateSet("BUG", "VULNERABILITY", "CODE_SMELL")] [string[]]$Types = @(), + [ValidateSet("js", "css", "cshtml", "cs", "all")] + [string]$FileType = "", + [switch]$IncludeResolved, [ValidateSet("None", "Quick", "QuickModerate", "All")] @@ -83,7 +99,7 @@ param( $ErrorActionPreference = "Stop" -$ApiBase = "https://sonarcloud.io/api/issues/search" +$ApiBase = "$($ServerUrl.TrimEnd('/'))/api/issues/search" $PageSize = 500 $SeverityOrder = @("BLOCKER", "CRITICAL", "MAJOR", "MINOR", "INFO") $WellKnownBranches = @("dev", "test", "main") @@ -151,6 +167,32 @@ function Read-BranchSelection { } } +function Read-FileTypeSelection { + $menu = [ordered]@{ + "1" = @{ Label = "JavaScript (.js)"; Extension = "js" } + "2" = @{ Label = "Stylesheet (.css)"; Extension = "css" } + "3" = @{ Label = "Razor (.cshtml)"; Extension = "cshtml" } + "4" = @{ Label = "C# (.cs)"; Extension = "cs" } + "5" = @{ Label = "All source file types"; Extension = "all" } + } + + Write-Host "" + Write-Host "Which source file type should be included?" -ForegroundColor Cyan + foreach ($key in $menu.Keys) { + Write-Host " [$key] $($menu[$key].Label)" + } + + while ($true) { + $choice = Read-Host "Enter choice (default: 5 - All source file types)" + if ([string]::IsNullOrWhiteSpace($choice)) { $choice = "5" } + + if ($menu.Contains($choice)) { + return $menu[$choice].Extension + } + Write-Host "Invalid choice '$choice' - try again." -ForegroundColor Yellow + } +} + # Maps a -FixLevel value (or the equivalent interactive menu choice) to the FixComplexity tiers # it covers. "None" intentionally maps to an empty array - nothing gets fixed. $FixLevelTierMap = [ordered]@{ @@ -190,6 +232,10 @@ if (-not $Branch) { $Branch = Read-BranchSelection -CurrentBranch (Get-CurrentGitBranch) } +if (-not $FileType) { + $FileType = Read-FileTypeSelection +} + if (-not $OutputPath) { $branchSlug = if ($Branch) { ($Branch -replace '[\\/:*?"<>|]', '-') } else { "default-branch" } $OutputPath = "sonar-issues-$branchSlug.md" @@ -287,6 +333,14 @@ function Get-IssueFilePath { return ($Issue.component -replace "^$([regex]::Escape($ProjectKey)):", "") } +if ($FileType -ne "all") { + $extension = ".$FileType" + $allIssues = [System.Collections.Generic.List[object]]@( + $allIssues | Where-Object { (Get-IssueFilePath $_).EndsWith($extension, [System.StringComparison]::OrdinalIgnoreCase) } + ) + Write-Host "Issues after file type filter '$FileType': $($allIssues.Count)" +} + # --- Fix-complexity classification --- # Goal: separate mechanical, low-risk fixes (rename, swap one API for another, drop an unused var) # from ones that ripple across every call site or need an actual design change, so the report can @@ -366,6 +420,7 @@ $md = New-Object System.Text.StringBuilder [void]$md.AppendLine("") [void]$md.AppendLine("- **Project:** $ProjectKey") [void]$md.AppendLine("- **Branch:** $(if ($Branch) { $Branch } else { '(default)' })") +[void]$md.AppendLine("- **Source file type:** $FileType") [void]$md.AppendLine("- **Generated:** $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')") [void]$md.AppendLine("- **Total open issues:** $($allIssues.Count)") [void]$md.AppendLine("") @@ -404,7 +459,7 @@ if ($quickWins.Count -gt 0) { $file = Get-IssueFilePath $issue $line = if ($issue.textRange -and $issue.textRange.startLine) { $issue.textRange.startLine } else { "-" } $message = ($issue.message -replace '\|', '\|') - $link = "https://sonarcloud.io/project/issues?id=$ProjectKey&issues=$($issue.key)&open=$($issue.key)$(if ($Branch) { "&branch=$Branch" })" + $link = "$($ServerUrl.TrimEnd('/'))/project/issues?id=$ProjectKey&issues=$($issue.key)&open=$($issue.key)$(if ($Branch) { "&branch=$Branch" })" [void]$md.AppendLine("| $($issue.severity) | ``$file`` | $line | [$($issue.rule)]($link) | $message |") } [void]$md.AppendLine("") @@ -425,7 +480,7 @@ foreach ($sev in $SeverityOrder) { $line = if ($issue.textRange -and $issue.textRange.startLine) { $issue.textRange.startLine } else { "-" } $message = ($issue.message -replace '\|', '\|') $effort = if ($issue.effort) { $issue.effort } else { "-" } - $link = "https://sonarcloud.io/project/issues?id=$ProjectKey&issues=$($issue.key)&open=$($issue.key)$(if ($Branch) { "&branch=$Branch" })" + $link = "$($ServerUrl.TrimEnd('/'))/project/issues?id=$ProjectKey&issues=$($issue.key)&open=$($issue.key)$(if ($Branch) { "&branch=$Branch" })" [void]$md.AppendLine("| ``$file`` | $line | $($issue.type) | $($issue.FixComplexity.Badge) | $effort | [$($issue.rule)]($link) | $message |") } [void]$md.AppendLine("") diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ExternalLinkDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ExternalLinkDto.cs new file mode 100644 index 0000000000..330d5121ed --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ExternalLinkDto.cs @@ -0,0 +1,12 @@ +namespace Unity.GrantManager.ApplicantProfile.ProfileData; + +/// +/// Represents a link to be used within the Applicant Portal, including the URL, title, and description. +/// +public class ExternalLinkDto +{ + public required string Uri { get; set; } + public string Title { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public int Order { get; set; } = -1; +} 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 index 38b7855fdd..9bce8b13f2 100644 --- 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 @@ -1,15 +1,19 @@ using System; +using System.Collections.Generic; -namespace Unity.GrantManager.ApplicantProfile.ProfileData +namespace Unity.GrantManager.ApplicantProfile.ProfileData; + +public class SubmissionInfoItemDto { - 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 Type { get; set; } = string.Empty; - public string Status { get; set; } = string.Empty; - } + 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 Type { get; set; } = string.Empty; + public string Status { get; set; } = string.Empty; + public ExternalLinkDto? RenewalLink { get; set; } + public List RelatedLinks { get; set; } = []; + public bool EligibleForRenewal { get; set; } + public string ApplicantMessage { get; set; } = string.Empty; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ApplicantListDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ApplicantListDto.cs index b8ab20408b..7b9eb63212 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ApplicantListDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ApplicantListDto.cs @@ -28,4 +28,5 @@ public class ApplicantListDto : AuditedEntityDto public int? FiscalDay { get; set; } public DateTime? StartedOperatingDate { get; set; } public bool IsDuplicated { get; set; } + public DateOnly? FiscalYearEnd { get; set; } } \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/ApplicationFormDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/ApplicationFormDto.cs index 02d5f484e5..07f08b9859 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/ApplicationFormDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/ApplicationFormDto.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Unity.GrantManager.GrantApplications; using Volo.Abp.Application.Dtos; @@ -32,5 +33,7 @@ public class ApplicationFormDto : EntityDto public string? Prefix { get; set; } public SuffixConfigType? SuffixType { get; set; } public int? DefaultPaymentGroup { get; set; } + public List ExternalLinks { get; set; } = []; + public string ApplicantMessage { get; set; } = string.Empty; } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/ExternalLinkConfigDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/ExternalLinkConfigDto.cs new file mode 100644 index 0000000000..0eb4abd51c --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/ExternalLinkConfigDto.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Unity.GrantManager.ApplicantProfile; + +namespace Unity.GrantManager.ApplicationForms; + +public class ExternalLinkConfigDto : IValidatableObject +{ + [Required] + [MaxLength(2048)] + public string Uri { get; set; } = string.Empty; + + [MaxLength(255)] + public string Title { get; set; } = string.Empty; + + [MaxLength(512)] + public string Description { get; set; } = string.Empty; + + public bool Published { get; set; } + + public ExternalLinkType ExternalLinkType { get; set; } = ExternalLinkType.Related; + + public int Order { get; set; } = -1; + + public IEnumerable Validate(ValidationContext validationContext) + { + if (!ExternalLinkUriValidator.IsValidHttpUri(Uri)) + { + yield return new ValidationResult( + "Uri must be an absolute, well-formed http or https URL.", + [nameof(Uri)]); + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/ExternalLinkUriValidator.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/ExternalLinkUriValidator.cs new file mode 100644 index 0000000000..1db4baae5e --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/ExternalLinkUriValidator.cs @@ -0,0 +1,20 @@ +using System; + +namespace Unity.GrantManager.ApplicationForms; + +/// +/// Shared http/https-only URI validation to block script-scheme injection (e.g. javascript:, data:). +/// +public static class ExternalLinkUriValidator +{ + public static bool IsValidHttpUri(string? uri) + { + if (string.IsNullOrWhiteSpace(uri)) + { + return false; + } + + return Uri.TryCreate(uri, UriKind.Absolute, out var parsed) + && (parsed.Scheme == Uri.UriSchemeHttp || parsed.Scheme == Uri.UriSchemeHttps); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/ExternalLinksConfigDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/ExternalLinksConfigDto.cs new file mode 100644 index 0000000000..0429e526fd --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/ExternalLinksConfigDto.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Unity.GrantManager.ApplicantProfile; + +namespace Unity.GrantManager.ApplicationForms; + +public class ExternalLinksConfigDto : IValidatableObject +{ + public const int MaxRelatedLinks = 8; + + public ExternalLinkConfigDto? RenewalLink { get; set; } + + public List RelatedLinks { get; set; } = []; + + [MaxLength(512)] + public string ApplicantMessage { get; set; } = string.Empty; + + public IEnumerable Validate(ValidationContext validationContext) + { + if (RenewalLink is { Published: true } && !ExternalLinkUriValidator.IsValidHttpUri(RenewalLink.Uri)) + { + yield return new ValidationResult( + "Renewal link visibility cannot be enabled without a valid renewal link URL.", + [nameof(RenewalLink)]); + } + + if (RelatedLinks is null || RelatedLinks.Exists(link => link is null)) + { + yield return new ValidationResult( + "Related links must be provided as a list of non-null items.", + [nameof(RelatedLinks)]); + yield break; + } + + if (RelatedLinks.Count > MaxRelatedLinks) + { + yield return new ValidationResult( + $"A maximum of {MaxRelatedLinks} related links is allowed.", + [nameof(RelatedLinks)]); + } + + if (RenewalLink is not null && RenewalLink.ExternalLinkType != ExternalLinkType.Renewal) + { + yield return new ValidationResult( + "Renewal link must be of type Renewal.", + [nameof(RenewalLink)]); + } + + if (RelatedLinks.Exists(l => l.ExternalLinkType != ExternalLinkType.Related)) + { + yield return new ValidationResult( + "Related links must all be of type Related.", + [nameof(RelatedLinks)]); + } + + for (var i = 0; i < RelatedLinks.Count; i++) + { + var link = RelatedLinks[i]; + if (link.Published && !ExternalLinkUriValidator.IsValidHttpUri(link.Uri)) + { + yield return new ValidationResult( + $"Related link visibility cannot be enabled without a valid URL (item {i + 1}).", + [nameof(RelatedLinks)]); + } + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/IApplicationFormAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/IApplicationFormAppService.cs index 5de7519628..d452abddec 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/IApplicationFormAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/IApplicationFormAppService.cs @@ -19,6 +19,7 @@ public interface IApplicationFormAppService : ICrudAppService< Task> GetPublishedVersionsAsync(Guid id); Task PatchOtherConfig(Guid id, OtherConfigDto config); Task PatchAiConfig(Guid id, AIConfigDto config); + Task PatchExternalLinksConfigAsync(Guid id, ExternalLinksConfigDto config); Task GetFormPaymentApprovalThresholdByApplicationIdAsync(Guid applicationId); Task GetFormPreventPaymentStatusByApplicationId(Guid applicationId); Task GetFormDetailsByApplicationIdAsync(Guid applicationId); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/IApplicationFormVersionService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/IApplicationFormVersionService.cs index ee59592b06..2c9d1bc64a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/IApplicationFormVersionService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/IApplicationFormVersionService.cs @@ -1,31 +1,40 @@ -using Newtonsoft.Json.Linq; -using System; -using System.Threading.Tasks; -using Unity.GrantManager.ApplicationForms.Mapping; -using Unity.GrantManager.Forms; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; - -namespace Unity.GrantManager.ApplicationForms -{ - public interface IApplicationFormVersionAppService : ICrudAppService< - ApplicationFormVersionDto, - Guid, - PagedAndSortedResultRequestDto, - CreateUpdateApplicationFormVersionDto> - { - Task FormVersionExists(string chefsFormVersionId); - Task InitializePublishedFormVersion(dynamic chefsForm, Guid applicationFormId, bool initializePublishedOnly); - Task GetFormVersionSubmissionMapping(string chefsFormVersionId); - Task UpdateOrCreateApplicationFormVersion(string chefsFormId, string chefsFormVersionId, Guid applicationFormId, dynamic chefsFormVersion); - Task TryInitializeApplicationFormVersionWithToken(JToken token, Guid applicationFormId, string formVersionId, bool published); - Task TryInitializeApplicationFormVersion(string? formId, int version, Guid applicationFormId, string formVersionId, bool published); - Task GetByChefsFormVersionId(Guid chefsFormVersionId); - Task GetFormVersionByApplicationIdAsync(Guid applicationId); +using Newtonsoft.Json.Linq; +using System; +using System.Threading.Tasks; +using Unity.GrantManager.ApplicationForms.Mapping; +using Unity.GrantManager.Forms; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; + +namespace Unity.GrantManager.ApplicationForms +{ + public interface IApplicationFormVersionAppService : ICrudAppService< + ApplicationFormVersionDto, + Guid, + PagedAndSortedResultRequestDto, + CreateUpdateApplicationFormVersionDto> + { + Task FormVersionExists(string chefsFormVersionId); + Task InitializePublishedFormVersion(dynamic chefsForm, Guid applicationFormId, bool initializePublishedOnly); + Task GetFormVersionSubmissionMapping(string chefsFormVersionId); + Task UpdateOrCreateApplicationFormVersion(string chefsFormId, string chefsFormVersionId, Guid applicationFormId, dynamic chefsFormVersion); + Task TryInitializeApplicationFormVersionWithToken(JToken token, Guid applicationFormId, string formVersionId, bool published); + Task TryInitializeApplicationFormVersion(string? formId, int version, Guid applicationFormId, string formVersionId, bool published); + Task GetByChefsFormVersionId(Guid chefsFormVersionId); + Task GetFormVersionByApplicationIdAsync(Guid applicationId); Task DeleteWorkSheetMappingByFormName(string formName, Guid formVersionId); Task GenerateMappingAsync(Guid id); Task GetPendingAiWorksheetAsync(Guid formVersionId); Task CreateAiWorksheetDraftAsync(Guid formVersionId, CreateAiWorksheetDraftDto input); Task DiscardAiWorksheetSuggestionsAsync(Guid formVersionId); + Task GetPendingAiScoresheetAsync(Guid formVersionId); + Task CreateAiScoresheetDraftAsync(Guid formVersionId, CreateAiScoresheetDraftDto input); + Task DiscardAiScoresheetSuggestionsAsync(Guid formVersionId); + Task GetMappingReviewAsync(Guid formVersionId); + Task AcceptMappingSuggestionsAsync(Guid formVersionId, AcceptMappingSuggestionsDto input); + Task DiscardMappingSuggestionsAsync(Guid formVersionId); + Task SetMappingReviewPhaseAsync(Guid formVersionId, FormMappingReviewPhase phase); + Task FinalizeMappingReviewAsync(Guid formVersionId); + Task ResetAiFlowAsync(Guid formVersionId); } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/Mapping/AcceptMappingSuggestionsDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/Mapping/AcceptMappingSuggestionsDto.cs new file mode 100644 index 0000000000..0593bd4312 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/Mapping/AcceptMappingSuggestionsDto.cs @@ -0,0 +1,9 @@ +using System; +using System.Collections.Generic; + +namespace Unity.GrantManager.ApplicationForms.Mapping; + +public sealed class AcceptMappingSuggestionsDto +{ + public List SuggestionIds { get; set; } = []; +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/Mapping/AcceptMappingSuggestionsResultDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/Mapping/AcceptMappingSuggestionsResultDto.cs new file mode 100644 index 0000000000..618a711982 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/Mapping/AcceptMappingSuggestionsResultDto.cs @@ -0,0 +1,6 @@ +namespace Unity.GrantManager.ApplicationForms.Mapping; + +public sealed class AcceptMappingSuggestionsResultDto +{ + public string SubmissionHeaderMapping { get; set; } = "{}"; +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/Mapping/AiScoresheetReviewDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/Mapping/AiScoresheetReviewDto.cs new file mode 100644 index 0000000000..5b4e5cf92c --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/Mapping/AiScoresheetReviewDto.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; + +namespace Unity.GrantManager.ApplicationForms.Mapping; + +public sealed class AiScoresheetReviewDto +{ + public Guid SessionId { get; set; } + public string Title { get; set; } = string.Empty; + public List Sections { get; set; } = []; +} + +public sealed class AiScoresheetReviewSectionDto +{ + public Guid Id { get; set; } + public string Name { get; set; } = string.Empty; + public uint Order { get; set; } + public List Questions { get; set; } = []; +} + +public sealed class AiScoresheetReviewQuestionDto +{ + public Guid Id { get; set; } + public Guid SectionId { get; set; } + public string Name { get; set; } = string.Empty; + public string Label { get; set; } = string.Empty; + public string? Description { get; set; } + public string Type { get; set; } = string.Empty; + public bool Selected { get; set; } = true; +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/Mapping/AiScoresheetSuggestionName.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/Mapping/AiScoresheetSuggestionName.cs new file mode 100644 index 0000000000..637caa2dae --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/Mapping/AiScoresheetSuggestionName.cs @@ -0,0 +1,9 @@ +using System; + +namespace Unity.GrantManager.ApplicationForms.Mapping; + +public static class AiScoresheetSuggestionName +{ + public static string Build(Guid formId, Guid formVersionId) => + $"ai-form-{formId}-version-{formVersionId}-scoresheet"; +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/Mapping/AiWorksheetSuggestionName.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/Mapping/AiWorksheetSuggestionName.cs index c85450926a..956fd1cb7b 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/Mapping/AiWorksheetSuggestionName.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/Mapping/AiWorksheetSuggestionName.cs @@ -5,5 +5,5 @@ namespace Unity.GrantManager.ApplicationForms.Mapping; public static class AiWorksheetSuggestionName { public static string Build(Guid formId, Guid formVersionId) => - $"ai-form-{formId}-version-{formVersionId}-field-suggestions"; + $"ai-form-{formId}-version-{formVersionId}-worksheet"; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/Mapping/CreateAiScoresheetDraftDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/Mapping/CreateAiScoresheetDraftDto.cs new file mode 100644 index 0000000000..435f4d9d72 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/Mapping/CreateAiScoresheetDraftDto.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Unity.GrantManager.ApplicationForms.Mapping; + +public sealed class CreateAiScoresheetDraftDto +{ + public Guid SessionId { get; set; } + + [Required] + public string Title { get; set; } = string.Empty; + + [MinLength(1)] + public List SelectedQuestionIds { get; set; } = []; +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/Mapping/FormMappingReviewDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/Mapping/FormMappingReviewDto.cs new file mode 100644 index 0000000000..f9ad45cf80 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/Mapping/FormMappingReviewDto.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; + +namespace Unity.GrantManager.ApplicationForms.Mapping; + +public class FormMappingReviewDto +{ + public Guid FormVersionId { get; set; } + public int Sequence { get; set; } + public GenerationReviewStatus Status { get; set; } + public FormMappingReviewPhase Phase { get; set; } + public FormGenerationWorkflowState WorkflowState { get; set; } + public FormGenerationWorkflowAction WorkflowAction { get; set; } + public string State { get; set; } = string.Empty; + public string Action { get; set; } = string.Empty; + public List AvailableActions { get; set; } = []; + public bool ActionEnabled { get; set; } + public string StateLabel { get; set; } = string.Empty; + public string ActionLabel { get; set; } = string.Empty; + public List PendingSuggestions { get; set; } = []; + public int UnchangedSuggestionCount { get; set; } + public bool NoSuggestionsGenerated { get; set; } + public bool NoWorksheetSuggestionsGenerated { get; set; } + public List DraftWorksheetIds { get; set; } = []; + public bool CanGenerateFinalMapping { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/Mapping/FormMappingReviewPayload.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/Mapping/FormMappingReviewPayload.cs new file mode 100644 index 0000000000..cddb445803 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/Mapping/FormMappingReviewPayload.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace Unity.GrantManager.ApplicationForms.Mapping; + +public sealed class FormMappingReviewPayload +{ + public List PendingSuggestions { get; set; } = []; + public int UnchangedSuggestionCount { get; set; } + public bool NoSuggestionsGenerated { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/Mapping/FormMappingSuggestionDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/Mapping/FormMappingSuggestionDto.cs new file mode 100644 index 0000000000..1d52982e6f --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/Mapping/FormMappingSuggestionDto.cs @@ -0,0 +1,15 @@ +using System; + +namespace Unity.GrantManager.ApplicationForms.Mapping; + +public class FormMappingSuggestionDto +{ + public Guid Id { get; set; } + public string SourceField { get; set; } = string.Empty; + public string TargetField { get; set; } = string.Empty; + public string Reason { get; set; } = string.Empty; + public decimal Confidence { get; set; } + public string ChangeType { get; set; } = "New"; + public string? PreviousTargetField { get; set; } + public string? ConflictSourceField { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/Mapping/FormWorksheetReviewPayload.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/Mapping/FormWorksheetReviewPayload.cs new file mode 100644 index 0000000000..9d5b2254b8 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicationForms/Mapping/FormWorksheetReviewPayload.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; + +namespace Unity.GrantManager.ApplicationForms.Mapping; + +public sealed class FormWorksheetReviewPayload +{ + public List DraftWorksheetIds { get; set; } = []; + public bool NoSuggestionsGenerated { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/ApplicantSummaryDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/ApplicantSummaryDto.cs index 332d0f7986..375cdd7b45 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/ApplicantSummaryDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/ApplicantSummaryDto.cs @@ -21,6 +21,7 @@ public class ApplicantSummaryDto public string? UnityApplicantId { get; set; } public string? FiscalDay { get; set; } public string? FiscalMonth { get; set; } + public DateOnly? FiscalYearEnd { get; set; } public string? ElectoralDistrict { get; set; } public bool IsDuplicated { get; set; } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/Generation/BackgroundJobs/AIGenerationBackgroundJobArgs.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/Generation/BackgroundJobs/AIGenerationBackgroundJobArgs.cs index 19d8c996c4..5320c270bb 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/Generation/BackgroundJobs/AIGenerationBackgroundJobArgs.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/Generation/BackgroundJobs/AIGenerationBackgroundJobArgs.cs @@ -11,6 +11,8 @@ public sealed class AIGenerationBackgroundJobArgs public Guid OperationId { get; set; } + public Guid? GenerationRequestId { get; set; } + public Guid? TenantId { get; set; } public Guid? RequestedByUserId { get; set; } 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 bcd98265cf..4861a89cb1 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 @@ -40,6 +40,8 @@ public class GrantApplicationDto : AuditedEntityDto public string? Notes { get; set; } = string.Empty; public string? AssessmentResultStatus { get; set; } = string.Empty; public bool ExternalStatusVisibility { get; set; } = false; + public string ExternalStatus { get; set; } = string.Empty; + public string? PublishedStatus { get; set; } public DateTime? AssessmentResultDate { get; set; } public GrantApplicationState StatusCode { get; set; } public DateTime? FinalDecisionDate { get; set; } @@ -88,4 +90,5 @@ public class GrantApplicationDto : AuditedEntityDto public Guid? DefaultSiteId { get; set; } public ApplicationAnalysisResponse? AIAnalysisData { get; set; } public string? AIScoresheetAnswers { get; set; } + public bool EligibleForRenewal { get; set; } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/QueueAttachmentSummaryRequestDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/QueueAttachmentSummaryRequestDto.cs deleted file mode 100644 index dd89554bfe..0000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/QueueAttachmentSummaryRequestDto.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Unity.GrantManager.GrantApplications; - -public class QueueAttachmentSummaryRequestDto -{ - public Guid ApplicationId { get; set; } - public List? AttachmentIds { get; set; } -} 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 f302199750..7f7bf9fb0a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantManagerFeaturesDefinitionProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantManagerFeaturesDefinitionProvider.cs @@ -13,7 +13,7 @@ public class GrantManagerFeaturesDefinitionProvider : FeatureDefinitionProvider { public override void Define(IFeatureDefinitionContext context) { - var myGroup = context.AddGroup("GrantManager"); + var myGroup = context.AddGroup("GrantManager", displayName: LocalizableString.Create("Grant Manager")); var defaultValue = "false"; myGroup.AddFeature("Unity.Payments", diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/CreateUpdateNotificationDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/CreateUpdateNotificationDto.cs index 9c8659e0d7..2c86cf9638 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/CreateUpdateNotificationDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/CreateUpdateNotificationDto.cs @@ -14,6 +14,8 @@ public class CreateUpdateNotificationDto [Required] public string TriggerType { get; set; } = "Event"; + public string? Module { get; set; } + public string? TriggerDetail { get; set; } public bool IsActive { get; set; } = true; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/NotificationDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/NotificationDto.cs index 815c3fadbe..f7071ef01f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/NotificationDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Notifications/NotificationDto.cs @@ -9,6 +9,7 @@ public class NotificationDto : EntityDto public Guid EmailTemplateId { get; set; } public string? TemplateName { get; set; } public string TriggerType { get; set; } = string.Empty; + public string? Module { get; set; } public string? TriggerDetail { get; set; } public bool IsActive { get; set; } public string? EventType { get; set; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/DataProviders/SubmissionInfoDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/DataProviders/SubmissionInfoDataProvider.cs index d56d98fa28..4c5fe02c7b 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/DataProviders/SubmissionInfoDataProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/DataProviders/SubmissionInfoDataProvider.cs @@ -66,27 +66,60 @@ join status in statusesQuery on application.ApplicationStatusId equals status.Id submission.CreationTime, submission.Submission, application.ReferenceNo, + application.EligibleForRenewal, FormName = form.ApplicationFormName ?? string.Empty, + form.ExternalLinksConfig, Status = application.ExternalStatusVisibility ? status.NotifiedStatus ?? status.ExternalStatus - : status.ExternalStatus + : status.ExternalStatus, }).ToListAsync(); - dto.Submissions.AddRange(results.Select(s => new SubmissionInfoItemDto + // ExternalLinksConfig is a JSON-mapped complex property; filtering its nested links as + // part of the join requires an APPLY operation that the SQLite test provider does not + // support, so the form's external links are resolved in-memory below instead of within + // the query. + dto.Submissions.AddRange(results.Select(s => { - Id = s.Id, - LinkId = s.LinkId, - ReceivedTime = s.CreationTime, - SubmissionTime = ResolveSubmissionTime(s.Submission, s.CreationTime), - ReferenceNo = s.ReferenceNo, - Type = s.FormName, - Status = s.Status + var renewalLinkEntity = s.EligibleForRenewal ? s.ExternalLinksConfig.Links + .Where(x => x.Published && x.ExternalLinkType == ExternalLinkType.Renewal) + .FirstOrDefault() : null; + + var renewalLink = renewalLinkEntity is null ? null : ToExternalLinkDto(renewalLinkEntity); + + var relatedLinks = s.ExternalLinksConfig.Links + .Where(x => x.Published && x.ExternalLinkType == ExternalLinkType.Related) + .OrderBy(x => x.Order == -1 ? int.MaxValue : x.Order) // Links without an order default to -1 + .Select(ToExternalLinkDto) + .ToList(); + + return new SubmissionInfoItemDto + { + Id = s.Id, + LinkId = s.LinkId, + ReceivedTime = s.CreationTime, + SubmissionTime = ResolveSubmissionTime(s.Submission, s.CreationTime), + ReferenceNo = s.ReferenceNo, + Type = s.FormName, + Status = s.Status, + RenewalLink = renewalLink, + RelatedLinks = relatedLinks, + EligibleForRenewal = s.EligibleForRenewal, + ApplicantMessage = renewalLinkEntity is null ? string.Empty : s.ExternalLinksConfig.ApplicantMessage + }; })); } return dto; } + private static ExternalLinkDto ToExternalLinkDto(ExternalLink link) => new() + { + Uri = link.Uri, + Order = link.Order, + Title = link.Title, + Description = link.Description + }; + /// /// Derives the CHEFS form view URL from the INTAKE_API_BASE dynamic URL setting. /// e.g. https://chefs-dev.apps.silver.devops.gov.bc.ca/app/api/v1 diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantAppService.cs index 15c346d508..8203ff5e4d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantAppService.cs @@ -5,23 +5,23 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using System.Text.Json; +using System.Threading.Tasks; using Unity.GrantManager.Applications; using Unity.GrantManager.GrantApplications; using Unity.GrantManager.Intakes; using Unity.GrantManager.Intakes.Mapping; -using Unity.Payments.Events; -using Volo.Abp; using Unity.GrantManager.Integrations.Orgbook; +using Unity.GrantManager.Permissions; using Unity.Modules.Shared; using Unity.Modules.Shared.Utils; using Unity.Payments.Domain.Suppliers; -using Unity.GrantManager.Permissions; +using Unity.Payments.Events; using Unity.Payments.Integrations.Cas; using Unity.Payments.Suppliers; -using Volo.Abp.DependencyInjection; +using Volo.Abp; using Volo.Abp.Application.Dtos; +using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories; namespace Unity.GrantManager.Applicants; @@ -798,8 +798,9 @@ public async Task> GetListAsync(ApplicantListRe : null, IsDuplicated = applicant.IsDuplicated, CreationTime = applicant.CreationTime, - LastModificationTime = applicant.LastModificationTime - }).ToList(); + LastModificationTime = applicant.LastModificationTime, + FiscalYearEnd = applicant.FiscalYearEnd, + }).ToList(); // Use items.Count while client side datatables are used. When going to server // side actually query the correct amount with paging enabled. return new PagedResultDto(items.Count, items); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormAppService.cs index 01642a6fcb..0081f20ae6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormAppService.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Unity.GrantManager.ApplicantProfile; using Unity.GrantManager.Applications; using Unity.GrantManager.Forms; using Unity.GrantManager.GrantApplications; @@ -125,6 +126,11 @@ public override async Task GetAsync(Guid id) var dto = await base.GetAsync(id); dto.ApiKey = _stringEncryptionService.Decrypt(dto.ApiKey); dto.ApiToken = _stringEncryptionService.Decrypt(dto.ApiToken); + + var form = await Repository.GetAsync(id); + dto.ExternalLinks = form.ExternalLinksConfig.Links.Select(MapToExternalLinkConfigDto).ToList(); + dto.ApplicantMessage = form.ExternalLinksConfig.ApplicantMessage; + return dto; } @@ -187,6 +193,41 @@ public async Task PatchAiConfig(Guid id, AIConfigDto config) await Repository.UpdateAsync(form); } + [Authorize(GrantManagerPermissions.ApplicationForms.Default)] + public async Task PatchExternalLinksConfigAsync(Guid id, ExternalLinksConfigDto config) + { + ArgumentNullException.ThrowIfNull(config); + + var form = await Repository.GetAsync(id); + + var renewalLink = config.RenewalLink is null ? null : MapToExternalLink(config.RenewalLink); + var relatedLinks = (config.RelatedLinks ?? []).Select(MapToExternalLink).ToList(); + + form.SetExternalLinks(renewalLink, relatedLinks, config.ApplicantMessage); + + await Repository.UpdateAsync(form); + } + + private static ExternalLink MapToExternalLink(ExternalLinkConfigDto dto) => new() + { + Uri = dto.Uri, + Title = dto.Title, + Description = dto.Description, + Published = dto.Published, + ExternalLinkType = dto.ExternalLinkType, + Order = dto.Order + }; + + private static ExternalLinkConfigDto MapToExternalLinkConfigDto(ExternalLink link) => new() + { + Uri = link.Uri, + Title = link.Title, + Description = link.Description, + Published = link.Published, + ExternalLinkType = link.ExternalLinkType, + Order = link.Order + }; + [Authorize(PaymentsPermissions.Payments.EditFormPaymentConfiguration)] public async Task GetFormPreventPaymentStatusByApplicationId(Guid applicationId) { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormVersionAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormVersionAppService.cs index 2211f051f5..c60183318f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormVersionAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/ApplicationFormVersionAppService.cs @@ -1,5 +1,7 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Localization; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; @@ -8,12 +10,15 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Unity.AI.Features; +using Unity.AI.Localization; using Unity.AI.Generation; using Unity.AI.Operations; -using Unity.AI.Permissions; using Unity.AI.Requests; using Unity.AI.Runtime.Execution; +using Unity.AI.Settings; using Unity.Flex.Domain.Worksheets; +using Unity.Flex.Domain.Scoresheets; +using Unity.Flex.Scoresheets.Enums; using Unity.GrantManager.ApplicationForms.Mapping; using Unity.GrantManager.Applications; using Unity.GrantManager.Forms; @@ -29,6 +34,11 @@ using Volo.Abp.Domain.Repositories; using Volo.Abp.Features; using Volo.Abp.Uow; +using Unity.Flex.Domain.WorksheetLinks; +using Unity.Flex.Domain.WorksheetInstances; +using Unity.Flex.Domain.ScoresheetInstances; +using Unity.Flex.Permissions; +using Unity.Modules.Shared.Correlation; namespace Unity.GrantManager.ApplicationForms { @@ -41,9 +51,16 @@ public class ApplicationFormVersionAppService( IApplicationFormSubmissionRepository formSubmissionRepository, IReportingFieldsGeneratorService reportingFieldsGeneratorService, IFeatureChecker featureChecker, + AIFeatureGuard aiFeatureGuard, + IStringLocalizer localizer, IAIGenerationAppService aiGenerationAppService, IWorksheetRepository worksheetRepository, - IRepository customFieldRepository) : + IRepository customFieldRepository, + IGenerationReviewRepository generationReviewRepository, + IWorksheetLinkRepository worksheetLinkRepository, + IScoresheetRepository scoresheetRepository, + IWorksheetInstanceRepository worksheetInstanceRepository, + IScoresheetInstanceRepository scoresheetInstanceRepository) : CrudAppService< ApplicationFormVersion, ApplicationFormVersionDto, @@ -54,10 +71,18 @@ public class ApplicationFormVersionAppService( { private readonly IAIGenerationAppService _aiGenerationAppService = aiGenerationAppService; + private async Task EnsureAiOperationAccessAsync(string operationType, bool requiresGeneratePermission) + { + var operation = AIGenerationOperations.Get(operationType); + await aiFeatureGuard.EnsureEnabledAsync(operation.FeatureName, operation.DisabledLocalizationKey); + await CheckPolicyAsync(requiresGeneratePermission ? operation.GeneratePermission : operation.ViewPermission); + } + public override async Task CreateAsync(CreateUpdateApplicationFormVersionDto input) => await base.CreateAsync(input); [RemoteService(false)] + [Authorize] public override async Task UpdateAsync(Guid id, CreateUpdateApplicationFormVersionDto input) => await base.UpdateAsync(id, input); @@ -338,6 +363,13 @@ public async Task DeleteWorkSheetMappingByFormName(string formName, Guid formVer public virtual async Task GenerateMappingAsync(Guid id) { var applicationFormVersion = await Repository.GetAsync(id); + var review = await generationReviewRepository.FindLatestByOperationAndFormVersionAsync( + AIGenerationOperations.FormMapping, + id); + if (review?.Status == GenerationReviewStatus.Active) + { + throw new UserFriendlyException(localizer[AILocalizationKeys.FormGenerationReviewActive]); + } await _aiGenerationAppService.SubmitAsync( AIGenerationOperations.FormMapping, new AIGenerationSubmissionDto @@ -352,10 +384,207 @@ await _aiGenerationAppService.SubmitAsync( }; } + [HttpGet("api/app/application-form-version/mapping-review")] + public virtual async Task GetMappingReviewAsync(Guid formVersionId) + { + await EnsureAiOperationAccessAsync(AIGenerationOperations.FormMapping, requiresGeneratePermission: false); + var review = await generationReviewRepository.FindLatestByOperationAndFormVersionAsync( + AIGenerationOperations.FormMapping, + formVersionId); + return await MapMappingReviewAsync(formVersionId, review); + } + + [HttpPost("api/app/application-form-version/accept-mapping-suggestions")] + public virtual async Task AcceptMappingSuggestionsAsync( + Guid formVersionId, + AcceptMappingSuggestionsDto input) + { + await EnsureAiOperationAccessAsync(AIGenerationOperations.FormMapping, requiresGeneratePermission: true); + var review = await generationReviewRepository.FindLatestByOperationAndFormVersionAsync( + AIGenerationOperations.FormMapping, + formVersionId) + ?? throw new UserFriendlyException(localizer[AILocalizationKeys.MappingReviewPending]); + if (review.Status != GenerationReviewStatus.Active) + { + throw new UserFriendlyException(localizer[AILocalizationKeys.MappingReviewInactive]); + } + + var suggestionIds = input?.SuggestionIds?.Distinct().ToHashSet() ?? []; + if (suggestionIds.Count == 0) + { + throw new UserFriendlyException(localizer[AILocalizationKeys.MappingSelectionRequired]); + } + + var payload = GetMappingReviewPayload(review); + var selectedSuggestions = payload.PendingSuggestions + .Where(suggestion => suggestionIds.Contains(suggestion.Id)) + .ToList(); + if (selectedSuggestions.Count != suggestionIds.Count) + { + throw new UserFriendlyException(localizer[AILocalizationKeys.MappingSelectionInvalid]); + } + + var formVersion = await Repository.GetAsync(formVersionId); + formVersion.SubmissionHeaderMapping = FormMappingResponseMapper.MergeSubmissionHeaderMapping( + formVersion.SubmissionHeaderMapping, + selectedSuggestions.Select(suggestion => new FormMappingDto + { + SourceField = suggestion.SourceField, + TargetField = suggestion.TargetField + }), + replaceExisting: review.Sequence > 1 && review.Sequence % 2 == 0); + await Repository.UpdateAsync(formVersion, true); + payload.PendingSuggestions.RemoveAll(suggestion => suggestionIds.Contains(suggestion.Id)); + SetMappingReviewPayload(review, payload); + await generationReviewRepository.UpdateAsync(review, true); + + return new AcceptMappingSuggestionsResultDto + { + SubmissionHeaderMapping = formVersion.SubmissionHeaderMapping + }; + } + + [HttpPost("api/app/application-form-version/discard-mapping-suggestions")] + public virtual async Task DiscardMappingSuggestionsAsync(Guid formVersionId) + { + await EnsureAiOperationAccessAsync(AIGenerationOperations.FormMapping, requiresGeneratePermission: true); + var review = await generationReviewRepository.FindLatestByOperationAndFormVersionAsync( + AIGenerationOperations.FormMapping, + formVersionId); + if (review == null) + { + return; + } + + var payload = GetMappingReviewPayload(review); + payload.PendingSuggestions = []; + review.Discard(); + SetMappingReviewPayload(review, payload); + await generationReviewRepository.UpdateAsync(review, true); + } + + [HttpPost("api/app/application-form-version/mapping-review-phase")] + public virtual async Task SetMappingReviewPhaseAsync(Guid formVersionId, FormMappingReviewPhase phase) + { + await EnsureAiOperationAccessAsync(AIGenerationOperations.FormMapping, requiresGeneratePermission: true); + var review = await generationReviewRepository.FindLatestByOperationAndFormVersionAsync( + AIGenerationOperations.FormMapping, + formVersionId); + if (review == null || review.Status != GenerationReviewStatus.Active) + { + if (phase == FormMappingReviewPhase.WorksheetReview && + review?.Sequence == 1) + { + review.Complete(); + await generationReviewRepository.UpdateAsync(review, true); + return; + } + + if (phase == FormMappingReviewPhase.Completed && review != null) + { + return; + } + + if (phase != FormMappingReviewPhase.WorksheetReview) + { + throw new UserFriendlyException(localizer[AILocalizationKeys.MappingReviewPending]); + } + + return; + } + + var payload = GetMappingReviewPayload(review); + if (phase == FormMappingReviewPhase.WorksheetReview) + { + if (payload.PendingSuggestions.Count > 0) + { + throw new UserFriendlyException(localizer[AILocalizationKeys.MappingReviewPendingSuggestions]); + } + + review.Complete(); + } + else if (phase == FormMappingReviewPhase.Completed) + { + review.Complete(); + } + else + { + throw new UserFriendlyException(localizer[AILocalizationKeys.MappingReviewTransitionInvalid]); + } + + SetMappingReviewPayload(review, payload); + await generationReviewRepository.UpdateAsync(review, true); + } + + [HttpPost("api/app/application-form-version/reset-ai-flow")] + public virtual async Task ResetAiFlowAsync(Guid formVersionId) + { + await EnsureAiOperationAccessAsync(AIGenerationOperations.FormMapping, requiresGeneratePermission: true); + await EnsureAiOperationAccessAsync(AIGenerationOperations.FormWorksheet, requiresGeneratePermission: true); + var formVersion = await Repository.GetAsync(formVersionId); + var mappingReviews = await generationReviewRepository.GetListByOperationAndFormVersionAsync(AIGenerationOperations.FormMapping, formVersionId); + var worksheetReviews = await generationReviewRepository.GetListByOperationAndFormVersionAsync(AIGenerationOperations.FormWorksheet, formVersionId); + var worksheetIds = worksheetReviews + .SelectMany(review => GetWorksheetReviewPayload(review).DraftWorksheetIds) + .Distinct() + .ToList(); + var suggestionWorksheet = await GetAiSuggestionWorksheetAsync(formVersion); + if (suggestionWorksheet != null && !worksheetIds.Contains(suggestionWorksheet.Id)) + { + worksheetIds.Add(suggestionWorksheet.Id); + } + + foreach (var worksheetId in worksheetIds) + { + var worksheet = await worksheetRepository.FindAsync(worksheetId); + if (worksheet == null) + { + continue; + } + + await DeleteAiWorksheetSuggestionAsync(worksheet, formVersionId); + } + await generationReviewRepository.DeleteManyAsync(mappingReviews.Concat(worksheetReviews), true); + formVersion.SubmissionHeaderMapping = "{}"; + await Repository.UpdateAsync(formVersion, true); + } + + [HttpPost("api/app/application-form-version/finalize-mapping-review")] + public virtual async Task FinalizeMappingReviewAsync(Guid formVersionId) + { + await EnsureAiOperationAccessAsync(AIGenerationOperations.FormMapping, requiresGeneratePermission: true); + var formVersion = await Repository.GetAsync(formVersionId); + var review = await generationReviewRepository.FindLatestByOperationAndFormVersionAsync( + AIGenerationOperations.FormMapping, + formVersionId) + ?? throw new UserFriendlyException(localizer[AILocalizationKeys.MappingReviewPending]); + var worksheetReview = await generationReviewRepository.FindLatestByOperationAndFormVersionAsync( + AIGenerationOperations.FormWorksheet, + formVersionId); + if (review.Sequence % 2 == 0 || + review.Status == GenerationReviewStatus.Active || + worksheetReview == null || + worksheetReview.Status == GenerationReviewStatus.Active || + GetWorksheetReviewPayload(worksheetReview).NoSuggestionsGenerated || + !await HasNoRemainingDraftsOrAssignedDraftAsync(worksheetReview)) + { + throw new UserFriendlyException(localizer[AILocalizationKeys.WorksheetDraftsMustBePublished]); + } + review.Complete(); + await generationReviewRepository.UpdateAsync(review, true); + await _aiGenerationAppService.SubmitAsync( + AIGenerationOperations.FormMapping, + new AIGenerationSubmissionDto + { + ApplicationId = formVersion.ApplicationFormId, + ApplicationFormVersionId = formVersionId + }); + } + [HttpGet("api/app/application-form-version/pending-ai-worksheet")] public virtual async Task GetPendingAiWorksheetAsync(Guid formVersionId) { - await CheckPolicyAsync(AIPermissions.Analysis.ViewFormWorksheet); + await EnsureAiOperationAccessAsync(AIGenerationOperations.FormWorksheet, requiresGeneratePermission: false); var worksheet = await GetPendingAiWorksheetEntityAsync(formVersionId); return worksheet == null ? null : MapAiWorksheetReview(worksheet); @@ -364,37 +593,36 @@ await _aiGenerationAppService.SubmitAsync( [HttpPost("api/app/application-form-version/create-ai-worksheet-draft")] public virtual async Task CreateAiWorksheetDraftAsync(Guid formVersionId, CreateAiWorksheetDraftDto input) { - await CheckPolicyAsync(AIPermissions.Analysis.GenerateFormWorksheet); - + await EnsureAiOperationAccessAsync(AIGenerationOperations.FormWorksheet, requiresGeneratePermission: true); var worksheet = await GetPendingAiWorksheetEntityAsync(formVersionId); if (worksheet == null || worksheet.Id != input.SessionId) { - throw new UserFriendlyException("The AI worksheet is no longer available for review."); + throw new UserFriendlyException(localizer[AILocalizationKeys.FormWorksheetUnavailable]); } var title = input.Title?.Trim(); if (string.IsNullOrWhiteSpace(title)) { - throw new UserFriendlyException("A worksheet title is required."); + throw new UserFriendlyException(localizer[AILocalizationKeys.WorksheetTitleRequired]); } var selectedFieldIds = input.SelectedFieldIds?.ToHashSet() ?? []; if (selectedFieldIds.Count == 0) { - throw new UserFriendlyException("Select at least one suggested field."); + throw new UserFriendlyException(localizer[AILocalizationKeys.WorksheetSelectionRequired]); } var fields = worksheet.Sections.SelectMany(section => section.Fields).ToList(); var unknownFieldIds = selectedFieldIds.Except(fields.Select(field => field.Id)).ToList(); if (unknownFieldIds.Count > 0) { - throw new UserFriendlyException("The AI worksheet selection is invalid."); + throw new UserFriendlyException(localizer[AILocalizationKeys.FormWorksheetSelectionInvalid]); } var draftName = await GetNextAiWorksheetDraftNameAsync(title); - var draft = new Worksheet(Guid.NewGuid(), draftName, title); + var draft = new Worksheet(GuidGenerator.Create(), draftName, title); - var draftSection = new WorksheetSection(Guid.NewGuid(), "Suggested Fields") + var draftSection = new WorksheetSection(GuidGenerator.Create(), "Suggested Fields") { Worksheet = draft }.SetOrder(1); @@ -407,7 +635,7 @@ public virtual async Task CreateAiWorksheetDraftAsync(Guid formVersionId, Create .Select((field, index) => (field, index))) { var draftField = new CustomField( - Guid.NewGuid(), + GuidGenerator.Create(), field.Key, draft.Name, field.Label, @@ -420,6 +648,17 @@ public virtual async Task CreateAiWorksheetDraftAsync(Guid formVersionId, Create await worksheetRepository.InsertAsync(draft, true); + var worksheetReview = await generationReviewRepository.FindLatestByOperationAndFormVersionAsync( + AIGenerationOperations.FormWorksheet, + formVersionId); + if (worksheetReview != null) + { + var worksheetPayload = GetWorksheetReviewPayload(worksheetReview); + worksheetPayload.DraftWorksheetIds.Add(draft.Id); + SetWorksheetReviewPayload(worksheetReview, worksheetPayload); + await generationReviewRepository.UpdateAsync(worksheetReview); + } + foreach (var field in fields.Where(field => selectedFieldIds.Contains(field.Id))) { field.Section.RemoveField(field); @@ -428,7 +667,12 @@ public virtual async Task CreateAiWorksheetDraftAsync(Guid formVersionId, Create if (worksheet.Sections.All(section => section.Fields.Count == 0)) { - await worksheetRepository.DeleteAsync(worksheet, true); + await DeleteAiWorksheetSuggestionAsync(worksheet, formVersionId); + if (worksheetReview != null) + { + worksheetReview.Complete(); + await generationReviewRepository.UpdateAsync(worksheetReview, true); + } return; } @@ -438,20 +682,233 @@ public virtual async Task CreateAiWorksheetDraftAsync(Guid formVersionId, Create [HttpPost("api/app/application-form-version/discard-ai-worksheet-suggestions")] public virtual async Task DiscardAiWorksheetSuggestionsAsync(Guid formVersionId) { - await CheckPolicyAsync(AIPermissions.Analysis.GenerateFormWorksheet); - + await EnsureAiOperationAccessAsync(AIGenerationOperations.FormWorksheet, requiresGeneratePermission: true); var worksheet = await GetPendingAiWorksheetEntityAsync(formVersionId); if (worksheet != null) { - await worksheetRepository.DeleteAsync(worksheet, true); + await DeleteAiWorksheetSuggestionAsync(worksheet, formVersionId); + var review = await generationReviewRepository.FindLatestByOperationAndFormVersionAsync( + AIGenerationOperations.FormWorksheet, + formVersionId); + if (review != null) + { + review.Discard(); + await generationReviewRepository.UpdateAsync(review, true); + } + } + } + + [HttpGet("api/app/application-form-version/pending-ai-scoresheet")] + public virtual async Task GetPendingAiScoresheetAsync(Guid formVersionId) + { + await EnsureAiOperationAccessAsync(AIGenerationOperations.FormScoresheet, requiresGeneratePermission: false); + + var review = await generationReviewRepository.FindLatestByOperationAndFormVersionAsync( + AIGenerationOperations.FormScoresheet, + formVersionId); + if (review == null || review.Status != GenerationReviewStatus.Active) + { + return null; + } + + var formVersion = await formVersionRepository.GetAsync(formVersionId); + var scoresheet = await scoresheetRepository.GetByNameAsync( + AiScoresheetSuggestionName.Build(formVersion.ApplicationFormId, formVersion.Id), true); + return scoresheet?.Published == false ? MapAiScoresheetReview(scoresheet) : null; + } + + [HttpPost("api/app/application-form-version/create-ai-scoresheet-draft")] + public virtual async Task CreateAiScoresheetDraftAsync(Guid formVersionId, CreateAiScoresheetDraftDto input) + { + await EnsureAiOperationAccessAsync(AIGenerationOperations.FormScoresheet, requiresGeneratePermission: true); + + var suggestion = await GetPendingAiScoresheetEntityAsync(formVersionId); + if (suggestion == null || suggestion.Id != input.SessionId) + { + throw new UserFriendlyException(localizer[AILocalizationKeys.FormScoresheetUnavailable]); + } + + var title = input.Title?.Trim(); + if (string.IsNullOrWhiteSpace(title)) + { + throw new UserFriendlyException(localizer[AILocalizationKeys.FormScoresheetTitleRequired]); + } + + var selectedIds = input.SelectedQuestionIds?.ToHashSet() ?? []; + if (selectedIds.Count == 0) + { + throw new UserFriendlyException(localizer[AILocalizationKeys.FormScoresheetSelectionRequired]); + } + + var questions = suggestion.Sections.SelectMany(section => section.Fields).ToList(); + if (selectedIds.Except(questions.Select(question => question.Id)).Any()) + { + throw new UserFriendlyException(localizer[AILocalizationKeys.FormScoresheetSelectionInvalid]); + } + + var draftName = await GetNextAiScoresheetDraftNameAsync(title); + var draft = new Scoresheet(GuidGenerator.Create(), title, draftName); + foreach (var sourceSection in suggestion.Sections.OrderBy(section => section.Order)) + { + var selectedQuestions = sourceSection.Fields + .Where(question => selectedIds.Contains(question.Id)) + .OrderBy(question => question.Order) + .ToList(); + if (selectedQuestions.Count == 0) + { + continue; + } + + var section = new ScoresheetSection(GuidGenerator.Create(), sourceSection.Name, sourceSection.Order); + draft.AddSection(section); + foreach (var sourceQuestion in selectedQuestions) + { + var draftQuestion = new Question( + GuidGenerator.Create(), + sourceQuestion.Name, + sourceQuestion.Label, + sourceQuestion.Type, + sourceQuestion.Order, + sourceQuestion.Description, + sourceQuestion.Definition) + { + SectionId = section.Id + }; + section.Fields.Add(draftQuestion); + } + } + + await scoresheetRepository.InsertAsync(draft, true); + + foreach (var question in questions.Where(question => selectedIds.Contains(question.Id)).ToList()) + { + var sourceSection = suggestion.Sections.First(section => section.Fields.Contains(question)); + sourceSection.Fields.Remove(question); + } + + if (suggestion.Sections.All(section => section.Fields.Count == 0)) + { + await DeleteAiScoresheetSuggestionAsync(suggestion); + var review = await generationReviewRepository.FindLatestByOperationAndFormVersionAsync( + AIGenerationOperations.FormScoresheet, + formVersionId); + review?.Complete(); + if (review != null) + { + await generationReviewRepository.UpdateAsync(review, true); + } + } + else + { + await scoresheetRepository.UpdateAsync(suggestion, true); + } + } + + [HttpPost("api/app/application-form-version/discard-ai-scoresheet-suggestions")] + public virtual async Task DiscardAiScoresheetSuggestionsAsync(Guid formVersionId) + { + await EnsureAiOperationAccessAsync(AIGenerationOperations.FormScoresheet, requiresGeneratePermission: true); + var suggestion = await GetPendingAiScoresheetEntityAsync(formVersionId); + if (suggestion == null) + { + return; + } + + await DeleteAiScoresheetSuggestionAsync(suggestion); + var review = await generationReviewRepository.FindLatestByOperationAndFormVersionAsync( + AIGenerationOperations.FormScoresheet, + formVersionId); + if (review != null) + { + review.Discard(); + await generationReviewRepository.UpdateAsync(review, true); + } + } + + private async Task GetPendingAiScoresheetEntityAsync(Guid formVersionId) + { + var review = await generationReviewRepository.FindLatestByOperationAndFormVersionAsync( + AIGenerationOperations.FormScoresheet, + formVersionId); + if (review == null || review.Status != GenerationReviewStatus.Active) + { + return null; + } + + var formVersion = await formVersionRepository.GetAsync(formVersionId); + var scoresheet = await scoresheetRepository.GetByNameAsync( + AiScoresheetSuggestionName.Build(formVersion.ApplicationFormId, formVersion.Id), true); + return scoresheet?.Published == false ? scoresheet : null; + } + + private static AiScoresheetReviewDto MapAiScoresheetReview(Scoresheet scoresheet) => new() + { + SessionId = scoresheet.Id, + Title = scoresheet.Title, + Sections = scoresheet.Sections + .OrderBy(section => section.Order) + .Select(section => new AiScoresheetReviewSectionDto + { + Id = section.Id, + Name = section.Name, + Order = section.Order, + Questions = section.Fields.OrderBy(question => question.Order) + .Select(question => new AiScoresheetReviewQuestionDto + { + Id = question.Id, + SectionId = section.Id, + Name = question.Name, + Label = question.Label, + Description = question.Description, + Type = question.Type.ToString(), + Selected = true + }).ToList() + }).ToList() + }; + + private async Task DeleteAiWorksheetSuggestionAsync(Worksheet worksheet, Guid formVersionId) + { + var links = await worksheetLinkRepository.GetListByWorksheetAsync(worksheet.Id, CorrelationConsts.FormVersion) ?? []; + if (worksheet.Published || + links.Any(link => link.CorrelationId != formVersionId) || + await worksheetInstanceRepository.AnyByWorksheetAndFormVersionAsync(worksheet.Id, formVersionId)) + { + throw new UserFriendlyException(localizer[AILocalizationKeys.FormWorksheetDeleteProtected]); } + + foreach (var link in links.Where(link => link.CorrelationId == formVersionId)) + { + await worksheetLinkRepository.DeleteAsync(link, true); + } + + await worksheetRepository.DeleteAsync(worksheet, true); } + private async Task DeleteAiScoresheetSuggestionAsync(Scoresheet scoresheet) + { + if (scoresheet.Published || scoresheet.IsArchived) + { + throw new UserFriendlyException(localizer[AILocalizationKeys.FormScoresheetDeleteProtected]); + } + if (await scoresheetInstanceRepository.AnyByScoresheetAsync(scoresheet.Id)) + { + throw new UserFriendlyException(localizer[AILocalizationKeys.FormScoresheetHasInstances]); + } + + await scoresheetRepository.DeleteAsync(scoresheet, true); + } private async Task GetPendingAiWorksheetEntityAsync(Guid formVersionId) { + var review = await generationReviewRepository.FindLatestByOperationAndFormVersionAsync( + AIGenerationOperations.FormWorksheet, + formVersionId); + if (review == null || review.Status != GenerationReviewStatus.Active) + { + return null; + } + var formVersion = await formVersionRepository.GetAsync(formVersionId); - var worksheet = await worksheetRepository.GetByNameAsync( - AiWorksheetSuggestionName.Build(formVersion.ApplicationFormId, formVersion.Id), true); + var worksheet = await GetAiSuggestionWorksheetAsync(formVersion); if (worksheet?.Published == false) { @@ -461,6 +918,12 @@ public virtual async Task DiscardAiWorksheetSuggestionsAsync(Guid formVersionId) return null; } + private async Task GetAiSuggestionWorksheetAsync(ApplicationFormVersion formVersion) + { + return await worksheetRepository.GetByNameAsync( + AiWorksheetSuggestionName.Build(formVersion.ApplicationFormId, formVersion.Id), true); + } + private static AiWorksheetReviewDto MapAiWorksheetReview(Worksheet worksheet) => new() { SessionId = worksheet.Id, @@ -478,6 +941,242 @@ public virtual async Task DiscardAiWorksheetSuggestionsAsync(Guid formVersionId) .ToList() }; + private async Task MapMappingReviewAsync( + Guid formVersionId, + GenerationReview? mappingReview) + { + var worksheetReview = await generationReviewRepository.FindLatestByOperationAndFormVersionAsync( + AIGenerationOperations.FormWorksheet, + formVersionId); + var workflow = await DeriveWorkflowAsync(mappingReview, worksheetReview); + var mappingPayload = mappingReview == null + ? new FormMappingReviewPayload() + : GetMappingReviewPayload(mappingReview); + var worksheetPayload = worksheetReview == null + ? new FormWorksheetReviewPayload() + : GetWorksheetReviewPayload(worksheetReview); + + return new FormMappingReviewDto + { + FormVersionId = formVersionId, + Sequence = mappingReview?.Sequence ?? 0, + Status = mappingReview?.Status ?? GenerationReviewStatus.Completed, + Phase = GetLegacyPhase(workflow.State), + WorkflowState = workflow.State, + WorkflowAction = workflow.Action, + State = workflow.State.ToString(), + Action = workflow.Action.ToString(), + AvailableActions = workflow.AvailableActions, + ActionEnabled = workflow.ActionEnabled, + StateLabel = GetWorkflowLabel(workflow.State), + ActionLabel = GetWorkflowLabel(workflow.Action), + PendingSuggestions = mappingPayload.PendingSuggestions, + UnchangedSuggestionCount = mappingPayload.UnchangedSuggestionCount, + NoSuggestionsGenerated = mappingPayload.NoSuggestionsGenerated, + NoWorksheetSuggestionsGenerated = worksheetPayload.NoSuggestionsGenerated, + DraftWorksheetIds = worksheetPayload.DraftWorksheetIds, + CanGenerateFinalMapping = workflow.State == FormGenerationWorkflowState.GenerateFinalMapping + }; + } + + private async Task DeriveWorkflowAsync( + GenerationReview? mappingReview, + GenerationReview? worksheetReview) + { + if (mappingReview == null) + { + return FormWorkflowResult.Single( + FormGenerationWorkflowState.GenerateInitialMapping, + FormGenerationWorkflowAction.GenerateInitialMapping, + true); + } + + if (mappingReview.Status == GenerationReviewStatus.Active) + { + var isFinalMapping = mappingReview.Sequence > 1 && mappingReview.Sequence % 2 == 0; + var state = !isFinalMapping + ? FormGenerationWorkflowState.ReviewInitialMapping + : FormGenerationWorkflowState.ReviewFinalMapping; + var action = !isFinalMapping + ? FormGenerationWorkflowAction.ReviewInitialMapping + : FormGenerationWorkflowAction.ReviewFinalMapping; + return FormWorkflowResult.Single(state, action, true); + } + + if (mappingReview.Sequence > 1 && + mappingReview.Sequence % 2 == 0 && + worksheetReview?.Status == GenerationReviewStatus.Active) + { + return FormWorkflowResult.Single( + FormGenerationWorkflowState.ReviewWorksheets, + FormGenerationWorkflowAction.ReviewWorksheets, + true); + } + + if (mappingReview.Sequence > 1 && mappingReview.Sequence % 2 == 0) + { + return new FormWorkflowResult( + FormGenerationWorkflowState.Completed, + FormGenerationWorkflowAction.GenerateMapping, + true, + [ + FormGenerationWorkflowAction.GenerateMapping, + FormGenerationWorkflowAction.GenerateWorksheetsNextCycle + ]); + } + + if (worksheetReview == null) + { + return FormWorkflowResult.Single( + FormGenerationWorkflowState.GenerateWorksheets, + FormGenerationWorkflowAction.GenerateWorksheets, + true); + } + + if (worksheetReview.Status == GenerationReviewStatus.Active) + { + return FormWorkflowResult.Single( + FormGenerationWorkflowState.ReviewWorksheets, + FormGenerationWorkflowAction.ReviewWorksheets, + true); + } + + if (GetWorksheetReviewPayload(worksheetReview).NoSuggestionsGenerated) + { + return FormWorkflowResult.Single( + FormGenerationWorkflowState.Completed, + FormGenerationWorkflowAction.GenerateMapping, + true); + } + + if (worksheetReview.Status == GenerationReviewStatus.Discarded) + { + return FormWorkflowResult.Single( + FormGenerationWorkflowState.Completed, + FormGenerationWorkflowAction.GenerateMapping, + true); + } + + return await HasNoRemainingDraftsOrAssignedDraftAsync(worksheetReview) + ? FormWorkflowResult.Single( + FormGenerationWorkflowState.GenerateFinalMapping, + FormGenerationWorkflowAction.GenerateFinalMapping, + true) + : FormWorkflowResult.Single( + FormGenerationWorkflowState.PublishAndAssignWorksheets, + FormGenerationWorkflowAction.PublishAndAssignWorksheets, + false); + } + + private async Task HasNoRemainingDraftsOrAssignedDraftAsync(GenerationReview review) + { + var draftWorksheetIds = GetWorksheetReviewPayload(review).DraftWorksheetIds; + if (draftWorksheetIds.Count == 0) + { + return true; + } + + var linkedWorksheetIds = (await worksheetLinkRepository.GetListByCorrelationAsync( + review.ContextId, + CorrelationConsts.FormVersion)) + .Select(link => link.WorksheetId) + .ToHashSet(); + + var hasRemainingDraft = false; + + foreach (var worksheetId in draftWorksheetIds) + { + var worksheet = await worksheetRepository.FindAsync(worksheetId); + if (worksheet == null) + { + continue; + } + + hasRemainingDraft = true; + if (worksheet.Published && linkedWorksheetIds.Contains(worksheetId)) + { + return true; + } + } + + return !hasRemainingDraft; + } + + private static FormMappingReviewPhase GetLegacyPhase(FormGenerationWorkflowState state) => + state switch + { + FormGenerationWorkflowState.ReviewInitialMapping => FormMappingReviewPhase.MappingReview, + FormGenerationWorkflowState.GenerateWorksheets or + FormGenerationWorkflowState.ReviewWorksheets => FormMappingReviewPhase.WorksheetReview, + FormGenerationWorkflowState.PublishAndAssignWorksheets or + FormGenerationWorkflowState.GenerateFinalMapping => FormMappingReviewPhase.PublishAndAssignWorksheets, + FormGenerationWorkflowState.ReviewFinalMapping => FormMappingReviewPhase.FinalMappingReview, + _ => FormMappingReviewPhase.Completed + }; + + private string GetWorkflowLabel(FormGenerationWorkflowState state) => + state switch + { + FormGenerationWorkflowState.GenerateInitialMapping => localizer[AILocalizationKeys.WorkflowGenerateInitialMapping], + FormGenerationWorkflowState.ReviewInitialMapping => localizer[AILocalizationKeys.WorkflowReviewInitialMapping], + FormGenerationWorkflowState.GenerateWorksheets => localizer[AILocalizationKeys.WorkflowGenerateWorksheets], + FormGenerationWorkflowState.ReviewWorksheets => localizer[AILocalizationKeys.WorkflowReviewWorksheets], + FormGenerationWorkflowState.PublishAndAssignWorksheets => localizer[AILocalizationKeys.WorkflowPublishAssignWorksheets], + FormGenerationWorkflowState.GenerateFinalMapping => localizer[AILocalizationKeys.WorkflowGenerateFinalMapping], + FormGenerationWorkflowState.ReviewFinalMapping => localizer[AILocalizationKeys.WorkflowReviewFinalMapping], + _ => localizer[AILocalizationKeys.WorkflowCompleted] + }; + + private string GetWorkflowLabel(FormGenerationWorkflowAction action) => + action switch + { + FormGenerationWorkflowAction.GenerateInitialMapping => localizer[AILocalizationKeys.WorkflowGenerateInitialMapping], + FormGenerationWorkflowAction.ReviewInitialMapping => localizer[AILocalizationKeys.WorkflowReviewInitialMapping], + FormGenerationWorkflowAction.GenerateWorksheets or + FormGenerationWorkflowAction.GenerateWorksheetsNextCycle => localizer[AILocalizationKeys.WorkflowGenerateWorksheets], + FormGenerationWorkflowAction.ReviewWorksheets => localizer[AILocalizationKeys.WorkflowReviewWorksheets], + FormGenerationWorkflowAction.PublishAndAssignWorksheets => localizer[AILocalizationKeys.WorkflowPublishAssignWorksheets], + FormGenerationWorkflowAction.GenerateFinalMapping => localizer[AILocalizationKeys.WorkflowGenerateFinalMapping], + FormGenerationWorkflowAction.GenerateMapping => localizer[AILocalizationKeys.WorkflowGenerateMapping], + FormGenerationWorkflowAction.ReviewFinalMapping => localizer[AILocalizationKeys.WorkflowReviewFinalMapping], + _ => localizer[AILocalizationKeys.WorkflowCompleted] + }; + + private sealed record FormWorkflowResult( + FormGenerationWorkflowState State, + FormGenerationWorkflowAction Action, + bool ActionEnabled, + List AvailableActions) + { + public static FormWorkflowResult Single( + FormGenerationWorkflowState state, + FormGenerationWorkflowAction action, + bool enabled) => + new(state, action, enabled, [action]); + } + + private static FormMappingReviewPayload GetMappingReviewPayload(GenerationReview review) => + string.IsNullOrWhiteSpace(review.ReviewData) + ? new FormMappingReviewPayload() + : JsonSerializer.Deserialize(review.ReviewData) + ?? new FormMappingReviewPayload(); + + private static void SetMappingReviewPayload( + GenerationReview review, + FormMappingReviewPayload payload) => + review.SetReviewData(JsonSerializer.Serialize(payload)); + + private static FormWorksheetReviewPayload GetWorksheetReviewPayload(GenerationReview review) => + string.IsNullOrWhiteSpace(review.ReviewData) + ? new FormWorksheetReviewPayload() + : JsonSerializer.Deserialize(review.ReviewData) + ?? new FormWorksheetReviewPayload(); + + private static void SetWorksheetReviewPayload( + GenerationReview review, + FormWorksheetReviewPayload payload) => + review.SetReviewData(JsonSerializer.Serialize(payload)); + private async Task GetNextAiWorksheetDraftNameAsync(string title) { var titlePart = Regex.Replace(title.Trim().ToLowerInvariant(), "[^a-z0-9]+", "-").Trim('-'); @@ -493,6 +1192,21 @@ private async Task GetNextAiWorksheetDraftNameAsync(string title) return candidate; } + private async Task GetNextAiScoresheetDraftNameAsync(string title) + { + var titlePart = Regex.Replace(title.Trim().ToLowerInvariant(), "[^a-z0-9]+", "-").Trim('-'); + var baseName = $"ai-{(string.IsNullOrEmpty(titlePart) ? "scoresheet" : titlePart)}"; + var candidate = baseName; + var suffix = 2; + + while (await scoresheetRepository.GetByNameAsync(candidate, false) != null) + { + candidate = $"{baseName}-{suffix++}"; + } + + return candidate; + } + private static string NormalizeCustomFieldDefinition(string definition) { try diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/Mapping/FormMappingResponseMapper.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/Mapping/FormMappingResponseMapper.cs index 06860cd8a6..d8a3ca2a8e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/Mapping/FormMappingResponseMapper.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicationForms/Mapping/FormMappingResponseMapper.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections.Generic; +using System.Linq; using System.Text.Json; using Unity.AI.Responses; @@ -22,13 +25,9 @@ internal static string BuildSubmissionHeaderMapping(FormMappingResponse response foreach (var property in document.RootElement.EnumerateObject()) { - if (property.Value.ValueKind != JsonValueKind.String) - { - return "{}"; - } - - var chefsField = property.Value.GetString(); - if (string.IsNullOrWhiteSpace(chefsField) || string.IsNullOrWhiteSpace(property.Name)) + if (property.Value.ValueKind != JsonValueKind.String + || string.IsNullOrWhiteSpace(property.Value.GetString()) + || string.IsNullOrWhiteSpace(property.Name)) { return "{}"; } @@ -41,4 +40,89 @@ internal static string BuildSubmissionHeaderMapping(FormMappingResponse response return "{}"; } } + + internal static List ParseSuggestions(string mapping) + { + if (string.IsNullOrWhiteSpace(mapping)) + { + return []; + } + + try + { + using var document = JsonDocument.Parse(mapping); + if (document.RootElement.ValueKind != JsonValueKind.Object) + { + return []; + } + + return document.RootElement.EnumerateObject() + .Where(property => property.Value.ValueKind == JsonValueKind.String + && !string.IsNullOrWhiteSpace(property.Name) + && !string.IsNullOrWhiteSpace(property.Value.GetString())) + .Select(property => new FormMappingDto + { + TargetField = property.Name, + SourceField = property.Value.GetString() ?? string.Empty + }) + .ToList(); + } + catch (JsonException) + { + return []; + } + } + + internal static string MergeSubmissionHeaderMapping( + string? existingMapping, + IEnumerable suggestions, + bool replaceExisting = false) + { + var mapping = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (!string.IsNullOrWhiteSpace(existingMapping)) + { + try + { + var existing = JsonSerializer.Deserialize>(existingMapping); + if (existing != null) + { + foreach (var pair in existing.Where(pair => + !string.IsNullOrWhiteSpace(pair.Key) && !string.IsNullOrWhiteSpace(pair.Value))) + { + mapping[pair.Key] = pair.Value; + } + } + } + catch (JsonException) + { + } + } + + foreach (var suggestion in suggestions) + { + if (!string.IsNullOrWhiteSpace(suggestion.TargetField) + && !string.IsNullOrWhiteSpace(suggestion.SourceField)) + { + if (replaceExisting) + { + foreach (var existing in mapping + .Where(pair => pair.Key.Equals(suggestion.TargetField, StringComparison.OrdinalIgnoreCase) + || pair.Value.Equals(suggestion.SourceField, StringComparison.OrdinalIgnoreCase)) + .Select(pair => pair.Key) + .ToList()) + { + mapping.Remove(existing); + } + + mapping[suggestion.TargetField] = suggestion.SourceField; + } + else if (!mapping.ContainsKey(suggestion.TargetField)) + { + mapping[suggestion.TargetField] = suggestion.SourceField; + } + } + } + + return JsonSerializer.Serialize(mapping); + } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Events/ScheduledNotificationEventHandler.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Events/ScheduledNotificationEventHandler.cs index 82200085a5..5e742ea5a5 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Events/ScheduledNotificationEventHandler.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Events/ScheduledNotificationEventHandler.cs @@ -9,6 +9,7 @@ using Unity.Notifications.Events; using Unity.Notifications.Settings; using Unity.Notifications.Templates; +using Unity.Payments.Events; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories; using Volo.Abp.EventBus; @@ -34,7 +35,7 @@ internal class ScheduledNotificationEventHandler( ICurrentTenant currentTenant, ScheduledNotificationHelper scheduledNotificationHelper, ILogger logger) - : ILocalEventHandler, ITransientDependency + : ILocalEventHandler, ILocalEventHandler, ITransientDependency { public async Task HandleEventAsync(ApplicationChangedEvent eventData) { @@ -58,6 +59,7 @@ public async Task HandleEventAsync(ApplicationChangedEvent eventData) n => n.FormId == application.ApplicationFormId && n.TriggerType == "Event" && n.IsActive + && (n.Module == null || n.Module == "Application") && n.ApplicationStatusId == application.ApplicationStatusId)) .ToList(); @@ -83,6 +85,53 @@ public async Task HandleEventAsync(ApplicationChangedEvent eventData) } } + public async Task HandleEventAsync(PaymentStatusChangedEvent eventData) + { + if (!await featureChecker.IsEnabledAsync("Unity.Notifications")) + { + return; + } + + try + { + var application = await applicationRepository.GetAsync(eventData.ApplicationId, includeDetails: true); + if (application == null) + { + logger.LogWarning("ScheduledNotificationEventHandler: Application {ApplicationId} not found for payment {PaymentRequestId}.", + eventData.ApplicationId, eventData.PaymentRequestId); + return; + } + + var notifications = (await scheduledNotificationRepository.GetListAsync( + n => n.FormId == application.ApplicationFormId + && n.TriggerType == "Event" + && n.IsActive + && n.Module == "Payment" + && n.EventType == eventData.Status.ToString())) + .ToList(); + + if (notifications.Count == 0) + { + return; + } + + var defaultFromAddress = await settingProvider.GetOrNullAsync(NotificationsSettings.Mailing.DefaultFromAddress); + string emailFrom = defaultFromAddress ?? "NoReply@gov.bc.ca"; + var applicantAgent = await applicantAgentRepository.FirstOrDefaultAsync(a => a.ApplicationId == application.Id); + + foreach (var notification in notifications) + { + await ProcessNotificationAsync(notification, application, applicantAgent, emailFrom); + } + } + catch (Exception ex) + { + logger.LogError(ex, + "ScheduledNotificationEventHandler: Error processing payment event for payment {PaymentRequestId}.", + eventData.PaymentRequestId); + } + } + private async Task ProcessNotificationAsync( ScheduledNotification notification, Application application, diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/AIGenerationPrerequisiteValidator.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/AIGenerationPrerequisiteValidator.cs index 36ec117a87..3241e7fb9e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/AIGenerationPrerequisiteValidator.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/AIGenerationPrerequisiteValidator.cs @@ -7,6 +7,7 @@ using Unity.AI.Operations; using Unity.Flex.Domain.Scoresheets; using Unity.GrantManager.Applications; +using Unity.GrantManager.ApplicationForms; using Volo.Abp; using Volo.Abp.DependencyInjection; using Volo.Abp.Linq; @@ -21,39 +22,32 @@ public class AIGenerationPrerequisiteValidator( IApplicationChefsFileAttachmentRepository applicationChefsFileAttachmentRepository, IScoresheetRepository scoresheetRepository, IAsyncQueryableExecuter asyncExecuter, - IStringLocalizer localizer) : IAIGenerationPrerequisiteValidator, ITransientDependency + IStringLocalizer localizer, + IGenerationReviewRepository generationReviewRepository) : IAIGenerationPrerequisiteValidator, ITransientDependency { - public Task EnsureAvailableAsync(string operationType, AIGenerationSubmissionDto request) + public Task EnsureAvailableAsync(string operationType, AIGenerationSubmissionDto request) => operationType switch { - return operationType switch - { - AIGenerationOperations.AttachmentSummary => EnsureAttachmentSummaryAvailableAsync(request.ApplicationId), - AIGenerationOperations.ApplicationAnalysis => EnsureApplicationAnalysisAvailableAsync(request.ApplicationId), - AIGenerationOperations.ApplicationScoring => EnsureApplicationScoringAvailableAsync(request.ApplicationId), - AIGenerationOperations.FormMapping => EnsureFormMappingAvailableAsync(request.ApplicationFormVersionId.GetValueOrDefault()), - AIGenerationOperations.FormWorksheet => EnsureFormWorksheetAvailableAsync(request.ApplicationFormVersionId.GetValueOrDefault()), - AIGenerationOperations.FormScoresheet => EnsureFormScoresheetAvailableAsync(request.ApplicationFormVersionId.GetValueOrDefault()), - _ => throw new UserFriendlyException($"Unsupported AI generation operation type: {operationType}") - }; - } + AIGenerationOperations.AttachmentSummary => EnsureAttachmentSummaryAvailableAsync(request.ApplicationId), + AIGenerationOperations.ApplicationAnalysis => EnsureApplicationAnalysisAvailableAsync(request.ApplicationId), + AIGenerationOperations.ApplicationScoring => EnsureApplicationScoringAvailableAsync(request.ApplicationId), + AIGenerationOperations.FormMapping => EnsureFormMappingAvailableAsync(request.ApplicationFormVersionId.GetValueOrDefault()), + AIGenerationOperations.FormWorksheet => EnsureFormWorksheetAvailableAsync(request.ApplicationFormVersionId.GetValueOrDefault()), + AIGenerationOperations.FormScoresheet => EnsureFormScoresheetAvailableAsync(request.ApplicationFormVersionId.GetValueOrDefault()), + _ => throw new UserFriendlyException($"Unsupported AI generation operation type: {operationType}") + }; public async Task EnsureAttachmentSummaryAvailableAsync(Guid applicationId) { var attachmentQuery = await applicationChefsFileAttachmentRepository.GetQueryableAsync(); var hasAttachments = await asyncExecuter.AnyAsync(attachmentQuery.Where(a => a.ApplicationId == applicationId)); - if (!hasAttachments) - { - throw new UserFriendlyException(localizer[AILocalizationKeys.NoAttachmentsAvailable]); - } + if (!hasAttachments) throw new UserFriendlyException(localizer[AILocalizationKeys.NoAttachmentsAvailable]); } public async Task EnsureApplicationAnalysisAvailableAsync(Guid applicationId) { var submission = await applicationFormSubmissionRepository.GetByApplicationAsync(applicationId); if (submission == null || string.IsNullOrWhiteSpace(submission.Submission)) - { throw new UserFriendlyException(localizer[AILocalizationKeys.ApplicationAnalysisRequiresSubmission]); - } } public async Task EnsureApplicationScoringAvailableAsync(Guid applicationId) @@ -61,41 +55,37 @@ public async Task EnsureApplicationScoringAvailableAsync(Guid applicationId) var application = await applicationRepository.GetAsync(applicationId); var applicationForm = await applicationFormRepository.GetAsync(application.ApplicationFormId); if (applicationForm.ScoresheetId == null) - { throw new UserFriendlyException(localizer[AILocalizationKeys.ScoringRequiresScoresheet]); - } - var scoresheet = await scoresheetRepository.GetWithChildrenAsync(applicationForm.ScoresheetId.Value); if (scoresheet == null || !scoresheet.Sections.Any() || !scoresheet.Sections.SelectMany(s => s.Fields).Any()) - { throw new UserFriendlyException(localizer[AILocalizationKeys.ScoringRequiresScoresheetFields]); - } } public async Task EnsureFormMappingAvailableAsync(Guid applicationFormVersionId) { var formVersion = await applicationFormVersionRepository.FindAsync(applicationFormVersionId); - if (formVersion == null) - { - throw new UserFriendlyException(localizer[AILocalizationKeys.FormMappingRequiresFormVersion]); - } + if (formVersion == null) throw new UserFriendlyException(localizer[AILocalizationKeys.FormMappingRequiresFormVersion]); + await EnsureNoActiveReviewAsync(AIGenerationOperations.FormMapping, applicationFormVersionId); } public async Task EnsureFormWorksheetAvailableAsync(Guid applicationFormVersionId) { var formVersion = await applicationFormVersionRepository.FindAsync(applicationFormVersionId); - if (formVersion == null) - { - throw new UserFriendlyException(localizer[AILocalizationKeys.FormWorksheetRequiresFormVersion]); - } + if (formVersion == null) throw new UserFriendlyException(localizer[AILocalizationKeys.FormWorksheetRequiresFormVersion]); + await EnsureNoActiveReviewAsync(AIGenerationOperations.FormWorksheet, applicationFormVersionId); } public async Task EnsureFormScoresheetAvailableAsync(Guid applicationFormVersionId) { var formVersion = await applicationFormVersionRepository.FindAsync(applicationFormVersionId); - if (formVersion == null) - { - throw new UserFriendlyException(localizer[AILocalizationKeys.FormScoresheetRequiresFormVersion]); - } + if (formVersion == null) throw new UserFriendlyException(localizer[AILocalizationKeys.FormScoresheetRequiresFormVersion]); + await EnsureNoActiveReviewAsync(AIGenerationOperations.FormScoresheet, applicationFormVersionId); + } + + private async Task EnsureNoActiveReviewAsync(string operation, Guid formVersionId) + { + var review = await generationReviewRepository.FindLatestByOperationAndFormVersionAsync(operation, formVersionId); + if (review?.Status == GenerationReviewStatus.Active) + throw new UserFriendlyException(localizer[AILocalizationKeys.FormGenerationReviewActive]); } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Generation/ApplicationAIGenerationQueue.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Generation/ApplicationAIGenerationQueue.cs index fa2331a192..0a530b6dca 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Generation/ApplicationAIGenerationQueue.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Generation/ApplicationAIGenerationQueue.cs @@ -138,6 +138,7 @@ private async Task EnsureRequestAndEnqueueAsync( var persistedOperation = await ResolveOperationAsync(operation); var requestLock = distributedLockProvider.CreateLock($"ai-generation:{tenantId}:{request.ApplicationId}:{persistedOperation.Id}"); + // The lock must cover the active-request check so each tenant/application/operation queues only once. using (await requestLock.AcquireAsync()) { var query = await generationRequestRepository.GetQueryableAsync(); @@ -159,8 +160,7 @@ private async Task EnsureRequestAndEnqueueAsync( await validateInput(); - // Single chokepoint for all AI generate flows (manual + auto). - // The limiter is a no-op for system/background callers without an authenticated user. + // Manual and automatic flows share this user-scoped limiter; system callers bypass it. await aiRateLimiter.EnsureAsync(currentUser.Id); var generationRequest = new AIGenerationRequest( @@ -180,6 +180,7 @@ await backgroundJobManager.EnqueueAsync(new AIGenerationBackgroundJobArgs ApplicationFormVersionId = request.ApplicationFormVersionId, AttachmentIds = request.AttachmentIds, OperationId = persistedOperation.Id, + GenerationRequestId = generationRequest.Id, PromptVersion = request.PromptVersion, RequestedByUserId = currentUser.Id, TenantId = tenantId diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Generation/BackgroundJobs/AIGenerationBackgroundJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Generation/BackgroundJobs/AIGenerationBackgroundJob.cs index 3252f0df84..f05621bc90 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Generation/BackgroundJobs/AIGenerationBackgroundJob.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Generation/BackgroundJobs/AIGenerationBackgroundJob.cs @@ -20,6 +20,7 @@ public sealed class AIGenerationBackgroundJob( { public override async Task ExecuteAsync(AIGenerationBackgroundJobArgs args) { + // The job owns tenant scope and request lifecycle; executors own AI input and result persistence. using var logScope = AIGenerationLogScope.Begin( logger, args.OperationType, @@ -35,7 +36,8 @@ await AIGenerationRequestJobHelper.MarkRunningInNewUowAsync( generationRequestRepository, args.TenantId, args.ApplicationId, - args.OperationId); + args.OperationId, + args.GenerationRequestId); try { @@ -55,7 +57,8 @@ await AIGenerationRequestJobHelper.MarkCompletedInNewUowAsync( generationRequestRepository, args.TenantId, args.ApplicationId, - args.OperationId); + args.OperationId, + args.GenerationRequestId); } catch (Exception ex) { @@ -65,7 +68,8 @@ await AIGenerationRequestJobHelper.MarkFailedInNewUowAsync( args.TenantId, args.ApplicationId, args.OperationId, - ex.Message); + ex.Message, + args.GenerationRequestId); throw; } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Generation/BackgroundJobs/AIGenerationRequestJobHelper.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Generation/BackgroundJobs/AIGenerationRequestJobHelper.cs index 2ab606a459..e51dea4f78 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Generation/BackgroundJobs/AIGenerationRequestJobHelper.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Generation/BackgroundJobs/AIGenerationRequestJobHelper.cs @@ -78,14 +78,11 @@ public static async Task MarkRunningInNewUowAsync( IRepository generationRequestRepository, Guid? tenantId, Guid applicationId, - Guid operationId) + Guid operationId, + Guid? generationRequestId = null) { using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false); - var request = await GetLatestRequestAsync( - generationRequestRepository, - x => x.TenantId == tenantId - && x.ApplicationId == applicationId - && x.OperationId == operationId); + var request = await GetRequestAsync(generationRequestRepository, generationRequestId, tenantId, applicationId, operationId); await MarkRunningAsync(generationRequestRepository, request); await uow.CompleteAsync(); } @@ -95,14 +92,11 @@ public static async Task MarkCompletedInNewUowAsync( IRepository generationRequestRepository, Guid? tenantId, Guid applicationId, - Guid operationId) + Guid operationId, + Guid? generationRequestId = null) { using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false); - var request = await GetLatestRequestAsync( - generationRequestRepository, - x => x.TenantId == tenantId - && x.ApplicationId == applicationId - && x.OperationId == operationId); + var request = await GetRequestAsync(generationRequestRepository, generationRequestId, tenantId, applicationId, operationId); await MarkCompletedAsync(generationRequestRepository, request); await uow.CompleteAsync(); } @@ -113,16 +107,31 @@ public static async Task MarkFailedInNewUowAsync( Guid? tenantId, Guid applicationId, Guid operationId, - string? failureReason) + string? failureReason, + Guid? generationRequestId = null) { using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false); - var request = await GetLatestRequestAsync( + var request = await GetRequestAsync(generationRequestRepository, generationRequestId, tenantId, applicationId, operationId); + await MarkFailedAsync(generationRequestRepository, request, failureReason); + await uow.CompleteAsync(); + } + private static async Task GetRequestAsync( + IRepository generationRequestRepository, + Guid? generationRequestId, + Guid? tenantId, + Guid applicationId, + Guid operationId) + { + if (generationRequestId.HasValue) + { + return await generationRequestRepository.FindAsync(generationRequestId.Value); + } + + return await GetLatestRequestAsync( generationRequestRepository, x => x.TenantId == tenantId && x.ApplicationId == applicationId && x.OperationId == operationId); - await MarkFailedAsync(generationRequestRepository, request, failureReason); - await uow.CompleteAsync(); } public static async Task StampCooldownBestEffortAsync( diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Generation/Handlers/QueueApplicationAIPipelineOnProcessHandler.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Generation/Handlers/QueueApplicationAIPipelineOnProcessHandler.cs index 2459de85c8..926ae0448d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Generation/Handlers/QueueApplicationAIPipelineOnProcessHandler.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Generation/Handlers/QueueApplicationAIPipelineOnProcessHandler.cs @@ -27,6 +27,7 @@ public async Task HandleEventAsync(ApplicationProcessEvent eventData) return; } + // Automatic generation requires tenant and form opt-in plus at least one enabled intake feature. var automaticGenerationEnabled = await settingProvider.GetAsync(AISettings.AutomaticGenerationEnabled, defaultValue: false); if (!automaticGenerationEnabled) { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Generation/README.md b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Generation/README.md new file mode 100644 index 0000000000..0dce7162d8 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Generation/README.md @@ -0,0 +1,13 @@ +# Grant Manager AI Generation + +This folder owns the Grant Manager side of AI generation. Unity.AI owns shared +contracts, runtime execution, and database configuration; Grant Manager owns request +queueing, background execution, application-specific input, and result persistence. + +`ApplicationGenerationQueue` serializes queueing per tenant, application, and +operation, then records one active request before enqueuing its job. +`AIGenerationBackgroundJob` owns tenant scope and request lifecycle state. +Operation executors own their input and persistence; they must not duplicate queue, +status, or cooldown behavior. + +See the shared [Unity.AI operation pipeline](../../../../../modules/Unity.AI/docs/operation-pipeline.md). diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Operations/FormMapping/FormMappingOperationExecutor.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Operations/FormMapping/FormMappingOperationExecutor.cs index 6ab7e487ca..53d2f83634 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Operations/FormMapping/FormMappingOperationExecutor.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Operations/FormMapping/FormMappingOperationExecutor.cs @@ -1,6 +1,9 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; +using System.Collections.Generic; using System.Threading.Tasks; +using System.Linq; +using System.Text.Json; using Unity.AI.Domain; using Unity.AI.Generation; using Unity.AI.Operations; @@ -9,11 +12,11 @@ using Unity.GrantManager.ApplicationForms; using Unity.GrantManager.ApplicationForms.Mapping; using Unity.GrantManager.Applications; -using Volo.Abp.Domain.Repositories; using Volo.Abp.BackgroundJobs; using Volo.Abp.DependencyInjection; using Volo.Abp.MultiTenancy; using Volo.Abp.Uow; +using Volo.Abp.Guids; using Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; @@ -22,7 +25,8 @@ namespace Unity.GrantManager.GrantApplications.Automation.Operations.FormMapping public sealed class FormMappingOperationExecutor( IApplicationFormVersionMappingReadService mappingReadService, IFormMappingService aiService, - IRepository applicationFormVersionRepository) : AIGenerationOperationExecutor, ITransientDependency + IGenerationReviewRepository generationReviewRepository, + IGuidGenerator guidGenerator) : AIGenerationOperationExecutor, ITransientDependency { public override string OperationType => AIGenerationOperations.FormMapping; @@ -31,17 +35,118 @@ protected override async Task ExecuteAsync(AIGenerationBackgroundJobArgs a var applicationFormVersionId = args.ApplicationFormVersionId ?? throw new InvalidOperationException("Form mapping generation requires an application form version."); var readModel = await mappingReadService.GetAsync(applicationFormVersionId); + var review = await generationReviewRepository.FindLatestByOperationAndFormVersionAsync( + AIGenerationOperations.FormMapping, + applicationFormVersionId); + if (review?.Status == GenerationReviewStatus.Active) + { + return false; + } + var response = await aiService.GenerateFormMappingAsync(new FormMappingRequest { Data = FormMappingPromptDataBuilder.Build(readModel), PromptVersion = args.PromptVersion }); - var submissionHeaderMapping = FormMappingResponseMapper.BuildSubmissionHeaderMapping(response); - var applicationFormVersion = await applicationFormVersionRepository.GetAsync(applicationFormVersionId); - applicationFormVersion.SubmissionHeaderMapping = submissionHeaderMapping; - await applicationFormVersionRepository.UpdateAsync(applicationFormVersion, true); + if (!string.IsNullOrWhiteSpace(response.FailureReason)) + { + throw new InvalidOperationException(response.FailureReason); + } + + if (review == null || review.Status != GenerationReviewStatus.Active) + { + var sequence = review?.Sequence + 1 ?? 1; + review = new GenerationReview( + guidGenerator.Create(), + AIGenerationOperations.FormMapping, + applicationFormVersionId, + sequence); + await generationReviewRepository.InsertAsync(review); + } + + var rawSuggestions = FormMappingResponseMapper.ParseSuggestions(response.Mapping) + .Select(suggestion => new FormMappingSuggestionDto + { + Id = guidGenerator.Create(), + SourceField = suggestion.SourceField, + TargetField = suggestion.TargetField, + Reason = suggestion.Reason, + Confidence = suggestion.Confidence + }) + .ToList(); + var isFinalMapping = review.Sequence > 1 && review.Sequence % 2 == 0; + var unchangedCount = 0; + var suggestions = isFinalMapping + ? ClassifyFinalSuggestions(readModel.ExistingMapping, rawSuggestions, out unchangedCount) + : rawSuggestions; + var payload = JsonSerializer.Deserialize(review.ReviewData) + ?? new FormMappingReviewPayload(); + payload.PendingSuggestions = suggestions; + payload.UnchangedSuggestionCount = isFinalMapping ? unchangedCount : 0; + payload.NoSuggestionsGenerated = suggestions.Count == 0; + if (suggestions.Count == 0) + { + review.Complete(); + } + review.SetReviewData(JsonSerializer.Serialize(payload)); + if (suggestions.Count > 0) + { + review.SetStatus(GenerationReviewStatus.Active); + } + await generationReviewRepository.UpdateAsync(review, true); return true; } + + internal static List ClassifyFinalSuggestions( + string? existingMapping, + List suggestions, + out int unchangedCount) + { + var existing = ParseMapping(existingMapping); + var bySource = existing.GroupBy(pair => pair.Value, StringComparer.OrdinalIgnoreCase) + .ToDictionary(group => group.Key, group => group.First().Key, StringComparer.OrdinalIgnoreCase); + var byTarget = existing.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); + unchangedCount = 0; + var actionable = new List(); + + foreach (var suggestion in suggestions) + { + if (bySource.TryGetValue(suggestion.SourceField, out var previousTarget) && + previousTarget.Equals(suggestion.TargetField, StringComparison.OrdinalIgnoreCase)) + { + unchangedCount++; + continue; + } + + suggestion.ChangeType = bySource.ContainsKey(suggestion.SourceField) ? "Changed" : "New"; + suggestion.PreviousTargetField = bySource.GetValueOrDefault(suggestion.SourceField); + suggestion.ConflictSourceField = byTarget.TryGetValue(suggestion.TargetField, out var conflictSource) && + !conflictSource.Equals(suggestion.SourceField, StringComparison.OrdinalIgnoreCase) + ? conflictSource + : null; + actionable.Add(suggestion); + } + + return actionable; + } + + private static Dictionary ParseMapping(string? mapping) + { + if (string.IsNullOrWhiteSpace(mapping)) + { + return new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + try + { + return JsonSerializer.Deserialize>(mapping) + ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + } + catch (JsonException) + { + return new Dictionary(StringComparer.OrdinalIgnoreCase); + } + } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Operations/FormScoresheet/FormScoresheetOperationExecutor.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Operations/FormScoresheet/FormScoresheetOperationExecutor.cs index 158ccd47ff..e256759e5a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Operations/FormScoresheet/FormScoresheetOperationExecutor.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Operations/FormScoresheet/FormScoresheetOperationExecutor.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Localization; using System; using System.Linq; using System.Text.Json; @@ -6,16 +7,20 @@ using Unity.AI.Domain; using Unity.AI.Generation; using Unity.AI.Operations; +using Unity.AI.Localization; using Unity.AI.Requests; using Unity.GrantManager.ApplicationForms; +using Unity.GrantManager.ApplicationForms.Mapping; using Unity.GrantManager.Applications; using Unity.Flex.Domain.Scoresheets; +using Unity.Flex.Domain.ScoresheetInstances; using Unity.Flex.Scoresheets; using Volo.Abp.BackgroundJobs; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories; using Volo.Abp.MultiTenancy; using Volo.Abp.Uow; +using Volo.Abp.Guids; using Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; @@ -25,7 +30,11 @@ public sealed class FormScoresheetOperationExecutor( IApplicationFormVersionRepository applicationFormVersionRepository, IApplicationFormRepository applicationFormRepository, IScoresheetRepository scoresheetRepository, - IFormScoresheetService aiService) : AIGenerationOperationExecutor, ITransientDependency + IScoresheetInstanceRepository scoresheetInstanceRepository, + IFormScoresheetService aiService, + IGenerationReviewRepository generationReviewRepository, + IGuidGenerator guidGenerator, + IStringLocalizer localizer) : AIGenerationOperationExecutor, ITransientDependency { private static readonly JsonSerializerOptions CaseInsensitiveJsonOptions = new() { @@ -37,14 +46,25 @@ public sealed class FormScoresheetOperationExecutor( protected override async Task ExecuteAsync(AIGenerationBackgroundJobArgs args) { var applicationFormVersionId = args.ApplicationFormVersionId - ?? throw new InvalidOperationException("Form scoresheet generation requires an application form version."); + ?? throw new InvalidOperationException(localizer[AILocalizationKeys.ScoresheetGenerationRequiresFormVersion]); var formVersion = await applicationFormVersionRepository.GetAsync(applicationFormVersionId); var applicationForm = await applicationFormRepository.GetAsync(formVersion.ApplicationFormId); - var scoresheetName = BuildScoresheetName(formVersion.Id, applicationForm.Id); - var existingScoresheet = await scoresheetRepository.GetByNameAsync(scoresheetName, true) - ?? (applicationForm.ScoresheetId.HasValue - ? await scoresheetRepository.GetWithChildrenAsync(applicationForm.ScoresheetId.Value) - : null); + var scoresheetName = AiScoresheetSuggestionName.Build(applicationForm.Id, formVersion.Id); + var existingScoresheet = await scoresheetRepository.GetByNameAsync(scoresheetName, true); + var review = await generationReviewRepository.FindLatestByOperationAndFormVersionAsync( + AIGenerationOperations.FormScoresheet, + applicationFormVersionId); + + if (review?.Status == GenerationReviewStatus.Active) + { + return false; + } + + if (existingScoresheet is { Published: true } || existingScoresheet?.IsArchived == true + || existingScoresheet != null && await scoresheetInstanceRepository.AnyByScoresheetAsync(existingScoresheet.Id)) + { + throw new InvalidOperationException(localizer[AILocalizationKeys.ScoresheetGenerationProtected]); + } var promptData = new { @@ -91,11 +111,35 @@ protected override async Task ExecuteAsync(AIGenerationBackgroundJobArgs a }); var scoresheetJson = scoresheetResponse.Scoresheet; + if (!string.IsNullOrWhiteSpace(scoresheetResponse.FailureReason)) + { + throw new InvalidOperationException( + localizer[AILocalizationKeys.ScoresheetGenerationInvalidOutput, scoresheetResponse.FailureReason]); + } + var importDto = ParseScoresheetDefinition(scoresheetJson); + var parsed = ParseScoresheetElement(scoresheetJson); + if (!HasGeneratedQuestions(parsed)) + { + if (review == null || review.Status != GenerationReviewStatus.Active) + { + review = new GenerationReview( + guidGenerator.Create(), + AIGenerationOperations.FormScoresheet, + applicationFormVersionId, + review?.Sequence + 1 ?? 1); + await generationReviewRepository.InsertAsync(review); + } + + review.Complete(); + await generationReviewRepository.UpdateAsync(review, true); + return false; + } + var scoresheet = existingScoresheet == null ? BuildScoresheet(importDto, scoresheetJson, scoresheetName) : RebuildScoresheet(existingScoresheet, importDto, scoresheetJson, scoresheetName); - scoresheet.Published = true; + scoresheet.Published = false; if (existingScoresheet == null) { await scoresheetRepository.InsertAsync(scoresheet); @@ -105,48 +149,52 @@ protected override async Task ExecuteAsync(AIGenerationBackgroundJobArgs a await scoresheetRepository.UpdateAsync(scoresheet); } - applicationForm.ScoresheetId = scoresheet.Id; - await applicationFormRepository.UpdateAsync(applicationForm); + if (review == null || review.Status != GenerationReviewStatus.Active) + { + review = new GenerationReview( + guidGenerator.Create(), + AIGenerationOperations.FormScoresheet, + applicationFormVersionId, + review?.Sequence + 1 ?? 1); + await generationReviewRepository.InsertAsync(review); + } + + await generationReviewRepository.UpdateAsync(review, true); return true; } - private static CreateScoresheetDto ParseScoresheetDefinition(string json) + private CreateScoresheetDto ParseScoresheetDefinition(string json) { if (string.IsNullOrWhiteSpace(json)) { - throw new InvalidOperationException("Scoresheet generation returned empty content."); + throw new InvalidOperationException(localizer[AILocalizationKeys.ScoresheetGenerationEmpty]); } var dto = JsonSerializer.Deserialize(json, CaseInsensitiveJsonOptions); if (dto == null || string.IsNullOrWhiteSpace(dto.Title) || string.IsNullOrWhiteSpace(dto.Name)) { - throw new InvalidOperationException("Scoresheet generation returned an unusable scoresheet definition."); + throw new InvalidOperationException(localizer[AILocalizationKeys.ScoresheetGenerationUnusable]); } return dto; } - private static string BuildScoresheetName(Guid formVersionId, Guid formId) - { - return $"ai-form-{formId}-version-{formVersionId}-scoresheet"; - } - - private static Scoresheet BuildScoresheet(CreateScoresheetDto dto, string json, string scoresheetName) + private Scoresheet BuildScoresheet(CreateScoresheetDto dto, string json, string scoresheetName) { var scoresheet = new Scoresheet(Guid.NewGuid(), dto.Title, scoresheetName); var parsed = ParseScoresheetElement(json); if (!TryGetNumberProperty(parsed, "Version", out var version)) { - throw new InvalidOperationException("Scoresheet generation returned a definition without a valid Version."); + throw new InvalidOperationException(localizer[AILocalizationKeys.ScoresheetGenerationNoVersion]); } scoresheet.Version = version; - if (!parsed.TryGetProperty("Sections", out var sectionsElement) || sectionsElement.ValueKind != JsonValueKind.Array) + if (!TryGetProperty(parsed, "Sections", out var sectionsElement) || sectionsElement.ValueKind != JsonValueKind.Array) { - throw new InvalidOperationException("Scoresheet generation returned a definition without Sections."); + throw new InvalidOperationException(localizer[AILocalizationKeys.ScoresheetGenerationNoSections]); } foreach (var section in sectionsElement.EnumerateArray()) @@ -156,9 +204,9 @@ private static Scoresheet BuildScoresheet(CreateScoresheetDto dto, string json, var scoresheetSection = new ScoresheetSection(Guid.NewGuid(), sectionName, sectionOrder); scoresheet.AddSection(scoresheetSection); - if (!section.TryGetProperty("Fields", out var fieldsElement) || fieldsElement.ValueKind != JsonValueKind.Array) + if (!TryGetProperty(section, "Fields", out var fieldsElement) || fieldsElement.ValueKind != JsonValueKind.Array) { - throw new InvalidOperationException($"Scoresheet generation returned section '{sectionName}' without Fields."); + throw new InvalidOperationException(localizer[AILocalizationKeys.ScoresheetGenerationSectionNoFields, sectionName]); } foreach (var field in fieldsElement.EnumerateArray()) @@ -169,10 +217,10 @@ private static Scoresheet BuildScoresheet(CreateScoresheetDto dto, string json, GetRequiredStringProperty(field, "Label", "field"), (Unity.Flex.Scoresheets.Enums.QuestionType)GetRequiredNumberProperty(field, "Type", "field"), GetRequiredNumberProperty(field, "Order", "field"), - field.TryGetProperty("Description", out var description) && description.ValueKind != JsonValueKind.Null + TryGetProperty(field, "Description", out var description) && description.ValueKind != JsonValueKind.Null ? description.GetString() : null, - field.TryGetProperty("Definition", out var definition) && definition.ValueKind != JsonValueKind.Null + TryGetProperty(field, "Definition", out var definition) && definition.ValueKind != JsonValueKind.Null ? definition.GetString() : null); question.SectionId = scoresheetSection.Id; @@ -188,12 +236,12 @@ private static Scoresheet BuildScoresheet(CreateScoresheetDto dto, string json, return scoresheet; } - private static Scoresheet RebuildScoresheet(Scoresheet scoresheet, CreateScoresheetDto dto, string json, string scoresheetName) + private Scoresheet RebuildScoresheet(Scoresheet scoresheet, CreateScoresheetDto dto, string json, string scoresheetName) { var parsed = ParseScoresheetElement(json); if (!TryGetNumberProperty(parsed, "Version", out var version)) { - throw new InvalidOperationException("Scoresheet generation returned a definition without a valid Version."); + throw new InvalidOperationException(localizer[AILocalizationKeys.ScoresheetGenerationNoVersion]); } scoresheet.SetName(scoresheetName); @@ -206,9 +254,9 @@ private static Scoresheet RebuildScoresheet(Scoresheet scoresheet, CreateScoresh scoresheet.Sections.Clear(); - if (!parsed.TryGetProperty("Sections", out var sectionsElement) || sectionsElement.ValueKind != JsonValueKind.Array) + if (!TryGetProperty(parsed, "Sections", out var sectionsElement) || sectionsElement.ValueKind != JsonValueKind.Array) { - throw new InvalidOperationException("Scoresheet generation returned a definition without Sections."); + throw new InvalidOperationException(localizer[AILocalizationKeys.ScoresheetGenerationNoSections]); } foreach (var section in sectionsElement.EnumerateArray()) @@ -218,9 +266,9 @@ private static Scoresheet RebuildScoresheet(Scoresheet scoresheet, CreateScoresh var scoresheetSection = new ScoresheetSection(Guid.NewGuid(), sectionName, sectionOrder); scoresheet.AddSection(scoresheetSection); - if (!section.TryGetProperty("Fields", out var fieldsElement) || fieldsElement.ValueKind != JsonValueKind.Array) + if (!TryGetProperty(section, "Fields", out var fieldsElement) || fieldsElement.ValueKind != JsonValueKind.Array) { - throw new InvalidOperationException($"Scoresheet generation returned section '{sectionName}' without Fields."); + throw new InvalidOperationException(localizer[AILocalizationKeys.ScoresheetGenerationSectionNoFields, sectionName]); } foreach (var field in fieldsElement.EnumerateArray()) @@ -231,10 +279,10 @@ private static Scoresheet RebuildScoresheet(Scoresheet scoresheet, CreateScoresh GetRequiredStringProperty(field, "Label", "field"), (Unity.Flex.Scoresheets.Enums.QuestionType)GetRequiredNumberProperty(field, "Type", "field"), GetRequiredNumberProperty(field, "Order", "field"), - field.TryGetProperty("Description", out var description) && description.ValueKind != JsonValueKind.Null + TryGetProperty(field, "Description", out var description) && description.ValueKind != JsonValueKind.Null ? description.GetString() : null, - field.TryGetProperty("Definition", out var definition) && definition.ValueKind != JsonValueKind.Null + TryGetProperty(field, "Definition", out var definition) && definition.ValueKind != JsonValueKind.Null ? definition.GetString() : null); question.SectionId = scoresheetSection.Id; @@ -245,6 +293,16 @@ private static Scoresheet RebuildScoresheet(Scoresheet scoresheet, CreateScoresh return scoresheet; } + private static bool HasGeneratedQuestions(JsonElement parsed) + { + return TryGetProperty(parsed, "Sections", out var sections) + && sections.ValueKind == JsonValueKind.Array + && sections.EnumerateArray().Any(section => + TryGetProperty(section, "Fields", out var fields) + && fields.ValueKind == JsonValueKind.Array + && fields.GetArrayLength() > 0); + } + private static JsonElement ParseScoresheetElement(string json) { return JsonSerializer.Deserialize(json, CaseInsensitiveJsonOptions); @@ -252,7 +310,7 @@ private static JsonElement ParseScoresheetElement(string json) private static bool TryGetNumberProperty(JsonElement element, string propertyName, out uint value) { - if (element.TryGetProperty(propertyName, out var property) && property.ValueKind == JsonValueKind.Number) + if (TryGetProperty(element, propertyName, out var property) && property.ValueKind == JsonValueKind.Number) { value = property.GetUInt32(); return true; @@ -262,25 +320,48 @@ private static bool TryGetNumberProperty(JsonElement element, string propertyNam return false; } - private static string GetRequiredStringProperty(JsonElement element, string propertyName, string sourceName, bool allowEmpty = false) + private string GetRequiredStringProperty(JsonElement element, string propertyName, string sourceName, bool allowEmpty = false) { - if (element.TryGetProperty(propertyName, out var property) + if (TryGetProperty(element, propertyName, out var property) && property.ValueKind == JsonValueKind.String && (allowEmpty || !string.IsNullOrWhiteSpace(property.GetString()))) { return property.GetString()!; } - throw new InvalidOperationException($"Scoresheet generation returned a {sourceName} without a valid {propertyName}."); + throw new InvalidOperationException(localizer[AILocalizationKeys.ScoresheetGenerationPropertyInvalid, sourceName, propertyName]); + } + + private static bool TryGetProperty(JsonElement element, string propertyName, out JsonElement property) + { + if (element.ValueKind == JsonValueKind.Object && element.TryGetProperty(propertyName, out property)) + { + return true; + } + + if (element.ValueKind == JsonValueKind.Object) + { + foreach (var candidate in element.EnumerateObject()) + { + if (string.Equals(candidate.Name, propertyName, StringComparison.OrdinalIgnoreCase)) + { + property = candidate.Value; + return true; + } + } + } + + property = default; + return false; } - private static uint GetRequiredNumberProperty(JsonElement element, string propertyName, string sourceName) + private uint GetRequiredNumberProperty(JsonElement element, string propertyName, string sourceName) { if (TryGetNumberProperty(element, propertyName, out var value)) { return value; } - throw new InvalidOperationException($"Scoresheet generation returned a {sourceName} without a valid {propertyName}."); + throw new InvalidOperationException(localizer[AILocalizationKeys.ScoresheetGenerationPropertyInvalid, sourceName, propertyName]); } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Operations/FormWorksheet/FormWorksheetOperationExecutor.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Operations/FormWorksheet/FormWorksheetOperationExecutor.cs index a4c8987cc7..c96228a233 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Operations/FormWorksheet/FormWorksheetOperationExecutor.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Operations/FormWorksheet/FormWorksheetOperationExecutor.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; @@ -20,6 +20,7 @@ using Volo.Abp.Domain.Repositories; using Volo.Abp.MultiTenancy; using Volo.Abp.Uow; +using Volo.Abp.Guids; using Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; @@ -31,6 +32,8 @@ public sealed class FormWorksheetOperationExecutor( IWorksheetRepository worksheetRepository, IApplicationFormVersionMappingReadService mappingReadService, IFormWorksheetService aiService, + IGenerationReviewRepository generationReviewRepository, + IGuidGenerator guidGenerator, ILogger logger) : AIGenerationOperationExecutor, ITransientDependency { private static readonly JsonSerializerOptions CaseInsensitiveJsonOptions = new() @@ -46,22 +49,25 @@ protected override async Task ExecuteAsync(AIGenerationBackgroundJobArgs a ?? throw new InvalidOperationException("Form worksheet generation requires an application form version."); var formVersion = await applicationFormVersionRepository.GetAsync(applicationFormVersionId); var applicationForm = await applicationFormRepository.GetAsync(formVersion.ApplicationFormId); - var worksheetName = AiWorksheetSuggestionName.Build(applicationForm.Id, formVersion.Id); - var existingWorksheet = await worksheetRepository.GetByNameAsync(worksheetName, true); + var baseWorksheetName = AiWorksheetSuggestionName.Build(applicationForm.Id, formVersion.Id); + var review = await generationReviewRepository.FindLatestByOperationAndFormVersionAsync( + AIGenerationOperations.FormWorksheet, + applicationFormVersionId); + if (review?.Status == GenerationReviewStatus.Active) + { + return false; + } + + var existingWorksheet = await worksheetRepository.GetByNameAsync(baseWorksheetName, true); + var worksheetName = existingWorksheet?.Name ?? baseWorksheetName; + EnsureCanonicalSuggestionWorksheetState(existingWorksheet); + var noSuggestionsGenerated = existingWorksheet != null && + existingWorksheet.Sections.SelectMany(section => section.Fields).Any() == false; if (existingWorksheet != null) { - if (existingWorksheet.Published) - { - logger.LogWarning( - "A published worksheet already uses AI suggestion name {WorksheetName}; leaving it unchanged.", - worksheetName); - } - else - { - logger.LogInformation( - "An AI suggestion worksheet is pending review for form version {FormVersionId}; leaving it unchanged.", - formVersion.Id); - } + logger.LogInformation( + "An AI suggestion worksheet is pending review for form version {FormVersionId}; leaving it unchanged.", + formVersion.Id); } else { @@ -94,16 +100,55 @@ protected override async Task ExecuteAsync(AIGenerationBackgroundJobArgs a PromptVersion = args.PromptVersion }); + if (!string.IsNullOrWhiteSpace(worksheetResponse.FailureReason)) + { + throw new InvalidOperationException(worksheetResponse.FailureReason); + } + var suggestions = ParseWorksheetDefinition(worksheetResponse.Worksheet); - var worksheet = BuildWorksheet(suggestions, worksheetName); - worksheet.SetPublished(false); - await worksheetRepository.InsertAsync(worksheet); + noSuggestionsGenerated = suggestions.Count == 0; + if (!noSuggestionsGenerated) + { + var worksheet = BuildWorksheet(suggestions, worksheetName); + worksheet.SetPublished(false); + await worksheetRepository.InsertAsync(worksheet); + } } + if (review == null || review.Status != GenerationReviewStatus.Active) + { + review = new GenerationReview( + guidGenerator.Create(), + AIGenerationOperations.FormWorksheet, + applicationFormVersionId, + review?.Sequence + 1 ?? 1); + await generationReviewRepository.InsertAsync(review); + } + + if (noSuggestionsGenerated) + { + review.SetReviewData(JsonSerializer.Serialize(new FormWorksheetReviewPayload + { + NoSuggestionsGenerated = true + })); + review.Complete(); + } + + await generationReviewRepository.UpdateAsync(review, true); + return existingWorksheet == null; } + internal static void EnsureCanonicalSuggestionWorksheetState(Worksheet? worksheet) + { + if (worksheet?.Published == true) + { + throw new InvalidOperationException( + "The canonical AI suggestion worksheet is published and cannot be regenerated."); + } + } + internal static List ParseWorksheetDefinition(string json) { if (string.IsNullOrWhiteSpace(json)) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs index f1ce5f881a..fc36d12956 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs @@ -12,9 +12,10 @@ using System.Linq; using System.Text.Json; using System.Threading.Tasks; +using Unity.AI.Generation; using Unity.AI.Models; -using Unity.AI.Permissions; using Unity.AI.Responses; +using Unity.AI.Settings; using Unity.Flex.WorksheetInstances; using Unity.Flex.Worksheets; using Unity.GrantManager.Applicants; @@ -25,7 +26,6 @@ using Unity.GrantManager.GlobalTag; using Unity.GrantManager.Identity; using Unity.GrantManager.Payments; -using Unity.GrantManager.GrantApplications.Automation; using Unity.Modules.Shared; using Unity.Modules.Shared.Correlation; using Unity.Modules.Shared.Specializations; @@ -48,7 +48,6 @@ namespace Unity.GrantManager.GrantApplications; public class GrantApplicationAppService( IApplicationManager applicationManager, IApplicationRepository applicationRepository, - IApplicationChefsFileAttachmentRepository applicationChefsFileAttachmentRepository, IApplicationStatusRepository applicationStatusRepository, IApplicationFormSubmissionRepository applicationFormSubmissionRepository, IApplicantRepository applicantRepository, @@ -57,7 +56,8 @@ public class GrantApplicationAppService( IApplicantAddressRepository applicantAddressRepository, IApplicantSupplierAppService applicantSupplierService, IPaymentRequestAppService paymentRequestService, - IFeatureChecker featureChecker) + IFeatureChecker featureChecker, + AIFeatureGuard aiFeatureGuard) : GrantManagerAppService, IGrantApplicationAppService #pragma warning restore S107 // Methods should not have too many parameters { @@ -71,6 +71,25 @@ public class GrantApplicationAppService( WriteIndented = true }; + private async Task EnsureAiOperationAccessAsync(string operationType, bool requiresGeneratePermission) + { + var operation = AIGenerationOperations.Get(operationType); + await aiFeatureGuard.EnsureEnabledAsync(operation.FeatureName, operation.DisabledLocalizationKey); + await CheckPolicyAsync(requiresGeneratePermission ? operation.GeneratePermission : operation.ViewPermission); + } + + private async Task CanAccessAiOperationAsync(string operationType, bool requiresGeneratePermission) + { + var operation = AIGenerationOperations.Get(operationType); + if (!await featureChecker.IsEnabledAsync(operation.FeatureName)) + { + return false; + } + + var permission = requiresGeneratePermission ? operation.GeneratePermission : operation.ViewPermission; + return await AuthorizationService.IsGrantedAsync(permission); + } + public async Task> GetListAsync(GrantApplicationListInputDto input) { var listRecords = await applicationRepository.GetApplicationListRecordsAsync( @@ -159,9 +178,11 @@ public async Task> GetListAsync(GrantApplica RiskRanking = rec.RiskRanking, UnityApplicationId = rec.UnityApplicationId, ExternalStatusVisibility = rec.ExternalStatusVisibility, - + // From ApplicationStatus Status = rec.Status, + ExternalStatus = rec.ExternalStatus, + PublishedStatus = rec.PublishedStatus, // From ApplicationForm Category = rec.Category, @@ -352,7 +373,15 @@ public async Task GetAsync(Guid id) appDto.SectorSubSectorIndustryDesc = application.Applicant.SectorSubSectorIndustryDesc; } - appDto.AIAnalysisData = ParseAiAnalysisData(appDto.AIAnalysis); + if (await CanAccessAiOperationAsync(AIGenerationOperations.ApplicationAnalysis, requiresGeneratePermission: false)) + { + appDto.AIAnalysisData = ParseAiAnalysisData(appDto.AIAnalysis); + } + else + { + appDto.AIAnalysis = null; + appDto.AIAnalysisData = null; + } return appDto; } @@ -1200,65 +1229,6 @@ await LocalEventBus.PublishAsync( return applicationManager.GetWorkflowDiagram(isDirectApproval); } - private async Task EnsureAttachmentSummariesEnabledAsync() - { - if (!await featureChecker.IsEnabledAsync("Unity.AI.AttachmentSummaries")) - { - throw new UserFriendlyException("AI attachment summaries are not enabled."); - } - } - - private async Task> ResolveAttachmentSummaryIdsAsync(QueueAttachmentSummaryRequestDto input) - { - if (input == null) - { - throw new UserFriendlyException("Attachment summary request is required."); - } - - if (input.ApplicationId == Guid.Empty) - { - throw new UserFriendlyException("Application id is required."); - } - - var applicationAttachmentIds = (await applicationChefsFileAttachmentRepository.GetListAsync(a => a.ApplicationId == input.ApplicationId)) - .Select(a => a.Id) - .ToList(); - - if (applicationAttachmentIds.Count == 0) - { - throw new UserFriendlyException("No attachments were found to generate summaries."); - } - - if (input.AttachmentIds is not { Count: > 0 }) - { - return applicationAttachmentIds; - } - - var applicationAttachmentIdSet = applicationAttachmentIds.ToHashSet(); - var selectedAttachmentIds = input.AttachmentIds.Distinct().ToList(); - if (selectedAttachmentIds.Any(id => !applicationAttachmentIdSet.Contains(id))) - { - throw new UserFriendlyException("One or more selected attachments do not belong to the application."); - } - - return selectedAttachmentIds; - } - - private async Task EnsureAIAnalysisEnabledAsync() - { - if (!await featureChecker.IsEnabledAsync("Unity.AI.ApplicationAnalysis")) - { - throw new UserFriendlyException("AI application analysis is not enabled."); - } - } - - private async Task EnsureScoringEnabledAsync() - { - if (!await featureChecker.IsEnabledAsync("Unity.AI.Scoring")) - { - throw new UserFriendlyException("AI scoring is not enabled."); - } - } #endregion APPLICATION WORKFLOW public async Task> GetAllApplicationsAsync() @@ -1329,11 +1299,13 @@ private static Dictionary ExtractCustomFieldsForWorksheet(dynami public async Task DismissAIAnalysisItemAsync(Guid applicationId, string itemId) { + await EnsureAiOperationAccessAsync(AIGenerationOperations.ApplicationAnalysis, requiresGeneratePermission: true); return await UpdateAIAnalysisItemDismissedStateAsync(applicationId, itemId, isDismissed: true); } public async Task RestoreAIAnalysisItemAsync(Guid applicationId, string itemId) { + await EnsureAiOperationAccessAsync(AIGenerationOperations.ApplicationAnalysis, requiresGeneratePermission: true); return await UpdateAIAnalysisItemDismissedStateAsync(applicationId, itemId, isDismissed: false); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationMapperlyProfile.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationMapperlyProfile.cs index 01bd3c112c..2da0edb47e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationMapperlyProfile.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationMapperlyProfile.cs @@ -345,11 +345,15 @@ public partial class ApplicationFormToDtoMapper : MapperBase { public override partial ApplicationFormVersionDto Map(ApplicationFormVersion source); public override partial void Map(ApplicationFormVersion source, ApplicationFormVersionDto destination); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Identity/UserImportAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Identity/UserImportAppService.cs index 0fdbe22eaf..e2aa151325 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Identity/UserImportAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Identity/UserImportAppService.cs @@ -20,6 +20,7 @@ public class UserImportAppService : GrantManagerAppService, IUserImportAppServic private readonly ICurrentTenant _currentTenant; private readonly IdentityUserManager _userManager; private readonly IPersonRepository _personRepository; + private readonly IUserAccountsRepository _userAccountsRepository; private readonly IIdentityUserRepository _identityUserRepository; private readonly IDataFilter _dataFilter; @@ -27,6 +28,7 @@ public UserImportAppService(ICssUsersApiService cssUsersApiService, ICurrentTenant currentTenant, IdentityUserManager userManager, IPersonRepository personRepository, + IUserAccountsRepository userAccountsRepository, IIdentityUserRepository identityUserRepository, IDataFilter dataFilter) { @@ -34,6 +36,7 @@ public UserImportAppService(ICssUsersApiService cssUsersApiService, _currentTenant = currentTenant; _userManager = userManager; _personRepository = personRepository; + _userAccountsRepository = userAccountsRepository; _identityUserRepository = identityUserRepository; _dataFilter = dataFilter; } @@ -47,15 +50,17 @@ public UserImportAppService(ICssUsersApiService cssUsersApiService, /// public async Task ImportUserAsync(ImportUserDto importUserDto) { - var newUserId = Guid.NewGuid(); - var result = await _cssUsersApiService.FindUserAsync(importUserDto.Directory, importUserDto.Guid); if (result.Data == null || result.Data.Length == 0) throw new AbpValidationException(); var cssUser = result.Data[0]; + var oidcSub = (cssUser.Attributes?.IdirUserGuid?[0] ?? Guid.NewGuid().ToString()).ToSubjectWithoutIdp(); + var existingPerson = await _personRepository.FindByOidcSub(oidcSub); + var newUserId = existingPerson?.Id ?? Guid.NewGuid(); - IdentityUser? identityUser = await ReactivateAndGetDeletedUserAsync(cssUser.Attributes?.IdirUsername?[0] ?? throw new AbpValidationException()); + IdentityUser? identityUser = await ReactivateAndGetDeletedUserAsync( + cssUser.Attributes?.IdirUsername?[0] ?? throw new AbpValidationException(), oidcSub, cssUser.FirstName, cssUser.LastName); identityUser ??= await CreateNewIdentityUserAsync(newUserId, cssUser.Attributes?.IdirUsername?[0], cssUser.FirstName, cssUser.LastName, cssUser.Email); if (identityUser == null) throw new UserFriendlyException("Error creating user account"); @@ -67,7 +72,6 @@ public async Task ImportUserAsync(ImportUserDto importUserDto) await _userManager.AddToRolesAsync(identityUser, importUserDto.Roles); } - var oidcSub = (cssUser.Attributes?.IdirUserGuid?[0] ?? newUserId.ToString()).ToSubjectWithoutIdp(); var displayName = cssUser.Attributes?.DisplayName?[0] ?? identityUser.NormalizedUserName.ToString(); await UpdateAdditionalUserPropertiesAsync(identityUser, oidcSub, displayName); @@ -89,9 +93,11 @@ public async Task AutoImportUserInternalAsync(ImportUserDto importUserDto, string oidcSub, string displayName) { - var newUserId = Guid.NewGuid(); + var normalizedOidcSub = oidcSub.ToSubjectWithoutIdp(); + var existingPerson = await _personRepository.FindByOidcSub(normalizedOidcSub); + var newUserId = existingPerson?.Id ?? Guid.NewGuid(); - IdentityUser? identityUser = await ReactivateAndGetDeletedUserAsync(username); + IdentityUser? identityUser = await ReactivateAndGetDeletedUserAsync(username, normalizedOidcSub, firstName, lastName); identityUser ??= await CreateNewIdentityUserAsync(newUserId, username, firstName, lastName, emailAddress); if (identityUser == null) throw new UserFriendlyException("Error creating user account"); @@ -103,8 +109,8 @@ public async Task AutoImportUserInternalAsync(ImportUserDto importUserDto, await _userManager.AddToRolesAsync(identityUser, importUserDto.Roles); } - await UpdateAdditionalUserPropertiesAsync(identityUser, oidcSub, displayName); - await SyncUserToCurrentTenantAsync(identityUser, oidcSub, displayName); + await UpdateAdditionalUserPropertiesAsync(identityUser, normalizedOidcSub, displayName); + await SyncUserToCurrentTenantAsync(identityUser, normalizedOidcSub, displayName); } /// @@ -217,7 +223,7 @@ public async Task SetUserRolesAsync(Guid userId, string[] roleNames) return identityUser; } - private async Task ReactivateAndGetDeletedUserAsync(string username) + private async Task ReactivateAndGetDeletedUserAsync(string username, string oidcSub, string? firstName, string? lastName) { //Temporary disable the ISoftDelete filter - find delete user account and reactivate for import using (_dataFilter.Disable()) @@ -225,8 +231,21 @@ public async Task SetUserRolesAsync(Guid userId, string[] roleNames) var identityUser = await _identityUserRepository .FindByTenantIdAndUserNameAsync(username, _currentTenant.Id); + identityUser ??= (await _userAccountsRepository.GetListByOidcSub(oidcSub)) + .FirstOrDefault(); + if (identityUser != null) { + // Directory details (e.g. surname) can change between imports - keep the + // reactivated account's name/username current rather than leaving it stale. + if (!string.IsNullOrWhiteSpace(username) && + !string.Equals(identityUser.UserName, username, StringComparison.OrdinalIgnoreCase)) + { + await _userManager.SetUserNameAsync(identityUser, username); + } + + identityUser.Name = firstName ?? identityUser.Name; + identityUser.Surname = lastName ?? identityUser.Surname; identityUser.IsDeleted = false; identityUser.DeleterId = null; identityUser.DeletionTime = null; @@ -236,12 +255,12 @@ public async Task SetUserRolesAsync(Guid userId, string[] roleNames) } return null; - } + } private async Task SyncUserToCurrentTenantAsync(IdentityUser user, string oidcSub, string displayName) { - var existingUser = await _personRepository.FindByOidcSub(oidcSub); - if (existingUser == null) + var existingPerson = await _personRepository.FindByOidcSub(oidcSub); + if (existingPerson == null) { await _personRepository.InsertAsync(new Person() { @@ -251,7 +270,13 @@ await _personRepository.InsertAsync(new Person() FullName = $"{user.Name} {user.Surname}", Badge = Utils.CreateUserBadge(user) }); + return; } + + existingPerson.OidcDisplayName = displayName; + existingPerson.FullName = $"{user.Name} {user.Surname}"; + existingPerson.Badge = Utils.CreateUserBadge(user); + await _personRepository.UpdateAsync(existingPerson); } private async Task UpdateAdditionalUserPropertiesAsync(IdentityUser user, string oidcSub, string displayName) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs index 5c03430b2e..c369d76dc6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs @@ -15,7 +15,8 @@ namespace Unity.GrantManager.Integrations.Endpoints { public class EndpointManagementAppService( IRepository repository, - IDistributedCache cache) : + IDistributedCache cache, + IUnitOfWorkManager unitOfWorkManager) : CrudAppService< DynamicUrl, DynamicUrlDto, @@ -25,6 +26,7 @@ public class EndpointManagementAppService( IEndpointManagementAppService { private readonly IDistributedCache _cache = cache; + private readonly IUnitOfWorkManager _unitOfWorkManager = unitOfWorkManager; private const string CACHE_KEY_SET_PREFIX = "DynamicUrl:KeySet"; private static string BuildCacheKey(string keyName, bool tenantSpecific, Guid? tenantId) @@ -156,13 +158,17 @@ await _cache.SetStringAsync( private async Task GetUrlValueAsync(string keyName, Guid? tenantId) { + using var uow = _unitOfWorkManager.Begin(requiresNew: true, isTransactional: false); var queryable = await Repository.GetQueryableAsync(); - return await AsyncExecuter.FirstOrDefaultAsync( + var url = await AsyncExecuter.FirstOrDefaultAsync( queryable .AsNoTracking() .Where(x => x.KeyName == keyName && x.TenantId == tenantId) .Select(x => x.Url)); + + await uow.CompleteAsync(); + return url; } // ------------------------------ diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Notifications/AutomatedNotificationAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Notifications/AutomatedNotificationAppService.cs index fba613275c..b7b5c0ac56 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Notifications/AutomatedNotificationAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Notifications/AutomatedNotificationAppService.cs @@ -19,6 +19,7 @@ public async Task CreateAsync(CreateUpdateNotificationDto input FormId = input.FormId, EmailTemplateId = input.EmailTemplateId, TriggerType = input.TriggerType, + Module = input.Module, TriggerDetail = input.TriggerDetail, IsActive = input.IsActive, EventType = input.EventType, @@ -38,6 +39,7 @@ public async Task CreateAsync(CreateUpdateNotificationDto input EmailTemplateId = entity.EmailTemplateId, TemplateName = null, TriggerType = entity.TriggerType, + Module = entity.Module, TriggerDetail = entity.TriggerDetail, IsActive = entity.IsActive, EventType = entity.EventType, @@ -69,6 +71,7 @@ public async Task GetAsync(Guid id) EmailTemplateId = e.EmailTemplateId, TemplateName = null, TriggerType = e.TriggerType, + Module = e.Module, TriggerDetail = e.TriggerDetail, IsActive = e.IsActive, EventType = e.EventType, @@ -104,6 +107,7 @@ public async Task> GetListAsync(GetNotifications EmailTemplateId = e.EmailTemplateId, TemplateName = null, TriggerType = e.TriggerType, + Module = e.Module, TriggerDetail = e.TriggerDetail, IsActive = e.IsActive, EventType = e.EventType, @@ -122,6 +126,7 @@ public async Task UpdateAsync(Guid id, CreateUpdateNotification var e = await _repository.GetAsync(id); e.EmailTemplateId = input.EmailTemplateId; e.TriggerType = input.TriggerType; + e.Module = input.Module; e.TriggerDetail = input.TriggerDetail; e.IsActive = input.IsActive; e.EventType = input.EventType; @@ -140,6 +145,7 @@ public async Task UpdateAsync(Guid id, CreateUpdateNotification EmailTemplateId = e.EmailTemplateId, TemplateName = null, TriggerType = e.TriggerType, + Module = e.Module, TriggerDetail = e.TriggerDetail, IsActive = e.IsActive, EventType = e.EventType, diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.DbMigrator/README.md b/applications/Unity.GrantManager/src/Unity.GrantManager.DbMigrator/README.md index 9aa057d9fd..bc1982b52d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.DbMigrator/README.md +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.DbMigrator/README.md @@ -51,4 +51,10 @@ Once you've configured your connection strings via `appsettings.secrets.json` (o dotnet run ``` +Migration history flattening is disabled by default. To reconcile databases that still +contain migration ids from the removed migration set, run the migrator once with +`Database__FlattenMigrations=true` (or set `Database:FlattenMigrations` to `true` in +`appsettings.secrets.json`). Do not leave this enabled for normal migration runs: it +deletes migration history rows that were added after the flattened `Initial` migration. + Or run it from Visual Studio by setting `Unity.GrantManager.DbMigrator` as the startup project. diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.DbMigrator/appsettings.json b/applications/Unity.GrantManager/src/Unity.GrantManager.DbMigrator/appsettings.json index 325101ebd6..1e3abd26c8 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.DbMigrator/appsettings.json +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.DbMigrator/appsettings.json @@ -4,6 +4,9 @@ "Tenant": "Host=localhost;port=5432;Database=UnityGrantTenant;Username=postgres;", "Onboarding": "Host=localhost;port=5432;Database=Onboarding;Username=postgres;" }, + "Database": { + "FlattenMigrations": false + }, "StringEncryption": { "DefaultPassPhrase": "g2IuZx7PwXDvCmlW" }, diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/ApplicantProfile/ExternalLink.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/ApplicantProfile/ExternalLink.cs new file mode 100644 index 0000000000..16dfe181b4 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/ApplicantProfile/ExternalLink.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; + +namespace Unity.GrantManager.ApplicantProfile; + +/// +/// Represents a link to be used within the Applicant Portal, including the URI, title, and description. +/// +public class ExternalLink +{ + [MaxLength(2048)] + public required string Uri { get; set; } + + [MaxLength(255)] + public string Title { get; set; } = string.Empty; + + [MaxLength(512)] + public string Description { get; set; } = string.Empty; + + public ExternalLinkType ExternalLinkType { get; set; } = ExternalLinkType.Related; + public bool Published { get; set; } = false; + public int Order { get; set; } = -1; +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/ApplicantProfile/ExternalLinkType.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/ApplicantProfile/ExternalLinkType.cs new file mode 100644 index 0000000000..4136ccd68f --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/ApplicantProfile/ExternalLinkType.cs @@ -0,0 +1,7 @@ +namespace Unity.GrantManager.ApplicantProfile; + +public enum ExternalLinkType +{ + Related = 1, + Renewal = 2 +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/ApplicantProfile/ExternalLinksConfig.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/ApplicantProfile/ExternalLinksConfig.cs new file mode 100644 index 0000000000..7a5022a822 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/ApplicantProfile/ExternalLinksConfig.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Unity.GrantManager.ApplicantProfile; + +/// +/// Represents the Applicant Portal external links configuration for an application form, +/// including the message shown to applicants alongside the renewal link. +/// +public class ExternalLinksConfig +{ + [MaxLength(512)] + public string ApplicantMessage { get; set; } = string.Empty; + + public List Links { get; set; } = []; +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/ApplicationForms/GenerationReviewStatus.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/ApplicationForms/GenerationReviewStatus.cs new file mode 100644 index 0000000000..50ec9803c5 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/ApplicationForms/GenerationReviewStatus.cs @@ -0,0 +1,8 @@ +namespace Unity.GrantManager.ApplicationForms; + +public enum GenerationReviewStatus +{ + Active = 0, + Completed = 1, + Discarded = 2 +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/ApplicationForms/Mapping/FormMappingReviewPhase.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/ApplicationForms/Mapping/FormMappingReviewPhase.cs new file mode 100644 index 0000000000..4b0fb7edd0 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/ApplicationForms/Mapping/FormMappingReviewPhase.cs @@ -0,0 +1,35 @@ +namespace Unity.GrantManager.ApplicationForms.Mapping; + +public enum FormMappingReviewPhase +{ + MappingReview = 0, + WorksheetReview = 1, + PublishAndAssignWorksheets = 2, + FinalMappingReview = 3, + Completed = 4 +} + +public enum FormGenerationWorkflowState +{ + GenerateInitialMapping = 10, + ReviewInitialMapping = 20, + GenerateWorksheets = 30, + ReviewWorksheets = 40, + PublishAndAssignWorksheets = 50, + GenerateFinalMapping = 60, + ReviewFinalMapping = 70, + Completed = 80 +} + +public enum FormGenerationWorkflowAction +{ + GenerateInitialMapping = 10, + ReviewInitialMapping = 20, + GenerateWorksheets = 30, + ReviewWorksheets = 40, + PublishAndAssignWorksheets = 50, + GenerateFinalMapping = 60, + ReviewFinalMapping = 70, + GenerateMapping = 80, + GenerateWorksheetsNextCycle = 90 +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/GrantManagerDomainErrorCodes.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/GrantManagerDomainErrorCodes.cs index c189918a53..bdab44ff44 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/GrantManagerDomainErrorCodes.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/GrantManagerDomainErrorCodes.cs @@ -26,4 +26,10 @@ public static class GrantManagerDomainErrorCodes public const string PayableFormRequiresHierarchy = "GrantManager:PayableFormRequiresHierarchy"; public const string ChildFormRequiresParentForm = "GrantManager:ChildFormRequiresParentForm"; public const string ChildFormCannotReferenceSelf = "GrantManager:ChildFormCannotReferenceSelf"; + + /* APPLICANT PORTAL EXTERNAL LINKS */ + public const string RenewalLinkRequiredForVisibility = "GrantManager:RenewalLinkRequiredForVisibility"; + public const string RenewalLinkInvalidUri = "GrantManager:RenewalLinkInvalidUri"; + public const string RelatedLinkInvalidUri = "GrantManager:RelatedLinkInvalidUri"; + public const string TooManyRelatedLinks = "GrantManager:TooManyRelatedLinks"; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json index 3f0b387506..4469751493 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json @@ -67,7 +67,9 @@ "Assignee": "Assignee", "Probability": "Probability", "GrantApplicationStatus": "Status", - "ExternalStatusVisibility": "Status Visibility", + "ExternalStatusVisibility": "External Status Visibility", + "ExternalStatus": "External Status", + "PublishedStatus": "Published Status", "ProposalDate": "Proposal Date", "SubmissionDate": "Submission Date", "GrantApplications": "Grant Applications", @@ -190,6 +192,24 @@ "ApplicationForms.Configuration.Notes:BypassAssessmentWorkflow": "Enabling this feature will bypass the Assessment workflow when you approve or deny submissions associated with this form.", "ApplicationForms.Configuration.Notes:SelectedApplicantElectoralAddress": "The selected address type will determine which submitted applicant address is used to extract the electoral district.", "ApplicationForms.Configuration.Warnings:ApplicantElectoralAddressTypeChange": "Changing the address type will only affect new submissions. Existing applications will retain the electoral district extracted from their original address type.", + "ApplicationForms.Configuration:ApplicantPortalLinks": "Applicant Portal Related Links & Visibility", + "ApplicationForms.Configuration:RenewalLink": "Renewal Link", + "ApplicationForms.Configuration:LinkUrl": "Link URL", + "ApplicationForms.Configuration:RenewalLinkDisplayName": "Renewal Link Display Name", + "ApplicationForms.Configuration:ShowRenewalLinksInPortal": "Show Renewal Links in Portal", + "ApplicationForms.Configuration:ApplicantMessage": "Applicant Message", + "ApplicationForms.Configuration:OtherLinks": "Other Links", + "ApplicationForms.Configuration:ShowOtherLinksInPortal": "Show in Portal", + "ApplicationForms.Configuration:LinkDisplayName": "Display Name", + "ApplicationForms.Configuration:LinkDescription": "Description", + "ApplicationForms.Configuration:AddLink": "Add Link", + "ApplicationForms.Configuration:RemoveLink": "Remove Link", + "ApplicationForms.Configuration.Notes:LinkVisibilityRequiresUrl": "If there is no link provided when Visibility is enabled, nothing will be displayed.", + "ApplicationForms.Configuration.Notes:MaxRelatedLinks": "A maximum of 8 other links can be added.", + "ApplicationForms.Configuration.Errors:InvalidUrl": "Please enter a valid, absolute http or https URL.", + "ApplicationForms.Configuration.Errors:RenewalLinkRequiredForVisibility": "A valid renewal link URL is required before enabling visibility.", + "ApplicationForms.Configuration.Errors:OtherLinkRequiredForVisibility": "A valid URL is required before enabling visibility for this link.", + "ApplicationForms.Configuration.Errors:MaxRelatedLinksReached": "A maximum of 8 other links is allowed.", "Intakes": "Intakes", @@ -269,6 +289,10 @@ "GrantManager:PayableFormRequiresHierarchy": "Please select a form hierarchy before saving a payable form.", "GrantManager:ChildFormRequiresParentForm": "Please select a parent form when the form hierarchy is set to Child.", "GrantManager:ChildFormCannotReferenceSelf": "A form cannot reference itself as the parent.", + "GrantManager:RenewalLinkRequiredForVisibility": "A valid renewal link URL is required before enabling renewal link visibility.", + "GrantManager:RenewalLinkInvalidUri": "The renewal link URL must be a valid, absolute http or https URL.", + "GrantManager:RelatedLinkInvalidUri": "Related link URLs must be valid, absolute http or https URLs.", + "GrantManager:TooManyRelatedLinks": "A maximum of 8 related links is allowed.", "GrantManager:CannotModifyAiAssessment": "AI assessments are read-only.", "GrantManager:CannotCloneNonAiAssessment": "Only AI assessments can be cloned.", "GrantManager:NotCommentOwner": "You can only delete your own comments.", @@ -614,6 +638,11 @@ "DataTable:ContextMenu:Copy": "Copy", "DataTable:ContextMenu:CopiedToClipboard": "Copied to clipboard", "DataTable:ContextMenu:Filter": "Filter", - "DataTable:ContextMenu:ClearFilter": "Clear Filters" + "DataTable:ContextMenu:ClearFilter": "Clear Filters", + + "WrongTenantError:Title": "An Error Occurred", + "WrongTenantError:ApplicationTenant": "This application is in Tenant: {0}", + "WrongTenantError:CurrentTenant": "You are currently in Tenant: {0}", + "WrongTenantError:Instructions": "Please click on the Profile menu at the top right of the corner, click on Switch Grant Programs, and select the correct Tenant before proceeding to view the link." } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/ApplicationForms/GenerationReview.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/ApplicationForms/GenerationReview.cs new file mode 100644 index 0000000000..1bc320a738 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/ApplicationForms/GenerationReview.cs @@ -0,0 +1,57 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; + +namespace Unity.GrantManager.ApplicationForms; + +public class GenerationReview : AuditedAggregateRoot, IMultiTenant +{ + protected GenerationReview() + { + ReviewData = "{}"; + } + + public GenerationReview( + Guid id, + string operation, + Guid contextId, + int sequence = 1) + : base(id) + { + Operation = operation; + ContextId = contextId; + Sequence = sequence; + Status = GenerationReviewStatus.Active; + ReviewData = "{}"; + } + + public string Operation { get; private set; } = null!; + public Guid ContextId { get; private set; } + public int Sequence { get; private set; } + public GenerationReviewStatus Status { get; private set; } + [Column(TypeName = "jsonb")] + public string ReviewData { get; private set; } + + public Guid? TenantId { get; set; } + + public void SetReviewData(string reviewData) + { + ReviewData = reviewData; + } + + public void SetStatus(GenerationReviewStatus status) + { + Status = status; + } + + public void Complete() + { + Status = GenerationReviewStatus.Completed; + } + + public void Discard() + { + Status = GenerationReviewStatus.Discarded; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/ApplicationForms/IGenerationReviewRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/ApplicationForms/IGenerationReviewRepository.cs new file mode 100644 index 0000000000..1857462a0b --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/ApplicationForms/IGenerationReviewRepository.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; + +namespace Unity.GrantManager.ApplicationForms; + +public interface IGenerationReviewRepository : IRepository +{ + Task FindLatestByOperationAndFormVersionAsync( + string operation, + Guid formVersionId); + + Task> GetListByOperationAndFormVersionAsync( + string operation, + Guid formVersionId); +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/Applicant.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/Applicant.cs index 12645d2962..94d6158dd4 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/Applicant.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/Applicant.cs @@ -24,6 +24,7 @@ public class Applicant : AuditedAggregateRoot, IMultiTenant public string? FiscalMonth { get; set; } public string? BusinessNumber { get; set; } public int? FiscalDay { get; set; } + public DateOnly? FiscalYearEnd { get; set; } public DateOnly? StartedOperatingDate { get; set; } public Guid? TenantId { get; set; } public Guid? SupplierId { get; set; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicantListRecord.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicantListRecord.cs index 72bccbb077..9ce7b8704e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicantListRecord.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicantListRecord.cs @@ -27,4 +27,5 @@ public class ApplicantListRecord public bool IsDuplicated { get; set; } public DateTime CreationTime { get; set; } public DateTime? LastModificationTime { get; set; } + public DateOnly? FiscalYearEnd { get; set; } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/Application.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/Application.cs index ff5f68410b..16d7a932c5 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/Application.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/Application.cs @@ -144,6 +144,8 @@ public virtual ApplicationStatus ApplicationStatus public string? AIAnalysis { get; set; } + public bool EligibleForRenewal { get; set; } + [Column(TypeName = "jsonb")] public string? AIScoresheetAnswers { get; set; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationForm.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationForm.cs index a2644e9363..b5d6139c57 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationForm.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationForm.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using Unity.GrantManager.ApplicantProfile; using Unity.GrantManager.ApplicationForms; using Unity.GrantManager.GrantApplications; +using Volo.Abp; using Volo.Abp.Domain.Entities.Auditing; using Volo.Abp.MultiTenancy; @@ -32,6 +34,8 @@ public class ApplicationForm : FullAuditedAggregateRoot, IMultiTenant public FormHierarchyType? FormHierarchy { get; set; } public Guid? ParentFormId { get; set; } public bool IsDirectApproval { get; set; } = false; + public ExternalLinksConfig ExternalLinksConfig { get; set; } = new(); + public bool AutomaticallyGenerateAIAnalysis { get; set; } = false; public bool ManuallyInitiateAIAnalysis { get; set; } = false; [MaxLength(100)] @@ -79,4 +83,55 @@ public static AddressType GetDefaultElectoralDistrictAddressType() { return AddressType.PhysicalAddress; } + + public const int MaxRelatedExternalLinks = 8; + + /// + /// Replaces the Renewal and Related external links as a set, enforcing that a link + /// cannot be marked visible in the Applicant Portal without a valid URI. + /// + public ApplicationForm SetExternalLinks(ExternalLink? renewalLink, List relatedLinks, string applicantMessage = "") + { + ArgumentNullException.ThrowIfNull(relatedLinks); + + // Cap the number of related links to the maximum allowed + if (relatedLinks.Count > MaxRelatedExternalLinks) + { + throw new BusinessException(GrantManagerDomainErrorCodes.TooManyRelatedLinks); + } + + // Validate that if a renewal link is published, it must have a valid URI + if (renewalLink is { Published: true } && string.IsNullOrWhiteSpace(renewalLink.Uri)) + { + throw new BusinessException(GrantManagerDomainErrorCodes.RenewalLinkRequiredForVisibility); + } + + // Validate that if any related link is published, it must have a valid URI + if (relatedLinks.Exists(l => l.Published && string.IsNullOrWhiteSpace(l.Uri))) + { + throw new BusinessException(GrantManagerDomainErrorCodes.RelatedLinkInvalidUri); + } + + var links = new List(); + + if (renewalLink is not null) + { + renewalLink.ExternalLinkType = ExternalLinkType.Renewal; + links.Add(renewalLink); + } + + for (var i = 0; i < relatedLinks.Count; i++) + { + relatedLinks[i].ExternalLinkType = ExternalLinkType.Related; + links.Add(relatedLinks[i]); + } + + ExternalLinksConfig = new ExternalLinksConfig + { + ApplicantMessage = applicantMessage, + Links = links + }; + + return this; + } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationListRecord.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationListRecord.cs index 7b4f4e11ca..8026f005c6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationListRecord.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationListRecord.cs @@ -61,6 +61,8 @@ public class ApplicationListRecord // ApplicationStatus (always joined) public string Status { get; init; } = string.Empty; + public string ExternalStatus { get; init; } = string.Empty; + public string? PublishedStatus { get; init; } // ApplicationForm (always joined) public string Category { get; init; } = string.Empty; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Notifications/ScheduledNotification.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Notifications/ScheduledNotification.cs index 6cd43e72fc..56d66838d9 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Notifications/ScheduledNotification.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Notifications/ScheduledNotification.cs @@ -17,6 +17,8 @@ public class ScheduledNotification : FullAuditedAggregateRoot, IMultiTenan public string TriggerType { get; set; } = string.Empty; // Date or Event + public string? Module { get; set; } + public string? TriggerDetail { get; set; } public bool IsActive { get; set; } = true; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/EntityFrameworkCoreGrantManagerDbSchemaMigrator.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/EntityFrameworkCoreGrantManagerDbSchemaMigrator.cs index 0b68fd752b..290e4d9a6b 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/EntityFrameworkCoreGrantManagerDbSchemaMigrator.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/EntityFrameworkCoreGrantManagerDbSchemaMigrator.cs @@ -20,6 +20,7 @@ namespace Unity.GrantManager.EntityFrameworkCore; public class EntityFrameworkCoreGrantManagerDbSchemaMigrator( IServiceProvider serviceProvider, IStringEncryptionService encryptionService, + IConfiguration configuration, ILogger logger) : IGrantManagerDbSchemaMigrator, ITransientDependency { @@ -30,13 +31,10 @@ public class EntityFrameworkCoreGrantManagerDbSchemaMigrator( * "Initial" and would make Database.MigrateAsync() below try to re-run Initial's * CreateTable operations against a schema that already has them. * - * ReconcileMigrationHistoryAsync resets history to just the Initial row *before* - * MigrateAsync() is called, so EF sees it as already applied and skips it. Brand - * new databases (including newly provisioned tenants) have an empty or nonexistent - * history table at this point, so the reconciliation is a no-op and MigrateAsync() - * runs Initial for real to build the schema. Safe to run unconditionally on every - * migrator invocation, forever - after the first run per database, history only - * ever contains the Initial row so the guard clause never fires again. + * ReconcileMigrationHistoryAsync resets history to just the Initial row before + * MigrateAsync() is called, so EF sees it as already applied and skips it. This is + * an explicit one-time operation because running it during normal migration startup + * would also remove legitimate migrations added after the flattening. */ private const string HostInitialMigrationId = "20260722193713_Initial"; private const string TenantInitialMigrationId = "20260721203242_Initial"; @@ -44,6 +42,7 @@ public class EntityFrameworkCoreGrantManagerDbSchemaMigrator( private readonly IServiceProvider _serviceProvider = serviceProvider; private readonly IStringEncryptionService _encryptionService = encryptionService; + private readonly bool _flattenMigrations = configuration.GetValue("Database:FlattenMigrations"); private readonly ILogger _logger = logger; public async Task MigrateAsync(Tenant? tenant) @@ -104,7 +103,10 @@ public async Task MigrateAsync(Tenant? tenant) await tenantDb.ExecuteSqlRawAsync( tenantDb.GetService().GetCreateIfNotExistsScript()); - await ReconcileMigrationHistoryAsync(tenantDb, TenantInitialMigrationId); + if (_flattenMigrations) + { + await ReconcileMigrationHistoryAsync(tenantDb, TenantInitialMigrationId); + } // Run migrations as admin against the tenant database await MigrateAndLogAsync(tenantDb, $"tenant:{tenant.Name}"); @@ -162,7 +164,10 @@ the correct one. */ await hostDb.ExecuteSqlRawAsync( hostDb.GetService().GetCreateIfNotExistsScript()); - await ReconcileMigrationHistoryAsync(hostDb, HostInitialMigrationId); + if (_flattenMigrations) + { + await ReconcileMigrationHistoryAsync(hostDb, HostInitialMigrationId); + } await MigrateAndLogAsync(hostDb, "host"); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantTenantDbContext.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantTenantDbContext.cs index c77da4df0b..a25e36b48c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantTenantDbContext.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantTenantDbContext.cs @@ -1,22 +1,27 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using System.Linq; +using System.Text.Json; +using Unity.Flex.EntityFrameworkCore; +using Unity.GrantManager.ApplicantProfile; +using Unity.GrantManager.ApplicationForms; using Unity.GrantManager.Applications; -using Unity.GrantManager.Intakes; using Unity.GrantManager.Assessments; using Unity.GrantManager.Comments; +using Unity.GrantManager.Contacts; +using Unity.GrantManager.GlobalTag; using Unity.GrantManager.GrantApplications; +using Unity.GrantManager.Identity; +using Unity.GrantManager.Intakes; using Unity.GrantManager.Notifications; +using Unity.Notifications.EntityFrameworkCore; +using Unity.Payments.EntityFrameworkCore; +using Unity.Reporting.EntityFrameworkCore; using Volo.Abp.Data; using Volo.Abp.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore.Modeling; -using Unity.GrantManager.Identity; -using Unity.Payments.EntityFrameworkCore; -using Unity.Flex.EntityFrameworkCore; -using Unity.Notifications.EntityFrameworkCore; -using Unity.Reporting.EntityFrameworkCore; -using Unity.GrantManager.GlobalTag; -using Unity.GrantManager.Contacts; namespace Unity.GrantManager.EntityFrameworkCore { @@ -27,6 +32,7 @@ public class GrantTenantDbContext : AbpDbContext public DbSet Intakes { get; set; } public DbSet ApplicationForms { get; set; } public DbSet ApplicationFormVersions { get; set; } + public DbSet GenerationReviews { get; set; } public DbSet Applicants { get; set; } public DbSet Applications { get; set; } public DbSet ApplicationStatuses { get; set; } @@ -65,6 +71,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); + // ExternalLinksConfig is mapped as a JSON-serialized scalar via HasConversion below, + // not as an owned/entity type, so exclude it (and its nested ExternalLink type) from + // convention-based entity discovery. + modelBuilder.Ignore(); + modelBuilder.Ignore(); + modelBuilder.Entity(b => { b.ToTable(GrantManagerConsts.TenantTablePrefix + "Persons", @@ -96,6 +108,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .WithOne(s => s.Applicant) .HasForeignKey(x => x.ApplicantId) .OnDelete(DeleteBehavior.NoAction); + + // FYE is updated through the compute_applicants_fiscal_year_end() trigger + var fyeProp = b.Property(x => x.FiscalYearEnd) + .HasColumnType("date") + .ValueGeneratedOnAddOrUpdate(); + fyeProp.Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Ignore); }); modelBuilder.Entity(b => @@ -114,6 +132,20 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.ConfigureByConvention(); //auto configure for the base class props b.Property(x => x.ApplicationFormName).IsRequired().HasMaxLength(255); + // Mapped as a JSON-serialized scalar (rather than EF's native JSON complex-type + // support) because that feature is relational-only and breaks under the + // EFCore.InMemory provider used by Unity.GrantManager.Web.Tests. + b.Property(x => x.ExternalLinksConfig) + .HasColumnName("ExternalLinks") + .HasColumnType("jsonb") + .IsRequired() + .HasConversion( + config => JsonSerializer.Serialize(config, (JsonSerializerOptions?)null), + json => JsonSerializer.Deserialize(json, (JsonSerializerOptions?)null)!, + new ValueComparer( + (left, right) => JsonSerializer.Serialize(left, (JsonSerializerOptions?)null) == JsonSerializer.Serialize(right, (JsonSerializerOptions?)null), + config => JsonSerializer.Serialize(config, (JsonSerializerOptions?)null).GetHashCode(), + config => JsonSerializer.Deserialize(JsonSerializer.Serialize(config, (JsonSerializerOptions?)null), (JsonSerializerOptions?)null)!)); b.HasOne().WithMany().HasForeignKey(x => x.IntakeId).IsRequired(); @@ -136,6 +168,16 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.Property(x => x.FormSchema).HasColumnType("jsonb"); }); + modelBuilder.Entity(b => + { + b.ToTable(GrantManagerConsts.TenantTablePrefix + "GenerationReviews", "AI"); + b.ConfigureByConvention(); + b.Property(x => x.Operation).IsRequired(); + b.Property(x => x.Status).HasConversion().IsRequired(); + b.Property(x => x.ReviewData).HasColumnType("jsonb").IsRequired(); + b.HasIndex(x => new { x.Operation, x.ContextId, x.Sequence }).IsUnique(); + }); + modelBuilder.Entity(b => { b.ToTable(GrantManagerConsts.TenantTablePrefix + "ApplicationStatuses", @@ -426,6 +468,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.Property(x => x.FormId).IsRequired(); b.Property(x => x.EmailTemplateId).IsRequired(); b.Property(x => x.TriggerType).IsRequired().HasMaxLength(64); + b.Property(x => x.Module).HasMaxLength(64); b.Property(x => x.TriggerDetail).HasMaxLength(1000); b.Property(x => x.EventType).HasMaxLength(128); b.Property(x => x.ApplicationStatus).HasMaxLength(128); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260805185000_RebuildAIModels.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260805185000_RebuildAIModels.cs new file mode 100644 index 0000000000..28d7d6c9a7 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260805185000_RebuildAIModels.cs @@ -0,0 +1,181 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Unity.GrantManager.EntityFrameworkCore; + +#nullable disable + +namespace Unity.GrantManager.Migrations.HostMigrations; + +[DbContext(typeof(GrantManagerDbContext))] +[Migration("20260805185000_RebuildAIModels")] +public partial class RebuildAIModels : Migration +{ + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql( + """ + ALTER TABLE "AI"."AIOperations" + DROP CONSTRAINT IF EXISTS "FK_AIOperations_AIModels_AIModelId"; + + DROP INDEX IF EXISTS "AI"."IX_AIOperations_AIModelId"; + DROP INDEX IF EXISTS "AI"."IX_AIModels_Name"; + + ALTER TABLE IF EXISTS "AI"."AIModels" + RENAME TO "AIModels_Legacy"; + + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 + FROM pg_constraint c + JOIN pg_class t ON t.oid = c.conrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'AI' + AND t.relname = 'AIModels_Legacy' + AND c.conname = 'PK_AIModels' + ) THEN + ALTER TABLE "AI"."AIModels_Legacy" + RENAME CONSTRAINT "PK_AIModels" TO "PK_AIModels_Legacy"; + END IF; + END $$; + + CREATE TABLE IF NOT EXISTS "AI"."AIModels" + ( + "Id" uuid NOT NULL, + "Name" character varying(200) NOT NULL, + "Provider" character varying(100) NOT NULL, + "IsActive" boolean NOT NULL, + "SettingsJson" jsonb NOT NULL, + "ExtraProperties" text NOT NULL, + "ConcurrencyStamp" character varying(40) NOT NULL, + "CreationTime" timestamp without time zone NOT NULL, + "CreatorId" uuid NULL, + "LastModificationTime" timestamp without time zone NULL, + "LastModifierId" uuid NULL, + CONSTRAINT "PK_AIModels" PRIMARY KEY ("Id") + ); + + DO $$ + BEGIN + IF to_regclass('"AI"."AIModels_Legacy"') IS NOT NULL THEN + INSERT INTO "AI"."AIModels" + ( + "Id", "Name", "Provider", "IsActive", "SettingsJson", + "ExtraProperties", "ConcurrencyStamp", "CreationTime", "CreatorId", + "LastModificationTime", "LastModifierId" + ) + SELECT + "Id", + CASE "Name" + WHEN 'Gpt4oMini' THEN 'gpt-4o-mini' + WHEN 'Gpt5Mini' THEN 'gpt-5-mini' + WHEN 'Gpt5Nano' THEN 'gpt-5-nano' + ELSE "Name" + END, + 'OpenAI', "IsActive", "SettingsJson", "ExtraProperties", + "ConcurrencyStamp", "CreationTime", "CreatorId", + "LastModificationTime", "LastModifierId" + FROM "AI"."AIModels_Legacy"; + END IF; + END $$; + + DROP TABLE IF EXISTS "AI"."AIModels_Legacy"; + + CREATE UNIQUE INDEX "IX_AIModels_Name" + ON "AI"."AIModels" ("Name"); + + CREATE INDEX "IX_AIOperations_AIModelId" + ON "AI"."AIOperations" ("AIModelId"); + + ALTER TABLE "AI"."AIOperations" + ADD CONSTRAINT "FK_AIOperations_AIModels_AIModelId" + FOREIGN KEY ("AIModelId") + REFERENCES "AI"."AIModels" ("Id") + ON DELETE RESTRICT; + """); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql( + """ + ALTER TABLE "AI"."AIOperations" + DROP CONSTRAINT IF EXISTS "FK_AIOperations_AIModels_AIModelId"; + + DROP INDEX IF EXISTS "AI"."IX_AIOperations_AIModelId"; + DROP INDEX IF EXISTS "AI"."IX_AIModels_Name"; + + ALTER TABLE IF EXISTS "AI"."AIModels" + RENAME TO "AIModels_Current"; + + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 + FROM pg_constraint c + JOIN pg_class t ON t.oid = c.conrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE n.nspname = 'AI' + AND t.relname = 'AIModels_Current' + AND c.conname = 'PK_AIModels' + ) THEN + ALTER TABLE "AI"."AIModels_Current" + RENAME CONSTRAINT "PK_AIModels" TO "PK_AIModels_Current"; + END IF; + END $$; + + CREATE TABLE IF NOT EXISTS "AI"."AIModels" + ( + "Id" uuid NOT NULL, + "Name" character varying(200) NOT NULL, + "IsActive" boolean NOT NULL, + "SettingsJson" jsonb NOT NULL, + "ExtraProperties" text NOT NULL, + "ConcurrencyStamp" character varying(40) NOT NULL, + "CreationTime" timestamp without time zone NOT NULL, + "CreatorId" uuid NULL, + "LastModificationTime" timestamp without time zone NULL, + "LastModifierId" uuid NULL, + CONSTRAINT "PK_AIModels" PRIMARY KEY ("Id") + ); + + DO $$ + BEGIN + IF to_regclass('"AI"."AIModels_Current"') IS NOT NULL THEN + INSERT INTO "AI"."AIModels" + ( + "Id", "Name", "IsActive", "SettingsJson", "ExtraProperties", + "ConcurrencyStamp", "CreationTime", "CreatorId", "LastModificationTime", + "LastModifierId" + ) + SELECT + "Id", + CASE "Name" + WHEN 'gpt-4o-mini' THEN 'Gpt4oMini' + WHEN 'gpt-5-mini' THEN 'Gpt5Mini' + WHEN 'gpt-5-nano' THEN 'Gpt5Nano' + ELSE "Name" + END, + "IsActive", "SettingsJson", "ExtraProperties", "ConcurrencyStamp", + "CreationTime", "CreatorId", "LastModificationTime", "LastModifierId" + FROM "AI"."AIModels_Current"; + END IF; + END $$; + + DROP TABLE IF EXISTS "AI"."AIModels_Current"; + + CREATE UNIQUE INDEX "IX_AIModels_Name" + ON "AI"."AIModels" ("Name"); + + CREATE INDEX "IX_AIOperations_AIModelId" + ON "AI"."AIOperations" ("AIModelId"); + + ALTER TABLE "AI"."AIOperations" + ADD CONSTRAINT "FK_AIOperations_AIModels_AIModelId" + FOREIGN KEY ("AIModelId") + REFERENCES "AI"."AIModels" ("Id") + ON DELETE RESTRICT; + """); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/GrantManagerDbContextModelSnapshot.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/GrantManagerDbContextModelSnapshot.cs index 0596cc6c64..af2ce821bf 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/GrantManagerDbContextModelSnapshot.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/GrantManagerDbContextModelSnapshot.cs @@ -508,6 +508,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(200) .HasColumnType("character varying(200)"); + b.Property("Provider") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + b.Property("SettingsJson") .IsRequired() .HasColumnType("jsonb"); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260804193000_AddModuleToScheduledNotifications.Designer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260804193000_AddModuleToScheduledNotifications.Designer.cs new file mode 100644 index 0000000000..3aeede4d72 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260804193000_AddModuleToScheduledNotifications.Designer.cs @@ -0,0 +1,15 @@ +// +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Unity.GrantManager.EntityFrameworkCore; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + [DbContext(typeof(GrantTenantDbContext))] + [Migration("20260804193000_AddModuleToScheduledNotifications")] + partial class AddModuleToScheduledNotifications + { + } +} \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260804193000_AddModuleToScheduledNotifications.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260804193000_AddModuleToScheduledNotifications.cs new file mode 100644 index 0000000000..e7a5e4a498 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260804193000_AddModuleToScheduledNotifications.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + /// + public partial class AddModuleToScheduledNotifications : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Module", + schema: "Notifications", + table: "ScheduledNotifications", + type: "character varying(64)", + maxLength: 64, + nullable: true); + + migrationBuilder.Sql(@" + UPDATE ""Notifications"".""ScheduledNotifications"" + SET ""Module"" = 'Application' + WHERE ""TriggerType"" = 'Event';"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Module", + schema: "Notifications", + table: "ScheduledNotifications"); + } + } +} \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260805212847_AddGenerationReviews.Designer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260805212847_AddGenerationReviews.Designer.cs new file mode 100644 index 0000000000..830984d400 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260805212847_AddGenerationReviews.Designer.cs @@ -0,0 +1,5340 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Unity.GrantManager.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + [DbContext(typeof(GrantTenantDbContext))] + [Migration("20260805212847_AddGenerationReviews")] + partial class AddGenerationReviews + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.PostgreSql) + .HasAnnotation("ProductVersion", "10.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScoresheetId"); + + b.ToTable("ScoresheetInstances", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Answer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("QuestionId") + .HasColumnType("uuid"); + + b.Property("ScoresheetInstanceId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("QuestionId"); + + b.HasIndex("ScoresheetInstanceId"); + + b.ToTable("Answers", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("SectionId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("Questions", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Scoresheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsArchived") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Scoresheets", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ScoresheetId"); + + b.ToTable("ScoresheetSections", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.CustomFieldValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CustomFieldId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("WorksheetInstanceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetInstanceId"); + + b.ToTable("CustomFieldValues", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UiAnchor") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetCorrelationId") + .HasColumnType("uuid"); + + b.Property("WorksheetCorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("WorksheetInstances", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetLinks.WorksheetLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UiAnchor") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetId"); + + b.ToTable("WorksheetLinks", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.CustomField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("SectionId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("CustomFields", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.Worksheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsArchived") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Worksheets", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetId"); + + b.ToTable("WorksheetSections", "Flex"); + }); + + modelBuilder.Entity("Unity.GrantManager.ApplicationForms.GenerationReview", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContextId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Operation") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReviewData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Sequence") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("Operation", "ContextId", "Sequence") + .IsUnique(); + + b.ToTable("GenerationReviews", "AI"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Applicant", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantName") + .IsRequired() + .HasMaxLength(600) + .HasColumnType("character varying(600)"); + + b.Property("ApproxNumberOfEmployees") + .HasColumnType("text"); + + b.Property("AuditComments") + .HasColumnType("text"); + + b.Property("BusinessNumber") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FiscalDay") + .HasColumnType("integer"); + + b.Property("FiscalMonth") + .HasColumnType("text"); + + b.Property("FiscalYearEnd") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("date"); + + b.Property("FundingHistoryComments") + .HasColumnType("text"); + + b.Property("IndigenousOrgInd") + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsDuplicated") + .HasColumnType("boolean"); + + b.Property("IssueTrackingComments") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MatchPercentage") + .HasColumnType("numeric"); + + b.Property("NonRegOrgName") + .HasColumnType("text"); + + b.Property("NonRegisteredBusinessName") + .HasColumnType("text"); + + b.Property("OrgName") + .HasColumnType("text"); + + b.Property("OrgNumber") + .HasColumnType("text"); + + b.Property("OrgStatus") + .HasColumnType("text"); + + b.Property("OrganizationType") + .HasColumnType("text"); + + b.Property("RedStop") + .HasColumnType("boolean"); + + b.Property("ReportsComments") + .HasColumnType("text"); + + b.Property("Sector") + .HasColumnType("text"); + + b.Property("SectorSubSectorIndustryDesc") + .HasColumnType("text"); + + b.Property("StartedOperatingDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("SubSector") + .HasColumnType("text"); + + b.Property("SupplierId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UnityApplicantId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantName"); + + b.HasIndex("OrgName"); + + b.HasIndex("OrgNumber"); + + b.HasIndex("Status"); + + b.HasIndex("SupplierId"); + + b.HasIndex("TenantId"); + + b.HasIndex("UnityApplicantId"); + + b.HasIndex("TenantId", "IsDeleted", "CreationTime") + .HasFilter("\"IsDeleted\" = false"); + + b.ToTable("Applicants", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAddress", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AddressType") + .HasColumnType("integer"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Postal") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("Street") + .HasColumnType("text"); + + b.Property("Street2") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Unit") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicantAddresses", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAgent", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("BceidBusinessGuid") + .HasColumnType("uuid"); + + b.Property("BceidBusinessName") + .HasColumnType("text"); + + b.Property("BceidUserGuid") + .HasColumnType("uuid"); + + b.Property("BceidUserName") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactOrder") + .HasColumnType("integer"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IdentityEmail") + .HasColumnType("text"); + + b.Property("IdentityName") + .HasColumnType("text"); + + b.Property("IdentityProvider") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsConfirmed") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OidcSubUser") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("Phone2") + .HasColumnType("text"); + + b.Property("Phone2Extension") + .HasColumnType("text"); + + b.Property("PhoneExtension") + .HasColumnType("text"); + + b.Property("RoleForApplicant") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationId") + .IsUnique(); + + b.HasIndex("TenantId", "ApplicationId"); + + b.ToTable("ApplicantAgents", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("ApplicantAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AIAnalysis") + .HasColumnType("text"); + + b.Property("AIScoresheetAnswers") + .HasColumnType("jsonb"); + + b.Property("Acquisition") + .HasColumnType("text"); + + b.Property("ApplicantElectoralDistrict") + .HasColumnType("text"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("ApplicationStatusId") + .HasColumnType("uuid"); + + b.Property("ApprovedAmount") + .HasColumnType("numeric"); + + b.Property("AssessmentResultDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AssessmentResultStatus") + .HasColumnType("text"); + + b.Property("AssessmentStartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Community") + .HasColumnType("text"); + + b.Property("CommunityPopulation") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContractExecutionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ContractNumber") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeclineRational") + .HasColumnType("text"); + + b.Property("DefaultSiteId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("DueDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DueDiligenceStatus") + .HasColumnType("text"); + + b.Property("EconomicRegion") + .HasColumnType("text"); + + b.Property("ElectoralDistrict") + .HasColumnType("text"); + + b.Property("ExternalStatusVisibility") + .HasColumnType("boolean"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FinalDecisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Forestry") + .HasColumnType("text"); + + b.Property("ForestryFocus") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LikelihoodOfFunding") + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("NotificationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Payload") + .HasColumnType("jsonb"); + + b.Property("PercentageTotalProjectBudget") + .HasColumnType("double precision"); + + b.Property("Place") + .HasColumnType("text"); + + b.Property("ProjectEndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ProjectFundingTotal") + .HasColumnType("numeric"); + + b.Property("ProjectName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProjectStartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ProjectSummary") + .HasColumnType("text"); + + b.Property("ProposalDate") + .HasColumnType("timestamp without time zone"); + + b.Property("RecommendedAmount") + .HasColumnType("numeric"); + + b.Property("ReferenceNo") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegionalDistrict") + .HasColumnType("text"); + + b.Property("RequestedAmount") + .HasColumnType("numeric"); + + b.Property("RiskRanking") + .HasColumnType("text"); + + b.Property("SigningAuthorityBusinessPhone") + .HasColumnType("text"); + + b.Property("SigningAuthorityCellPhone") + .HasColumnType("text"); + + b.Property("SigningAuthorityEmail") + .HasColumnType("text"); + + b.Property("SigningAuthorityFullName") + .HasColumnType("text"); + + b.Property("SigningAuthorityTitle") + .HasColumnType("text"); + + b.Property("SubStatus") + .HasColumnType("text"); + + b.Property("SubmissionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TotalProjectBudget") + .HasColumnType("numeric"); + + b.Property("TotalScore") + .HasColumnType("integer"); + + b.Property("UnityApplicationId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationFormId"); + + b.HasIndex("ApplicationStatusId"); + + b.HasIndex("OwnerId"); + + b.HasIndex("ReferenceNo"); + + b.HasIndex("TenantId", "SubmissionDate") + .HasFilter("\"IsDeleted\" = false"); + + b.ToTable("Applications", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAssignment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AssigneeId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Duty") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("AssigneeId"); + + b.HasIndex("TenantId", "ApplicationId"); + + b.ToTable("ApplicationAssignments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationChefsFileAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AISummary") + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ChefsFileId") + .HasColumnType("text"); + + b.Property("ChefsSubmissionId") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationChefsFileAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationContact", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactEmail") + .HasColumnType("text"); + + b.Property("ContactFullName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactMobilePhone") + .HasColumnType("text"); + + b.Property("ContactTitle") + .HasColumnType("text"); + + b.Property("ContactType") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactWorkPhone") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationContact", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationForm", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountCodingId") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .HasColumnType("text"); + + b.Property("ApplicationFormDescription") + .HasColumnType("text"); + + b.Property("ApplicationFormName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AttemptedConnectionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AutomaticallyGenerateAIAnalysis") + .HasColumnType("boolean"); + + b.Property("AvailableChefsFields") + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("text"); + + b.Property("ChefsApplicationFormGuid") + .HasColumnType("text"); + + b.Property("ChefsCriteriaFormGuid") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ConnectionHttpStatus") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DefaultPaymentGroup") + .HasColumnType("integer"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ElectoralDistrictAddressType") + .HasColumnType("integer"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormHierarchy") + .HasColumnType("integer"); + + b.Property("IntakeId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsDirectApproval") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ManuallyInitiateAIAnalysis") + .HasColumnType("boolean"); + + b.Property("ParentFormId") + .HasColumnType("uuid"); + + b.Property("Payable") + .HasColumnType("boolean"); + + b.Property("PaymentApprovalThreshold") + .HasColumnType("numeric"); + + b.Property("Prefix") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PreventPayment") + .HasColumnType("boolean"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("SuffixType") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IntakeId"); + + b.HasIndex("ParentFormId"); + + b.HasIndex("TenantId", "IsDeleted") + .HasFilter("\"IsDeleted\" = false"); + + b.ToTable("ApplicationForms", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormSubmission", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormVersionId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ChefsSubmissionGuid") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormVersionId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OidcSub") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Submission") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormSubmissions", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormVersion", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("AvailableChefsFields") + .HasColumnType("text"); + + b.Property("ChefsApplicationFormGuid") + .HasColumnType("text"); + + b.Property("ChefsFormVersionGuid") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormSchema") + .HasColumnType("jsonb"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SubmissionHeaderMapping") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormVersion", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationLink", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LinkType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("Related"); + + b.Property("LinkedApplicationId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("TenantId", "ApplicationId"); + + b.ToTable("ApplicationLinks", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationStatus", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExternalStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("InternalStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("NotifiedStatus") + .HasColumnType("text"); + + b.Property("StatusCode") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("StatusCode") + .IsUnique(); + + b.ToTable("ApplicationStatuses", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationTags", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TagId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("TagId"); + + b.HasIndex("TenantId", "ApplicationId"); + + b.ToTable("ApplicationTags", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AssessmentAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AssessmentId"); + + b.ToTable("AssessmentAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AuditHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("AuditDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AuditNote") + .HasColumnType("text"); + + b.Property("AuditStatus") + .HasColumnType("text"); + + b.Property("AuditTrackingNumber") + .HasColumnType("text"); + + b.Property("AuditorName") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("AuditHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.FundingHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApprovedAmount") + .HasColumnType("numeric"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FundingNotes") + .HasColumnType("text"); + + b.Property("FundingYear") + .HasColumnType("text"); + + b.Property("GrantCategory") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OneTimeConsideration") + .HasColumnType("numeric"); + + b.Property("PaidDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ReconsiderationAmount") + .HasColumnType("numeric"); + + b.Property("RenewedFunding") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TotalGrantAmount") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("FundingHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.IssueTracking", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IssueDescription") + .HasColumnType("text"); + + b.Property("IssueHeading") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ResolutionNote") + .HasColumnType("text"); + + b.Property("Resolved") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Year") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("IssueTrackings", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ReportsHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FiscalYear") + .HasColumnType("text"); + + b.Property("IncompleteReport") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("Outstanding") + .HasColumnType("boolean"); + + b.Property("ReportDate") + .HasColumnType("timestamp without time zone"); + + b.Property("SignedOff") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("ReportsHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Assessments.Assessment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ApprovalRecommended") + .HasColumnType("boolean"); + + b.Property("AssessorId") + .HasColumnType("uuid"); + + b.Property("CleanGrowth") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("EconomicImpact") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FinancialAnalysis") + .HasColumnType("integer"); + + b.Property("InclusiveGrowth") + .HasColumnType("integer"); + + b.Property("IsAiAssessment") + .HasColumnType("boolean"); + + b.Property("IsComplete") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("AssessorId"); + + b.ToTable("Assessments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicantComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PinDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("CommenterId"); + + b.ToTable("ApplicantComments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicationComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PinDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("CommenterId"); + + b.ToTable("ApplicationComments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.AssessmentComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PinDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AssessmentId"); + + b.HasIndex("CommenterId"); + + b.ToTable("AssessmentComments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Contacts.Contact", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("HomePhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MobilePhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("WorkPhoneExtension") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("WorkPhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.ToTable("Contacts", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Contacts.ContactLink", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("RelatedEntityId") + .HasColumnType("uuid"); + + b.Property("RelatedEntityType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Role") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("RelatedEntityType", "RelatedEntityId"); + + b.HasIndex("ContactId", "RelatedEntityType", "RelatedEntityId"); + + b.ToTable("ContactLinks", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.GlobalTag.Tag", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Tags", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Identity.Person", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Badge") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FullName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OidcDisplayName") + .IsRequired() + .HasColumnType("text"); + + b.Property("OidcSub") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("OidcSub"); + + b.HasIndex("TenantId"); + + b.ToTable("Persons", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Intakes.Intake", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Budget") + .HasColumnType("double precision"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IntakeName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("StartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Intakes", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Notifications.ScheduledNotification", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationStatus") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ApplicationStatusId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DateField") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EmailTemplateId") + .HasColumnType("uuid"); + + b.Property("EventType") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("RecipientCategory") + .HasColumnType("text"); + + b.Property("RecipientIdentifier") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TriggerDetail") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("TriggerType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("ScheduledNotifications", "Notifications"); + }); + + modelBuilder.Entity("Unity.GrantManager.Notifications.ScheduledNotificationTracking", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatorId") + .HasColumnType("uuid"); + + b.Property("DateField") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("NotificationSentDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ScheduledNotificationId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("CreationTime"); + + b.HasIndex("ScheduledNotificationId"); + + b.HasIndex("ApplicationId", "ScheduledNotificationId", "DateField") + .IsUnique(); + + b.ToTable("ScheduledNotificationTracking", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EmailGroups", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroupUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.ToTable("EmailGroupUsers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("BCC") + .IsRequired() + .HasColumnType("text"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CC") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChesHttpStatusCode") + .HasColumnType("text"); + + b.Property("ChesMsgId") + .HasColumnType("uuid"); + + b.Property("ChesResponse") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChesStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("EmailType") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FromAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestIds") + .IsRequired() + .HasColumnType("text"); + + b.Property("Priority") + .IsRequired() + .HasColumnType("text"); + + b.Property("Recipient") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("RetryAttempts") + .HasColumnType("integer"); + + b.Property("ScheduledNotificationId") + .HasColumnType("uuid"); + + b.Property("SendOnDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("SentDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("text"); + + b.Property("TemplateName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("ToAddress") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EmailLogs", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLogAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("EmailLogId") + .HasColumnType("uuid"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OriginTemplateId") + .HasColumnType("uuid"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TemplateId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("EmailLogId"); + + b.HasIndex("S3ObjectKey"); + + b.HasIndex("TemplateId"); + + b.ToTable("EmailLogAttachments", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.EmailTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BodyHTML") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyText") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RecipientCategory") + .HasColumnType("text"); + + b.Property("RecipientIdentifier") + .HasColumnType("text"); + + b.Property("SendFrom") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("EmailTemplates", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.Subscriber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Subscribers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("SubscriptionGroups", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroupSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SubscriberId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.HasIndex("SubscriberId"); + + b.ToTable("SubscriptionGroupSubscribers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TemplateVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MapTo") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("TemplateVariables", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.Trigger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("InternalName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Triggers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TriggerSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SubscriptionGroupId") + .HasColumnType("uuid"); + + b.Property("TemplateId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TriggerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionGroupId"); + + b.HasIndex("TemplateId"); + + b.HasIndex("TriggerId"); + + b.ToTable("TriggerSubscriptions", "Notifications"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.AccountCodings.AccountCoding", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Description") + .HasMaxLength(35) + .HasColumnType("character varying(35)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MinistryClient") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("Responsibility") + .IsRequired() + .HasColumnType("text"); + + b.Property("ServiceLine") + .IsRequired() + .HasColumnType("text"); + + b.Property("Stob") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("AccountCodings", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentConfigurations.PaymentConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DefaultAccountCodingId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentIdPrefix") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("PaymentConfigurations", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.ExpenseApproval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DecisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DecisionUserId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PaymentRequestId"); + + b.ToTable("ExpenseApprovals", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountCodingId") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("BatchName") + .IsRequired() + .HasColumnType("text"); + + b.Property("BatchNumber") + .HasColumnType("numeric"); + + b.Property("CancelledBy") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("CancelledBy"); + + b.Property("CancelledById") + .HasColumnType("uuid") + .HasColumnName("CancelledById"); + + b.Property("CancelledOn") + .HasColumnType("timestamp without time zone") + .HasColumnName("CancelledOn"); + + b.Property("CasHttpStatusCode") + .HasColumnType("integer"); + + b.Property("CasResponse") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContractNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FsbApNotified") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("FsbNotificationEmailLogId") + .HasColumnType("uuid"); + + b.Property("FsbNotificationSentDate") + .HasColumnType("timestamp without time zone"); + + b.Property("InvoiceNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("InvoiceStatus") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsRecon") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("PayeeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentDate") + .HasColumnType("text"); + + b.Property("PaymentNumber") + .HasColumnType("text"); + + b.Property("PaymentStatus") + .HasColumnType("text"); + + b.Property("ReferenceNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("RequesterName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SiteId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("SubmissionConfirmationCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SupplierName") + .HasColumnType("text"); + + b.Property("SupplierNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AccountCodingId"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("CreationTime"); + + b.HasIndex("FsbNotificationEmailLogId"); + + b.HasIndex("ReferenceNumber") + .IsUnique(); + + b.HasIndex("SiteId"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId", "CreationTime") + .HasFilter("\"IsDeleted\" = false"); + + b.ToTable("PaymentRequests", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentTags.PaymentTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestId") + .HasColumnType("uuid"); + + b.Property("TagId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("PaymentRequestId"); + + b.HasIndex("TagId"); + + b.ToTable("PaymentTags", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentThresholds.PaymentThreshold", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Threshold") + .HasColumnType("numeric"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("PaymentThresholds", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Site", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddressLine1") + .HasColumnType("text"); + + b.Property("AddressLine2") + .HasColumnType("text"); + + b.Property("AddressLine3") + .HasColumnType("text"); + + b.Property("BankAccount") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EFTAdvicePref") + .HasColumnType("text"); + + b.Property("EmailAddress") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastUpdatedInCas") + .HasColumnType("timestamp without time zone"); + + b.Property("MarkDeletedInUse") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentGroup") + .HasColumnType("integer"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("SiteProtected") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("SupplierId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("SupplierId"); + + b.ToTable("Sites", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Supplier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BusinessNumber") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastUpdatedInCAS") + .HasColumnType("timestamp without time zone"); + + b.Property("MailingAddress") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("text"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("SIN") + .HasColumnType("text"); + + b.Property("StandardIndustryClassification") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("Subcategory") + .HasColumnType("text"); + + b.Property("SupplierProtected") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Suppliers", "Payments"); + }); + + modelBuilder.Entity("Unity.Reporting.Domain.Configuration.ReportColumnsMap", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Mapping") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RoleStatus") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("ViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ViewStatus") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ReportColumnsMaps", "Reporting"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Scoresheet", "Scoresheet") + .WithMany("Instances") + .HasForeignKey("ScoresheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scoresheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Answer", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Question", "Question") + .WithMany("Answers") + .HasForeignKey("QuestionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", null) + .WithMany("Answers") + .HasForeignKey("ScoresheetInstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Question"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.ScoresheetSection", "Section") + .WithMany("Fields") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Scoresheet", "Scoresheet") + .WithMany("Sections") + .HasForeignKey("ScoresheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scoresheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.CustomFieldValue", b => + { + b.HasOne("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", null) + .WithMany("Values") + .HasForeignKey("WorksheetInstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetLinks.WorksheetLink", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.Worksheet", "Worksheet") + .WithMany("Links") + .HasForeignKey("WorksheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Worksheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.CustomField", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.WorksheetSection", "Section") + .WithMany("Fields") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.Worksheet", "Worksheet") + .WithMany("Sections") + .HasForeignKey("WorksheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Worksheet"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAddress", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", "Applicant") + .WithMany("ApplicantAddresses") + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicantAddresses") + .HasForeignKey("ApplicationId"); + + b.Navigation("Applicant"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAgent", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithOne("ApplicantAgent") + .HasForeignKey("Unity.GrantManager.Applications.ApplicantAgent", "ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAttachment", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", "Applicant") + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", "ApplicationForm") + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationStatus", "ApplicationStatus") + .WithMany("Applications") + .HasForeignKey("ApplicationStatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Applicant"); + + b.Navigation("ApplicationForm"); + + b.Navigation("ApplicationStatus"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAssignment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicationAssignments") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", "Assignee") + .WithMany() + .HasForeignKey("AssigneeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Assignee"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAttachment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationChefsFileAttachment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationContact", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationForm", b => + { + b.HasOne("Unity.GrantManager.Intakes.Intake", null) + .WithMany() + .HasForeignKey("IntakeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) + .WithMany() + .HasForeignKey("ParentFormId") + .OnDelete(DeleteBehavior.NoAction); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormSubmission", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormVersion", b => + { + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationLink", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany("ApplicationLinks") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationTags", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicationTags") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.GlobalTag.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AssessmentAttachment", b => + { + b.HasOne("Unity.GrantManager.Assessments.Assessment", null) + .WithMany() + .HasForeignKey("AssessmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AuditHistory", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.FundingHistory", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.IssueTracking", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ReportsHistory", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Assessments.Assessment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("Assessments") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("AssessorId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicantComment", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicationComment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.AssessmentComment", b => + { + b.HasOne("Unity.GrantManager.Assessments.Assessment", null) + .WithMany() + .HasForeignKey("AssessmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Contacts.ContactLink", b => + { + b.HasOne("Unity.GrantManager.Contacts.Contact", null) + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Notifications.ScheduledNotificationTracking", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Notifications.ScheduledNotification", null) + .WithMany() + .HasForeignKey("ScheduledNotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroupUser", b => + { + b.HasOne("Unity.Notifications.EmailGroups.EmailGroup", null) + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLogAttachment", b => + { + b.HasOne("Unity.Notifications.Emails.EmailLog", null) + .WithMany() + .HasForeignKey("EmailLogId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Unity.Notifications.Templates.EmailTemplate", null) + .WithMany() + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroupSubscription", b => + { + b.HasOne("Unity.Notifications.Templates.SubscriptionGroup", "SubscriptionGroup") + .WithMany() + .HasForeignKey("GroupId"); + + b.HasOne("Unity.Notifications.Templates.Subscriber", "Subscriber") + .WithMany() + .HasForeignKey("SubscriberId"); + + b.Navigation("Subscriber"); + + b.Navigation("SubscriptionGroup"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TriggerSubscription", b => + { + b.HasOne("Unity.Notifications.Templates.SubscriptionGroup", "SubscriptionGroup") + .WithMany() + .HasForeignKey("SubscriptionGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Notifications.Templates.EmailTemplate", "EmailTemplate") + .WithMany() + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Notifications.Templates.Trigger", "Trigger") + .WithMany() + .HasForeignKey("TriggerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmailTemplate"); + + b.Navigation("SubscriptionGroup"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.ExpenseApproval", b => + { + b.HasOne("Unity.Payments.Domain.PaymentRequests.PaymentRequest", "PaymentRequest") + .WithMany("ExpenseApprovals") + .HasForeignKey("PaymentRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PaymentRequest"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.HasOne("Unity.Payments.Domain.AccountCodings.AccountCoding", "AccountCoding") + .WithMany() + .HasForeignKey("AccountCodingId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Unity.Payments.Domain.Suppliers.Site", "Site") + .WithMany() + .HasForeignKey("SiteId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("AccountCoding"); + + b.Navigation("Site"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentTags.PaymentTag", b => + { + b.HasOne("Unity.Payments.Domain.PaymentRequests.PaymentRequest", null) + .WithMany("PaymentTags") + .HasForeignKey("PaymentRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.GlobalTag.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Site", b => + { + b.HasOne("Unity.Payments.Domain.Suppliers.Supplier", "Supplier") + .WithMany("Sites") + .HasForeignKey("SupplierId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.Navigation("Answers"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.Navigation("Answers"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Scoresheet", b => + { + b.Navigation("Instances"); + + b.Navigation("Sections"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.Navigation("Fields"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", b => + { + b.Navigation("Values"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.Worksheet", b => + { + b.Navigation("Links"); + + b.Navigation("Sections"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.Navigation("Fields"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Applicant", b => + { + b.Navigation("ApplicantAddresses"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.Navigation("ApplicantAddresses"); + + b.Navigation("ApplicantAgent"); + + b.Navigation("ApplicationAssignments"); + + b.Navigation("ApplicationLinks"); + + b.Navigation("ApplicationTags"); + + b.Navigation("Assessments"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationStatus", b => + { + b.Navigation("Applications"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.Navigation("ExpenseApprovals"); + + b.Navigation("PaymentTags"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Supplier", b => + { + b.Navigation("Sites"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260805212847_AddGenerationReviews.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260805212847_AddGenerationReviews.cs new file mode 100644 index 0000000000..c48a7ad854 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260805212847_AddGenerationReviews.cs @@ -0,0 +1,53 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations; + +public partial class AddGenerationReviews : Migration +{ + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "AI"); + + migrationBuilder.CreateTable( + name: "GenerationReviews", + schema: "AI", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Operation = table.Column(type: "text", nullable: false), + ContextId = table.Column(type: "uuid", nullable: false), + Sequence = table.Column(type: "integer", nullable: false), + Status = table.Column(type: "text", nullable: false), + ReviewData = table.Column(type: "jsonb", nullable: false), + TenantId = table.Column(type: "uuid", nullable: true), + ExtraProperties = table.Column(type: "text", nullable: false), + ConcurrencyStamp = table.Column(type: "character varying(40)", maxLength: 40, nullable: false), + CreationTime = table.Column(type: "timestamp without time zone", nullable: false), + CreatorId = table.Column(type: "uuid", nullable: true), + LastModificationTime = table.Column(type: "timestamp without time zone", nullable: true), + LastModifierId = table.Column(type: "uuid", nullable: true), + }, + constraints: table => + { + table.PrimaryKey("PK_GenerationReviews", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_GenerationReviews_Operation_ContextId_Sequence", + schema: "AI", + table: "GenerationReviews", + columns: new[] { "Operation", "ContextId", "Sequence" }, + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "GenerationReviews", + schema: "AI"); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260806201536_AB33799_HardenCheckboxGroupReportingViews.Designer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260806201536_AB33799_HardenCheckboxGroupReportingViews.Designer.cs new file mode 100644 index 0000000000..3915718f85 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260806201536_AB33799_HardenCheckboxGroupReportingViews.Designer.cs @@ -0,0 +1,5336 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Unity.GrantManager.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + [DbContext(typeof(GrantTenantDbContext))] + [Migration("20260806201536_AB33799_HardenCheckboxGroupReportingViews")] + partial class AB33799_HardenCheckboxGroupReportingViews + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.PostgreSql) + .HasAnnotation("ProductVersion", "10.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScoresheetId"); + + b.ToTable("ScoresheetInstances", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Answer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("QuestionId") + .HasColumnType("uuid"); + + b.Property("ScoresheetInstanceId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("QuestionId"); + + b.HasIndex("ScoresheetInstanceId"); + + b.ToTable("Answers", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("SectionId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("Questions", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Scoresheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsArchived") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Scoresheets", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ScoresheetId"); + + b.ToTable("ScoresheetSections", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.CustomFieldValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CustomFieldId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("WorksheetInstanceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetInstanceId"); + + b.ToTable("CustomFieldValues", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UiAnchor") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetCorrelationId") + .HasColumnType("uuid"); + + b.Property("WorksheetCorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("WorksheetInstances", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetLinks.WorksheetLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UiAnchor") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetId"); + + b.ToTable("WorksheetLinks", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.CustomField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("SectionId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("CustomFields", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.Worksheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsArchived") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Worksheets", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetId"); + + b.ToTable("WorksheetSections", "Flex"); + }); + + modelBuilder.Entity("Unity.GrantManager.ApplicationForms.GenerationReview", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContextId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Operation") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReviewData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Sequence") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("Operation", "ContextId", "Sequence") + .IsUnique(); + + b.ToTable("GenerationReviews", "AI"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Applicant", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantName") + .IsRequired() + .HasMaxLength(600) + .HasColumnType("character varying(600)"); + + b.Property("ApproxNumberOfEmployees") + .HasColumnType("text"); + + b.Property("AuditComments") + .HasColumnType("text"); + + b.Property("BusinessNumber") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FiscalDay") + .HasColumnType("integer"); + + b.Property("FiscalMonth") + .HasColumnType("text"); + + b.Property("FundingHistoryComments") + .HasColumnType("text"); + + b.Property("IndigenousOrgInd") + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsDuplicated") + .HasColumnType("boolean"); + + b.Property("IssueTrackingComments") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MatchPercentage") + .HasColumnType("numeric"); + + b.Property("NonRegOrgName") + .HasColumnType("text"); + + b.Property("NonRegisteredBusinessName") + .HasColumnType("text"); + + b.Property("OrgName") + .HasColumnType("text"); + + b.Property("OrgNumber") + .HasColumnType("text"); + + b.Property("OrgStatus") + .HasColumnType("text"); + + b.Property("OrganizationType") + .HasColumnType("text"); + + b.Property("RedStop") + .HasColumnType("boolean"); + + b.Property("ReportsComments") + .HasColumnType("text"); + + b.Property("Sector") + .HasColumnType("text"); + + b.Property("SectorSubSectorIndustryDesc") + .HasColumnType("text"); + + b.Property("StartedOperatingDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("SubSector") + .HasColumnType("text"); + + b.Property("SupplierId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UnityApplicantId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantName"); + + b.HasIndex("OrgName"); + + b.HasIndex("OrgNumber"); + + b.HasIndex("Status"); + + b.HasIndex("SupplierId"); + + b.HasIndex("TenantId"); + + b.HasIndex("UnityApplicantId"); + + b.HasIndex("TenantId", "IsDeleted", "CreationTime") + .HasFilter("\"IsDeleted\" = false"); + + b.ToTable("Applicants", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAddress", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AddressType") + .HasColumnType("integer"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Postal") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("Street") + .HasColumnType("text"); + + b.Property("Street2") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Unit") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicantAddresses", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAgent", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("BceidBusinessGuid") + .HasColumnType("uuid"); + + b.Property("BceidBusinessName") + .HasColumnType("text"); + + b.Property("BceidUserGuid") + .HasColumnType("uuid"); + + b.Property("BceidUserName") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactOrder") + .HasColumnType("integer"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IdentityEmail") + .HasColumnType("text"); + + b.Property("IdentityName") + .HasColumnType("text"); + + b.Property("IdentityProvider") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsConfirmed") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OidcSubUser") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("Phone2") + .HasColumnType("text"); + + b.Property("Phone2Extension") + .HasColumnType("text"); + + b.Property("PhoneExtension") + .HasColumnType("text"); + + b.Property("RoleForApplicant") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationId") + .IsUnique(); + + b.HasIndex("TenantId", "ApplicationId"); + + b.ToTable("ApplicantAgents", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("ApplicantAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AIAnalysis") + .HasColumnType("text"); + + b.Property("AIScoresheetAnswers") + .HasColumnType("jsonb"); + + b.Property("Acquisition") + .HasColumnType("text"); + + b.Property("ApplicantElectoralDistrict") + .HasColumnType("text"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("ApplicationStatusId") + .HasColumnType("uuid"); + + b.Property("ApprovedAmount") + .HasColumnType("numeric"); + + b.Property("AssessmentResultDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AssessmentResultStatus") + .HasColumnType("text"); + + b.Property("AssessmentStartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Community") + .HasColumnType("text"); + + b.Property("CommunityPopulation") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContractExecutionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ContractNumber") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeclineRational") + .HasColumnType("text"); + + b.Property("DefaultSiteId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("DueDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DueDiligenceStatus") + .HasColumnType("text"); + + b.Property("EconomicRegion") + .HasColumnType("text"); + + b.Property("ElectoralDistrict") + .HasColumnType("text"); + + b.Property("ExternalStatusVisibility") + .HasColumnType("boolean"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FinalDecisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Forestry") + .HasColumnType("text"); + + b.Property("ForestryFocus") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LikelihoodOfFunding") + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("NotificationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Payload") + .HasColumnType("jsonb"); + + b.Property("PercentageTotalProjectBudget") + .HasColumnType("double precision"); + + b.Property("Place") + .HasColumnType("text"); + + b.Property("ProjectEndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ProjectFundingTotal") + .HasColumnType("numeric"); + + b.Property("ProjectName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProjectStartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ProjectSummary") + .HasColumnType("text"); + + b.Property("ProposalDate") + .HasColumnType("timestamp without time zone"); + + b.Property("RecommendedAmount") + .HasColumnType("numeric"); + + b.Property("ReferenceNo") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegionalDistrict") + .HasColumnType("text"); + + b.Property("RequestedAmount") + .HasColumnType("numeric"); + + b.Property("RiskRanking") + .HasColumnType("text"); + + b.Property("SigningAuthorityBusinessPhone") + .HasColumnType("text"); + + b.Property("SigningAuthorityCellPhone") + .HasColumnType("text"); + + b.Property("SigningAuthorityEmail") + .HasColumnType("text"); + + b.Property("SigningAuthorityFullName") + .HasColumnType("text"); + + b.Property("SigningAuthorityTitle") + .HasColumnType("text"); + + b.Property("SubStatus") + .HasColumnType("text"); + + b.Property("SubmissionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TotalProjectBudget") + .HasColumnType("numeric"); + + b.Property("TotalScore") + .HasColumnType("integer"); + + b.Property("UnityApplicationId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationFormId"); + + b.HasIndex("ApplicationStatusId"); + + b.HasIndex("OwnerId"); + + b.HasIndex("ReferenceNo"); + + b.HasIndex("TenantId", "SubmissionDate") + .HasFilter("\"IsDeleted\" = false"); + + b.ToTable("Applications", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAssignment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AssigneeId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Duty") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("AssigneeId"); + + b.HasIndex("TenantId", "ApplicationId"); + + b.ToTable("ApplicationAssignments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationChefsFileAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AISummary") + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ChefsFileId") + .HasColumnType("text"); + + b.Property("ChefsSubmissionId") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationChefsFileAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationContact", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactEmail") + .HasColumnType("text"); + + b.Property("ContactFullName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactMobilePhone") + .HasColumnType("text"); + + b.Property("ContactTitle") + .HasColumnType("text"); + + b.Property("ContactType") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactWorkPhone") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationContact", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationForm", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountCodingId") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .HasColumnType("text"); + + b.Property("ApplicationFormDescription") + .HasColumnType("text"); + + b.Property("ApplicationFormName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AttemptedConnectionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AutomaticallyGenerateAIAnalysis") + .HasColumnType("boolean"); + + b.Property("AvailableChefsFields") + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("text"); + + b.Property("ChefsApplicationFormGuid") + .HasColumnType("text"); + + b.Property("ChefsCriteriaFormGuid") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ConnectionHttpStatus") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DefaultPaymentGroup") + .HasColumnType("integer"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ElectoralDistrictAddressType") + .HasColumnType("integer"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormHierarchy") + .HasColumnType("integer"); + + b.Property("IntakeId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsDirectApproval") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ManuallyInitiateAIAnalysis") + .HasColumnType("boolean"); + + b.Property("ParentFormId") + .HasColumnType("uuid"); + + b.Property("Payable") + .HasColumnType("boolean"); + + b.Property("PaymentApprovalThreshold") + .HasColumnType("numeric"); + + b.Property("Prefix") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PreventPayment") + .HasColumnType("boolean"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("SuffixType") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IntakeId"); + + b.HasIndex("ParentFormId"); + + b.HasIndex("TenantId", "IsDeleted") + .HasFilter("\"IsDeleted\" = false"); + + b.ToTable("ApplicationForms", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormSubmission", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormVersionId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ChefsSubmissionGuid") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormVersionId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OidcSub") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Submission") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormSubmissions", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormVersion", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("AvailableChefsFields") + .HasColumnType("text"); + + b.Property("ChefsApplicationFormGuid") + .HasColumnType("text"); + + b.Property("ChefsFormVersionGuid") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormSchema") + .HasColumnType("jsonb"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SubmissionHeaderMapping") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormVersion", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationLink", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LinkType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("Related"); + + b.Property("LinkedApplicationId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("TenantId", "ApplicationId"); + + b.ToTable("ApplicationLinks", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationStatus", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExternalStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("InternalStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("NotifiedStatus") + .HasColumnType("text"); + + b.Property("StatusCode") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("StatusCode") + .IsUnique(); + + b.ToTable("ApplicationStatuses", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationTags", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TagId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("TagId"); + + b.HasIndex("TenantId", "ApplicationId"); + + b.ToTable("ApplicationTags", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AssessmentAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AssessmentId"); + + b.ToTable("AssessmentAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AuditHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("AuditDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AuditNote") + .HasColumnType("text"); + + b.Property("AuditStatus") + .HasColumnType("text"); + + b.Property("AuditTrackingNumber") + .HasColumnType("text"); + + b.Property("AuditorName") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("AuditHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.FundingHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApprovedAmount") + .HasColumnType("numeric"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FundingNotes") + .HasColumnType("text"); + + b.Property("FundingYear") + .HasColumnType("text"); + + b.Property("GrantCategory") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OneTimeConsideration") + .HasColumnType("numeric"); + + b.Property("PaidDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ReconsiderationAmount") + .HasColumnType("numeric"); + + b.Property("RenewedFunding") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TotalGrantAmount") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("FundingHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.IssueTracking", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IssueDescription") + .HasColumnType("text"); + + b.Property("IssueHeading") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ResolutionNote") + .HasColumnType("text"); + + b.Property("Resolved") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Year") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("IssueTrackings", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ReportsHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FiscalYear") + .HasColumnType("text"); + + b.Property("IncompleteReport") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("Outstanding") + .HasColumnType("boolean"); + + b.Property("ReportDate") + .HasColumnType("timestamp without time zone"); + + b.Property("SignedOff") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("ReportsHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Assessments.Assessment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ApprovalRecommended") + .HasColumnType("boolean"); + + b.Property("AssessorId") + .HasColumnType("uuid"); + + b.Property("CleanGrowth") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("EconomicImpact") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FinancialAnalysis") + .HasColumnType("integer"); + + b.Property("InclusiveGrowth") + .HasColumnType("integer"); + + b.Property("IsAiAssessment") + .HasColumnType("boolean"); + + b.Property("IsComplete") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("AssessorId"); + + b.ToTable("Assessments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicantComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PinDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("CommenterId"); + + b.ToTable("ApplicantComments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicationComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PinDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("CommenterId"); + + b.ToTable("ApplicationComments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.AssessmentComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PinDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AssessmentId"); + + b.HasIndex("CommenterId"); + + b.ToTable("AssessmentComments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Contacts.Contact", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("HomePhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MobilePhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("WorkPhoneExtension") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("WorkPhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.ToTable("Contacts", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Contacts.ContactLink", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("RelatedEntityId") + .HasColumnType("uuid"); + + b.Property("RelatedEntityType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Role") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("RelatedEntityType", "RelatedEntityId"); + + b.HasIndex("ContactId", "RelatedEntityType", "RelatedEntityId"); + + b.ToTable("ContactLinks", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.GlobalTag.Tag", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Tags", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Identity.Person", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Badge") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FullName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OidcDisplayName") + .IsRequired() + .HasColumnType("text"); + + b.Property("OidcSub") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("OidcSub"); + + b.HasIndex("TenantId"); + + b.ToTable("Persons", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Intakes.Intake", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Budget") + .HasColumnType("double precision"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IntakeName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("StartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Intakes", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Notifications.ScheduledNotification", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationStatus") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ApplicationStatusId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DateField") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EmailTemplateId") + .HasColumnType("uuid"); + + b.Property("EventType") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("RecipientCategory") + .HasColumnType("text"); + + b.Property("RecipientIdentifier") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TriggerDetail") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("TriggerType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("ScheduledNotifications", "Notifications"); + }); + + modelBuilder.Entity("Unity.GrantManager.Notifications.ScheduledNotificationTracking", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatorId") + .HasColumnType("uuid"); + + b.Property("DateField") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("NotificationSentDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ScheduledNotificationId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("CreationTime"); + + b.HasIndex("ScheduledNotificationId"); + + b.HasIndex("ApplicationId", "ScheduledNotificationId", "DateField") + .IsUnique(); + + b.ToTable("ScheduledNotificationTracking", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EmailGroups", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroupUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.ToTable("EmailGroupUsers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("BCC") + .IsRequired() + .HasColumnType("text"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CC") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChesHttpStatusCode") + .HasColumnType("text"); + + b.Property("ChesMsgId") + .HasColumnType("uuid"); + + b.Property("ChesResponse") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChesStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("EmailType") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FromAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestIds") + .IsRequired() + .HasColumnType("text"); + + b.Property("Priority") + .IsRequired() + .HasColumnType("text"); + + b.Property("Recipient") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("RetryAttempts") + .HasColumnType("integer"); + + b.Property("ScheduledNotificationId") + .HasColumnType("uuid"); + + b.Property("SendOnDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("SentDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("text"); + + b.Property("TemplateName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("ToAddress") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EmailLogs", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLogAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("EmailLogId") + .HasColumnType("uuid"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OriginTemplateId") + .HasColumnType("uuid"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TemplateId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("EmailLogId"); + + b.HasIndex("S3ObjectKey"); + + b.HasIndex("TemplateId"); + + b.ToTable("EmailLogAttachments", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.EmailTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BodyHTML") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyText") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RecipientCategory") + .HasColumnType("text"); + + b.Property("RecipientIdentifier") + .HasColumnType("text"); + + b.Property("SendFrom") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("EmailTemplates", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.Subscriber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Subscribers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("SubscriptionGroups", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroupSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SubscriberId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.HasIndex("SubscriberId"); + + b.ToTable("SubscriptionGroupSubscribers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TemplateVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MapTo") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("TemplateVariables", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.Trigger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("InternalName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Triggers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TriggerSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SubscriptionGroupId") + .HasColumnType("uuid"); + + b.Property("TemplateId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TriggerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionGroupId"); + + b.HasIndex("TemplateId"); + + b.HasIndex("TriggerId"); + + b.ToTable("TriggerSubscriptions", "Notifications"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.AccountCodings.AccountCoding", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Description") + .HasMaxLength(35) + .HasColumnType("character varying(35)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MinistryClient") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("Responsibility") + .IsRequired() + .HasColumnType("text"); + + b.Property("ServiceLine") + .IsRequired() + .HasColumnType("text"); + + b.Property("Stob") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("AccountCodings", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentConfigurations.PaymentConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DefaultAccountCodingId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentIdPrefix") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("PaymentConfigurations", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.ExpenseApproval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DecisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DecisionUserId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PaymentRequestId"); + + b.ToTable("ExpenseApprovals", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountCodingId") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("BatchName") + .IsRequired() + .HasColumnType("text"); + + b.Property("BatchNumber") + .HasColumnType("numeric"); + + b.Property("CancelledBy") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("CancelledBy"); + + b.Property("CancelledById") + .HasColumnType("uuid") + .HasColumnName("CancelledById"); + + b.Property("CancelledOn") + .HasColumnType("timestamp without time zone") + .HasColumnName("CancelledOn"); + + b.Property("CasHttpStatusCode") + .HasColumnType("integer"); + + b.Property("CasResponse") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContractNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FsbApNotified") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("FsbNotificationEmailLogId") + .HasColumnType("uuid"); + + b.Property("FsbNotificationSentDate") + .HasColumnType("timestamp without time zone"); + + b.Property("InvoiceNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("InvoiceStatus") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsRecon") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("PayeeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentDate") + .HasColumnType("text"); + + b.Property("PaymentNumber") + .HasColumnType("text"); + + b.Property("PaymentStatus") + .HasColumnType("text"); + + b.Property("ReferenceNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("RequesterName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SiteId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("SubmissionConfirmationCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SupplierName") + .HasColumnType("text"); + + b.Property("SupplierNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AccountCodingId"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("CreationTime"); + + b.HasIndex("FsbNotificationEmailLogId"); + + b.HasIndex("ReferenceNumber") + .IsUnique(); + + b.HasIndex("SiteId"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId", "CreationTime") + .HasFilter("\"IsDeleted\" = false"); + + b.ToTable("PaymentRequests", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentTags.PaymentTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestId") + .HasColumnType("uuid"); + + b.Property("TagId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("PaymentRequestId"); + + b.HasIndex("TagId"); + + b.ToTable("PaymentTags", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentThresholds.PaymentThreshold", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Threshold") + .HasColumnType("numeric"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("PaymentThresholds", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Site", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddressLine1") + .HasColumnType("text"); + + b.Property("AddressLine2") + .HasColumnType("text"); + + b.Property("AddressLine3") + .HasColumnType("text"); + + b.Property("BankAccount") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EFTAdvicePref") + .HasColumnType("text"); + + b.Property("EmailAddress") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastUpdatedInCas") + .HasColumnType("timestamp without time zone"); + + b.Property("MarkDeletedInUse") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentGroup") + .HasColumnType("integer"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("SiteProtected") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("SupplierId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("SupplierId"); + + b.ToTable("Sites", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Supplier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BusinessNumber") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastUpdatedInCAS") + .HasColumnType("timestamp without time zone"); + + b.Property("MailingAddress") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("text"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("SIN") + .HasColumnType("text"); + + b.Property("StandardIndustryClassification") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("Subcategory") + .HasColumnType("text"); + + b.Property("SupplierProtected") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Suppliers", "Payments"); + }); + + modelBuilder.Entity("Unity.Reporting.Domain.Configuration.ReportColumnsMap", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Mapping") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RoleStatus") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("ViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ViewStatus") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ReportColumnsMaps", "Reporting"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Scoresheet", "Scoresheet") + .WithMany("Instances") + .HasForeignKey("ScoresheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scoresheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Answer", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Question", "Question") + .WithMany("Answers") + .HasForeignKey("QuestionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", null) + .WithMany("Answers") + .HasForeignKey("ScoresheetInstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Question"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.ScoresheetSection", "Section") + .WithMany("Fields") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Scoresheet", "Scoresheet") + .WithMany("Sections") + .HasForeignKey("ScoresheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scoresheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.CustomFieldValue", b => + { + b.HasOne("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", null) + .WithMany("Values") + .HasForeignKey("WorksheetInstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetLinks.WorksheetLink", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.Worksheet", "Worksheet") + .WithMany("Links") + .HasForeignKey("WorksheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Worksheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.CustomField", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.WorksheetSection", "Section") + .WithMany("Fields") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.Worksheet", "Worksheet") + .WithMany("Sections") + .HasForeignKey("WorksheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Worksheet"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAddress", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", "Applicant") + .WithMany("ApplicantAddresses") + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicantAddresses") + .HasForeignKey("ApplicationId"); + + b.Navigation("Applicant"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAgent", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithOne("ApplicantAgent") + .HasForeignKey("Unity.GrantManager.Applications.ApplicantAgent", "ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAttachment", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", "Applicant") + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", "ApplicationForm") + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationStatus", "ApplicationStatus") + .WithMany("Applications") + .HasForeignKey("ApplicationStatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Applicant"); + + b.Navigation("ApplicationForm"); + + b.Navigation("ApplicationStatus"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAssignment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicationAssignments") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", "Assignee") + .WithMany() + .HasForeignKey("AssigneeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Assignee"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAttachment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationChefsFileAttachment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationContact", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationForm", b => + { + b.HasOne("Unity.GrantManager.Intakes.Intake", null) + .WithMany() + .HasForeignKey("IntakeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) + .WithMany() + .HasForeignKey("ParentFormId") + .OnDelete(DeleteBehavior.NoAction); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormSubmission", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormVersion", b => + { + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationLink", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany("ApplicationLinks") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationTags", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicationTags") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.GlobalTag.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AssessmentAttachment", b => + { + b.HasOne("Unity.GrantManager.Assessments.Assessment", null) + .WithMany() + .HasForeignKey("AssessmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AuditHistory", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.FundingHistory", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.IssueTracking", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ReportsHistory", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Assessments.Assessment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("Assessments") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("AssessorId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicantComment", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicationComment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.AssessmentComment", b => + { + b.HasOne("Unity.GrantManager.Assessments.Assessment", null) + .WithMany() + .HasForeignKey("AssessmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Contacts.ContactLink", b => + { + b.HasOne("Unity.GrantManager.Contacts.Contact", null) + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Notifications.ScheduledNotificationTracking", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Notifications.ScheduledNotification", null) + .WithMany() + .HasForeignKey("ScheduledNotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroupUser", b => + { + b.HasOne("Unity.Notifications.EmailGroups.EmailGroup", null) + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLogAttachment", b => + { + b.HasOne("Unity.Notifications.Emails.EmailLog", null) + .WithMany() + .HasForeignKey("EmailLogId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Unity.Notifications.Templates.EmailTemplate", null) + .WithMany() + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroupSubscription", b => + { + b.HasOne("Unity.Notifications.Templates.SubscriptionGroup", "SubscriptionGroup") + .WithMany() + .HasForeignKey("GroupId"); + + b.HasOne("Unity.Notifications.Templates.Subscriber", "Subscriber") + .WithMany() + .HasForeignKey("SubscriberId"); + + b.Navigation("Subscriber"); + + b.Navigation("SubscriptionGroup"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TriggerSubscription", b => + { + b.HasOne("Unity.Notifications.Templates.SubscriptionGroup", "SubscriptionGroup") + .WithMany() + .HasForeignKey("SubscriptionGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Notifications.Templates.EmailTemplate", "EmailTemplate") + .WithMany() + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Notifications.Templates.Trigger", "Trigger") + .WithMany() + .HasForeignKey("TriggerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmailTemplate"); + + b.Navigation("SubscriptionGroup"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.ExpenseApproval", b => + { + b.HasOne("Unity.Payments.Domain.PaymentRequests.PaymentRequest", "PaymentRequest") + .WithMany("ExpenseApprovals") + .HasForeignKey("PaymentRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PaymentRequest"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.HasOne("Unity.Payments.Domain.AccountCodings.AccountCoding", "AccountCoding") + .WithMany() + .HasForeignKey("AccountCodingId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Unity.Payments.Domain.Suppliers.Site", "Site") + .WithMany() + .HasForeignKey("SiteId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("AccountCoding"); + + b.Navigation("Site"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentTags.PaymentTag", b => + { + b.HasOne("Unity.Payments.Domain.PaymentRequests.PaymentRequest", null) + .WithMany("PaymentTags") + .HasForeignKey("PaymentRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.GlobalTag.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Site", b => + { + b.HasOne("Unity.Payments.Domain.Suppliers.Supplier", "Supplier") + .WithMany("Sites") + .HasForeignKey("SupplierId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.Navigation("Answers"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.Navigation("Answers"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Scoresheet", b => + { + b.Navigation("Instances"); + + b.Navigation("Sections"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.Navigation("Fields"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", b => + { + b.Navigation("Values"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.Worksheet", b => + { + b.Navigation("Links"); + + b.Navigation("Sections"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.Navigation("Fields"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Applicant", b => + { + b.Navigation("ApplicantAddresses"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.Navigation("ApplicantAddresses"); + + b.Navigation("ApplicantAgent"); + + b.Navigation("ApplicationAssignments"); + + b.Navigation("ApplicationLinks"); + + b.Navigation("ApplicationTags"); + + b.Navigation("Assessments"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationStatus", b => + { + b.Navigation("Applications"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.Navigation("ExpenseApprovals"); + + b.Navigation("PaymentTags"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Supplier", b => + { + b.Navigation("Sites"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260806201536_AB33799_HardenCheckboxGroupReportingViews.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260806201536_AB33799_HardenCheckboxGroupReportingViews.cs new file mode 100644 index 0000000000..766ddd60ff --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260806201536_AB33799_HardenCheckboxGroupReportingViews.cs @@ -0,0 +1,43 @@ +using System; +using System.IO; +using System.Reflection; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + /// + public partial class AB33799_HardenCheckboxGroupReportingViews : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // Re-deploy get_worksheet_data / get_consolidated_worksheet_data with a guard around the + // checkbox-group ::jsonb cast: a stored value that is null, '', or otherwise not a JSON + // array (e.g. from a checkbox-group field saved with zero selections) now resolves to + // NULL for that column instead of raising "invalid input syntax for type json" and + // breaking the whole generated view. CREATE OR REPLACE is idempotent. + RunEmbeddedScript(migrationBuilder, "get_worksheet_data.sql"); + RunEmbeddedScript(migrationBuilder, "get_consolidated_worksheet_data.sql"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + // No-op: the prior function bodies are not preserved: refer to source control history + // for the pre-hardening SQL if a rollback is ever required. + } + + private static void RunEmbeddedScript(MigrationBuilder migrationBuilder, string scriptFileName) + { + var assembly = Assembly.GetExecutingAssembly(); + var resourceName = $"Unity.GrantManager.Scripts.{scriptFileName}"; + + using var stream = assembly.GetManifestResourceStream(resourceName) + ?? throw new InvalidOperationException($"Could not find embedded resource: {resourceName}"); + using var reader = new StreamReader(stream); + migrationBuilder.Sql(reader.ReadToEnd()); + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260806202244_AB33799_FixCheckboxGroupEmptyValues.Designer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260806202244_AB33799_FixCheckboxGroupEmptyValues.Designer.cs new file mode 100644 index 0000000000..3ad2b6d6eb --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260806202244_AB33799_FixCheckboxGroupEmptyValues.Designer.cs @@ -0,0 +1,5273 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Unity.GrantManager.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + [DbContext(typeof(GrantTenantDbContext))] + [Migration("20260806202244_AB33799_FixCheckboxGroupEmptyValues")] + partial class AB33799_FixCheckboxGroupEmptyValues + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.PostgreSql) + .HasAnnotation("ProductVersion", "10.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScoresheetId"); + + b.ToTable("ScoresheetInstances", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Answer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("QuestionId") + .HasColumnType("uuid"); + + b.Property("ScoresheetInstanceId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("QuestionId"); + + b.HasIndex("ScoresheetInstanceId"); + + b.ToTable("Answers", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("SectionId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("Questions", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Scoresheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsArchived") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Scoresheets", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ScoresheetId"); + + b.ToTable("ScoresheetSections", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.CustomFieldValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CustomFieldId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("WorksheetInstanceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetInstanceId"); + + b.ToTable("CustomFieldValues", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UiAnchor") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetCorrelationId") + .HasColumnType("uuid"); + + b.Property("WorksheetCorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("WorksheetInstances", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetLinks.WorksheetLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UiAnchor") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetId"); + + b.ToTable("WorksheetLinks", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.CustomField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("SectionId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("CustomFields", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.Worksheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsArchived") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Worksheets", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetId"); + + b.ToTable("WorksheetSections", "Flex"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Applicant", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantName") + .IsRequired() + .HasMaxLength(600) + .HasColumnType("character varying(600)"); + + b.Property("ApproxNumberOfEmployees") + .HasColumnType("text"); + + b.Property("AuditComments") + .HasColumnType("text"); + + b.Property("BusinessNumber") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FiscalDay") + .HasColumnType("integer"); + + b.Property("FiscalMonth") + .HasColumnType("text"); + + b.Property("FundingHistoryComments") + .HasColumnType("text"); + + b.Property("IndigenousOrgInd") + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsDuplicated") + .HasColumnType("boolean"); + + b.Property("IssueTrackingComments") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MatchPercentage") + .HasColumnType("numeric"); + + b.Property("NonRegOrgName") + .HasColumnType("text"); + + b.Property("NonRegisteredBusinessName") + .HasColumnType("text"); + + b.Property("OrgName") + .HasColumnType("text"); + + b.Property("OrgNumber") + .HasColumnType("text"); + + b.Property("OrgStatus") + .HasColumnType("text"); + + b.Property("OrganizationType") + .HasColumnType("text"); + + b.Property("RedStop") + .HasColumnType("boolean"); + + b.Property("ReportsComments") + .HasColumnType("text"); + + b.Property("Sector") + .HasColumnType("text"); + + b.Property("SectorSubSectorIndustryDesc") + .HasColumnType("text"); + + b.Property("StartedOperatingDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("SubSector") + .HasColumnType("text"); + + b.Property("SupplierId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UnityApplicantId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantName"); + + b.HasIndex("OrgName"); + + b.HasIndex("OrgNumber"); + + b.HasIndex("Status"); + + b.HasIndex("SupplierId"); + + b.HasIndex("TenantId"); + + b.HasIndex("UnityApplicantId"); + + b.HasIndex("TenantId", "IsDeleted", "CreationTime") + .HasFilter("\"IsDeleted\" = false"); + + b.ToTable("Applicants", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAddress", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AddressType") + .HasColumnType("integer"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Postal") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("Street") + .HasColumnType("text"); + + b.Property("Street2") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Unit") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicantAddresses", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAgent", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("BceidBusinessGuid") + .HasColumnType("uuid"); + + b.Property("BceidBusinessName") + .HasColumnType("text"); + + b.Property("BceidUserGuid") + .HasColumnType("uuid"); + + b.Property("BceidUserName") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactOrder") + .HasColumnType("integer"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IdentityEmail") + .HasColumnType("text"); + + b.Property("IdentityName") + .HasColumnType("text"); + + b.Property("IdentityProvider") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsConfirmed") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OidcSubUser") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("Phone2") + .HasColumnType("text"); + + b.Property("Phone2Extension") + .HasColumnType("text"); + + b.Property("PhoneExtension") + .HasColumnType("text"); + + b.Property("RoleForApplicant") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationId") + .IsUnique(); + + b.HasIndex("TenantId", "ApplicationId"); + + b.ToTable("ApplicantAgents", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("ApplicantAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AIAnalysis") + .HasColumnType("text"); + + b.Property("AIScoresheetAnswers") + .HasColumnType("jsonb"); + + b.Property("Acquisition") + .HasColumnType("text"); + + b.Property("ApplicantElectoralDistrict") + .HasColumnType("text"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("ApplicationStatusId") + .HasColumnType("uuid"); + + b.Property("ApprovedAmount") + .HasColumnType("numeric"); + + b.Property("AssessmentResultDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AssessmentResultStatus") + .HasColumnType("text"); + + b.Property("AssessmentStartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Community") + .HasColumnType("text"); + + b.Property("CommunityPopulation") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContractExecutionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ContractNumber") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeclineRational") + .HasColumnType("text"); + + b.Property("DefaultSiteId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("DueDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DueDiligenceStatus") + .HasColumnType("text"); + + b.Property("EconomicRegion") + .HasColumnType("text"); + + b.Property("ElectoralDistrict") + .HasColumnType("text"); + + b.Property("ExternalStatusVisibility") + .HasColumnType("boolean"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FinalDecisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Forestry") + .HasColumnType("text"); + + b.Property("ForestryFocus") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LikelihoodOfFunding") + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("NotificationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Payload") + .HasColumnType("jsonb"); + + b.Property("PercentageTotalProjectBudget") + .HasColumnType("double precision"); + + b.Property("Place") + .HasColumnType("text"); + + b.Property("ProjectEndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ProjectFundingTotal") + .HasColumnType("numeric"); + + b.Property("ProjectName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProjectStartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ProjectSummary") + .HasColumnType("text"); + + b.Property("ProposalDate") + .HasColumnType("timestamp without time zone"); + + b.Property("RecommendedAmount") + .HasColumnType("numeric"); + + b.Property("ReferenceNo") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegionalDistrict") + .HasColumnType("text"); + + b.Property("RequestedAmount") + .HasColumnType("numeric"); + + b.Property("RiskRanking") + .HasColumnType("text"); + + b.Property("SigningAuthorityBusinessPhone") + .HasColumnType("text"); + + b.Property("SigningAuthorityCellPhone") + .HasColumnType("text"); + + b.Property("SigningAuthorityEmail") + .HasColumnType("text"); + + b.Property("SigningAuthorityFullName") + .HasColumnType("text"); + + b.Property("SigningAuthorityTitle") + .HasColumnType("text"); + + b.Property("SubStatus") + .HasColumnType("text"); + + b.Property("SubmissionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TotalProjectBudget") + .HasColumnType("numeric"); + + b.Property("TotalScore") + .HasColumnType("integer"); + + b.Property("UnityApplicationId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationFormId"); + + b.HasIndex("ApplicationStatusId"); + + b.HasIndex("OwnerId"); + + b.HasIndex("ReferenceNo"); + + b.HasIndex("TenantId", "SubmissionDate") + .HasFilter("\"IsDeleted\" = false"); + + b.ToTable("Applications", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAssignment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AssigneeId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Duty") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("AssigneeId"); + + b.HasIndex("TenantId", "ApplicationId"); + + b.ToTable("ApplicationAssignments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationChefsFileAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AISummary") + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ChefsFileId") + .HasColumnType("text"); + + b.Property("ChefsSubmissionId") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationChefsFileAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationContact", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactEmail") + .HasColumnType("text"); + + b.Property("ContactFullName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactMobilePhone") + .HasColumnType("text"); + + b.Property("ContactTitle") + .HasColumnType("text"); + + b.Property("ContactType") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactWorkPhone") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationContact", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationForm", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountCodingId") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .HasColumnType("text"); + + b.Property("ApplicationFormDescription") + .HasColumnType("text"); + + b.Property("ApplicationFormName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AttemptedConnectionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AutomaticallyGenerateAIAnalysis") + .HasColumnType("boolean"); + + b.Property("AvailableChefsFields") + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("text"); + + b.Property("ChefsApplicationFormGuid") + .HasColumnType("text"); + + b.Property("ChefsCriteriaFormGuid") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ConnectionHttpStatus") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DefaultPaymentGroup") + .HasColumnType("integer"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ElectoralDistrictAddressType") + .HasColumnType("integer"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormHierarchy") + .HasColumnType("integer"); + + b.Property("IntakeId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsDirectApproval") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ManuallyInitiateAIAnalysis") + .HasColumnType("boolean"); + + b.Property("ParentFormId") + .HasColumnType("uuid"); + + b.Property("Payable") + .HasColumnType("boolean"); + + b.Property("PaymentApprovalThreshold") + .HasColumnType("numeric"); + + b.Property("Prefix") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PreventPayment") + .HasColumnType("boolean"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("SuffixType") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IntakeId"); + + b.HasIndex("ParentFormId"); + + b.HasIndex("TenantId", "IsDeleted") + .HasFilter("\"IsDeleted\" = false"); + + b.ToTable("ApplicationForms", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormSubmission", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormVersionId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ChefsSubmissionGuid") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormVersionId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OidcSub") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Submission") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormSubmissions", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormVersion", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("AvailableChefsFields") + .HasColumnType("text"); + + b.Property("ChefsApplicationFormGuid") + .HasColumnType("text"); + + b.Property("ChefsFormVersionGuid") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormSchema") + .HasColumnType("jsonb"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SubmissionHeaderMapping") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormVersion", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationLink", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LinkType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("Related"); + + b.Property("LinkedApplicationId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("TenantId", "ApplicationId"); + + b.ToTable("ApplicationLinks", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationStatus", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExternalStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("InternalStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("NotifiedStatus") + .HasColumnType("text"); + + b.Property("StatusCode") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("StatusCode") + .IsUnique(); + + b.ToTable("ApplicationStatuses", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationTags", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TagId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("TagId"); + + b.HasIndex("TenantId", "ApplicationId"); + + b.ToTable("ApplicationTags", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AssessmentAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AssessmentId"); + + b.ToTable("AssessmentAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AuditHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("AuditDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AuditNote") + .HasColumnType("text"); + + b.Property("AuditStatus") + .HasColumnType("text"); + + b.Property("AuditTrackingNumber") + .HasColumnType("text"); + + b.Property("AuditorName") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("AuditHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.FundingHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApprovedAmount") + .HasColumnType("numeric"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FundingNotes") + .HasColumnType("text"); + + b.Property("FundingYear") + .HasColumnType("text"); + + b.Property("GrantCategory") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OneTimeConsideration") + .HasColumnType("numeric"); + + b.Property("PaidDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ReconsiderationAmount") + .HasColumnType("numeric"); + + b.Property("RenewedFunding") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TotalGrantAmount") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("FundingHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.IssueTracking", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IssueDescription") + .HasColumnType("text"); + + b.Property("IssueHeading") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ResolutionNote") + .HasColumnType("text"); + + b.Property("Resolved") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Year") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("IssueTrackings", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ReportsHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FiscalYear") + .HasColumnType("text"); + + b.Property("IncompleteReport") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("Outstanding") + .HasColumnType("boolean"); + + b.Property("ReportDate") + .HasColumnType("timestamp without time zone"); + + b.Property("SignedOff") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("ReportsHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Assessments.Assessment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ApprovalRecommended") + .HasColumnType("boolean"); + + b.Property("AssessorId") + .HasColumnType("uuid"); + + b.Property("CleanGrowth") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("EconomicImpact") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FinancialAnalysis") + .HasColumnType("integer"); + + b.Property("InclusiveGrowth") + .HasColumnType("integer"); + + b.Property("IsAiAssessment") + .HasColumnType("boolean"); + + b.Property("IsComplete") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("AssessorId"); + + b.ToTable("Assessments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicantComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PinDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("CommenterId"); + + b.ToTable("ApplicantComments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicationComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PinDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("CommenterId"); + + b.ToTable("ApplicationComments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.AssessmentComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PinDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AssessmentId"); + + b.HasIndex("CommenterId"); + + b.ToTable("AssessmentComments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Contacts.Contact", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("HomePhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MobilePhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("WorkPhoneExtension") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("WorkPhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.ToTable("Contacts", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Contacts.ContactLink", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("RelatedEntityId") + .HasColumnType("uuid"); + + b.Property("RelatedEntityType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Role") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("RelatedEntityType", "RelatedEntityId"); + + b.HasIndex("ContactId", "RelatedEntityType", "RelatedEntityId"); + + b.ToTable("ContactLinks", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.GlobalTag.Tag", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Tags", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Identity.Person", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Badge") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FullName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OidcDisplayName") + .IsRequired() + .HasColumnType("text"); + + b.Property("OidcSub") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("OidcSub"); + + b.HasIndex("TenantId"); + + b.ToTable("Persons", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Intakes.Intake", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Budget") + .HasColumnType("double precision"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IntakeName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("StartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Intakes", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Notifications.ScheduledNotification", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationStatus") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ApplicationStatusId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DateField") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EmailTemplateId") + .HasColumnType("uuid"); + + b.Property("EventType") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("RecipientCategory") + .HasColumnType("text"); + + b.Property("RecipientIdentifier") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TriggerDetail") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("TriggerType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("ScheduledNotifications", "Notifications"); + }); + + modelBuilder.Entity("Unity.GrantManager.Notifications.ScheduledNotificationTracking", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatorId") + .HasColumnType("uuid"); + + b.Property("DateField") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("NotificationSentDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ScheduledNotificationId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("CreationTime"); + + b.HasIndex("ScheduledNotificationId"); + + b.HasIndex("ApplicationId", "ScheduledNotificationId", "DateField") + .IsUnique(); + + b.ToTable("ScheduledNotificationTracking", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EmailGroups", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroupUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.ToTable("EmailGroupUsers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("BCC") + .IsRequired() + .HasColumnType("text"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CC") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChesHttpStatusCode") + .HasColumnType("text"); + + b.Property("ChesMsgId") + .HasColumnType("uuid"); + + b.Property("ChesResponse") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChesStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("EmailType") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FromAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestIds") + .IsRequired() + .HasColumnType("text"); + + b.Property("Priority") + .IsRequired() + .HasColumnType("text"); + + b.Property("Recipient") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("RetryAttempts") + .HasColumnType("integer"); + + b.Property("ScheduledNotificationId") + .HasColumnType("uuid"); + + b.Property("SendOnDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("SentDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("text"); + + b.Property("TemplateName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("ToAddress") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EmailLogs", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLogAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("EmailLogId") + .HasColumnType("uuid"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OriginTemplateId") + .HasColumnType("uuid"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TemplateId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("EmailLogId"); + + b.HasIndex("S3ObjectKey"); + + b.HasIndex("TemplateId"); + + b.ToTable("EmailLogAttachments", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.EmailTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BodyHTML") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyText") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RecipientCategory") + .HasColumnType("text"); + + b.Property("RecipientIdentifier") + .HasColumnType("text"); + + b.Property("SendFrom") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("EmailTemplates", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.Subscriber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Subscribers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("SubscriptionGroups", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroupSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SubscriberId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.HasIndex("SubscriberId"); + + b.ToTable("SubscriptionGroupSubscribers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TemplateVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MapTo") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("TemplateVariables", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.Trigger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("InternalName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Triggers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TriggerSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SubscriptionGroupId") + .HasColumnType("uuid"); + + b.Property("TemplateId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TriggerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionGroupId"); + + b.HasIndex("TemplateId"); + + b.HasIndex("TriggerId"); + + b.ToTable("TriggerSubscriptions", "Notifications"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.AccountCodings.AccountCoding", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Description") + .HasMaxLength(35) + .HasColumnType("character varying(35)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MinistryClient") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("Responsibility") + .IsRequired() + .HasColumnType("text"); + + b.Property("ServiceLine") + .IsRequired() + .HasColumnType("text"); + + b.Property("Stob") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("AccountCodings", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentConfigurations.PaymentConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DefaultAccountCodingId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentIdPrefix") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("PaymentConfigurations", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.ExpenseApproval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DecisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DecisionUserId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PaymentRequestId"); + + b.ToTable("ExpenseApprovals", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountCodingId") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("BatchName") + .IsRequired() + .HasColumnType("text"); + + b.Property("BatchNumber") + .HasColumnType("numeric"); + + b.Property("CancelledBy") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("CancelledBy"); + + b.Property("CancelledById") + .HasColumnType("uuid") + .HasColumnName("CancelledById"); + + b.Property("CancelledOn") + .HasColumnType("timestamp without time zone") + .HasColumnName("CancelledOn"); + + b.Property("CasHttpStatusCode") + .HasColumnType("integer"); + + b.Property("CasResponse") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContractNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FsbApNotified") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("FsbNotificationEmailLogId") + .HasColumnType("uuid"); + + b.Property("FsbNotificationSentDate") + .HasColumnType("timestamp without time zone"); + + b.Property("InvoiceNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("InvoiceStatus") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsRecon") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("PayeeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentDate") + .HasColumnType("text"); + + b.Property("PaymentNumber") + .HasColumnType("text"); + + b.Property("PaymentStatus") + .HasColumnType("text"); + + b.Property("ReferenceNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("RequesterName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SiteId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("SubmissionConfirmationCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SupplierName") + .HasColumnType("text"); + + b.Property("SupplierNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AccountCodingId"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("CreationTime"); + + b.HasIndex("FsbNotificationEmailLogId"); + + b.HasIndex("ReferenceNumber") + .IsUnique(); + + b.HasIndex("SiteId"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId", "CreationTime") + .HasFilter("\"IsDeleted\" = false"); + + b.ToTable("PaymentRequests", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentTags.PaymentTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestId") + .HasColumnType("uuid"); + + b.Property("TagId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("PaymentRequestId"); + + b.HasIndex("TagId"); + + b.ToTable("PaymentTags", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentThresholds.PaymentThreshold", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Threshold") + .HasColumnType("numeric"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("PaymentThresholds", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Site", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddressLine1") + .HasColumnType("text"); + + b.Property("AddressLine2") + .HasColumnType("text"); + + b.Property("AddressLine3") + .HasColumnType("text"); + + b.Property("BankAccount") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EFTAdvicePref") + .HasColumnType("text"); + + b.Property("EmailAddress") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastUpdatedInCas") + .HasColumnType("timestamp without time zone"); + + b.Property("MarkDeletedInUse") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentGroup") + .HasColumnType("integer"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("SiteProtected") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("SupplierId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("SupplierId"); + + b.ToTable("Sites", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Supplier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BusinessNumber") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastUpdatedInCAS") + .HasColumnType("timestamp without time zone"); + + b.Property("MailingAddress") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("text"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("SIN") + .HasColumnType("text"); + + b.Property("StandardIndustryClassification") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("Subcategory") + .HasColumnType("text"); + + b.Property("SupplierProtected") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Suppliers", "Payments"); + }); + + modelBuilder.Entity("Unity.Reporting.Domain.Configuration.ReportColumnsMap", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Mapping") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RoleStatus") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("ViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ViewStatus") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ReportColumnsMaps", "Reporting"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Scoresheet", "Scoresheet") + .WithMany("Instances") + .HasForeignKey("ScoresheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scoresheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Answer", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Question", "Question") + .WithMany("Answers") + .HasForeignKey("QuestionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", null) + .WithMany("Answers") + .HasForeignKey("ScoresheetInstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Question"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.ScoresheetSection", "Section") + .WithMany("Fields") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Scoresheet", "Scoresheet") + .WithMany("Sections") + .HasForeignKey("ScoresheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scoresheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.CustomFieldValue", b => + { + b.HasOne("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", null) + .WithMany("Values") + .HasForeignKey("WorksheetInstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetLinks.WorksheetLink", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.Worksheet", "Worksheet") + .WithMany("Links") + .HasForeignKey("WorksheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Worksheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.CustomField", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.WorksheetSection", "Section") + .WithMany("Fields") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.Worksheet", "Worksheet") + .WithMany("Sections") + .HasForeignKey("WorksheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Worksheet"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAddress", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", "Applicant") + .WithMany("ApplicantAddresses") + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicantAddresses") + .HasForeignKey("ApplicationId"); + + b.Navigation("Applicant"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAgent", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithOne("ApplicantAgent") + .HasForeignKey("Unity.GrantManager.Applications.ApplicantAgent", "ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAttachment", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", "Applicant") + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", "ApplicationForm") + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationStatus", "ApplicationStatus") + .WithMany("Applications") + .HasForeignKey("ApplicationStatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Applicant"); + + b.Navigation("ApplicationForm"); + + b.Navigation("ApplicationStatus"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAssignment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicationAssignments") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", "Assignee") + .WithMany() + .HasForeignKey("AssigneeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Assignee"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAttachment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationChefsFileAttachment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationContact", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationForm", b => + { + b.HasOne("Unity.GrantManager.Intakes.Intake", null) + .WithMany() + .HasForeignKey("IntakeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) + .WithMany() + .HasForeignKey("ParentFormId") + .OnDelete(DeleteBehavior.NoAction); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormSubmission", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormVersion", b => + { + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationLink", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany("ApplicationLinks") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationTags", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicationTags") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.GlobalTag.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AssessmentAttachment", b => + { + b.HasOne("Unity.GrantManager.Assessments.Assessment", null) + .WithMany() + .HasForeignKey("AssessmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AuditHistory", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.FundingHistory", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.IssueTracking", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ReportsHistory", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Assessments.Assessment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("Assessments") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("AssessorId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicantComment", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicationComment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.AssessmentComment", b => + { + b.HasOne("Unity.GrantManager.Assessments.Assessment", null) + .WithMany() + .HasForeignKey("AssessmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Contacts.ContactLink", b => + { + b.HasOne("Unity.GrantManager.Contacts.Contact", null) + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Notifications.ScheduledNotificationTracking", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Notifications.ScheduledNotification", null) + .WithMany() + .HasForeignKey("ScheduledNotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroupUser", b => + { + b.HasOne("Unity.Notifications.EmailGroups.EmailGroup", null) + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLogAttachment", b => + { + b.HasOne("Unity.Notifications.Emails.EmailLog", null) + .WithMany() + .HasForeignKey("EmailLogId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Unity.Notifications.Templates.EmailTemplate", null) + .WithMany() + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroupSubscription", b => + { + b.HasOne("Unity.Notifications.Templates.SubscriptionGroup", "SubscriptionGroup") + .WithMany() + .HasForeignKey("GroupId"); + + b.HasOne("Unity.Notifications.Templates.Subscriber", "Subscriber") + .WithMany() + .HasForeignKey("SubscriberId"); + + b.Navigation("Subscriber"); + + b.Navigation("SubscriptionGroup"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TriggerSubscription", b => + { + b.HasOne("Unity.Notifications.Templates.SubscriptionGroup", "SubscriptionGroup") + .WithMany() + .HasForeignKey("SubscriptionGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Notifications.Templates.EmailTemplate", "EmailTemplate") + .WithMany() + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Notifications.Templates.Trigger", "Trigger") + .WithMany() + .HasForeignKey("TriggerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmailTemplate"); + + b.Navigation("SubscriptionGroup"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.ExpenseApproval", b => + { + b.HasOne("Unity.Payments.Domain.PaymentRequests.PaymentRequest", "PaymentRequest") + .WithMany("ExpenseApprovals") + .HasForeignKey("PaymentRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PaymentRequest"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.HasOne("Unity.Payments.Domain.AccountCodings.AccountCoding", "AccountCoding") + .WithMany() + .HasForeignKey("AccountCodingId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Unity.Payments.Domain.Suppliers.Site", "Site") + .WithMany() + .HasForeignKey("SiteId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("AccountCoding"); + + b.Navigation("Site"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentTags.PaymentTag", b => + { + b.HasOne("Unity.Payments.Domain.PaymentRequests.PaymentRequest", null) + .WithMany("PaymentTags") + .HasForeignKey("PaymentRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.GlobalTag.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Site", b => + { + b.HasOne("Unity.Payments.Domain.Suppliers.Supplier", "Supplier") + .WithMany("Sites") + .HasForeignKey("SupplierId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.Navigation("Answers"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.Navigation("Answers"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Scoresheet", b => + { + b.Navigation("Instances"); + + b.Navigation("Sections"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.Navigation("Fields"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", b => + { + b.Navigation("Values"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.Worksheet", b => + { + b.Navigation("Links"); + + b.Navigation("Sections"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.Navigation("Fields"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Applicant", b => + { + b.Navigation("ApplicantAddresses"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.Navigation("ApplicantAddresses"); + + b.Navigation("ApplicantAgent"); + + b.Navigation("ApplicationAssignments"); + + b.Navigation("ApplicationLinks"); + + b.Navigation("ApplicationTags"); + + b.Navigation("Assessments"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationStatus", b => + { + b.Navigation("Applications"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.Navigation("ExpenseApprovals"); + + b.Navigation("PaymentTags"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Supplier", b => + { + b.Navigation("Sites"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260806202244_AB33799_FixCheckboxGroupEmptyValues.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260806202244_AB33799_FixCheckboxGroupEmptyValues.cs new file mode 100644 index 0000000000..e518f36760 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260806202244_AB33799_FixCheckboxGroupEmptyValues.cs @@ -0,0 +1,71 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + /// + public partial class AB33799_FixCheckboxGroupEmptyValues : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // One-off backfill for AB#33799: root-level checkbox-group fields (e.g. HowDidYouHearAboutUs, + // TrainingEducation, BeneficiaryGroups) that were saved with an empty string / non-JSON-array + // value instead of null. That shape breaks the reporting views' ::jsonb cast with + // "invalid input syntax for type json". Reuses the checkbox-group field-key detection from + // "Reporting"."ReportColumnsMaps" to find every worksheet/field affected, not just the ones + // already found manually on UAT. Idempotent: once fixed, the value is JSON null and no + // longer matches the WHERE clause, so re-running this migration is a no-op. + migrationBuilder.Sql(@" + WITH checkboxgroup_fields AS ( + SELECT DISTINCT + split_part( + COALESCE( + CASE + WHEN row_data->>'DataPath' ~ '^\(' + THEN regexp_replace(row_data->>'DataPath', '^\([^)]+\)', '') + ELSE row_data->>'DataPath' + END, + row_data->>'PropertyName' + ), '->', 1 + ) AS field_key + FROM ""Reporting"".""ReportColumnsMaps"" rcm, + jsonb_array_elements(rcm.""Mapping""->'Rows') AS row_data + WHERE row_data->>'TypePath' ILIKE '%checkboxgroup%' + AND row_data->>'TypePath' NOT ILIKE '%datagrid%' + ) + UPDATE ""Flex"".""WorksheetInstances"" wi + SET ""CurrentValue"" = jsonb_set( + wi.""CurrentValue"", + '{values}', + ( + SELECT jsonb_agg( + CASE + WHEN elem->>'key' IN (SELECT field_key FROM checkboxgroup_fields) + AND elem->>'value' IS NOT NULL + AND jsonb_typeof(""Reporting"".safe_to_jsonb(elem->>'value')) IS DISTINCT FROM 'array' + THEN jsonb_set(elem, '{value}', 'null'::jsonb) + ELSE elem + END + ) + FROM jsonb_array_elements(wi.""CurrentValue""->'values') AS elem + ) + ) + WHERE EXISTS ( + SELECT 1 + FROM jsonb_array_elements(wi.""CurrentValue""->'values') AS elem + WHERE elem->>'key' IN (SELECT field_key FROM checkboxgroup_fields) + AND elem->>'value' IS NOT NULL + AND jsonb_typeof(""Reporting"".safe_to_jsonb(elem->>'value')) IS DISTINCT FROM 'array' + );"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + // No-op: the original (invalid) empty-string values are not preserved, and re-introducing + // them would just reproduce the bug this migration fixes. + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260807224046_AB33996_AddApplicantFiscalYearEndRestricted.Designer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260807224046_AB33996_AddApplicantFiscalYearEndRestricted.Designer.cs new file mode 100644 index 0000000000..5994040904 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260807224046_AB33996_AddApplicantFiscalYearEndRestricted.Designer.cs @@ -0,0 +1,5277 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Unity.GrantManager.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + [DbContext(typeof(GrantTenantDbContext))] + [Migration("20260807224046_AB33996_AddApplicantFiscalYearEndRestricted")] + partial class AB33996_AddApplicantFiscalYearEndRestricted + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.PostgreSql) + .HasAnnotation("ProductVersion", "10.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScoresheetId"); + + b.ToTable("ScoresheetInstances", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Answer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("QuestionId") + .HasColumnType("uuid"); + + b.Property("ScoresheetInstanceId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("QuestionId"); + + b.HasIndex("ScoresheetInstanceId"); + + b.ToTable("Answers", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("SectionId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("Questions", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Scoresheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsArchived") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Scoresheets", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ScoresheetId"); + + b.ToTable("ScoresheetSections", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.CustomFieldValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CustomFieldId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("WorksheetInstanceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetInstanceId"); + + b.ToTable("CustomFieldValues", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UiAnchor") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetCorrelationId") + .HasColumnType("uuid"); + + b.Property("WorksheetCorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("WorksheetInstances", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetLinks.WorksheetLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UiAnchor") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetId"); + + b.ToTable("WorksheetLinks", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.CustomField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("SectionId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("CustomFields", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.Worksheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsArchived") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Worksheets", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetId"); + + b.ToTable("WorksheetSections", "Flex"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Applicant", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantName") + .IsRequired() + .HasMaxLength(600) + .HasColumnType("character varying(600)"); + + b.Property("ApproxNumberOfEmployees") + .HasColumnType("text"); + + b.Property("AuditComments") + .HasColumnType("text"); + + b.Property("BusinessNumber") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FiscalDay") + .HasColumnType("integer"); + + b.Property("FiscalMonth") + .HasColumnType("text"); + + b.Property("FiscalYearEnd") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("date"); + + b.Property("FundingHistoryComments") + .HasColumnType("text"); + + b.Property("IndigenousOrgInd") + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsDuplicated") + .HasColumnType("boolean"); + + b.Property("IssueTrackingComments") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MatchPercentage") + .HasColumnType("numeric"); + + b.Property("NonRegOrgName") + .HasColumnType("text"); + + b.Property("NonRegisteredBusinessName") + .HasColumnType("text"); + + b.Property("OrgName") + .HasColumnType("text"); + + b.Property("OrgNumber") + .HasColumnType("text"); + + b.Property("OrgStatus") + .HasColumnType("text"); + + b.Property("OrganizationType") + .HasColumnType("text"); + + b.Property("RedStop") + .HasColumnType("boolean"); + + b.Property("ReportsComments") + .HasColumnType("text"); + + b.Property("Sector") + .HasColumnType("text"); + + b.Property("SectorSubSectorIndustryDesc") + .HasColumnType("text"); + + b.Property("StartedOperatingDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("SubSector") + .HasColumnType("text"); + + b.Property("SupplierId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UnityApplicantId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantName"); + + b.HasIndex("OrgName"); + + b.HasIndex("OrgNumber"); + + b.HasIndex("Status"); + + b.HasIndex("SupplierId"); + + b.HasIndex("TenantId"); + + b.HasIndex("UnityApplicantId"); + + b.HasIndex("TenantId", "IsDeleted", "CreationTime") + .HasFilter("\"IsDeleted\" = false"); + + b.ToTable("Applicants", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAddress", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AddressType") + .HasColumnType("integer"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Postal") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("Street") + .HasColumnType("text"); + + b.Property("Street2") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Unit") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicantAddresses", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAgent", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("BceidBusinessGuid") + .HasColumnType("uuid"); + + b.Property("BceidBusinessName") + .HasColumnType("text"); + + b.Property("BceidUserGuid") + .HasColumnType("uuid"); + + b.Property("BceidUserName") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactOrder") + .HasColumnType("integer"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IdentityEmail") + .HasColumnType("text"); + + b.Property("IdentityName") + .HasColumnType("text"); + + b.Property("IdentityProvider") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsConfirmed") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OidcSubUser") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("Phone2") + .HasColumnType("text"); + + b.Property("Phone2Extension") + .HasColumnType("text"); + + b.Property("PhoneExtension") + .HasColumnType("text"); + + b.Property("RoleForApplicant") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationId") + .IsUnique(); + + b.HasIndex("TenantId", "ApplicationId"); + + b.ToTable("ApplicantAgents", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("ApplicantAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AIAnalysis") + .HasColumnType("text"); + + b.Property("AIScoresheetAnswers") + .HasColumnType("jsonb"); + + b.Property("Acquisition") + .HasColumnType("text"); + + b.Property("ApplicantElectoralDistrict") + .HasColumnType("text"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("ApplicationStatusId") + .HasColumnType("uuid"); + + b.Property("ApprovedAmount") + .HasColumnType("numeric"); + + b.Property("AssessmentResultDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AssessmentResultStatus") + .HasColumnType("text"); + + b.Property("AssessmentStartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Community") + .HasColumnType("text"); + + b.Property("CommunityPopulation") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContractExecutionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ContractNumber") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeclineRational") + .HasColumnType("text"); + + b.Property("DefaultSiteId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("DueDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DueDiligenceStatus") + .HasColumnType("text"); + + b.Property("EconomicRegion") + .HasColumnType("text"); + + b.Property("ElectoralDistrict") + .HasColumnType("text"); + + b.Property("ExternalStatusVisibility") + .HasColumnType("boolean"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FinalDecisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Forestry") + .HasColumnType("text"); + + b.Property("ForestryFocus") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LikelihoodOfFunding") + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("NotificationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Payload") + .HasColumnType("jsonb"); + + b.Property("PercentageTotalProjectBudget") + .HasColumnType("double precision"); + + b.Property("Place") + .HasColumnType("text"); + + b.Property("ProjectEndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ProjectFundingTotal") + .HasColumnType("numeric"); + + b.Property("ProjectName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProjectStartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ProjectSummary") + .HasColumnType("text"); + + b.Property("ProposalDate") + .HasColumnType("timestamp without time zone"); + + b.Property("RecommendedAmount") + .HasColumnType("numeric"); + + b.Property("ReferenceNo") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegionalDistrict") + .HasColumnType("text"); + + b.Property("RequestedAmount") + .HasColumnType("numeric"); + + b.Property("RiskRanking") + .HasColumnType("text"); + + b.Property("SigningAuthorityBusinessPhone") + .HasColumnType("text"); + + b.Property("SigningAuthorityCellPhone") + .HasColumnType("text"); + + b.Property("SigningAuthorityEmail") + .HasColumnType("text"); + + b.Property("SigningAuthorityFullName") + .HasColumnType("text"); + + b.Property("SigningAuthorityTitle") + .HasColumnType("text"); + + b.Property("SubStatus") + .HasColumnType("text"); + + b.Property("SubmissionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TotalProjectBudget") + .HasColumnType("numeric"); + + b.Property("TotalScore") + .HasColumnType("integer"); + + b.Property("UnityApplicationId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationFormId"); + + b.HasIndex("ApplicationStatusId"); + + b.HasIndex("OwnerId"); + + b.HasIndex("ReferenceNo"); + + b.HasIndex("TenantId", "SubmissionDate") + .HasFilter("\"IsDeleted\" = false"); + + b.ToTable("Applications", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAssignment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AssigneeId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Duty") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("AssigneeId"); + + b.HasIndex("TenantId", "ApplicationId"); + + b.ToTable("ApplicationAssignments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationChefsFileAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AISummary") + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ChefsFileId") + .HasColumnType("text"); + + b.Property("ChefsSubmissionId") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationChefsFileAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationContact", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactEmail") + .HasColumnType("text"); + + b.Property("ContactFullName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactMobilePhone") + .HasColumnType("text"); + + b.Property("ContactTitle") + .HasColumnType("text"); + + b.Property("ContactType") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactWorkPhone") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationContact", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationForm", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountCodingId") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .HasColumnType("text"); + + b.Property("ApplicationFormDescription") + .HasColumnType("text"); + + b.Property("ApplicationFormName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AttemptedConnectionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AutomaticallyGenerateAIAnalysis") + .HasColumnType("boolean"); + + b.Property("AvailableChefsFields") + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("text"); + + b.Property("ChefsApplicationFormGuid") + .HasColumnType("text"); + + b.Property("ChefsCriteriaFormGuid") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ConnectionHttpStatus") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DefaultPaymentGroup") + .HasColumnType("integer"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ElectoralDistrictAddressType") + .HasColumnType("integer"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormHierarchy") + .HasColumnType("integer"); + + b.Property("IntakeId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsDirectApproval") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ManuallyInitiateAIAnalysis") + .HasColumnType("boolean"); + + b.Property("ParentFormId") + .HasColumnType("uuid"); + + b.Property("Payable") + .HasColumnType("boolean"); + + b.Property("PaymentApprovalThreshold") + .HasColumnType("numeric"); + + b.Property("Prefix") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PreventPayment") + .HasColumnType("boolean"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("SuffixType") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IntakeId"); + + b.HasIndex("ParentFormId"); + + b.HasIndex("TenantId", "IsDeleted") + .HasFilter("\"IsDeleted\" = false"); + + b.ToTable("ApplicationForms", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormSubmission", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormVersionId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ChefsSubmissionGuid") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormVersionId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OidcSub") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Submission") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormSubmissions", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormVersion", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("AvailableChefsFields") + .HasColumnType("text"); + + b.Property("ChefsApplicationFormGuid") + .HasColumnType("text"); + + b.Property("ChefsFormVersionGuid") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormSchema") + .HasColumnType("jsonb"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SubmissionHeaderMapping") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormVersion", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationLink", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LinkType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("Related"); + + b.Property("LinkedApplicationId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("TenantId", "ApplicationId"); + + b.ToTable("ApplicationLinks", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationStatus", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExternalStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("InternalStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("NotifiedStatus") + .HasColumnType("text"); + + b.Property("StatusCode") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("StatusCode") + .IsUnique(); + + b.ToTable("ApplicationStatuses", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationTags", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TagId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("TagId"); + + b.HasIndex("TenantId", "ApplicationId"); + + b.ToTable("ApplicationTags", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AssessmentAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AssessmentId"); + + b.ToTable("AssessmentAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AuditHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("AuditDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AuditNote") + .HasColumnType("text"); + + b.Property("AuditStatus") + .HasColumnType("text"); + + b.Property("AuditTrackingNumber") + .HasColumnType("text"); + + b.Property("AuditorName") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("AuditHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.FundingHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApprovedAmount") + .HasColumnType("numeric"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FundingNotes") + .HasColumnType("text"); + + b.Property("FundingYear") + .HasColumnType("text"); + + b.Property("GrantCategory") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OneTimeConsideration") + .HasColumnType("numeric"); + + b.Property("PaidDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ReconsiderationAmount") + .HasColumnType("numeric"); + + b.Property("RenewedFunding") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TotalGrantAmount") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("FundingHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.IssueTracking", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IssueDescription") + .HasColumnType("text"); + + b.Property("IssueHeading") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ResolutionNote") + .HasColumnType("text"); + + b.Property("Resolved") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Year") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("IssueTrackings", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ReportsHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FiscalYear") + .HasColumnType("text"); + + b.Property("IncompleteReport") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("Outstanding") + .HasColumnType("boolean"); + + b.Property("ReportDate") + .HasColumnType("timestamp without time zone"); + + b.Property("SignedOff") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("ReportsHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Assessments.Assessment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ApprovalRecommended") + .HasColumnType("boolean"); + + b.Property("AssessorId") + .HasColumnType("uuid"); + + b.Property("CleanGrowth") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("EconomicImpact") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FinancialAnalysis") + .HasColumnType("integer"); + + b.Property("InclusiveGrowth") + .HasColumnType("integer"); + + b.Property("IsAiAssessment") + .HasColumnType("boolean"); + + b.Property("IsComplete") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("AssessorId"); + + b.ToTable("Assessments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicantComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PinDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("CommenterId"); + + b.ToTable("ApplicantComments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicationComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PinDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("CommenterId"); + + b.ToTable("ApplicationComments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.AssessmentComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PinDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AssessmentId"); + + b.HasIndex("CommenterId"); + + b.ToTable("AssessmentComments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Contacts.Contact", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("HomePhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MobilePhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("WorkPhoneExtension") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("WorkPhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.ToTable("Contacts", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Contacts.ContactLink", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("RelatedEntityId") + .HasColumnType("uuid"); + + b.Property("RelatedEntityType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Role") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("RelatedEntityType", "RelatedEntityId"); + + b.HasIndex("ContactId", "RelatedEntityType", "RelatedEntityId"); + + b.ToTable("ContactLinks", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.GlobalTag.Tag", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Tags", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Identity.Person", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Badge") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FullName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OidcDisplayName") + .IsRequired() + .HasColumnType("text"); + + b.Property("OidcSub") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("OidcSub"); + + b.HasIndex("TenantId"); + + b.ToTable("Persons", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Intakes.Intake", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Budget") + .HasColumnType("double precision"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IntakeName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("StartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Intakes", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Notifications.ScheduledNotification", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationStatus") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ApplicationStatusId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DateField") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EmailTemplateId") + .HasColumnType("uuid"); + + b.Property("EventType") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("RecipientCategory") + .HasColumnType("text"); + + b.Property("RecipientIdentifier") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TriggerDetail") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("TriggerType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("ScheduledNotifications", "Notifications"); + }); + + modelBuilder.Entity("Unity.GrantManager.Notifications.ScheduledNotificationTracking", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatorId") + .HasColumnType("uuid"); + + b.Property("DateField") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("NotificationSentDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ScheduledNotificationId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("CreationTime"); + + b.HasIndex("ScheduledNotificationId"); + + b.HasIndex("ApplicationId", "ScheduledNotificationId", "DateField") + .IsUnique(); + + b.ToTable("ScheduledNotificationTracking", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EmailGroups", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroupUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.ToTable("EmailGroupUsers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("BCC") + .IsRequired() + .HasColumnType("text"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CC") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChesHttpStatusCode") + .HasColumnType("text"); + + b.Property("ChesMsgId") + .HasColumnType("uuid"); + + b.Property("ChesResponse") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChesStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("EmailType") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FromAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestIds") + .IsRequired() + .HasColumnType("text"); + + b.Property("Priority") + .IsRequired() + .HasColumnType("text"); + + b.Property("Recipient") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("RetryAttempts") + .HasColumnType("integer"); + + b.Property("ScheduledNotificationId") + .HasColumnType("uuid"); + + b.Property("SendOnDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("SentDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("text"); + + b.Property("TemplateName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("ToAddress") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EmailLogs", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLogAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("EmailLogId") + .HasColumnType("uuid"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OriginTemplateId") + .HasColumnType("uuid"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TemplateId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("EmailLogId"); + + b.HasIndex("S3ObjectKey"); + + b.HasIndex("TemplateId"); + + b.ToTable("EmailLogAttachments", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.EmailTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BodyHTML") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyText") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RecipientCategory") + .HasColumnType("text"); + + b.Property("RecipientIdentifier") + .HasColumnType("text"); + + b.Property("SendFrom") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("EmailTemplates", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.Subscriber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Subscribers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("SubscriptionGroups", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroupSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SubscriberId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.HasIndex("SubscriberId"); + + b.ToTable("SubscriptionGroupSubscribers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TemplateVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MapTo") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("TemplateVariables", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.Trigger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("InternalName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Triggers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TriggerSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SubscriptionGroupId") + .HasColumnType("uuid"); + + b.Property("TemplateId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TriggerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionGroupId"); + + b.HasIndex("TemplateId"); + + b.HasIndex("TriggerId"); + + b.ToTable("TriggerSubscriptions", "Notifications"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.AccountCodings.AccountCoding", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Description") + .HasMaxLength(35) + .HasColumnType("character varying(35)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MinistryClient") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("Responsibility") + .IsRequired() + .HasColumnType("text"); + + b.Property("ServiceLine") + .IsRequired() + .HasColumnType("text"); + + b.Property("Stob") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("AccountCodings", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentConfigurations.PaymentConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DefaultAccountCodingId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentIdPrefix") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("PaymentConfigurations", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.ExpenseApproval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DecisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DecisionUserId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PaymentRequestId"); + + b.ToTable("ExpenseApprovals", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountCodingId") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("BatchName") + .IsRequired() + .HasColumnType("text"); + + b.Property("BatchNumber") + .HasColumnType("numeric"); + + b.Property("CancelledBy") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("CancelledBy"); + + b.Property("CancelledById") + .HasColumnType("uuid") + .HasColumnName("CancelledById"); + + b.Property("CancelledOn") + .HasColumnType("timestamp without time zone") + .HasColumnName("CancelledOn"); + + b.Property("CasHttpStatusCode") + .HasColumnType("integer"); + + b.Property("CasResponse") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContractNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FsbApNotified") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("FsbNotificationEmailLogId") + .HasColumnType("uuid"); + + b.Property("FsbNotificationSentDate") + .HasColumnType("timestamp without time zone"); + + b.Property("InvoiceNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("InvoiceStatus") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsRecon") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("PayeeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentDate") + .HasColumnType("text"); + + b.Property("PaymentNumber") + .HasColumnType("text"); + + b.Property("PaymentStatus") + .HasColumnType("text"); + + b.Property("ReferenceNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("RequesterName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SiteId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("SubmissionConfirmationCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SupplierName") + .HasColumnType("text"); + + b.Property("SupplierNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AccountCodingId"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("CreationTime"); + + b.HasIndex("FsbNotificationEmailLogId"); + + b.HasIndex("ReferenceNumber") + .IsUnique(); + + b.HasIndex("SiteId"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId", "CreationTime") + .HasFilter("\"IsDeleted\" = false"); + + b.ToTable("PaymentRequests", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentTags.PaymentTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestId") + .HasColumnType("uuid"); + + b.Property("TagId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("PaymentRequestId"); + + b.HasIndex("TagId"); + + b.ToTable("PaymentTags", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentThresholds.PaymentThreshold", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Threshold") + .HasColumnType("numeric"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("PaymentThresholds", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Site", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddressLine1") + .HasColumnType("text"); + + b.Property("AddressLine2") + .HasColumnType("text"); + + b.Property("AddressLine3") + .HasColumnType("text"); + + b.Property("BankAccount") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EFTAdvicePref") + .HasColumnType("text"); + + b.Property("EmailAddress") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastUpdatedInCas") + .HasColumnType("timestamp without time zone"); + + b.Property("MarkDeletedInUse") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentGroup") + .HasColumnType("integer"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("SiteProtected") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("SupplierId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("SupplierId"); + + b.ToTable("Sites", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Supplier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BusinessNumber") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastUpdatedInCAS") + .HasColumnType("timestamp without time zone"); + + b.Property("MailingAddress") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("text"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("SIN") + .HasColumnType("text"); + + b.Property("StandardIndustryClassification") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("Subcategory") + .HasColumnType("text"); + + b.Property("SupplierProtected") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Suppliers", "Payments"); + }); + + modelBuilder.Entity("Unity.Reporting.Domain.Configuration.ReportColumnsMap", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Mapping") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RoleStatus") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("ViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ViewStatus") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ReportColumnsMaps", "Reporting"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Scoresheet", "Scoresheet") + .WithMany("Instances") + .HasForeignKey("ScoresheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scoresheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Answer", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Question", "Question") + .WithMany("Answers") + .HasForeignKey("QuestionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", null) + .WithMany("Answers") + .HasForeignKey("ScoresheetInstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Question"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.ScoresheetSection", "Section") + .WithMany("Fields") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Scoresheet", "Scoresheet") + .WithMany("Sections") + .HasForeignKey("ScoresheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scoresheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.CustomFieldValue", b => + { + b.HasOne("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", null) + .WithMany("Values") + .HasForeignKey("WorksheetInstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetLinks.WorksheetLink", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.Worksheet", "Worksheet") + .WithMany("Links") + .HasForeignKey("WorksheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Worksheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.CustomField", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.WorksheetSection", "Section") + .WithMany("Fields") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.Worksheet", "Worksheet") + .WithMany("Sections") + .HasForeignKey("WorksheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Worksheet"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAddress", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", "Applicant") + .WithMany("ApplicantAddresses") + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicantAddresses") + .HasForeignKey("ApplicationId"); + + b.Navigation("Applicant"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAgent", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithOne("ApplicantAgent") + .HasForeignKey("Unity.GrantManager.Applications.ApplicantAgent", "ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAttachment", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", "Applicant") + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", "ApplicationForm") + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationStatus", "ApplicationStatus") + .WithMany("Applications") + .HasForeignKey("ApplicationStatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Applicant"); + + b.Navigation("ApplicationForm"); + + b.Navigation("ApplicationStatus"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAssignment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicationAssignments") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", "Assignee") + .WithMany() + .HasForeignKey("AssigneeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Assignee"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAttachment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationChefsFileAttachment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationContact", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationForm", b => + { + b.HasOne("Unity.GrantManager.Intakes.Intake", null) + .WithMany() + .HasForeignKey("IntakeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) + .WithMany() + .HasForeignKey("ParentFormId") + .OnDelete(DeleteBehavior.NoAction); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormSubmission", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormVersion", b => + { + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationLink", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany("ApplicationLinks") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationTags", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicationTags") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.GlobalTag.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AssessmentAttachment", b => + { + b.HasOne("Unity.GrantManager.Assessments.Assessment", null) + .WithMany() + .HasForeignKey("AssessmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AuditHistory", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.FundingHistory", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.IssueTracking", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ReportsHistory", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Assessments.Assessment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("Assessments") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("AssessorId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicantComment", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicationComment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.AssessmentComment", b => + { + b.HasOne("Unity.GrantManager.Assessments.Assessment", null) + .WithMany() + .HasForeignKey("AssessmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Contacts.ContactLink", b => + { + b.HasOne("Unity.GrantManager.Contacts.Contact", null) + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Notifications.ScheduledNotificationTracking", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Notifications.ScheduledNotification", null) + .WithMany() + .HasForeignKey("ScheduledNotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroupUser", b => + { + b.HasOne("Unity.Notifications.EmailGroups.EmailGroup", null) + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLogAttachment", b => + { + b.HasOne("Unity.Notifications.Emails.EmailLog", null) + .WithMany() + .HasForeignKey("EmailLogId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Unity.Notifications.Templates.EmailTemplate", null) + .WithMany() + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroupSubscription", b => + { + b.HasOne("Unity.Notifications.Templates.SubscriptionGroup", "SubscriptionGroup") + .WithMany() + .HasForeignKey("GroupId"); + + b.HasOne("Unity.Notifications.Templates.Subscriber", "Subscriber") + .WithMany() + .HasForeignKey("SubscriberId"); + + b.Navigation("Subscriber"); + + b.Navigation("SubscriptionGroup"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TriggerSubscription", b => + { + b.HasOne("Unity.Notifications.Templates.SubscriptionGroup", "SubscriptionGroup") + .WithMany() + .HasForeignKey("SubscriptionGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Notifications.Templates.EmailTemplate", "EmailTemplate") + .WithMany() + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Notifications.Templates.Trigger", "Trigger") + .WithMany() + .HasForeignKey("TriggerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmailTemplate"); + + b.Navigation("SubscriptionGroup"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.ExpenseApproval", b => + { + b.HasOne("Unity.Payments.Domain.PaymentRequests.PaymentRequest", "PaymentRequest") + .WithMany("ExpenseApprovals") + .HasForeignKey("PaymentRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PaymentRequest"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.HasOne("Unity.Payments.Domain.AccountCodings.AccountCoding", "AccountCoding") + .WithMany() + .HasForeignKey("AccountCodingId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Unity.Payments.Domain.Suppliers.Site", "Site") + .WithMany() + .HasForeignKey("SiteId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("AccountCoding"); + + b.Navigation("Site"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentTags.PaymentTag", b => + { + b.HasOne("Unity.Payments.Domain.PaymentRequests.PaymentRequest", null) + .WithMany("PaymentTags") + .HasForeignKey("PaymentRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.GlobalTag.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Site", b => + { + b.HasOne("Unity.Payments.Domain.Suppliers.Supplier", "Supplier") + .WithMany("Sites") + .HasForeignKey("SupplierId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.Navigation("Answers"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.Navigation("Answers"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Scoresheet", b => + { + b.Navigation("Instances"); + + b.Navigation("Sections"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.Navigation("Fields"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", b => + { + b.Navigation("Values"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.Worksheet", b => + { + b.Navigation("Links"); + + b.Navigation("Sections"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.Navigation("Fields"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Applicant", b => + { + b.Navigation("ApplicantAddresses"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.Navigation("ApplicantAddresses"); + + b.Navigation("ApplicantAgent"); + + b.Navigation("ApplicationAssignments"); + + b.Navigation("ApplicationLinks"); + + b.Navigation("ApplicationTags"); + + b.Navigation("Assessments"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationStatus", b => + { + b.Navigation("Applications"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.Navigation("ExpenseApprovals"); + + b.Navigation("PaymentTags"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Supplier", b => + { + b.Navigation("Sites"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260807224046_AB33996_AddApplicantFiscalYearEndRestricted.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260807224046_AB33996_AddApplicantFiscalYearEndRestricted.cs new file mode 100644 index 0000000000..16cad7af90 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260807224046_AB33996_AddApplicantFiscalYearEndRestricted.cs @@ -0,0 +1,82 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + /// + public partial class AB33996_AddApplicantFiscalYearEndRestricted : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "FiscalYearEnd", + table: "Applicants", + type: "date", + nullable: true); + + // Trigger keeps FiscalYearEnd in sync whenever FiscalMonth or FiscalDay change. + // PostgreSQL generated columns cannot reference volatile functions (e.g. CURRENT_DATE), + // so a BEFORE trigger is used instead of a GENERATED ALWAYS AS column. + migrationBuilder.Sql(@" +CREATE OR REPLACE FUNCTION compute_applicants_fiscal_year_end() +RETURNS TRIGGER AS $$ +DECLARE + month_num integer; +BEGIN + month_num := CASE LEFT(NEW.""FiscalMonth"", 3) + WHEN 'Jan' THEN 1 WHEN 'Feb' THEN 2 WHEN 'Mar' THEN 3 + WHEN 'Apr' THEN 4 WHEN 'May' THEN 5 WHEN 'Jun' THEN 6 + WHEN 'Jul' THEN 7 WHEN 'Aug' THEN 8 WHEN 'Sep' THEN 9 + WHEN 'Oct' THEN 10 WHEN 'Nov' THEN 11 WHEN 'Dec' THEN 12 + ELSE NULL + END; + + IF month_num IS NOT NULL AND NEW.""FiscalDay"" IS NOT NULL THEN + BEGIN + NEW.""FiscalYearEnd"" := MAKE_DATE( + EXTRACT(YEAR FROM CURRENT_DATE)::int, + month_num, + NEW.""FiscalDay"" + ); + EXCEPTION WHEN others THEN + NEW.""FiscalYearEnd"" := NULL; + END; + ELSE + NEW.""FiscalYearEnd"" := NULL; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trg_applicants_fiscal_year_end +BEFORE INSERT OR UPDATE OF ""FiscalMonth"", ""FiscalDay"" +ON ""Applicants"" +FOR EACH ROW EXECUTE FUNCTION compute_applicants_fiscal_year_end(); +"); + + // Backfill existing rows by touching FiscalDay, which fires the trigger above. + migrationBuilder.Sql(@" +UPDATE ""Applicants"" +SET ""FiscalDay"" = ""FiscalDay"" +WHERE ""FiscalMonth"" IS NOT NULL AND ""FiscalDay"" IS NOT NULL; +"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" +DROP TRIGGER IF EXISTS trg_applicants_fiscal_year_end ON ""Applicants""; +DROP FUNCTION IF EXISTS compute_applicants_fiscal_year_end(); +"); + + migrationBuilder.DropColumn( + name: "FiscalYearEnd", + table: "Applicants"); + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260813164219_AB33234_ApplicantPortalExternalLinks.Designer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260813164219_AB33234_ApplicantPortalExternalLinks.Designer.cs new file mode 100644 index 0000000000..ca97882d1f --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260813164219_AB33234_ApplicantPortalExternalLinks.Designer.cs @@ -0,0 +1,5309 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Unity.GrantManager.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + [DbContext(typeof(GrantTenantDbContext))] + [Migration("20260813164219_AB33234_ApplicantPortalExternalLinks")] + partial class AB33234_ApplicantPortalExternalLinks + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.PostgreSql) + .HasAnnotation("ProductVersion", "10.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScoresheetId"); + + b.ToTable("ScoresheetInstances", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Answer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("QuestionId") + .HasColumnType("uuid"); + + b.Property("ScoresheetInstanceId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("QuestionId"); + + b.HasIndex("ScoresheetInstanceId"); + + b.ToTable("Answers", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("SectionId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("Questions", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Scoresheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsArchived") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Scoresheets", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ScoresheetId"); + + b.ToTable("ScoresheetSections", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.CustomFieldValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CustomFieldId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("WorksheetInstanceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetInstanceId"); + + b.ToTable("CustomFieldValues", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UiAnchor") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetCorrelationId") + .HasColumnType("uuid"); + + b.Property("WorksheetCorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("WorksheetInstances", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetLinks.WorksheetLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UiAnchor") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetId"); + + b.ToTable("WorksheetLinks", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.CustomField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("SectionId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("CustomFields", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.Worksheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsArchived") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Worksheets", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetId"); + + b.ToTable("WorksheetSections", "Flex"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Applicant", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantName") + .IsRequired() + .HasMaxLength(600) + .HasColumnType("character varying(600)"); + + b.Property("ApproxNumberOfEmployees") + .HasColumnType("text"); + + b.Property("AuditComments") + .HasColumnType("text"); + + b.Property("BusinessNumber") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FiscalDay") + .HasColumnType("integer"); + + b.Property("FiscalMonth") + .HasColumnType("text"); + + b.Property("FiscalYearEnd") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("date"); + + b.Property("FundingHistoryComments") + .HasColumnType("text"); + + b.Property("IndigenousOrgInd") + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsDuplicated") + .HasColumnType("boolean"); + + b.Property("IssueTrackingComments") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MatchPercentage") + .HasColumnType("numeric"); + + b.Property("NonRegOrgName") + .HasColumnType("text"); + + b.Property("NonRegisteredBusinessName") + .HasColumnType("text"); + + b.Property("OrgName") + .HasColumnType("text"); + + b.Property("OrgNumber") + .HasColumnType("text"); + + b.Property("OrgStatus") + .HasColumnType("text"); + + b.Property("OrganizationType") + .HasColumnType("text"); + + b.Property("RedStop") + .HasColumnType("boolean"); + + b.Property("ReportsComments") + .HasColumnType("text"); + + b.Property("Sector") + .HasColumnType("text"); + + b.Property("SectorSubSectorIndustryDesc") + .HasColumnType("text"); + + b.Property("StartedOperatingDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("SubSector") + .HasColumnType("text"); + + b.Property("SupplierId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UnityApplicantId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantName"); + + b.HasIndex("OrgName"); + + b.HasIndex("OrgNumber"); + + b.HasIndex("Status"); + + b.HasIndex("SupplierId"); + + b.HasIndex("TenantId"); + + b.HasIndex("UnityApplicantId"); + + b.HasIndex("TenantId", "IsDeleted", "CreationTime") + .HasFilter("\"IsDeleted\" = false"); + + b.ToTable("Applicants", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAddress", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AddressType") + .HasColumnType("integer"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Postal") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("Street") + .HasColumnType("text"); + + b.Property("Street2") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Unit") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicantAddresses", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAgent", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("BceidBusinessGuid") + .HasColumnType("uuid"); + + b.Property("BceidBusinessName") + .HasColumnType("text"); + + b.Property("BceidUserGuid") + .HasColumnType("uuid"); + + b.Property("BceidUserName") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactOrder") + .HasColumnType("integer"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IdentityEmail") + .HasColumnType("text"); + + b.Property("IdentityName") + .HasColumnType("text"); + + b.Property("IdentityProvider") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsConfirmed") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OidcSubUser") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("Phone2") + .HasColumnType("text"); + + b.Property("Phone2Extension") + .HasColumnType("text"); + + b.Property("PhoneExtension") + .HasColumnType("text"); + + b.Property("RoleForApplicant") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationId") + .IsUnique(); + + b.HasIndex("TenantId", "ApplicationId"); + + b.ToTable("ApplicantAgents", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("ApplicantAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AIAnalysis") + .HasColumnType("text"); + + b.Property("AIScoresheetAnswers") + .HasColumnType("jsonb"); + + b.Property("Acquisition") + .HasColumnType("text"); + + b.Property("ApplicantElectoralDistrict") + .HasColumnType("text"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("ApplicationStatusId") + .HasColumnType("uuid"); + + b.Property("ApprovedAmount") + .HasColumnType("numeric"); + + b.Property("AssessmentResultDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AssessmentResultStatus") + .HasColumnType("text"); + + b.Property("AssessmentStartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Community") + .HasColumnType("text"); + + b.Property("CommunityPopulation") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContractExecutionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ContractNumber") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeclineRational") + .HasColumnType("text"); + + b.Property("DefaultSiteId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("DueDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DueDiligenceStatus") + .HasColumnType("text"); + + b.Property("EconomicRegion") + .HasColumnType("text"); + + b.Property("ElectoralDistrict") + .HasColumnType("text"); + + b.Property("EligibleForRenewal") + .HasColumnType("boolean"); + + b.Property("ExternalStatusVisibility") + .HasColumnType("boolean"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FinalDecisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Forestry") + .HasColumnType("text"); + + b.Property("ForestryFocus") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LikelihoodOfFunding") + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("NotificationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Payload") + .HasColumnType("jsonb"); + + b.Property("PercentageTotalProjectBudget") + .HasColumnType("double precision"); + + b.Property("Place") + .HasColumnType("text"); + + b.Property("ProjectEndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ProjectFundingTotal") + .HasColumnType("numeric"); + + b.Property("ProjectName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProjectStartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ProjectSummary") + .HasColumnType("text"); + + b.Property("ProposalDate") + .HasColumnType("timestamp without time zone"); + + b.Property("RecommendedAmount") + .HasColumnType("numeric"); + + b.Property("ReferenceNo") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegionalDistrict") + .HasColumnType("text"); + + b.Property("RequestedAmount") + .HasColumnType("numeric"); + + b.Property("RiskRanking") + .HasColumnType("text"); + + b.Property("SigningAuthorityBusinessPhone") + .HasColumnType("text"); + + b.Property("SigningAuthorityCellPhone") + .HasColumnType("text"); + + b.Property("SigningAuthorityEmail") + .HasColumnType("text"); + + b.Property("SigningAuthorityFullName") + .HasColumnType("text"); + + b.Property("SigningAuthorityTitle") + .HasColumnType("text"); + + b.Property("SubStatus") + .HasColumnType("text"); + + b.Property("SubmissionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TotalProjectBudget") + .HasColumnType("numeric"); + + b.Property("TotalScore") + .HasColumnType("integer"); + + b.Property("UnityApplicationId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationFormId"); + + b.HasIndex("ApplicationStatusId"); + + b.HasIndex("OwnerId"); + + b.HasIndex("ReferenceNo"); + + b.HasIndex("TenantId", "SubmissionDate") + .HasFilter("\"IsDeleted\" = false"); + + b.ToTable("Applications", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAssignment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AssigneeId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Duty") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("AssigneeId"); + + b.HasIndex("TenantId", "ApplicationId"); + + b.ToTable("ApplicationAssignments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationChefsFileAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AISummary") + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ChefsFileId") + .HasColumnType("text"); + + b.Property("ChefsSubmissionId") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationChefsFileAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationContact", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactEmail") + .HasColumnType("text"); + + b.Property("ContactFullName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactMobilePhone") + .HasColumnType("text"); + + b.Property("ContactTitle") + .HasColumnType("text"); + + b.Property("ContactType") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactWorkPhone") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationContact", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationForm", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountCodingId") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .HasColumnType("text"); + + b.Property("ApplicationFormDescription") + .HasColumnType("text"); + + b.Property("ApplicationFormName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AttemptedConnectionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AutomaticallyGenerateAIAnalysis") + .HasColumnType("boolean"); + + b.Property("AvailableChefsFields") + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("text"); + + b.Property("ChefsApplicationFormGuid") + .HasColumnType("text"); + + b.Property("ChefsCriteriaFormGuid") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ConnectionHttpStatus") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DefaultPaymentGroup") + .HasColumnType("integer"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ElectoralDistrictAddressType") + .HasColumnType("integer"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormHierarchy") + .HasColumnType("integer"); + + b.Property("IntakeId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsDirectApproval") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ManuallyInitiateAIAnalysis") + .HasColumnType("boolean"); + + b.Property("ParentFormId") + .HasColumnType("uuid"); + + b.Property("Payable") + .HasColumnType("boolean"); + + b.Property("PaymentApprovalThreshold") + .HasColumnType("numeric"); + + b.Property("Prefix") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PreventPayment") + .HasColumnType("boolean"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("SuffixType") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.ComplexCollection(typeof(List>), "ExternalLinks", "Unity.GrantManager.Applications.ApplicationForm.ExternalLinks#ExternalLink", b1 => + { + b1.IsRequired(); + + b1.Property("Description") + .IsRequired(); + + b1.Property("ExternalLinkType"); + + b1.Property("Order"); + + b1.Property("Published"); + + b1.Property("Title") + .IsRequired(); + + b1.Property("Uri") + .IsRequired(); + + b1 + .ToJson("ExternalLinks") + .HasColumnType("jsonb"); + }); + + b.HasKey("Id"); + + b.HasIndex("IntakeId"); + + b.HasIndex("ParentFormId"); + + b.HasIndex("TenantId", "IsDeleted") + .HasFilter("\"IsDeleted\" = false"); + + b.ToTable("ApplicationForms", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormSubmission", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormVersionId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ChefsSubmissionGuid") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormVersionId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OidcSub") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Submission") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormSubmissions", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormVersion", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("AvailableChefsFields") + .HasColumnType("text"); + + b.Property("ChefsApplicationFormGuid") + .HasColumnType("text"); + + b.Property("ChefsFormVersionGuid") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormSchema") + .HasColumnType("jsonb"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SubmissionHeaderMapping") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormVersion", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationLink", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LinkType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("Related"); + + b.Property("LinkedApplicationId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("TenantId", "ApplicationId"); + + b.ToTable("ApplicationLinks", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationStatus", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExternalStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("InternalStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("NotifiedStatus") + .HasColumnType("text"); + + b.Property("StatusCode") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("StatusCode") + .IsUnique(); + + b.ToTable("ApplicationStatuses", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationTags", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TagId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("TagId"); + + b.HasIndex("TenantId", "ApplicationId"); + + b.ToTable("ApplicationTags", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AssessmentAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AssessmentId"); + + b.ToTable("AssessmentAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AuditHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("AuditDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AuditNote") + .HasColumnType("text"); + + b.Property("AuditStatus") + .HasColumnType("text"); + + b.Property("AuditTrackingNumber") + .HasColumnType("text"); + + b.Property("AuditorName") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("AuditHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.FundingHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApprovedAmount") + .HasColumnType("numeric"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FundingNotes") + .HasColumnType("text"); + + b.Property("FundingYear") + .HasColumnType("text"); + + b.Property("GrantCategory") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OneTimeConsideration") + .HasColumnType("numeric"); + + b.Property("PaidDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ReconsiderationAmount") + .HasColumnType("numeric"); + + b.Property("RenewedFunding") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TotalGrantAmount") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("FundingHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.IssueTracking", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IssueDescription") + .HasColumnType("text"); + + b.Property("IssueHeading") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ResolutionNote") + .HasColumnType("text"); + + b.Property("Resolved") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Year") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("IssueTrackings", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ReportsHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FiscalYear") + .HasColumnType("text"); + + b.Property("IncompleteReport") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("Outstanding") + .HasColumnType("boolean"); + + b.Property("ReportDate") + .HasColumnType("timestamp without time zone"); + + b.Property("SignedOff") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("ReportsHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Assessments.Assessment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ApprovalRecommended") + .HasColumnType("boolean"); + + b.Property("AssessorId") + .HasColumnType("uuid"); + + b.Property("CleanGrowth") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("EconomicImpact") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FinancialAnalysis") + .HasColumnType("integer"); + + b.Property("InclusiveGrowth") + .HasColumnType("integer"); + + b.Property("IsAiAssessment") + .HasColumnType("boolean"); + + b.Property("IsComplete") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("AssessorId"); + + b.ToTable("Assessments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicantComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PinDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("CommenterId"); + + b.ToTable("ApplicantComments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicationComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PinDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("CommenterId"); + + b.ToTable("ApplicationComments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.AssessmentComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PinDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AssessmentId"); + + b.HasIndex("CommenterId"); + + b.ToTable("AssessmentComments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Contacts.Contact", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("HomePhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MobilePhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("WorkPhoneExtension") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("WorkPhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.ToTable("Contacts", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Contacts.ContactLink", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("RelatedEntityId") + .HasColumnType("uuid"); + + b.Property("RelatedEntityType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Role") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("RelatedEntityType", "RelatedEntityId"); + + b.HasIndex("ContactId", "RelatedEntityType", "RelatedEntityId"); + + b.ToTable("ContactLinks", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.GlobalTag.Tag", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Tags", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Identity.Person", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Badge") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FullName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OidcDisplayName") + .IsRequired() + .HasColumnType("text"); + + b.Property("OidcSub") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("OidcSub"); + + b.HasIndex("TenantId"); + + b.ToTable("Persons", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Intakes.Intake", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Budget") + .HasColumnType("double precision"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IntakeName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("StartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Intakes", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Notifications.ScheduledNotification", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationStatus") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ApplicationStatusId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DateField") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EmailTemplateId") + .HasColumnType("uuid"); + + b.Property("EventType") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Module") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("RecipientCategory") + .HasColumnType("text"); + + b.Property("RecipientIdentifier") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TriggerDetail") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("TriggerType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("ScheduledNotifications", "Notifications"); + }); + + modelBuilder.Entity("Unity.GrantManager.Notifications.ScheduledNotificationTracking", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatorId") + .HasColumnType("uuid"); + + b.Property("DateField") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("NotificationSentDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ScheduledNotificationId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("CreationTime"); + + b.HasIndex("ScheduledNotificationId"); + + b.HasIndex("ApplicationId", "ScheduledNotificationId", "DateField") + .IsUnique(); + + b.ToTable("ScheduledNotificationTracking", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EmailGroups", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroupUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.ToTable("EmailGroupUsers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("BCC") + .IsRequired() + .HasColumnType("text"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CC") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChesHttpStatusCode") + .HasColumnType("text"); + + b.Property("ChesMsgId") + .HasColumnType("uuid"); + + b.Property("ChesResponse") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChesStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("EmailType") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FromAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestIds") + .IsRequired() + .HasColumnType("text"); + + b.Property("Priority") + .IsRequired() + .HasColumnType("text"); + + b.Property("Recipient") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("RetryAttempts") + .HasColumnType("integer"); + + b.Property("ScheduledNotificationId") + .HasColumnType("uuid"); + + b.Property("SendOnDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("SentDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("text"); + + b.Property("TemplateName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("ToAddress") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EmailLogs", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLogAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("EmailLogId") + .HasColumnType("uuid"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OriginTemplateId") + .HasColumnType("uuid"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TemplateId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("EmailLogId"); + + b.HasIndex("S3ObjectKey"); + + b.HasIndex("TemplateId"); + + b.ToTable("EmailLogAttachments", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.EmailTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BodyHTML") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyText") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RecipientCategory") + .HasColumnType("text"); + + b.Property("RecipientIdentifier") + .HasColumnType("text"); + + b.Property("SendFrom") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("EmailTemplates", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.Subscriber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Subscribers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("SubscriptionGroups", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroupSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SubscriberId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.HasIndex("SubscriberId"); + + b.ToTable("SubscriptionGroupSubscribers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TemplateVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MapTo") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("TemplateVariables", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.Trigger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("InternalName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Triggers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TriggerSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SubscriptionGroupId") + .HasColumnType("uuid"); + + b.Property("TemplateId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TriggerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionGroupId"); + + b.HasIndex("TemplateId"); + + b.HasIndex("TriggerId"); + + b.ToTable("TriggerSubscriptions", "Notifications"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.AccountCodings.AccountCoding", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Description") + .HasMaxLength(35) + .HasColumnType("character varying(35)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MinistryClient") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("Responsibility") + .IsRequired() + .HasColumnType("text"); + + b.Property("ServiceLine") + .IsRequired() + .HasColumnType("text"); + + b.Property("Stob") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("AccountCodings", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentConfigurations.PaymentConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DefaultAccountCodingId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentIdPrefix") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("PaymentConfigurations", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.ExpenseApproval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DecisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DecisionUserId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PaymentRequestId"); + + b.ToTable("ExpenseApprovals", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountCodingId") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("BatchName") + .IsRequired() + .HasColumnType("text"); + + b.Property("BatchNumber") + .HasColumnType("numeric"); + + b.Property("CancelledBy") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("CancelledBy"); + + b.Property("CancelledById") + .HasColumnType("uuid") + .HasColumnName("CancelledById"); + + b.Property("CancelledOn") + .HasColumnType("timestamp without time zone") + .HasColumnName("CancelledOn"); + + b.Property("CasHttpStatusCode") + .HasColumnType("integer"); + + b.Property("CasResponse") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContractNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FsbApNotified") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("FsbNotificationEmailLogId") + .HasColumnType("uuid"); + + b.Property("FsbNotificationSentDate") + .HasColumnType("timestamp without time zone"); + + b.Property("InvoiceNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("InvoiceStatus") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsRecon") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("PayeeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentDate") + .HasColumnType("text"); + + b.Property("PaymentNumber") + .HasColumnType("text"); + + b.Property("PaymentStatus") + .HasColumnType("text"); + + b.Property("ReferenceNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("RequesterName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SiteId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("SubmissionConfirmationCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SupplierName") + .HasColumnType("text"); + + b.Property("SupplierNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AccountCodingId"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("CreationTime"); + + b.HasIndex("FsbNotificationEmailLogId"); + + b.HasIndex("ReferenceNumber") + .IsUnique(); + + b.HasIndex("SiteId"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId", "CreationTime") + .HasFilter("\"IsDeleted\" = false"); + + b.ToTable("PaymentRequests", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentTags.PaymentTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestId") + .HasColumnType("uuid"); + + b.Property("TagId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("PaymentRequestId"); + + b.HasIndex("TagId"); + + b.ToTable("PaymentTags", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentThresholds.PaymentThreshold", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Threshold") + .HasColumnType("numeric"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("PaymentThresholds", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Site", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddressLine1") + .HasColumnType("text"); + + b.Property("AddressLine2") + .HasColumnType("text"); + + b.Property("AddressLine3") + .HasColumnType("text"); + + b.Property("BankAccount") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EFTAdvicePref") + .HasColumnType("text"); + + b.Property("EmailAddress") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastUpdatedInCas") + .HasColumnType("timestamp without time zone"); + + b.Property("MarkDeletedInUse") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentGroup") + .HasColumnType("integer"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("SiteProtected") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("SupplierId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("SupplierId"); + + b.ToTable("Sites", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Supplier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BusinessNumber") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastUpdatedInCAS") + .HasColumnType("timestamp without time zone"); + + b.Property("MailingAddress") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("text"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("SIN") + .HasColumnType("text"); + + b.Property("StandardIndustryClassification") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("Subcategory") + .HasColumnType("text"); + + b.Property("SupplierProtected") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Suppliers", "Payments"); + }); + + modelBuilder.Entity("Unity.Reporting.Domain.Configuration.ReportColumnsMap", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Mapping") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RoleStatus") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("ViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ViewStatus") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ReportColumnsMaps", "Reporting"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Scoresheet", "Scoresheet") + .WithMany("Instances") + .HasForeignKey("ScoresheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scoresheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Answer", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Question", "Question") + .WithMany("Answers") + .HasForeignKey("QuestionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", null) + .WithMany("Answers") + .HasForeignKey("ScoresheetInstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Question"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.ScoresheetSection", "Section") + .WithMany("Fields") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Scoresheet", "Scoresheet") + .WithMany("Sections") + .HasForeignKey("ScoresheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scoresheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.CustomFieldValue", b => + { + b.HasOne("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", null) + .WithMany("Values") + .HasForeignKey("WorksheetInstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetLinks.WorksheetLink", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.Worksheet", "Worksheet") + .WithMany("Links") + .HasForeignKey("WorksheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Worksheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.CustomField", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.WorksheetSection", "Section") + .WithMany("Fields") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.Worksheet", "Worksheet") + .WithMany("Sections") + .HasForeignKey("WorksheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Worksheet"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAddress", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", "Applicant") + .WithMany("ApplicantAddresses") + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicantAddresses") + .HasForeignKey("ApplicationId"); + + b.Navigation("Applicant"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAgent", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithOne("ApplicantAgent") + .HasForeignKey("Unity.GrantManager.Applications.ApplicantAgent", "ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAttachment", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", "Applicant") + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", "ApplicationForm") + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationStatus", "ApplicationStatus") + .WithMany("Applications") + .HasForeignKey("ApplicationStatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Applicant"); + + b.Navigation("ApplicationForm"); + + b.Navigation("ApplicationStatus"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAssignment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicationAssignments") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", "Assignee") + .WithMany() + .HasForeignKey("AssigneeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Assignee"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAttachment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationChefsFileAttachment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationContact", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationForm", b => + { + b.HasOne("Unity.GrantManager.Intakes.Intake", null) + .WithMany() + .HasForeignKey("IntakeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) + .WithMany() + .HasForeignKey("ParentFormId") + .OnDelete(DeleteBehavior.NoAction); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormSubmission", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormVersion", b => + { + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationLink", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany("ApplicationLinks") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationTags", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicationTags") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.GlobalTag.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AssessmentAttachment", b => + { + b.HasOne("Unity.GrantManager.Assessments.Assessment", null) + .WithMany() + .HasForeignKey("AssessmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AuditHistory", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.FundingHistory", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.IssueTracking", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ReportsHistory", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Assessments.Assessment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("Assessments") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("AssessorId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicantComment", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicationComment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.AssessmentComment", b => + { + b.HasOne("Unity.GrantManager.Assessments.Assessment", null) + .WithMany() + .HasForeignKey("AssessmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Contacts.ContactLink", b => + { + b.HasOne("Unity.GrantManager.Contacts.Contact", null) + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Notifications.ScheduledNotificationTracking", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Notifications.ScheduledNotification", null) + .WithMany() + .HasForeignKey("ScheduledNotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroupUser", b => + { + b.HasOne("Unity.Notifications.EmailGroups.EmailGroup", null) + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLogAttachment", b => + { + b.HasOne("Unity.Notifications.Emails.EmailLog", null) + .WithMany() + .HasForeignKey("EmailLogId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Unity.Notifications.Templates.EmailTemplate", null) + .WithMany() + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroupSubscription", b => + { + b.HasOne("Unity.Notifications.Templates.SubscriptionGroup", "SubscriptionGroup") + .WithMany() + .HasForeignKey("GroupId"); + + b.HasOne("Unity.Notifications.Templates.Subscriber", "Subscriber") + .WithMany() + .HasForeignKey("SubscriberId"); + + b.Navigation("Subscriber"); + + b.Navigation("SubscriptionGroup"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TriggerSubscription", b => + { + b.HasOne("Unity.Notifications.Templates.SubscriptionGroup", "SubscriptionGroup") + .WithMany() + .HasForeignKey("SubscriptionGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Notifications.Templates.EmailTemplate", "EmailTemplate") + .WithMany() + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Notifications.Templates.Trigger", "Trigger") + .WithMany() + .HasForeignKey("TriggerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmailTemplate"); + + b.Navigation("SubscriptionGroup"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.ExpenseApproval", b => + { + b.HasOne("Unity.Payments.Domain.PaymentRequests.PaymentRequest", "PaymentRequest") + .WithMany("ExpenseApprovals") + .HasForeignKey("PaymentRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PaymentRequest"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.HasOne("Unity.Payments.Domain.AccountCodings.AccountCoding", "AccountCoding") + .WithMany() + .HasForeignKey("AccountCodingId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Unity.Payments.Domain.Suppliers.Site", "Site") + .WithMany() + .HasForeignKey("SiteId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("AccountCoding"); + + b.Navigation("Site"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentTags.PaymentTag", b => + { + b.HasOne("Unity.Payments.Domain.PaymentRequests.PaymentRequest", null) + .WithMany("PaymentTags") + .HasForeignKey("PaymentRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.GlobalTag.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Site", b => + { + b.HasOne("Unity.Payments.Domain.Suppliers.Supplier", "Supplier") + .WithMany("Sites") + .HasForeignKey("SupplierId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.Navigation("Answers"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.Navigation("Answers"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Scoresheet", b => + { + b.Navigation("Instances"); + + b.Navigation("Sections"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.Navigation("Fields"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", b => + { + b.Navigation("Values"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.Worksheet", b => + { + b.Navigation("Links"); + + b.Navigation("Sections"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.Navigation("Fields"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Applicant", b => + { + b.Navigation("ApplicantAddresses"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.Navigation("ApplicantAddresses"); + + b.Navigation("ApplicantAgent"); + + b.Navigation("ApplicationAssignments"); + + b.Navigation("ApplicationLinks"); + + b.Navigation("ApplicationTags"); + + b.Navigation("Assessments"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationStatus", b => + { + b.Navigation("Applications"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.Navigation("ExpenseApprovals"); + + b.Navigation("PaymentTags"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Supplier", b => + { + b.Navigation("Sites"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260813164219_AB33234_ApplicantPortalExternalLinks.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260813164219_AB33234_ApplicantPortalExternalLinks.cs new file mode 100644 index 0000000000..db8b26e47f --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260813164219_AB33234_ApplicantPortalExternalLinks.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + /// + public partial class AB33234_ApplicantPortalExternalLinks : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "EligibleForRenewal", + table: "Applications", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "ExternalLinks", + table: "ApplicationForms", + type: "jsonb", + nullable: false, + defaultValue: "[]"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "EligibleForRenewal", + table: "Applications"); + + migrationBuilder.DropColumn( + name: "ExternalLinks", + table: "ApplicationForms"); + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260818211028_AB33234_ExternalLinksApplicantMessage.Designer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260818211028_AB33234_ExternalLinksApplicantMessage.Designer.cs new file mode 100644 index 0000000000..f321798dc0 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260818211028_AB33234_ExternalLinksApplicantMessage.Designer.cs @@ -0,0 +1,5352 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Unity.GrantManager.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + [DbContext(typeof(GrantTenantDbContext))] + [Migration("20260818211028_AB33234_ExternalLinksApplicantMessage")] + partial class AB33234_ExternalLinksApplicantMessage + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.PostgreSql) + .HasAnnotation("ProductVersion", "10.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScoresheetId"); + + b.ToTable("ScoresheetInstances", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Answer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("QuestionId") + .HasColumnType("uuid"); + + b.Property("ScoresheetInstanceId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("QuestionId"); + + b.HasIndex("ScoresheetInstanceId"); + + b.ToTable("Answers", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("SectionId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("Questions", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Scoresheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsArchived") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Scoresheets", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ScoresheetId"); + + b.ToTable("ScoresheetSections", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.CustomFieldValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CustomFieldId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("WorksheetInstanceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetInstanceId"); + + b.ToTable("CustomFieldValues", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UiAnchor") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetCorrelationId") + .HasColumnType("uuid"); + + b.Property("WorksheetCorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("WorksheetInstances", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetLinks.WorksheetLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UiAnchor") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetId"); + + b.ToTable("WorksheetLinks", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.CustomField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("SectionId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("CustomFields", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.Worksheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsArchived") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Worksheets", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetId"); + + b.ToTable("WorksheetSections", "Flex"); + }); + + modelBuilder.Entity("Unity.GrantManager.ApplicationForms.GenerationReview", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContextId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Operation") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReviewData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Sequence") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("Operation", "ContextId", "Sequence") + .IsUnique(); + + b.ToTable("GenerationReviews", "AI"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Applicant", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantName") + .IsRequired() + .HasMaxLength(600) + .HasColumnType("character varying(600)"); + + b.Property("ApproxNumberOfEmployees") + .HasColumnType("text"); + + b.Property("AuditComments") + .HasColumnType("text"); + + b.Property("BusinessNumber") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FiscalDay") + .HasColumnType("integer"); + + b.Property("FiscalMonth") + .HasColumnType("text"); + + b.Property("FiscalYearEnd") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("date"); + + b.Property("FundingHistoryComments") + .HasColumnType("text"); + + b.Property("IndigenousOrgInd") + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsDuplicated") + .HasColumnType("boolean"); + + b.Property("IssueTrackingComments") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MatchPercentage") + .HasColumnType("numeric"); + + b.Property("NonRegOrgName") + .HasColumnType("text"); + + b.Property("NonRegisteredBusinessName") + .HasColumnType("text"); + + b.Property("OrgName") + .HasColumnType("text"); + + b.Property("OrgNumber") + .HasColumnType("text"); + + b.Property("OrgStatus") + .HasColumnType("text"); + + b.Property("OrganizationType") + .HasColumnType("text"); + + b.Property("RedStop") + .HasColumnType("boolean"); + + b.Property("ReportsComments") + .HasColumnType("text"); + + b.Property("Sector") + .HasColumnType("text"); + + b.Property("SectorSubSectorIndustryDesc") + .HasColumnType("text"); + + b.Property("StartedOperatingDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("SubSector") + .HasColumnType("text"); + + b.Property("SupplierId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UnityApplicantId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantName"); + + b.HasIndex("OrgName"); + + b.HasIndex("OrgNumber"); + + b.HasIndex("Status"); + + b.HasIndex("SupplierId"); + + b.HasIndex("TenantId"); + + b.HasIndex("UnityApplicantId"); + + b.HasIndex("TenantId", "IsDeleted", "CreationTime") + .HasFilter("\"IsDeleted\" = false"); + + b.ToTable("Applicants", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAddress", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AddressType") + .HasColumnType("integer"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Postal") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("Street") + .HasColumnType("text"); + + b.Property("Street2") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Unit") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicantAddresses", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAgent", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("BceidBusinessGuid") + .HasColumnType("uuid"); + + b.Property("BceidBusinessName") + .HasColumnType("text"); + + b.Property("BceidUserGuid") + .HasColumnType("uuid"); + + b.Property("BceidUserName") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactOrder") + .HasColumnType("integer"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IdentityEmail") + .HasColumnType("text"); + + b.Property("IdentityName") + .HasColumnType("text"); + + b.Property("IdentityProvider") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsConfirmed") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OidcSubUser") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("Phone2") + .HasColumnType("text"); + + b.Property("Phone2Extension") + .HasColumnType("text"); + + b.Property("PhoneExtension") + .HasColumnType("text"); + + b.Property("RoleForApplicant") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationId") + .IsUnique(); + + b.HasIndex("TenantId", "ApplicationId"); + + b.ToTable("ApplicantAgents", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("ApplicantAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AIAnalysis") + .HasColumnType("text"); + + b.Property("AIScoresheetAnswers") + .HasColumnType("jsonb"); + + b.Property("Acquisition") + .HasColumnType("text"); + + b.Property("ApplicantElectoralDistrict") + .HasColumnType("text"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("ApplicationStatusId") + .HasColumnType("uuid"); + + b.Property("ApprovedAmount") + .HasColumnType("numeric"); + + b.Property("AssessmentResultDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AssessmentResultStatus") + .HasColumnType("text"); + + b.Property("AssessmentStartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Community") + .HasColumnType("text"); + + b.Property("CommunityPopulation") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContractExecutionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ContractNumber") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeclineRational") + .HasColumnType("text"); + + b.Property("DefaultSiteId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("DueDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DueDiligenceStatus") + .HasColumnType("text"); + + b.Property("EconomicRegion") + .HasColumnType("text"); + + b.Property("ElectoralDistrict") + .HasColumnType("text"); + + b.Property("EligibleForRenewal") + .HasColumnType("boolean"); + + b.Property("ExternalStatusVisibility") + .HasColumnType("boolean"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FinalDecisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Forestry") + .HasColumnType("text"); + + b.Property("ForestryFocus") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LikelihoodOfFunding") + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("NotificationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Payload") + .HasColumnType("jsonb"); + + b.Property("PercentageTotalProjectBudget") + .HasColumnType("double precision"); + + b.Property("Place") + .HasColumnType("text"); + + b.Property("ProjectEndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ProjectFundingTotal") + .HasColumnType("numeric"); + + b.Property("ProjectName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProjectStartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ProjectSummary") + .HasColumnType("text"); + + b.Property("ProposalDate") + .HasColumnType("timestamp without time zone"); + + b.Property("RecommendedAmount") + .HasColumnType("numeric"); + + b.Property("ReferenceNo") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegionalDistrict") + .HasColumnType("text"); + + b.Property("RequestedAmount") + .HasColumnType("numeric"); + + b.Property("RiskRanking") + .HasColumnType("text"); + + b.Property("SigningAuthorityBusinessPhone") + .HasColumnType("text"); + + b.Property("SigningAuthorityCellPhone") + .HasColumnType("text"); + + b.Property("SigningAuthorityEmail") + .HasColumnType("text"); + + b.Property("SigningAuthorityFullName") + .HasColumnType("text"); + + b.Property("SigningAuthorityTitle") + .HasColumnType("text"); + + b.Property("SubStatus") + .HasColumnType("text"); + + b.Property("SubmissionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TotalProjectBudget") + .HasColumnType("numeric"); + + b.Property("TotalScore") + .HasColumnType("integer"); + + b.Property("UnityApplicationId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationFormId"); + + b.HasIndex("ApplicationStatusId"); + + b.HasIndex("OwnerId"); + + b.HasIndex("ReferenceNo"); + + b.HasIndex("TenantId", "SubmissionDate") + .HasFilter("\"IsDeleted\" = false"); + + b.ToTable("Applications", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAssignment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AssigneeId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Duty") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("AssigneeId"); + + b.HasIndex("TenantId", "ApplicationId"); + + b.ToTable("ApplicationAssignments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationChefsFileAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AISummary") + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ChefsFileId") + .HasColumnType("text"); + + b.Property("ChefsSubmissionId") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationChefsFileAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationContact", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactEmail") + .HasColumnType("text"); + + b.Property("ContactFullName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactMobilePhone") + .HasColumnType("text"); + + b.Property("ContactTitle") + .HasColumnType("text"); + + b.Property("ContactType") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactWorkPhone") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationContact", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationForm", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountCodingId") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .HasColumnType("text"); + + b.Property("ApplicationFormDescription") + .HasColumnType("text"); + + b.Property("ApplicationFormName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AttemptedConnectionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AutomaticallyGenerateAIAnalysis") + .HasColumnType("boolean"); + + b.Property("AvailableChefsFields") + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("text"); + + b.Property("ChefsApplicationFormGuid") + .HasColumnType("text"); + + b.Property("ChefsCriteriaFormGuid") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ConnectionHttpStatus") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DefaultPaymentGroup") + .HasColumnType("integer"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ElectoralDistrictAddressType") + .HasColumnType("integer"); + + b.Property("ExternalLinksConfig") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("ExternalLinks"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormHierarchy") + .HasColumnType("integer"); + + b.Property("IntakeId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsDirectApproval") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ManuallyInitiateAIAnalysis") + .HasColumnType("boolean"); + + b.Property("ParentFormId") + .HasColumnType("uuid"); + + b.Property("Payable") + .HasColumnType("boolean"); + + b.Property("PaymentApprovalThreshold") + .HasColumnType("numeric"); + + b.Property("Prefix") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PreventPayment") + .HasColumnType("boolean"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("SuffixType") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IntakeId"); + + b.HasIndex("ParentFormId"); + + b.HasIndex("TenantId", "IsDeleted") + .HasFilter("\"IsDeleted\" = false"); + + b.ToTable("ApplicationForms", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormSubmission", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormVersionId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ChefsSubmissionGuid") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormVersionId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OidcSub") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Submission") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormSubmissions", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormVersion", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("AvailableChefsFields") + .HasColumnType("text"); + + b.Property("ChefsApplicationFormGuid") + .HasColumnType("text"); + + b.Property("ChefsFormVersionGuid") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormSchema") + .HasColumnType("jsonb"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SubmissionHeaderMapping") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormVersion", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationLink", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LinkType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("Related"); + + b.Property("LinkedApplicationId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("TenantId", "ApplicationId"); + + b.ToTable("ApplicationLinks", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationStatus", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExternalStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("InternalStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("NotifiedStatus") + .HasColumnType("text"); + + b.Property("StatusCode") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("StatusCode") + .IsUnique(); + + b.ToTable("ApplicationStatuses", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationTags", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TagId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("TagId"); + + b.HasIndex("TenantId", "ApplicationId"); + + b.ToTable("ApplicationTags", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AssessmentAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AssessmentId"); + + b.ToTable("AssessmentAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AuditHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("AuditDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AuditNote") + .HasColumnType("text"); + + b.Property("AuditStatus") + .HasColumnType("text"); + + b.Property("AuditTrackingNumber") + .HasColumnType("text"); + + b.Property("AuditorName") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("AuditHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.FundingHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApprovedAmount") + .HasColumnType("numeric"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FundingNotes") + .HasColumnType("text"); + + b.Property("FundingYear") + .HasColumnType("text"); + + b.Property("GrantCategory") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OneTimeConsideration") + .HasColumnType("numeric"); + + b.Property("PaidDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ReconsiderationAmount") + .HasColumnType("numeric"); + + b.Property("RenewedFunding") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TotalGrantAmount") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("FundingHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.IssueTracking", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IssueDescription") + .HasColumnType("text"); + + b.Property("IssueHeading") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ResolutionNote") + .HasColumnType("text"); + + b.Property("Resolved") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Year") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("IssueTrackings", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ReportsHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FiscalYear") + .HasColumnType("text"); + + b.Property("IncompleteReport") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("Outstanding") + .HasColumnType("boolean"); + + b.Property("ReportDate") + .HasColumnType("timestamp without time zone"); + + b.Property("SignedOff") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("ReportsHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Assessments.Assessment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ApprovalRecommended") + .HasColumnType("boolean"); + + b.Property("AssessorId") + .HasColumnType("uuid"); + + b.Property("CleanGrowth") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("EconomicImpact") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FinancialAnalysis") + .HasColumnType("integer"); + + b.Property("InclusiveGrowth") + .HasColumnType("integer"); + + b.Property("IsAiAssessment") + .HasColumnType("boolean"); + + b.Property("IsComplete") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("AssessorId"); + + b.ToTable("Assessments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicantComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PinDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("CommenterId"); + + b.ToTable("ApplicantComments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicationComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PinDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("CommenterId"); + + b.ToTable("ApplicationComments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.AssessmentComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PinDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AssessmentId"); + + b.HasIndex("CommenterId"); + + b.ToTable("AssessmentComments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Contacts.Contact", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("HomePhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MobilePhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("WorkPhoneExtension") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("WorkPhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.ToTable("Contacts", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Contacts.ContactLink", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("RelatedEntityId") + .HasColumnType("uuid"); + + b.Property("RelatedEntityType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Role") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("RelatedEntityType", "RelatedEntityId"); + + b.HasIndex("ContactId", "RelatedEntityType", "RelatedEntityId"); + + b.ToTable("ContactLinks", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.GlobalTag.Tag", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Tags", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Identity.Person", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Badge") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FullName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OidcDisplayName") + .IsRequired() + .HasColumnType("text"); + + b.Property("OidcSub") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("OidcSub"); + + b.HasIndex("TenantId"); + + b.ToTable("Persons", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Intakes.Intake", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Budget") + .HasColumnType("double precision"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IntakeName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("StartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Intakes", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Notifications.ScheduledNotification", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationStatus") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ApplicationStatusId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DateField") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EmailTemplateId") + .HasColumnType("uuid"); + + b.Property("EventType") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Module") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("RecipientCategory") + .HasColumnType("text"); + + b.Property("RecipientIdentifier") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TriggerDetail") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("TriggerType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("ScheduledNotifications", "Notifications"); + }); + + modelBuilder.Entity("Unity.GrantManager.Notifications.ScheduledNotificationTracking", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatorId") + .HasColumnType("uuid"); + + b.Property("DateField") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("NotificationSentDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ScheduledNotificationId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("CreationTime"); + + b.HasIndex("ScheduledNotificationId"); + + b.HasIndex("ApplicationId", "ScheduledNotificationId", "DateField") + .IsUnique(); + + b.ToTable("ScheduledNotificationTracking", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EmailGroups", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroupUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.ToTable("EmailGroupUsers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("BCC") + .IsRequired() + .HasColumnType("text"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CC") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChesHttpStatusCode") + .HasColumnType("text"); + + b.Property("ChesMsgId") + .HasColumnType("uuid"); + + b.Property("ChesResponse") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChesStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("EmailType") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FromAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestIds") + .IsRequired() + .HasColumnType("text"); + + b.Property("Priority") + .IsRequired() + .HasColumnType("text"); + + b.Property("Recipient") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("RetryAttempts") + .HasColumnType("integer"); + + b.Property("ScheduledNotificationId") + .HasColumnType("uuid"); + + b.Property("SendOnDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("SentDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("text"); + + b.Property("TemplateName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("ToAddress") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EmailLogs", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLogAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("EmailLogId") + .HasColumnType("uuid"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OriginTemplateId") + .HasColumnType("uuid"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TemplateId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("EmailLogId"); + + b.HasIndex("S3ObjectKey"); + + b.HasIndex("TemplateId"); + + b.ToTable("EmailLogAttachments", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.EmailTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BodyHTML") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyText") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RecipientCategory") + .HasColumnType("text"); + + b.Property("RecipientIdentifier") + .HasColumnType("text"); + + b.Property("SendFrom") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("EmailTemplates", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.Subscriber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Subscribers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("SubscriptionGroups", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroupSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SubscriberId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.HasIndex("SubscriberId"); + + b.ToTable("SubscriptionGroupSubscribers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TemplateVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MapTo") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("TemplateVariables", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.Trigger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("InternalName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Triggers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TriggerSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SubscriptionGroupId") + .HasColumnType("uuid"); + + b.Property("TemplateId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TriggerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionGroupId"); + + b.HasIndex("TemplateId"); + + b.HasIndex("TriggerId"); + + b.ToTable("TriggerSubscriptions", "Notifications"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.AccountCodings.AccountCoding", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Description") + .HasMaxLength(35) + .HasColumnType("character varying(35)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MinistryClient") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("Responsibility") + .IsRequired() + .HasColumnType("text"); + + b.Property("ServiceLine") + .IsRequired() + .HasColumnType("text"); + + b.Property("Stob") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("AccountCodings", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentConfigurations.PaymentConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DefaultAccountCodingId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentIdPrefix") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("PaymentConfigurations", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.ExpenseApproval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DecisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DecisionUserId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PaymentRequestId"); + + b.ToTable("ExpenseApprovals", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountCodingId") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("BatchName") + .IsRequired() + .HasColumnType("text"); + + b.Property("BatchNumber") + .HasColumnType("numeric"); + + b.Property("CancelledBy") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("CancelledBy"); + + b.Property("CancelledById") + .HasColumnType("uuid") + .HasColumnName("CancelledById"); + + b.Property("CancelledOn") + .HasColumnType("timestamp without time zone") + .HasColumnName("CancelledOn"); + + b.Property("CasHttpStatusCode") + .HasColumnType("integer"); + + b.Property("CasResponse") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContractNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FsbApNotified") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("FsbNotificationEmailLogId") + .HasColumnType("uuid"); + + b.Property("FsbNotificationSentDate") + .HasColumnType("timestamp without time zone"); + + b.Property("InvoiceNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("InvoiceStatus") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsRecon") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("PayeeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentDate") + .HasColumnType("text"); + + b.Property("PaymentNumber") + .HasColumnType("text"); + + b.Property("PaymentStatus") + .HasColumnType("text"); + + b.Property("ReferenceNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("RequesterName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SiteId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("SubmissionConfirmationCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SupplierName") + .HasColumnType("text"); + + b.Property("SupplierNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AccountCodingId"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("CreationTime"); + + b.HasIndex("FsbNotificationEmailLogId"); + + b.HasIndex("ReferenceNumber") + .IsUnique(); + + b.HasIndex("SiteId"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId", "CreationTime") + .HasFilter("\"IsDeleted\" = false"); + + b.ToTable("PaymentRequests", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentTags.PaymentTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestId") + .HasColumnType("uuid"); + + b.Property("TagId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("PaymentRequestId"); + + b.HasIndex("TagId"); + + b.ToTable("PaymentTags", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentThresholds.PaymentThreshold", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Threshold") + .HasColumnType("numeric"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("PaymentThresholds", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Site", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddressLine1") + .HasColumnType("text"); + + b.Property("AddressLine2") + .HasColumnType("text"); + + b.Property("AddressLine3") + .HasColumnType("text"); + + b.Property("BankAccount") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EFTAdvicePref") + .HasColumnType("text"); + + b.Property("EmailAddress") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastUpdatedInCas") + .HasColumnType("timestamp without time zone"); + + b.Property("MarkDeletedInUse") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentGroup") + .HasColumnType("integer"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("SiteProtected") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("SupplierId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("SupplierId"); + + b.ToTable("Sites", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Supplier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BusinessNumber") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastUpdatedInCAS") + .HasColumnType("timestamp without time zone"); + + b.Property("MailingAddress") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("text"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("SIN") + .HasColumnType("text"); + + b.Property("StandardIndustryClassification") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("Subcategory") + .HasColumnType("text"); + + b.Property("SupplierProtected") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Suppliers", "Payments"); + }); + + modelBuilder.Entity("Unity.Reporting.Domain.Configuration.ReportColumnsMap", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Mapping") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RoleStatus") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("ViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ViewStatus") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ReportColumnsMaps", "Reporting"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Scoresheet", "Scoresheet") + .WithMany("Instances") + .HasForeignKey("ScoresheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scoresheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Answer", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Question", "Question") + .WithMany("Answers") + .HasForeignKey("QuestionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", null) + .WithMany("Answers") + .HasForeignKey("ScoresheetInstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Question"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.ScoresheetSection", "Section") + .WithMany("Fields") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Scoresheet", "Scoresheet") + .WithMany("Sections") + .HasForeignKey("ScoresheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scoresheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.CustomFieldValue", b => + { + b.HasOne("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", null) + .WithMany("Values") + .HasForeignKey("WorksheetInstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetLinks.WorksheetLink", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.Worksheet", "Worksheet") + .WithMany("Links") + .HasForeignKey("WorksheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Worksheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.CustomField", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.WorksheetSection", "Section") + .WithMany("Fields") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.Worksheet", "Worksheet") + .WithMany("Sections") + .HasForeignKey("WorksheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Worksheet"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAddress", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", "Applicant") + .WithMany("ApplicantAddresses") + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicantAddresses") + .HasForeignKey("ApplicationId"); + + b.Navigation("Applicant"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAgent", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithOne("ApplicantAgent") + .HasForeignKey("Unity.GrantManager.Applications.ApplicantAgent", "ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAttachment", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", "Applicant") + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", "ApplicationForm") + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationStatus", "ApplicationStatus") + .WithMany("Applications") + .HasForeignKey("ApplicationStatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Applicant"); + + b.Navigation("ApplicationForm"); + + b.Navigation("ApplicationStatus"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAssignment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicationAssignments") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", "Assignee") + .WithMany() + .HasForeignKey("AssigneeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Assignee"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAttachment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationChefsFileAttachment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationContact", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationForm", b => + { + b.HasOne("Unity.GrantManager.Intakes.Intake", null) + .WithMany() + .HasForeignKey("IntakeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) + .WithMany() + .HasForeignKey("ParentFormId") + .OnDelete(DeleteBehavior.NoAction); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormSubmission", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormVersion", b => + { + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationLink", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany("ApplicationLinks") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationTags", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicationTags") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.GlobalTag.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AssessmentAttachment", b => + { + b.HasOne("Unity.GrantManager.Assessments.Assessment", null) + .WithMany() + .HasForeignKey("AssessmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AuditHistory", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.FundingHistory", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.IssueTracking", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ReportsHistory", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Assessments.Assessment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("Assessments") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("AssessorId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicantComment", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicationComment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.AssessmentComment", b => + { + b.HasOne("Unity.GrantManager.Assessments.Assessment", null) + .WithMany() + .HasForeignKey("AssessmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Contacts.ContactLink", b => + { + b.HasOne("Unity.GrantManager.Contacts.Contact", null) + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Notifications.ScheduledNotificationTracking", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Notifications.ScheduledNotification", null) + .WithMany() + .HasForeignKey("ScheduledNotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroupUser", b => + { + b.HasOne("Unity.Notifications.EmailGroups.EmailGroup", null) + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLogAttachment", b => + { + b.HasOne("Unity.Notifications.Emails.EmailLog", null) + .WithMany() + .HasForeignKey("EmailLogId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Unity.Notifications.Templates.EmailTemplate", null) + .WithMany() + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroupSubscription", b => + { + b.HasOne("Unity.Notifications.Templates.SubscriptionGroup", "SubscriptionGroup") + .WithMany() + .HasForeignKey("GroupId"); + + b.HasOne("Unity.Notifications.Templates.Subscriber", "Subscriber") + .WithMany() + .HasForeignKey("SubscriberId"); + + b.Navigation("Subscriber"); + + b.Navigation("SubscriptionGroup"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TriggerSubscription", b => + { + b.HasOne("Unity.Notifications.Templates.SubscriptionGroup", "SubscriptionGroup") + .WithMany() + .HasForeignKey("SubscriptionGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Notifications.Templates.EmailTemplate", "EmailTemplate") + .WithMany() + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Notifications.Templates.Trigger", "Trigger") + .WithMany() + .HasForeignKey("TriggerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmailTemplate"); + + b.Navigation("SubscriptionGroup"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.ExpenseApproval", b => + { + b.HasOne("Unity.Payments.Domain.PaymentRequests.PaymentRequest", "PaymentRequest") + .WithMany("ExpenseApprovals") + .HasForeignKey("PaymentRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PaymentRequest"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.HasOne("Unity.Payments.Domain.AccountCodings.AccountCoding", "AccountCoding") + .WithMany() + .HasForeignKey("AccountCodingId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Unity.Payments.Domain.Suppliers.Site", "Site") + .WithMany() + .HasForeignKey("SiteId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("AccountCoding"); + + b.Navigation("Site"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentTags.PaymentTag", b => + { + b.HasOne("Unity.Payments.Domain.PaymentRequests.PaymentRequest", null) + .WithMany("PaymentTags") + .HasForeignKey("PaymentRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.GlobalTag.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Site", b => + { + b.HasOne("Unity.Payments.Domain.Suppliers.Supplier", "Supplier") + .WithMany("Sites") + .HasForeignKey("SupplierId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.Navigation("Answers"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.Navigation("Answers"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Scoresheet", b => + { + b.Navigation("Instances"); + + b.Navigation("Sections"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.Navigation("Fields"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", b => + { + b.Navigation("Values"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.Worksheet", b => + { + b.Navigation("Links"); + + b.Navigation("Sections"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.Navigation("Fields"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Applicant", b => + { + b.Navigation("ApplicantAddresses"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.Navigation("ApplicantAddresses"); + + b.Navigation("ApplicantAgent"); + + b.Navigation("ApplicationAssignments"); + + b.Navigation("ApplicationLinks"); + + b.Navigation("ApplicationTags"); + + b.Navigation("Assessments"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationStatus", b => + { + b.Navigation("Applications"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.Navigation("ExpenseApprovals"); + + b.Navigation("PaymentTags"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Supplier", b => + { + b.Navigation("Sites"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260818211028_AB33234_ExternalLinksApplicantMessage.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260818211028_AB33234_ExternalLinksApplicantMessage.cs new file mode 100644 index 0000000000..0090a9c4b3 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260818211028_AB33234_ExternalLinksApplicantMessage.cs @@ -0,0 +1,75 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + /// + public partial class AB33234_ExternalLinksApplicantMessage : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // Existing rows store ExternalLinks as a bare JSON array, with the applicant message + // (if any) saved in the renewal link's (ExternalLinkType 2) Description. Wrap them in + // the new { ApplicantMessage, Links } object shape, lifting that message out so it + // isn't silently lost. + migrationBuilder.Sql( + """ + UPDATE "ApplicationForms" AS form + SET "ExternalLinks" = jsonb_build_object( + 'ApplicantMessage', + COALESCE(( + SELECT link ->> 'Description' + FROM jsonb_array_elements(form."ExternalLinks") AS link + WHERE link ->> 'ExternalLinkType' = '2' + LIMIT 1 + ), ''), + 'Links', form."ExternalLinks") + WHERE jsonb_typeof(form."ExternalLinks") = 'array'; + """); + + migrationBuilder.AlterColumn( + name: "ExternalLinks", + table: "ApplicationForms", + type: "jsonb", + nullable: false, + defaultValue: "{\"ApplicantMessage\":\"\",\"Links\":[]}", + oldClrType: typeof(string), + oldType: "jsonb", + oldDefaultValue: "[]"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "ExternalLinks", + table: "ApplicationForms", + type: "jsonb", + nullable: false, + defaultValue: "[]", + oldClrType: typeof(string), + oldType: "jsonb", + oldDefaultValue: "{\"ApplicantMessage\":\"\",\"Links\":[]}"); + + // Push the applicant message back into the renewal link's Description before + // dropping the wrapper, so a rollback doesn't lose a message edited after the + // forward migration ran. + migrationBuilder.Sql( + """ + UPDATE "ApplicationForms" AS form + SET "ExternalLinks" = COALESCE(( + SELECT jsonb_agg( + CASE + WHEN link ->> 'ExternalLinkType' = '2' + THEN jsonb_set(link, '{Description}', to_jsonb(form."ExternalLinks" ->> 'ApplicantMessage')) + ELSE link + END) + FROM jsonb_array_elements(form."ExternalLinks" -> 'Links') AS link + ), '[]'::jsonb) + WHERE jsonb_typeof(form."ExternalLinks") = 'object'; + """); + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/GrantTenantDbContextModelSnapshot.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/GrantTenantDbContextModelSnapshot.cs index 3df92980ba..1e8af23bf4 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/GrantTenantDbContextModelSnapshot.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/GrantTenantDbContextModelSnapshot.cs @@ -19,7 +19,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.PostgreSql) - .HasAnnotation("ProductVersion", "10.0.3") + .HasAnnotation("ProductVersion", "10.0.10") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -794,6 +794,69 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("WorksheetSections", "Flex"); }); + modelBuilder.Entity("Unity.GrantManager.ApplicationForms.GenerationReview", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContextId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Operation") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReviewData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Sequence") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("Operation", "ContextId", "Sequence") + .IsUnique(); + + b.ToTable("GenerationReviews", "AI"); + }); + modelBuilder.Entity("Unity.GrantManager.Applications.Applicant", b => { b.Property("Id") @@ -845,6 +908,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("FiscalMonth") .HasColumnType("text"); + b.Property("FiscalYearEnd") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("date"); + b.Property("FundingHistoryComments") .HasColumnType("text"); @@ -1290,6 +1357,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ElectoralDistrict") .HasColumnType("text"); + b.Property("EligibleForRenewal") + .HasColumnType("boolean"); + b.Property("ExternalStatusVisibility") .HasColumnType("boolean"); @@ -1750,6 +1820,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ElectoralDistrictAddressType") .HasColumnType("integer"); + b.Property("ExternalLinksConfig") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("ExternalLinks"); + b.Property("ExtraProperties") .IsRequired() .HasColumnType("text") @@ -3124,6 +3199,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid") .HasColumnName("LastModifierId"); + b.Property("Module") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + b.Property("RecipientCategory") .HasColumnType("text"); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicantRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicantRepository.cs index fe9dd193a3..d90e39d505 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicantRepository.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicantRepository.cs @@ -1,9 +1,9 @@ -using System; +using Microsoft.EntityFrameworkCore; +using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; using Unity.GrantManager.Applications; using Unity.GrantManager.EntityFrameworkCore; using Unity.Payments.Domain.Suppliers; @@ -165,7 +165,8 @@ public async Task> GetApplicantListRecordsAsync(IReadO StartedOperatingDate = a.StartedOperatingDate, IsDuplicated = a.IsDuplicated, CreationTime = a.CreationTime, - LastModificationTime = a.LastModificationTime + LastModificationTime = a.LastModificationTime, + FiscalYearEnd = a.FiscalYearEnd }) .ToListAsync(); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicationRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicationRepository.cs index 80432f764a..08219b0dfe 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicationRepository.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicationRepository.cs @@ -323,6 +323,8 @@ public async Task> GetApplicationListRecordsAsync( a.UnityApplicationId, Status = a.ApplicationStatus.InternalStatus, // ApplicationStatus (always joined) a.ExternalStatusVisibility, + a.ApplicationStatus.ExternalStatus, + a.ApplicationStatus.NotifiedStatus, Category = a.ApplicationForm.Category ?? string.Empty, // ApplicationForm (always joined) a.ApplicantId, ApplicantName = a.Applicant.ApplicantName, // Applicant (always joined) @@ -508,6 +510,10 @@ public async Task> GetApplicationListRecordsAsync( UnityApplicationId = a.UnityApplicationId, Status = a.Status, ExternalStatusVisibility = a.ExternalStatusVisibility, + ExternalStatus = a.ExternalStatus, + PublishedStatus = a.ExternalStatusVisibility + ? a.NotifiedStatus ?? a.ExternalStatus + : a.ExternalStatus, Category = a.Category, ApplicantId = a.ApplicantId, ApplicantName = a.ApplicantName, diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/GenerationReviewRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/GenerationReviewRepository.cs new file mode 100644 index 0000000000..c750952537 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/GenerationReviewRepository.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Unity.GrantManager.ApplicationForms; +using Unity.GrantManager.EntityFrameworkCore; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; +using Volo.Abp.DependencyInjection; + +namespace Unity.GrantManager.Repositories; + +[ExposeServices(typeof(IGenerationReviewRepository))] +public class GenerationReviewRepository( + IDbContextProvider dbContextProvider) + : EfCoreRepository(dbContextProvider), + IGenerationReviewRepository +{ + public async Task FindLatestByOperationAndFormVersionAsync( + string operation, + Guid formVersionId) + { + var dbContext = await GetDbContextAsync(); + return await dbContext.GenerationReviews + .Where(review => + review.Operation == operation && + review.ContextId == formVersionId) + .OrderByDescending(review => review.Sequence) + .FirstOrDefaultAsync(); + } + + public async Task> GetListByOperationAndFormVersionAsync( + string operation, + Guid formVersionId) + { + var dbContext = await GetDbContextAsync(); + return await dbContext.GenerationReviews + .Where(review => + review.Operation == operation && + review.ContextId == formVersionId) + .OrderBy(review => review.Sequence) + .ToListAsync(); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Scripts/get_consolidated_worksheet_data.sql b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Scripts/get_consolidated_worksheet_data.sql index cc1fcc39cc..467f0ae94b 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Scripts/get_consolidated_worksheet_data.sql +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Scripts/get_consolidated_worksheet_data.sql @@ -124,7 +124,9 @@ BEGIN WHEN 'checkbox' THEN CASE WHEN um.type_path LIKE '%checkboxgroup%' THEN - format('(CASE WHEN ((SELECT cell_elem->>''value'' FROM jsonb_array_elements(dg_tbl.dg_data->''cells'') AS cell_elem WHERE cell_elem->>''key'' = %L)) IS NULL THEN NULL ELSE (SELECT (checkbox_elem->>''value'')::BOOLEAN FROM jsonb_array_elements(((SELECT cell_elem->>''value'' FROM jsonb_array_elements(dg_tbl.dg_data->''cells'') AS cell_elem WHERE cell_elem->>''key'' = %L))::jsonb) AS checkbox_elem WHERE checkbox_elem->>''key'' = %L) END) AS %I', + -- Validate via safe_to_jsonb + jsonb_typeof = 'array' rather than a bracket-shape regex, so a + -- null/empty/malformed stored value (e.g. '' or '[invalid]') never reaches the ::jsonb cast + format('(CASE WHEN jsonb_typeof("Reporting".safe_to_jsonb((SELECT cell_elem->>''value'' FROM jsonb_array_elements(dg_tbl.dg_data->''cells'') AS cell_elem WHERE cell_elem->>''key'' = %L))) = ''array'' THEN (SELECT (checkbox_elem->>''value'')::BOOLEAN FROM jsonb_array_elements("Reporting".safe_to_jsonb((SELECT cell_elem->>''value'' FROM jsonb_array_elements(dg_tbl.dg_data->''cells'') AS cell_elem WHERE cell_elem->>''key'' = %L))) AS checkbox_elem WHERE checkbox_elem->>''key'' = %L) ELSE NULL END) AS %I', split_part(um.clean_data_path, '->', 1), split_part(um.clean_data_path, '->', 1), split_part(um.clean_data_path, '->', 2), @@ -218,7 +220,9 @@ BEGIN WHEN 'checkbox' THEN CASE WHEN um.type_path LIKE '%checkboxgroup%' THEN - format('(CASE WHEN ((SELECT v_elem->>''value'' FROM jsonb_array_elements(wi."CurrentValue"->''values'') AS v_elem WHERE v_elem->>''key'' = %L)) IS NULL THEN NULL ELSE (SELECT (checkbox_elem->>''value'')::BOOLEAN FROM jsonb_array_elements(((SELECT v_elem->>''value'' FROM jsonb_array_elements(wi."CurrentValue"->''values'') AS v_elem WHERE v_elem->>''key'' = %L))::jsonb) AS checkbox_elem WHERE checkbox_elem->>''key'' = %L) END) AS %I', + -- Validate via safe_to_jsonb + jsonb_typeof = 'array' rather than a bracket-shape regex, so a + -- null/empty/malformed stored value (e.g. '' or '[invalid]') never reaches the ::jsonb cast + format('(CASE WHEN jsonb_typeof("Reporting".safe_to_jsonb((SELECT v_elem->>''value'' FROM jsonb_array_elements(wi."CurrentValue"->''values'') AS v_elem WHERE v_elem->>''key'' = %L))) = ''array'' THEN (SELECT (checkbox_elem->>''value'')::BOOLEAN FROM jsonb_array_elements("Reporting".safe_to_jsonb((SELECT v_elem->>''value'' FROM jsonb_array_elements(wi."CurrentValue"->''values'') AS v_elem WHERE v_elem->>''key'' = %L))) AS checkbox_elem WHERE checkbox_elem->>''key'' = %L) ELSE NULL END) AS %I', split_part(COALESCE(um.clean_data_path, um.property_name), '->', 1), split_part(COALESCE(um.clean_data_path, um.property_name), '->', 1), split_part(COALESCE(um.clean_data_path, um.property_name), '->', 2), diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Scripts/get_worksheet_data.sql b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Scripts/get_worksheet_data.sql index 21e58fbed9..8ca3b51108 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Scripts/get_worksheet_data.sql +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Scripts/get_worksheet_data.sql @@ -111,7 +111,9 @@ BEGIN CASE WHEN um.type_path LIKE '%checkboxgroup%' THEN -- For checkbox group, parse the JSON array and extract the specific checkbox value - format('(CASE WHEN ((SELECT cell_elem->>''value'' FROM jsonb_array_elements(dg_tbl.dg_data->''cells'') AS cell_elem WHERE cell_elem->>''key'' = %L)) IS NULL THEN NULL ELSE (SELECT (checkbox_elem->>''value'')::BOOLEAN FROM jsonb_array_elements(((SELECT cell_elem->>''value'' FROM jsonb_array_elements(dg_tbl.dg_data->''cells'') AS cell_elem WHERE cell_elem->>''key'' = %L))::jsonb) AS checkbox_elem WHERE checkbox_elem->>''key'' = %L) END) AS %I', + -- Validate via safe_to_jsonb + jsonb_typeof = 'array' rather than a bracket-shape regex, so a + -- null/empty/malformed stored value (e.g. '' or '[invalid]') never reaches the ::jsonb cast + format('(CASE WHEN jsonb_typeof("Reporting".safe_to_jsonb((SELECT cell_elem->>''value'' FROM jsonb_array_elements(dg_tbl.dg_data->''cells'') AS cell_elem WHERE cell_elem->>''key'' = %L))) = ''array'' THEN (SELECT (checkbox_elem->>''value'')::BOOLEAN FROM jsonb_array_elements("Reporting".safe_to_jsonb((SELECT cell_elem->>''value'' FROM jsonb_array_elements(dg_tbl.dg_data->''cells'') AS cell_elem WHERE cell_elem->>''key'' = %L))) AS checkbox_elem WHERE checkbox_elem->>''key'' = %L) ELSE NULL END) AS %I', split_part(um.clean_data_path, '->', 1), -- Field10 equivalent in datagrid split_part(um.clean_data_path, '->', 1), -- Field10 equivalent in datagrid split_part(um.clean_data_path, '->', 2), -- check1/check2/etc @@ -203,9 +205,11 @@ BEGIN CASE WHEN um.type_path LIKE '%checkboxgroup%' THEN -- For checkbox group, parse the JSON array and extract the specific checkbox value - format('(CASE WHEN ((SELECT v_elem->>''value'' FROM jsonb_array_elements(wi."CurrentValue"->''values'') AS v_elem WHERE v_elem->>''key'' = %L)) IS NULL THEN NULL ELSE (SELECT (checkbox_elem->>''value'')::BOOLEAN FROM jsonb_array_elements(((SELECT v_elem->>''value'' FROM jsonb_array_elements(wi."CurrentValue"->''values'') AS v_elem WHERE v_elem->>''key'' = %L))::jsonb) AS checkbox_elem WHERE checkbox_elem->>''key'' = %L) END) AS %I', + -- Validate via safe_to_jsonb + jsonb_typeof = 'array' rather than a bracket-shape regex, so a + -- null/empty/malformed stored value (e.g. '' or '[invalid]') never reaches the ::jsonb cast + format('(CASE WHEN jsonb_typeof("Reporting".safe_to_jsonb((SELECT v_elem->>''value'' FROM jsonb_array_elements(wi."CurrentValue"->''values'') AS v_elem WHERE v_elem->>''key'' = %L))) = ''array'' THEN (SELECT (checkbox_elem->>''value'')::BOOLEAN FROM jsonb_array_elements("Reporting".safe_to_jsonb((SELECT v_elem->>''value'' FROM jsonb_array_elements(wi."CurrentValue"->''values'') AS v_elem WHERE v_elem->>''key'' = %L))) AS checkbox_elem WHERE checkbox_elem->>''key'' = %L) ELSE NULL END) AS %I', + split_part(COALESCE(um.clean_data_path, um.property_name), '->', 1), -- Field10 split_part(COALESCE(um.clean_data_path, um.property_name), '->', 1), -- Field10 - split_part(COALESCE(um.clean_data_path, um.property_name), '->', 1), -- Field10 split_part(COALESCE(um.clean_data_path, um.property_name), '->', 2), -- check1/check2/etc um.column_name) ELSE diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Controllers/FormNotificationsApiController.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Controllers/FormNotificationsApiController.cs index 1850dcb94e..200fe8a01d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Controllers/FormNotificationsApiController.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Controllers/FormNotificationsApiController.cs @@ -8,6 +8,7 @@ using Unity.Notifications.Emails; using Volo.Abp.Users; using Unity.GrantManager.Events; +using Unity.Payments.Enums; using Volo.Abp.Identity.Integration; namespace Unity.GrantManager.Web.Controllers @@ -40,6 +41,15 @@ public FormNotificationsApiController(IApplicationStatusService statusService, I _grantApplicationAppService = grantApplicationAppService; _scheduledNotificationHelper = scheduledNotificationHelper; } + + [HttpGet("payment-statuses")] + public ActionResult> GetPaymentStatuses() + { + var statuses = Enum.GetNames() + .Select(status => (object)new { id = status, internalStatus = status }) + .ToList(); + return Ok(statuses); + } // In-memory storage removed; persisting to ScheduledNotifications table via IAutomatedNotificationAppService @@ -245,8 +255,9 @@ public async Task>> GetForForm(strin TemplateId = e.EmailTemplateId, TemplateName = templateMap.TryGetValue(e.EmailTemplateId, out var t) && t != null ? t.Name : string.Empty, TriggerType = e.TriggerType, + Module = e.Module ?? (e.TriggerType == "Event" ? "Application" : null), DateType = e.DateField, - EventStatus = e.ApplicationStatus, + EventStatus = e.EventType ?? e.ApplicationStatus, ApplicationStatusId = e.ApplicationStatusId, RecipientCategory = e.RecipientCategory, RecipientIdentifier = e.RecipientIdentifier, @@ -262,11 +273,27 @@ public async Task> CreateForForm(string f { if (input.TemplateId == Guid.Empty) return BadRequest("TemplateId required"); + if (!ValidateModule(input, out var moduleError)) return BadRequest(moduleError); + if (string.Equals(input.TriggerType, "Event", StringComparison.OrdinalIgnoreCase) && string.IsNullOrWhiteSpace(input.RecipientIdentifier)) { return BadRequest("RecipientIdentifier required for Event trigger"); } + if (string.Equals(input.TriggerType, "Event", StringComparison.OrdinalIgnoreCase) && + string.Equals(input.Module, "Payment", StringComparison.OrdinalIgnoreCase) && + string.IsNullOrWhiteSpace(input.EventStatus)) + { + return BadRequest("EventStatus required for Event trigger"); + } + + if (string.Equals(input.TriggerType, "Event", StringComparison.OrdinalIgnoreCase) && + string.Equals(input.Module, "Application", StringComparison.OrdinalIgnoreCase) && + !input.ApplicationStatusId.HasValue) + { + return BadRequest("ApplicationStatusId required for Application event trigger"); + } + var template = await _templateService.GetTemplateById(input.TemplateId); if (template == null) return BadRequest("Template not found"); if (!Guid.TryParse(formId, out var parsedFormId)) return BadRequest("Invalid form id"); @@ -285,10 +312,11 @@ public async Task> CreateForForm(string f FormId = parsedFormId, EmailTemplateId = template.Id, TriggerType = input.TriggerType, - TriggerDetail = input.TriggerType == "Date" ? input.DateType : statusLabel, + Module = input.Module, + TriggerDetail = input.TriggerType == "Date" ? input.DateType : input.Module == "Payment" ? input.EventStatus : statusLabel, IsActive = true, - EventType = null, - ApplicationStatusId = input.ApplicationStatusId, + EventType = input.Module == "Payment" ? input.EventStatus : null, + ApplicationStatusId = input.Module == "Application" ? input.ApplicationStatusId : null, ApplicationStatus = statusLabel, DateField = input.DateType, RecipientCategory = input.RecipientCategory, @@ -303,8 +331,9 @@ public async Task> CreateForForm(string f TemplateId = input.TemplateId, TemplateName = template.Name, TriggerType = created.TriggerType, + Module = created.Module, DateType = created.DateField, - EventStatus = created.ApplicationStatus, + EventStatus = created.EventType ?? created.ApplicationStatus, ApplicationStatusId = created.ApplicationStatusId, RecipientCategory = created.RecipientCategory, RecipientIdentifier = created.RecipientIdentifier, @@ -335,18 +364,47 @@ public async Task> CanDeleteTemplate(Guid templateId) var result = await _automatedNotificationAppService.GetListAsync( new Notifications.GetNotificationsInput { MaxResultCount = 1000 }); - var inUse = result.Items.Any(n => n.EmailTemplateId == templateId); + var associatedPlans = result.Items + .Where(n => n.EmailTemplateId == templateId) + .Select(n => + string.IsNullOrWhiteSpace(n.TriggerDetail) + ? $"{n.TriggerType} notification" + : $"{n.TriggerType} notification - {n.TriggerDetail}") + .Distinct() + .ToList(); + + var inUse = associatedPlans.Count > 0; if (inUse) { return Ok(new { canDelete = false, - errorMessage = "This template cannot be deleted because it is assigned to one or more Scheduled Notifications. Please remove the template from all Scheduled Notifications before deleting." + errorMessage = "This template cannot be deleted because it is assigned to one or more Scheduled Notifications. Please remove the template from all Scheduled Notifications before deleting.", + notificationPlanNames = associatedPlans }); } - return Ok(new { canDelete = true, errorMessage = (string?)null }); + return Ok(new { canDelete = true, errorMessage = (string?)null, notificationPlanNames = Array.Empty() }); + } + + [HttpGet("template-notification-plans/{templateId:guid}")] + public async Task> GetTemplateNotificationPlans(Guid templateId) + { + var result = await _automatedNotificationAppService.GetListAsync( + new Notifications.GetNotificationsInput { MaxResultCount = 1000 }); + var template = await _templateService.GetTemplateById(templateId); + + var templateName = template?.Name ?? "Template"; + var planNames = result.Items + .Where(n => n.IsActive && n.EmailTemplateId == templateId) + .Select(n => string.IsNullOrWhiteSpace(n.TriggerDetail) + ? $"{templateName} - {n.TriggerType} notification" + : $"{templateName} - {n.TriggerType} notification - {n.TriggerDetail}") + .Distinct() + .ToList(); + + return Ok(new { notificationPlanNames = planNames }); } [HttpPut("{formId}/{id:guid}")] @@ -355,6 +413,8 @@ public async Task> UpdateForForm(string f if (!Guid.TryParse(formId, out var parsedFormId)) return BadRequest("Invalid form id"); if (input.TemplateId == Guid.Empty) return BadRequest("TemplateId required"); + if (!ValidateModule(input, out var moduleError)) return BadRequest(moduleError); + var template = await _templateService.GetTemplateById(input.TemplateId); if (template == null) return BadRequest("Template not found"); @@ -371,10 +431,11 @@ public async Task> UpdateForForm(string f FormId = parsedFormId, EmailTemplateId = template.Id, TriggerType = input.TriggerType, - TriggerDetail = input.TriggerType == "Date" ? input.DateType : statusLabel, + Module = input.Module, + TriggerDetail = input.TriggerType == "Date" ? input.DateType : input.Module == "Payment" ? input.EventStatus : statusLabel, IsActive = true, - EventType = null, - ApplicationStatusId = input.ApplicationStatusId, + EventType = input.Module == "Payment" ? input.EventStatus : null, + ApplicationStatusId = input.Module == "Application" ? input.ApplicationStatusId : null, ApplicationStatus = statusLabel, DateField = input.DateType, RecipientCategory = input.RecipientCategory, @@ -389,8 +450,9 @@ public async Task> UpdateForForm(string f TemplateId = input.TemplateId, TemplateName = template.Name, TriggerType = updated.TriggerType, + Module = updated.Module, DateType = updated.DateField, - EventStatus = updated.ApplicationStatus, + EventStatus = updated.EventType ?? updated.ApplicationStatus, ApplicationStatusId = updated.ApplicationStatusId, RecipientCategory = updated.RecipientCategory, RecipientIdentifier = updated.RecipientIdentifier, @@ -399,6 +461,30 @@ public async Task> UpdateForForm(string f return Ok(dto); } + + private static bool ValidateModule(CreateScheduledNotificationInput input, out string error) + { + error = string.Empty; + if (!string.Equals(input.TriggerType, "Event", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + if (string.IsNullOrWhiteSpace(input.Module)) + { + error = "Module required for Event trigger"; + return false; + } + + if (!string.Equals(input.Module, "Application", StringComparison.OrdinalIgnoreCase) && + !string.Equals(input.Module, "Payment", StringComparison.OrdinalIgnoreCase)) + { + error = "Module must be Application or Payment"; + return false; + } + + return true; + } } public record EmailTemplateDto @@ -418,6 +504,7 @@ public record ScheduledNotificationDto public Guid TemplateId { get; init; } public string TemplateName { get; init; } = string.Empty; public string TriggerType { get; init; } = string.Empty; + public string? Module { get; init; } public string? DateType { get; init; } public string? EventStatus { get; init; } public Guid? ApplicationStatusId { get; init; } @@ -431,6 +518,7 @@ public record CreateScheduledNotificationInput { public Guid TemplateId { get; init; } public string TriggerType { get; init; } = "Date"; + public string? Module { get; init; } public string? DateType { get; init; } public Guid? ApplicationStatusId { get; init; } public string? EventStatus { get; init; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/PolicyRegistrant.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/PolicyRegistrant.cs index 2a94b89bde..8f74478cba 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/PolicyRegistrant.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/PolicyRegistrant.cs @@ -68,19 +68,24 @@ internal static void Register(ServiceConfigurationContext context) ITAdminOrITOperationsRoles, TenantManagementPermissions.Tenants.Create))); // ITAdmin-only: Tenant delete/connection-string management and Identity user - // creation/lookup - previously covered by the removed admin claim stamp. + // lookup - previously covered by the removed admin claim stamp. authorizationBuilder.AddPolicy(TenantManagementPermissions.Tenants.Delete, policy => policy.AddRequirements(new RoleOrPermissionRequirement( [IdentityConsts.ITAdminRoleName], TenantManagementPermissions.Tenants.Delete))); authorizationBuilder.AddPolicy(TenantManagementPermissions.Tenants.ManageConnectionStrings, policy => policy.AddRequirements(new RoleOrPermissionRequirement( [IdentityConsts.ITAdminRoleName], TenantManagementPermissions.Tenants.ManageConnectionStrings))); - authorizationBuilder.AddPolicy(IdentityPermissions.Users.Create, - policy => policy.AddRequirements(new RoleOrPermissionRequirement( - [IdentityConsts.ITAdminRoleName], IdentityPermissions.Users.Create))); authorizationBuilder.AddPolicy(IdentityPermissions.UserLookup.Default, policy => policy.AddRequirements(new RoleOrPermissionRequirement( [IdentityConsts.ITAdminRoleName], IdentityPermissions.UserLookup.Default))); + + // ITAdmin or ITOperations: Identity user creation/import - UserImportAppService is + // also invoked internally (via TenantCreatedEventHandler/TenantManagerAssignmentEventHandler) + // to import the Program Manager(s) during ITOperations-driven tenant onboarding, so it can't + // be ITAdmin-only like the other host-scoped policies above without breaking that flow. + authorizationBuilder.AddPolicy(IdentityPermissions.Users.Create, + policy => policy.AddRequirements(new RoleOrPermissionRequirement( + ITAdminOrITOperationsRoles, IdentityPermissions.Users.Create))); } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Middleware/ErrorCountingLoggerSink.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Middleware/ErrorCountingLoggerSink.cs index aff8108b90..cd9c75e401 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Middleware/ErrorCountingLoggerSink.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Middleware/ErrorCountingLoggerSink.cs @@ -18,8 +18,12 @@ namespace Unity.GrantManager.Web.Middleware; /// public sealed class ErrorCountingLoggerSink : ILogEventSink { + private static readonly TimeSpan PersistenceBackoff = TimeSpan.FromSeconds(30); private static IServiceScopeFactory? _scopeFactory; private static readonly AsyncLocal IsPersistingExceptionLog = new(); + private readonly object _persistenceGate = new(); + private bool _persistenceInFlight; + private DateTimeOffset _persistenceDisabledUntil; internal static readonly Counter ErrorCounter = Metrics.CreateCounter( @@ -59,6 +63,16 @@ public void Emit(LogEvent logEvent) return; } + lock (_persistenceGate) + { + if (_persistenceInFlight || DateTimeOffset.UtcNow < _persistenceDisabledUntil) + { + return; + } + + _persistenceInFlight = true; + } + _ = Task.Run(async () => { IsPersistingExceptionLog.Value = true; @@ -116,11 +130,19 @@ await exceptionLogs.CreateAsync(new CreateExceptionLogDto } catch { - // Swallow to avoid recursive logging from logger sink failures. + lock (_persistenceGate) + { + _persistenceDisabledUntil = DateTimeOffset.UtcNow.Add(PersistenceBackoff); + } } finally { IsPersistingExceptionLog.Value = false; + + lock (_persistenceGate) + { + _persistenceInFlight = false; + } } }); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Middleware/ExceptionCounterMiddleware.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Middleware/ExceptionCounterMiddleware.cs index d8bf6faef4..9cdff84b83 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Middleware/ExceptionCounterMiddleware.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Middleware/ExceptionCounterMiddleware.cs @@ -19,6 +19,11 @@ public class ExceptionCounterMiddleware( ExceptionNotificationThrottle throttle, ILogger logger) { + private static readonly TimeSpan PersistenceBackoff = TimeSpan.FromSeconds(30); + private readonly object _persistenceGate = new(); + private bool _persistenceInFlight; + private DateTimeOffset _persistenceDisabledUntil; + // Notify only in these environments; add "Staging" if desired private static readonly HashSet NotifyEnvironments = new(StringComparer.OrdinalIgnoreCase) @@ -122,6 +127,19 @@ private void QueueLogNotification(HttpContext context, Exception ex) // we can safely use it after the request scope has ended var scopeFactory = context.RequestServices.GetRequiredService(); + // Acquire the single-flight gate only once the synchronous, potentially-throwing prep + // above has succeeded — otherwise an exception here would leave persistenceInFlight + // stuck "true" forever, since the Task.Run below (whose finally resets it) never starts. + lock (_persistenceGate) + { + if (_persistenceInFlight || DateTimeOffset.UtcNow < _persistenceDisabledUntil) + { + return; + } + + _persistenceInFlight = true; + } + _ = Task.Run(async () => { try @@ -132,6 +150,11 @@ private void QueueLogNotification(HttpContext context, Exception ex) var notifications = scope.ServiceProvider.GetRequiredService(); var exceptionLogs = scope.ServiceProvider.GetService(); + if (exceptionLogs == null) + { + OpenPersistenceBackoff(); + } + // Get current user and tenant name var userId = AbpUserTenantAccessor.GetCurrentUserId(scope.ServiceProvider); var userName = AbpUserTenantAccessor.GetCurrentUserName(scope.ServiceProvider) ?? "unknown"; @@ -262,6 +285,7 @@ await exceptionLogs.CreateAsync(new CreateExceptionLogDto } catch (Exception logEx) { + OpenPersistenceBackoff(); logger.LogWarning(logEx, "Failed to create exception log within UnitOfWork"); } } @@ -270,6 +294,7 @@ await exceptionLogs.CreateAsync(new CreateExceptionLogDto } catch (Exception uowEx) { + OpenPersistenceBackoff(); logger.LogWarning(uowEx, "Failed to complete UnitOfWork for exception handling"); } } @@ -279,9 +304,24 @@ await exceptionLogs.CreateAsync(new CreateExceptionLogDto notifyEx, "Failed to send Teams exception notification"); } + finally + { + lock (_persistenceGate) + { + _persistenceInFlight = false; + } + } }); } + private void OpenPersistenceBackoff() + { + lock (_persistenceGate) + { + _persistenceDisabledUntil = DateTimeOffset.UtcNow.Add(PersistenceBackoff); + } + } + private static string BuildApplicationStackExcerpt(Exception ex) { return ExceptionNotificationHelpers.BuildApplicationStackExcerpt(ex); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml.cs index bd48ebec63..c3e28bf1d6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Unity.GrantManager.Applications; using Unity.GrantManager.Permissions; +using Volo.Abp.TenantManagement; using Volo.Abp.Users; namespace Unity.GrantManager.Web.Pages.Applicants @@ -14,6 +15,7 @@ public class DetailsModel : GrantManagerPageModel { private readonly IApplicantRepository _applicantRepository; private readonly IApplicationRepository _applicationRepository; + private readonly ITenantRepository _tenantRepository; [BindProperty(SupportsGet = true)] public Guid ApplicantId { get; set; } @@ -21,6 +23,9 @@ public class DetailsModel : GrantManagerPageModel [BindProperty(SupportsGet = true)] public Guid? ApplicationId { get; set; } = null; + [BindProperty(SupportsGet = true)] + public Guid? TenantId { get; set; } + public Applicant? Applicant { get; set; } public bool ApplicantIsDeleted { get; set; } public string ApplicantDisplayName { get; set; } = string.Empty; @@ -34,11 +39,13 @@ public class DetailsModel : GrantManagerPageModel public DetailsModel( IApplicantRepository applicantRepository, IApplicationRepository applicationRepository, + ITenantRepository tenantRepository, ICurrentUser currentUser, IConfiguration configuration) { _applicantRepository = applicantRepository; _applicationRepository = applicationRepository; + _tenantRepository = tenantRepository; CurrentUserId = currentUser.Id; CurrentUserName = currentUser.SurName + ", " + currentUser.Name; AllowedFileTypes = configuration["S3:AllowedFileTypes"] ?? ""; @@ -47,6 +54,17 @@ public DetailsModel( public async Task OnGetAsync() { + if (TenantId.HasValue && TenantId.Value != CurrentTenant.Id) + { + var applicationTenant = await _tenantRepository.FindAsync(TenantId.Value); + return RedirectToPage("/Error", new + { + httpStatusCode = 409, + applicationTenantName = applicationTenant?.Name ?? TenantId.Value.ToString(), + currentTenantName = CurrentTenant.Name ?? "Host" + }); + } + // Resolve ApplicantId from ApplicationId if needed if (ApplicantId == Guid.Empty && ApplicationId.HasValue) { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Index.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Index.js index 79bf2852df..e20f6c8488 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Index.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Index.js @@ -64,6 +64,7 @@ $(function () { getFiscalMonthColumn(columnIndex++), getBusinessNumberColumn(columnIndex++), getFiscalDayColumn(columnIndex++), + getFiscalYearEndColumn(columnIndex), getStartedOperatingDateColumn(columnIndex++), getIsDuplicatedColumn(columnIndex++), getCreationTimeColumn(columnIndex++), @@ -346,6 +347,18 @@ $(function () { } } + function getFiscalYearEndColumn(columnIndex) { + return { + title: 'Fiscal Year End', + data: 'fiscalYearEnd', + name: 'fiscalYearEnd', + className: 'data-table-header text-nowrap', + visible: false, + render: DataTable.render.date('YYYY-MM-DD', currentCultureName), + index: columnIndex + } + } + function getStartedOperatingDateColumn(columnIndex) { return { title: 'Started Operating Date', diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/AiSuggestionReviewModalModel.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/AiSuggestionReviewModalModel.cs new file mode 100644 index 0000000000..3d8b2af0a8 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/AiSuggestionReviewModalModel.cs @@ -0,0 +1,27 @@ +namespace Unity.GrantManager.Web.Pages.ApplicationForms; + +public sealed class AiSuggestionReviewModalModel +{ + public string ModalId { get; init; } = string.Empty; + public string ModalLabelId { get; init; } = string.Empty; + public string Title { get; init; } = string.Empty; + public string SourceColumnTitle { get; init; } = string.Empty; + public string TargetColumnTitle { get; init; } = string.Empty; + public bool HideTargetColumn { get; init; } + public string FieldsId { get; init; } = string.Empty; + public string EmptyId { get; init; } = string.Empty; + public string EmptyText { get; init; } = string.Empty; + public string EmptyTitle { get; init; } = string.Empty; + public string EmptyIconClass { get; init; } = "fa-wand-magic-sparkles"; + public string SelectAllId { get; init; } = string.Empty; + public string? TitleInputId { get; init; } + public string? TitleInputLabel { get; init; } + public string? TitleInputPlaceholder { get; init; } + public bool CanMutate { get; init; } = true; + public string PrimaryActionId { get; init; } = string.Empty; + public string PrimaryActionText { get; init; } = string.Empty; + public bool PrimaryActionDisabled { get; init; } + public string ReviewLaterActionId { get; init; } = string.Empty; + public string DiscardActionId { get; init; } = string.Empty; + public string? SectionDataAttribute { get; init; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/Mapping.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/Mapping.cshtml index 4d20a2bf76..c825bd8936 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/Mapping.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/Mapping.cshtml @@ -1,358 +1,456 @@ -@page -@using Unity.GrantManager.ApplicationForms +@page +@using Unity.GrantManager.ApplicationForms @using Unity.Reporting.Permissions @using Volo.Abp.AspNetCore.Mvc.UI.Layout; @using Unity.GrantManager.Web.Pages.ApplicationForms; @using Unity.GrantManager.Permissions; @using Unity.Notifications.Permissions; @using Unity.AI.Permissions; - -@using Volo.Abp.Authorization.Permissions; -@using Unity.GrantManager.Web.Views.Shared.Components.Notifications; -@using Unity.GrantManager.Web.Views.Shared.Components.ApplicationFormConfigWidget; -@using Volo.Abp.Features - -@model MappingModel -@inject IPageLayout PageLayout -@inject IPermissionChecker PermissionChecker -@inject IFeatureChecker FeatureChecker - -@{ - PageLayout.Content.MenuItemName = "GrantManager.ApplicationForms"; - PageLayout.Content.Title = "Application Mapping"; - ViewBag.PageTitle = "Application Forms Mapping"; -} -@section scripts -{ + +@using Volo.Abp.Authorization.Permissions; +@using Unity.GrantManager.Web.Views.Shared.Components.Notifications; +@using Unity.GrantManager.Web.Views.Shared.Components.ApplicationFormConfigWidget; +@using Volo.Abp.Features + +@model MappingModel +@inject IPageLayout PageLayout +@inject IPermissionChecker PermissionChecker +@inject IFeatureChecker FeatureChecker + +@{ + PageLayout.Content.MenuItemName = "GrantManager.ApplicationForms"; + PageLayout.Content.Title = "Application Mapping"; + ViewBag.PageTitle = "Application Forms Mapping"; + var canViewFormMapping = await FeatureChecker.IsEnabledAsync("Unity.AI.FormMapping") && await PermissionChecker.IsGrantedAsync(AIPermissions.FormMapping.View); + var canGenerateFormMapping = await FeatureChecker.IsEnabledAsync("Unity.AI.FormMapping") && await PermissionChecker.IsGrantedAsync(AIPermissions.FormMapping.Generate); + var canViewFormWorksheet = await FeatureChecker.IsEnabledAsync("Unity.AI.FormWorksheet") && await PermissionChecker.IsGrantedAsync(AIPermissions.FormWorksheet.View); + var canGenerateFormWorksheet = await FeatureChecker.IsEnabledAsync("Unity.AI.FormWorksheet") && await PermissionChecker.IsGrantedAsync(AIPermissions.FormWorksheet.Generate); + var canViewFormScoresheet = await FeatureChecker.IsEnabledAsync("Unity.AI.FormScoresheet") && await PermissionChecker.IsGrantedAsync(AIPermissions.FormScoresheet.View); + var canGenerateFormScoresheet = await FeatureChecker.IsEnabledAsync("Unity.AI.FormScoresheet") && await PermissionChecker.IsGrantedAsync(AIPermissions.FormScoresheet.Generate); + var mappingReviewModal = new AiSuggestionReviewModalModel + { + ModalId = "aiMappingReviewModal", + ModalLabelId = "aiMappingReviewModalLabel", + CanMutate = canGenerateFormMapping, + Title = "Review Mapping Suggestions", + SourceColumnTitle = "CHEFS Field", + TargetColumnTitle = "Unity Core Field", + FieldsId = "aiMappingReviewFields", + EmptyId = "aiMappingReviewEmpty", + EmptyTitle = "No mapping changes found", + EmptyText = "AI completed its review but did not find any new mappings that need attention. Existing mappings were left unchanged.", + SelectAllId = "aiMappingReviewSelectAll", + PrimaryActionId = "btn-add-ai-mapping", + PrimaryActionText = "Add selected to map", + ReviewLaterActionId = "btn-review-later-ai-mapping", + DiscardActionId = "btn-discard-ai-mapping" + }; + var worksheetReviewModal = new AiSuggestionReviewModalModel + { + ModalId = "aiWorksheetReviewModal", + ModalLabelId = "aiWorksheetReviewModalLabel", + CanMutate = canGenerateFormWorksheet, + Title = "Review Worksheet Suggestions", + SourceColumnTitle = "Source Field", + TargetColumnTitle = "Worksheet Field", + FieldsId = "aiWorksheetReviewFields", + EmptyId = "aiWorksheetReviewEmpty", + EmptyTitle = "No worksheet fields found", + EmptyText = "AI completed its review but did not find any additional worksheet fields that need to be created.", + SelectAllId = "aiWorksheetReviewSelectAll", + TitleInputId = "aiWorksheetTitle", + TitleInputLabel = "Worksheet Title", + TitleInputPlaceholder = "e.g., Project worksheet", + PrimaryActionId = "btn-create-ai-worksheet-draft", + PrimaryActionText = "Create Draft", + PrimaryActionDisabled = true, + ReviewLaterActionId = "btn-review-later-ai-worksheet", + DiscardActionId = "btn-discard-ai-worksheet" + }; + var scoresheetReviewModal = new AiSuggestionReviewModalModel + { + ModalId = "aiScoresheetReviewModal", + ModalLabelId = "aiScoresheetReviewModalLabel", + CanMutate = canGenerateFormScoresheet, + Title = "Review Scoresheet Suggestions", + SourceColumnTitle = "Generated Question", + TargetColumnTitle = "Section", + FieldsId = "aiScoresheetReviewFields", + EmptyId = "aiScoresheetReviewEmpty", + EmptyTitle = "No scoresheet questions found", + EmptyText = "AI completed its review but did not find any additional scoresheet questions that need to be added.", + SelectAllId = "aiScoresheetReviewSelectAll", + TitleInputId = "aiScoresheetTitle", + TitleInputLabel = "Scoresheet Title", + TitleInputPlaceholder = "e.g., Project scoresheet", + PrimaryActionId = "btn-create-ai-scoresheet-draft", + PrimaryActionText = "Add to Scoresheet", + PrimaryActionDisabled = true, + ReviewLaterActionId = "btn-review-later-ai-scoresheet", + DiscardActionId = "btn-discard-ai-scoresheet", + HideTargetColumn = true, + SectionDataAttribute = "section-id" + }; +} +@section scripts +{ + + } - -@section styles { - -} - - - - - - - - - - -
    -
    -
    -
    @Model.ApplicationFormDto?.ApplicationFormName
    -
    -
    - - - +
    + NOTE: @L["ApplicationForms.Configuration.Notes:LinkVisibilityRequiresUrl"].Value +
    + +
    + @L["ApplicationForms.Configuration:OtherLinks"].Value +
    + + +
    + +
    + NOTE: @L["ApplicationForms.Configuration.Notes:MaxRelatedLinks"].Value +
    +
    diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationFormConfigWidget/Default.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationFormConfigWidget/Default.css index 4a9ddf92ef..f35b14fc2e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationFormConfigWidget/Default.css +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationFormConfigWidget/Default.css @@ -37,3 +37,24 @@ border-radius: 0.25rem; border: 1px solid #e9ecef; } + +.field-error { + display: block; + min-height: 1.1rem; + font-size: 0.8rem; +} + +.related-link-row { + align-items: flex-start; + padding-bottom: 0.25rem; +} + +.related-link-row .form-control.is-invalid, +#renewalLinkUri.is-invalid { + border-color: var(--lpx-danger); +} + +#btn-add-related-link:disabled { + opacity: 0.6; +} + diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationFormConfigWidget/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationFormConfigWidget/Default.js index 1b72bac2d6..e27eae4740 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationFormConfigWidget/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationFormConfigWidget/Default.js @@ -14,12 +14,246 @@ const cancelButton = document.getElementById('btn-cancel-other-config'); const backButton = document.getElementById('btn-back-other-config'); + const renewalLinkUri = document.getElementById('renewalLinkUri'); + const renewalLinkTitle = document.getElementById('renewalLinkTitle'); + const renewalLinkPublished = document.getElementById('renewalLinkPublished'); + const applicantMessage = document.getElementById('applicantMessage'); + const relatedLinksContainer = document.getElementById('relatedLinksContainer'); + const addRelatedLinkButton = document.getElementById('btn-add-related-link'); + + const MAX_RELATED_LINKS = 8; + const URL_PATTERN = /^https?:\/\/\S+$/i; + const EXTERNAL_LINK_TYPE_RENEWAL = 2; + const EXTERNAL_LINK_TYPE_RELATED = 1; + + const l = abp.localization.getResource('GrantManager'); + + function isValidUrl(value) { + return !!value && URL_PATTERN.test(value.trim()); + } + + function getFieldErrorElement(input) { + return input.parentElement.querySelector('.field-error'); + } + + function clearFieldError(input) { + const errorEl = getFieldErrorElement(input); + if (errorEl) { + errorEl.textContent = ''; + } + input.classList.remove('is-invalid'); + } + + function setFieldError(input, message) { + const errorEl = getFieldErrorElement(input); + if (errorEl) { + errorEl.textContent = message; + } + input.classList.add('is-invalid'); + } + + function collectRelatedLinkRows() { + return Array.from(relatedLinksContainer.querySelectorAll('.related-link-row')); + } + + function collectRelatedLinksSnapshot() { + return collectRelatedLinkRows().map(function (row) { + return { + uri: row.querySelector('.related-link-uri').value, + title: row.querySelector('.related-link-title').value, + description: row.querySelector('.related-link-description').value, + published: row.querySelector('.related-link-published').checked + }; + }); + } + + function updateAddButtonState() { + addRelatedLinkButton.disabled = collectRelatedLinkRows().length >= MAX_RELATED_LINKS; + } + + function createRelatedLinkRow(data) { + data = data || { uri: '', title: '', description: '', published: false }; + + const row = document.createElement('div'); + row.className = 'related-link-row row mt-2'; + + const uriCol = document.createElement('div'); + uriCol.className = 'col-12 col-md-4'; + const uriInput = document.createElement('input'); + uriInput.type = 'url'; + uriInput.className = 'form-control related-link-uri'; + uriInput.maxLength = 2048; + uriInput.placeholder = 'https://...'; + uriInput.value = data.uri; + const uriError = document.createElement('span'); + uriError.className = 'field-error text-danger small'; + uriCol.appendChild(uriInput); + uriCol.appendChild(uriError); + + const titleCol = document.createElement('div'); + titleCol.className = 'col-12 col-md-3'; + const titleInput = document.createElement('input'); + titleInput.type = 'text'; + titleInput.className = 'form-control related-link-title'; + titleInput.maxLength = 255; + titleInput.placeholder = l('ApplicationForms.Configuration:LinkDisplayName'); + titleInput.value = data.title; + titleCol.appendChild(titleInput); + + const descCol = document.createElement('div'); + descCol.className = 'col-12 col-md-3'; + const descInput = document.createElement('input'); + descInput.type = 'text'; + descInput.className = 'form-control related-link-description'; + descInput.maxLength = 512; + descInput.placeholder = l('ApplicationForms.Configuration:LinkDescription'); + descInput.value = data.description; + descCol.appendChild(descInput); + + const toggleCol = document.createElement('div'); + toggleCol.className = 'col-6 col-md-1 d-flex align-items-center'; + const switchWrapper = document.createElement('div'); + switchWrapper.className = 'form-check unt-form-switch form-switch'; + const toggleInput = document.createElement('input'); + toggleInput.type = 'checkbox'; + toggleInput.className = 'form-check-input related-link-published'; + toggleInput.setAttribute('aria-label', l('ApplicationForms.Configuration:ShowOtherLinksInPortal')); + toggleInput.style.cursor = 'pointer'; + toggleInput.checked = data.published; + switchWrapper.appendChild(toggleInput); + toggleCol.appendChild(switchWrapper); + + const removeCol = document.createElement('div'); + removeCol.className = 'col-6 col-md-1 d-flex align-items-center'; + const removeButton = document.createElement('button'); + removeButton.type = 'button'; + removeButton.className = 'btn btn-sm btn-outline-danger btn-remove-related-link'; + removeButton.setAttribute('aria-label', 'Remove Link'); + const removeIcon = document.createElement('i'); + removeIcon.className = 'fl fl-trash'; + removeButton.appendChild(removeIcon); + removeCol.appendChild(removeButton); + + row.appendChild(uriCol); + row.appendChild(titleCol); + row.appendChild(descCol); + row.appendChild(toggleCol); + row.appendChild(removeCol); + + return row; + } + + relatedLinksContainer.addEventListener('click', function (event) { + const removeButton = event.target.closest('.btn-remove-related-link'); + if (!removeButton || !relatedLinksContainer.contains(removeButton)) { + return; + } + + const row = removeButton.closest('.related-link-row'); + if (!row) { + return; + } + + row.remove(); + updateAddButtonState(); + saveButton.disabled = false; + cancelButton.disabled = false; + }); + + function rebuildRelatedLinkRows(links) { + relatedLinksContainer.innerHTML = ''; + links.forEach(function (link) { + relatedLinksContainer.appendChild(createRelatedLinkRow(link)); + }); + updateAddButtonState(); + } + + addRelatedLinkButton.addEventListener('click', function () { + if (collectRelatedLinkRows().length >= MAX_RELATED_LINKS) { + return; + } + relatedLinksContainer.appendChild(createRelatedLinkRow()); + updateAddButtonState(); + saveButton.disabled = false; + cancelButton.disabled = false; + }); + + updateAddButtonState(); + + function validateExternalLinksConfig() { + let isValid = true; + + clearFieldError(renewalLinkUri); + const renewalUriValue = renewalLinkUri.value.trim(); + if (renewalLinkPublished.checked && !isValidUrl(renewalUriValue)) { + setFieldError(renewalLinkUri, l('ApplicationForms.Configuration.Errors:RenewalLinkRequiredForVisibility')); + isValid = false; + } else if (renewalUriValue && !isValidUrl(renewalUriValue)) { + setFieldError(renewalLinkUri, l('ApplicationForms.Configuration.Errors:InvalidUrl')); + isValid = false; + } + + const rows = collectRelatedLinkRows(); + if (rows.length > MAX_RELATED_LINKS) { + abp.notify.error(l('ApplicationForms.Configuration.Errors:MaxRelatedLinksReached')); + isValid = false; + } + + rows.forEach(function (row) { + const uriInput = row.querySelector('.related-link-uri'); + const publishedInput = row.querySelector('.related-link-published'); + clearFieldError(uriInput); + const value = uriInput.value.trim(); + if (publishedInput.checked && !isValidUrl(value)) { + setFieldError(uriInput, l('ApplicationForms.Configuration.Errors:OtherLinkRequiredForVisibility')); + isValid = false; + } else if (value && !isValidUrl(value)) { + setFieldError(uriInput, l('ApplicationForms.Configuration.Errors:InvalidUrl')); + isValid = false; + } + }); + + return isValid; + } + + function buildExternalLinksConfigPayload() { + const renewalUriValue = renewalLinkUri.value.trim(); + + return { + renewalLink: renewalUriValue ? { + uri: renewalUriValue, + title: renewalLinkTitle.value, + published: renewalLinkPublished.checked, + externalLinkType: EXTERNAL_LINK_TYPE_RENEWAL, + order: 0 + } : null, + relatedLinks: collectRelatedLinkRows() + .map(function (row, index) { + return { + uri: row.querySelector('.related-link-uri').value.trim(), + title: row.querySelector('.related-link-title').value, + description: row.querySelector('.related-link-description').value, + published: row.querySelector('.related-link-published').checked, + externalLinkType: EXTERNAL_LINK_TYPE_RELATED, + order: index + }; + }) + .filter(function (link) { return link.uri; }), + applicantMessage: applicantMessage.value + }; + } + // Store last saved values let lastSavedValues = { directApproval: directApproval.checked, electoralDistrictAddressType: electoralDistrictAddressType.value, prefix: prefix.value, - suffixType: suffixType.value + suffixType: suffixType.value, + renewalLinkUri: renewalLinkUri.value, + renewalLinkTitle: renewalLinkTitle.value, + renewalLinkPublished: renewalLinkPublished.checked, + applicantMessage: applicantMessage.value, + relatedLinks: collectRelatedLinksSnapshot() }; // Initially disable the save and cancel buttons @@ -81,7 +315,13 @@ electoralDistrictAddressType.value = lastSavedValues.electoralDistrictAddressType; prefix.value = lastSavedValues.prefix; suffixType.value = lastSavedValues.suffixType; - + renewalLinkUri.value = lastSavedValues.renewalLinkUri; + renewalLinkTitle.value = lastSavedValues.renewalLinkTitle; + renewalLinkPublished.checked = lastSavedValues.renewalLinkPublished; + applicantMessage.value = lastSavedValues.applicantMessage; + rebuildRelatedLinkRows(lastSavedValues.relatedLinks); + clearFieldError(renewalLinkUri); + // Update preview after restoring values updateUnityIdPreview(); @@ -98,17 +338,15 @@ let isSaving = false; saveButton.addEventListener('click', function (event) { - console.log('Save button clicked'); - console.log(event); - console.log( - electoralDistrictAddressType.value, - prefix.value, - suffixType.value - ); if (isSaving || saveButton.disabled) { event.preventDefault(); return; } + + if (!validateExternalLinksConfig()) { + return; + } + isSaving = true; saveButton.disabled = true; // Disable immediately to prevent double click cancelButton.disabled = true; @@ -125,21 +363,39 @@ }), contentType: 'application/json', }) - .done(function () { - // Update last saved values after successful save + .then(function () { + // Only save external links config once other-config succeeds, + // keeping the two saves sequential. + return abp.ajax({ + url: `/api/app/application-form/${applicationFormId}/external-links-config`, + type: 'PATCH', + data: JSON.stringify(buildExternalLinksConfigPayload()), + contentType: 'application/json', + }); + }) + .then(function () { + // Clear dirty state only after both saves succeed. lastSavedValues = { directApproval: directApproval.checked, electoralDistrictAddressType: electoralDistrictAddressType.value, prefix: prefix.value, - suffixType: suffixType.value + suffixType: suffixType.value, + renewalLinkUri: renewalLinkUri.value, + renewalLinkTitle: renewalLinkTitle.value, + renewalLinkPublished: renewalLinkPublished.checked, + applicantMessage: applicantMessage.value, + relatedLinks: collectRelatedLinksSnapshot() }; abp.notify.success('Other configuration saved successfully.'); + resetFormState(); }) - .fail(function (error) { - abp.notify.error('Failed to save other configuration.'); + .catch(function () { + // Keep the form dirty so the user can retry after a partial failure. + abp.notify.error('Failed to save configuration.'); + saveButton.disabled = false; + cancelButton.disabled = false; }) - .always(function () { - resetFormState(); + .then(function () { isSaving = false; }); }); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationFormConfigWidget/RelatedLinkItemViewModel.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationFormConfigWidget/RelatedLinkItemViewModel.cs new file mode 100644 index 0000000000..57a9e0a9c6 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationFormConfigWidget/RelatedLinkItemViewModel.cs @@ -0,0 +1,9 @@ +namespace Unity.GrantManager.Web.Views.Shared.Components.ApplicationFormConfigWidget; + +public class RelatedLinkItemViewModel +{ + public string Uri { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public bool Published { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.cshtml index 2e7f8e8796..f3cbf078b3 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.cshtml @@ -31,7 +31,7 @@
    @if (Model.IsAIScoringEnabled && Model.IsAiAssessment) { -
    - - @if (await PermissionChecker.IsGrantedAsync(AIPermissions.FormWorksheet.Generate)) + @if (Model.CanGenerateScoresheet) { - - } - @if (await PermissionChecker.IsGrantedAsync(AIPermissions.FormScoresheet.Generate)) + @if (Model.CanViewScoresheet) { - } +
    - - diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/Default.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/Default.css index e5511e4b19..7a9c3bad91 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/Default.css +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/Default.css @@ -112,27 +112,27 @@ height: calc(100vh - 250px); } -.ai-worksheet-review { +.ai-suggestion-review { border: 0; border-radius: 10px; overflow: hidden; } -.ai-worksheet-review__panel { +.ai-suggestion-review__panel { display: flex; flex-direction: column; gap: 12px; padding: 20px; } -.ai-worksheet-review__panel > .modal-header, -.ai-worksheet-review__panel > .modal-body, -.ai-worksheet-review__panel > .modal-footer { +.ai-suggestion-review__panel > .modal-header, +.ai-suggestion-review__panel > .modal-body, +.ai-suggestion-review__panel > .modal-footer { margin: 0; padding: 0; } -.ai-worksheet-review__body { +.ai-suggestion-review__body { display: flex; flex: 0 1 auto; flex-direction: column; @@ -140,17 +140,17 @@ max-height: min(65vh, 42rem); } -.ai-worksheet-review__list { +.ai-suggestion-review__list { flex: 0 1 auto; min-height: 0; overflow-y: auto; } -.ai-worksheet-review__header { +.ai-suggestion-review__header { border-bottom: 0; } -.ai-worksheet-review .modal-title { +.ai-suggestion-review .modal-title { color: #1f2933; font-size: 1.25rem; font-weight: 600; @@ -158,30 +158,30 @@ margin: 0; } -.ai-worksheet-review__title-group .form-label { +.ai-suggestion-review__title-group .form-label { color: #334e68; font-size: 0.875rem; font-weight: 600; margin-bottom: 0.25rem; } -.ai-worksheet-review__title-group .form-control { +.ai-suggestion-review__title-group .form-control { border-color: #b8c7d6; font-size: 0.9375rem; } -.ai-worksheet-review__title-group .form-control:focus { +.ai-suggestion-review__title-group .form-control:focus { border-color: var(--bc-colors-blue-primary, #255a90); box-shadow: 0 0 0 0.2rem rgb(37 90 144 / 15%); } -.ai-worksheet-review__table-header, -.ai-worksheet-review__field { +.ai-suggestion-review__table-header, +.ai-suggestion-review__field { display: grid; grid-template-columns: minmax(0, 1fr) 2rem minmax(0, 1fr) auto; } -.ai-worksheet-review__table-header { +.ai-suggestion-review__table-header { align-items: center; background: #f4f7fa; border-bottom: 1px solid #d8e1eb; @@ -197,17 +197,58 @@ text-transform: uppercase; } -.ai-worksheet-review__field { +.ai-suggestion-review__field { align-items: center; min-height: 3.5rem; padding-inline: 0.75rem; } -.ai-worksheet-review__field + .ai-worksheet-review__field { +.ai-suggestion-review__panel[data-hide-target-column="true"] .ai-suggestion-review__table-header, +.ai-suggestion-review__panel[data-hide-target-column="true"] .ai-suggestion-review__field { + grid-template-columns: minmax(0, 1fr) 2rem auto; +} + +.ai-suggestion-review__panel[data-hide-target-column="true"] .ai-suggestion-review__switch, +.ai-suggestion-review__panel[data-hide-target-column="true"] .ai-suggestion-review__section-header .form-switch { + align-items: center; + display: flex; + justify-content: flex-end; + min-height: 2rem; + min-width: 2.5rem; +} + +.ai-suggestion-review__panel[data-hide-target-column="true"] .ai-suggestion-review__switch .form-check-input, +.ai-suggestion-review__panel[data-hide-target-column="true"] .ai-suggestion-review__section-header .form-check-input { + transform: scale(0.9); +} + +.ai-suggestion-review__panel[data-hide-target-column="true"] .ai-suggestion-review__select-all .form-check-input { + transform: scale(0.9); +} + +.ai-suggestion-review__section-header { + align-items: center; + background: #eef3f7; + border-top: 1px solid #d8e1eb; + color: #334e68; + display: flex; + font-weight: 700; + justify-content: space-between; + padding: 0.65rem 0.75rem; +} + +.ai-suggestion-review__source { + align-items: center; + display: flex; + gap: 0.5rem; + overflow-wrap: anywhere; +} + +.ai-suggestion-review__field + .ai-suggestion-review__field { border-top: 1px solid #edf1f5; } -.ai-worksheet-review__field-name { +.ai-suggestion-review__field-name { color: #1f2933; font-size: 0.9375rem; font-weight: 600; @@ -215,46 +256,130 @@ overflow-wrap: anywhere; } -.ai-worksheet-review__arrow { +.ai-suggestion-review__arrow { color: #52789d; justify-self: center; } -.ai-worksheet-review__switch { +.ai-suggestion-review__switch { justify-self: end; } -.ai-worksheet-review__field:hover { +.ai-suggestion-review__field:hover { background: #f9fbfd; } -.ai-worksheet-review__select-all { +.ai-suggestion-review__select-all { align-items: center; display: flex; justify-self: end; } -.ai-worksheet-review__empty { +.ai-suggestion-review__empty { color: #486581; text-align: center; } -.ai-worksheet-review__footer { +.ai-suggestion-review[data-empty-confirmation] .ai-suggestion-review__header { + justify-content: center; + padding-top: 0.5rem; + text-align: center; +} + +.ai-suggestion-review[data-empty-confirmation] .ai-suggestion-review__body { + min-height: 12rem; +} + +.ai-suggestion-review[data-empty-confirmation] .ai-suggestion-review__list { + align-items: center; + display: flex; + justify-content: center; + min-height: 10rem; +} + +.ai-suggestion-review[data-empty-confirmation] .ai-suggestion-review__empty { + align-items: center; + display: flex; + flex-direction: column; + font-size: 1rem; + font-weight: 500; + line-height: 1.5; + max-width: 28rem; + padding: 1.5rem 1rem; + text-align: center; +} + +.ai-suggestion-review__empty-icon { + align-items: center; + background: #e8f1f8; + border-radius: 50%; + color: #255a90; + display: inline-flex; + font-size: 1.35rem; + height: 3.5rem; + justify-content: center; + margin-bottom: 0.9rem; + position: relative; + width: 3.5rem; +} + +.ai-suggestion-review__empty-check { + align-items: center; + background: #2f855a; + border: 2px solid #fff; + border-radius: 50%; + bottom: -0.15rem; + color: #fff; + display: inline-flex; + font-size: 0.55rem; + height: 1.25rem; + justify-content: center; + position: absolute; + right: -0.15rem; + width: 1.25rem; +} + +.ai-suggestion-review__empty-title { + color: #1f2933; + font-size: 1.08rem; + font-weight: 600; + line-height: 1.35; + margin-bottom: 0.4rem; +} + +.ai-suggestion-review__empty-text { + color: #627d98; + font-size: 0.92rem; + font-weight: 400; + line-height: 1.55; +} + +.ai-suggestion-review[data-empty-confirmation] .ai-suggestion-review__footer { + justify-content: center; +} + +.ai-suggestion-review[data-empty-confirmation] .ai-suggestion-review__footer .btn-primary { + font-size: 0.9rem; + min-width: 0; + padding: 0.5rem 1.25rem; +} + +.ai-suggestion-review__footer { border-top: 0; gap: 0.5rem; justify-content: flex-start; } -.ai-worksheet-review__footer > * { +.ai-suggestion-review__footer > * { margin: 0; } -.ai-worksheet-review__footer .btn { +.ai-suggestion-review__footer .btn { font-size: inherit; font-weight: 400; } -.ai-worksheet-review__discard { +.ai-suggestion-review__discard { --bs-btn-color: #b42318; --bs-btn-border-color: #b42318; --bs-btn-hover-bg: #b42318; @@ -263,33 +388,33 @@ } @media (max-width: 767.98px) { - .ai-worksheet-review__body { + .ai-suggestion-review__body { max-height: 70vh; } - .ai-worksheet-review__table-header { + .ai-suggestion-review__table-header { display: flex; justify-content: flex-end; } - .ai-worksheet-review__table-header > span { + .ai-suggestion-review__table-header > span { display: none; } - .ai-worksheet-review__field { + .ai-suggestion-review__field { grid-template-columns: minmax(0, 1fr) 1.5rem auto; row-gap: 0.5rem; } - .ai-worksheet-review__field-name:first-child { + .ai-suggestion-review__field-name:first-child { grid-column: 1 / 2; } - .ai-worksheet-review__field-name:nth-child(3) { + .ai-suggestion-review__field-name:nth-child(3) { grid-column: 1 / 2; } - .ai-worksheet-review__field-name::before { + .ai-suggestion-review__field-name::before { color: #596777; content: attr(data-field-role); display: block; @@ -300,12 +425,12 @@ text-transform: uppercase; } - .ai-worksheet-review__arrow { + .ai-suggestion-review__arrow { grid-column: 2 / 3; grid-row: 1 / 3; } - .ai-worksheet-review__switch { + .ai-suggestion-review__switch { grid-column: 3 / 4; grid-row: 1 / 3; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js index 167abe5906..a361aef984 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js @@ -912,25 +912,31 @@ $select.find('option').not($placeholder).remove(); const seenTemplateIds = new Set(); - templates.forEach((template) => { - const templateName = template.name || template.Name || 'Unnamed Template'; - const templateId = (template.id || template.Id || '').toString(); - if (!templateId || seenTemplateIds.has(templateId)) { - return; - } + [...templates] + .sort((left, right) => { + const leftName = (left.name || left.Name || 'Unnamed Template').trim(); + const rightName = (right.name || right.Name || 'Unnamed Template').trim(); + return leftName.localeCompare(rightName, undefined, { sensitivity: 'base' }); + }) + .forEach((template) => { + const templateName = template.name || template.Name || 'Unnamed Template'; + const templateId = (template.id || template.Id || '').toString(); + if (!templateId || seenTemplateIds.has(templateId)) { + return; + } - seenTemplateIds.add(templateId); + seenTemplateIds.add(templateId); - const $option = $('
    +
    + + +
    Please select a module.
    +
    @@ -113,11 +122,27 @@
    +
    Attachments (0)
    +
    + + + + + + + + + +
    Document NameDateAttached byFile Size
    +
    +
    + Note: Email templates and attachments cannot be edited here. To make changes, update the selected template in Configuration Management. +
    Note: If Recipients are not found, then no email will be drafted or sent.
    diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Default.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Default.css index 2ed7dbd718..84c4bdd445 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Default.css +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Default.css @@ -8,8 +8,18 @@ .notifications-widget .card { border: 0; } .notifications-widget .card .card-body { background: #fff; } -/* Select2 Bootstrap 5 Theme - Use default styling */ -/* Let Select2's Bootstrap 5 theme handle the layout naturally */ +/* Keep every notification form control aligned to the left column. */ +#notificationForm .left-col .form-select, +#notificationForm .left-col .form-control, +#notificationForm .left-col .select2, +#notificationForm .left-col .select2-container { + display: block; + width: 100% !important; + max-width: 100%; + box-sizing: border-box; +} + +/* Select2 Bootstrap 5 theme */ .select2-container--bootstrap-5 .select2-selection--multiple { min-height: 38px; height: auto; @@ -54,11 +64,13 @@ display: flex; flex: 1 1 auto; min-height: 0; + min-width: 0; } .left-col { - flex: 0 0 33%; + flex: 0 1 33%; min-width: 320px; + max-width: 100%; overflow-y: auto; } @@ -80,13 +92,15 @@ .notification-modal-content { display: flex; flex-direction: column; - min-width: 900px; + width: min(100%, 1200px); + min-width: min(900px, 100%); min-height: 480px; max-height: 85vh; } .notification-modal-body { - overflow: hidden; + overflow-y: auto; + overflow-x: hidden; flex: 1 1 auto; display: flex; flex-direction: column; @@ -96,13 +110,43 @@ flex-shrink: 0; } +#notificationModal .notification-modal-footer { + padding-bottom: 0.67rem; +} + + +#notificationModal .modal-dialog .modal-footer { + padding: 1rem 1.5rem 2rem; +} + +@media (max-width: 991.98px) { + #modalColumns { + flex-direction: column; + gap: 1.5rem; + } + + .left-col { + flex: 0 1 auto; + min-width: 0; + } + + .right-col { + min-height: 220px; + } + + .notification-modal-content { + min-width: 0; + width: 100%; + } +} + /* Notification info note styling */ .notification-info-note { background-color: #d1ecf1; border: 1px solid #bee5eb; color: #0c5460; padding: 12px 16px; - margin: 16px; + margin: 6px; border-radius: 4px; font-size: 0.95rem; line-height: 1.5; @@ -126,6 +170,24 @@ box-sizing: border-box; } +.template-attachments-section { + flex-shrink: 0; + margin-top: 0; + border: 1px solid #dee2e6; + background: #fff; + box-sizing: border-box; + padding: 12px; +} + +#templateAttachmentsLabel { + margin-top: 1rem; +} + +.template-attachments-section .attachments-table { + width: 100%; + margin-bottom: 0; +} + /* Hidden sections (display:none handled by JavaScript) */ .hidden-section { display: none; @@ -150,13 +212,13 @@ .dt-column-title, table.dataTable thead th { color: #fff; font-weight: 500; - font-size: 18px; + font-size: 16px; } table.dataTable td { word-wrap: break-word; max-width: 250px; - font-size: 15px; + font-size: 14px; } .unt-btn-outline-primary { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Default.js index ee3fdc4b0b..6a1e912ecc 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Default.js @@ -137,6 +137,9 @@ function fetchStatuses() { return fetch('/api/form-notifications/statuses').then(r => r.json()); } + function fetchPaymentStatuses() { + return fetch('/api/form-notifications/payment-statuses').then(r => r.json()); + } function fetchRecipients(category) { return fetch('/api/form-notifications/recipients?category=' + encodeURIComponent(category)).then(r => r.json()); @@ -158,11 +161,19 @@ return detail; } + function renderTriggerType(data, type, row) { + if (row.triggerType === 'Event' && row.module) { + return 'Event - ' + row.module; + } + + return row.triggerType || ''; + } + function getNotificationColumns() { let index = 0; return [ { title: 'Template', name: 'templateName', data: 'templateName', visible: true, index: index++ }, - { title: 'Trigger Type', name: 'triggerType', data: 'triggerType', visible: true, index: index++ }, + { title: 'Trigger Type', name: 'triggerType', data: 'triggerType', visible: true, index: index++, render: renderTriggerType }, { title: 'Trigger Detail',name: 'triggerDetail',data: null, visible: true, orderable: true, defaultContent: '', index: index++, render: renderTriggerDetail }, { title: 'Status', name: 'status', data: 'isActive', visible: true, orderable: true, index: index++, @@ -208,7 +219,10 @@ responseCallback, actionButtons: [], pagingEnabled: true, + scrollY: 'calc(100vh - 280px)', + scrollCollapse: true, reorderEnabled: false, + fixedHeaders: true, languageSetValues: {}, dynamicButtonContainerId: null, useNullPlaceholder: false, @@ -232,6 +246,51 @@ } } + // ScrollResize skips sizing while the table is hidden (e.g. an inactive Bootstrap + // tab pane), so nothing pins the pagination bar to the bottom until we recalculate + // it ourselves once the tab holding this widget becomes visible. + function resizeNotificationsScrollBody() { + if (!notificationsTable) return; + const scrollResize = notificationsTable.settings?.()[0]?._scrollResize; + if (scrollResize && typeof scrollResize._size === 'function') { + scrollResize._size(); + return; + } + try { + notificationsTable.columns.adjust(); + } catch (e) { + console.debug('Notifications table column adjust failed:', e.message); + } + } + + function bindTabVisibilityResize() { + document.addEventListener('shown.bs.tab', function (e) { + const target = e.target; + if (!target) return; + const isNotificationsTab = + target.id === 'nav-notifications-tab' || + target.getAttribute?.('data-bs-target') === '#nav-notifications'; + if (isNotificationsTab) { + resizeNotificationsScrollBody(); + } + }); + + const notificationsSection = document.getElementById('notifications-div'); + const notificationsMenuItem = document.getElementById('notifications-menu-item'); + const resizeWhenVisible = () => { + if (!notificationsSection?.offsetParent) return; + setTimeout(resizeNotificationsScrollBody, 0); + }; + + notificationsMenuItem?.addEventListener('click', resizeWhenVisible); + if (notificationsSection && typeof MutationObserver !== 'undefined') { + const observer = new MutationObserver(resizeWhenVisible); + observer.observe(notificationsSection, { attributes: true, attributeFilter: ['class', 'style'] }); + } + + window.addEventListener('resize', resizeWhenVisible); + } + function onCancelNotification(id) { if (!id) return; Swal.fire({ @@ -295,7 +354,7 @@ if (modalEl) { modalEl.dataset.editId = row.id; } - document.getElementById('notificationModal')?.addEventListener('shown.bs.modal', function () { + document.getElementById('notificationModal')?.addEventListener('shown.bs.modal', async function () { const setVal = (id, val) => { document.getElementById(id).value = val ?? ''; }; @@ -326,7 +385,9 @@ const values = row.recipientIdentifier ? row.recipientIdentifier.split(',').map(v => v.trim()) : []; setSelectedRecipients(values); } else if (row.triggerType === 'Event') { - setVal('statusSelect', row.applicationStatusId); + setVal('moduleSelect', row.module); + await loadStatusesForModule(row.module); + setVal('statusSelect', row.applicationStatusId || row.eventStatus); setVal('recipientCategory', row.recipientCategory); // Set multiple values for recipient select const values = row.recipientIdentifier ? row.recipientIdentifier.split(',').map(v => v.trim()) : []; @@ -344,13 +405,15 @@ blank.value = ''; blank.text = ''; sel.appendChild(blank); - templates.forEach(t => { - const opt = document.createElement('option'); - // Use template id as the option value so we can reference templates reliably - opt.value = t.id; - opt.text = t.name + ' — ' + t.subject; - sel.appendChild(opt); - }); + [...templates] + .sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: 'base' })) + .forEach(t => { + const opt = document.createElement('option'); + // Use template id as the option value so we can reference templates reliably + opt.value = t.id; + opt.text = t.name + ' — ' + t.subject; + sel.appendChild(opt); + }); updatePreview(); } @@ -369,6 +432,25 @@ sel.appendChild(opt); }); } + async function loadStatusesForModule(module) { + const statusSelect = document.getElementById('statusSelect'); + if (!statusSelect) return; + + statusSelect.innerHTML = ''; + const blank = document.createElement('option'); + blank.value = ''; + blank.text = ''; + statusSelect.appendChild(blank); + statusSelect.disabled = !module; + + if (!module) return; + + const statuses = module === 'Payment' + ? await fetchPaymentStatuses() + : await fetchStatuses(); + populateStatuses(statuses); + statusSelect.disabled = false; + } function populateRecipients(list) { const sel = document.getElementById('recipientSelect'); @@ -471,18 +553,119 @@ const preview = document.getElementById('templatePreview'); if (sel === null || preview === null) return; const val = sel.value; + updateTemplateAttachments(val); fetch('/api/form-notifications/templates').then(r => r.json()).then(list => { const t = list.find(x => String(x.id) === String(val)); renderTemplatePreview(preview, t); }); } + function notifyAttachmentCount(count) { + if (count === 0) return; + + Swal.fire({ + toast: true, + position: 'top-end', + icon: 'info', + text: count === 1 ? '1 attachment is associated with this template.' : `${count} attachments are associated with this template.`, + showConfirmButton: false, + timer: 3000, + timerProgressBar: true + }); + } + + function updateTemplateAttachments(templateId) { + const section = document.getElementById('template-attachments-section'); + const label = document.getElementById('templateAttachmentsLabel'); + const countLabel = document.getElementById('templateAttachmentsCount'); + const table = $('#TemplateAttachmentsTable'); + if (!section || !table.length) return; + + if ($.fn.dataTable.isDataTable(table)) { + table.DataTable().destroy(); + } + section.classList.add('hidden-section'); + label?.classList.add('hidden-section'); + if (countLabel) countLabel.textContent = '0'; + + if (!templateId) { + return; + } + + table.DataTable( + abp.libs.datatables.normalizeConfiguration({ + serverSide: false, + order: [[2, 'asc']], + searching: false, + paging: false, + select: false, + info: false, + scrollX: true, + scrollY: '80px', // ~2 rows visible before scrolling + scrollCollapse: true, + drawCallback: function () { + const count = this.api().rows().count(); + if (countLabel) countLabel.textContent = String(count); + section.classList.toggle('hidden-section', count === 0); + label?.classList.toggle('hidden-section', count === 0); + notifyAttachmentCount(count); + }, + ajax: abp.libs.datatables.createAjax( + unity.notifications.emails.emailLogAttachment.getListByTemplateId, + function () { return templateId; }, + function (result) { return { data: result }; } + ), + columnDefs: [ + { + title: '', + width: '40px', + className: 'text-center', + orderable: false, + render: function () { + return ''; + } + }, + { + title: 'Document Name', + data: 'fileName', + className: 'data-table-header text-break', + width: '55%' + }, + { + title: 'Date', + data: 'time', + className: 'data-table-header', + width: '130px', + render: function (data, type) { + if (type === 'display' || type === 'filter') { + return new Date(data).toDateString(); + } + return data; + } + }, + { + title: 'File Size', + data: 'fileSize', + className: 'data-table-header', + width: '90px', + render: function (data) { + if (data === null || data === undefined) return '—'; + const mb = data * 0.000001; + return mb >= 1 ? mb.toFixed(2) + ' MB' : (data / 1024).toFixed(0) + ' KB'; + } + } + ] + }) + ); + } + function showModal() { resetValidationState(); - ['templateSelect', 'triggerType', 'dateType', 'statusSelect', 'recipientCategory'].forEach(id => { + ['templateSelect', 'triggerType', 'dateType', 'moduleSelect', 'statusSelect', 'recipientCategory'].forEach(id => { document.getElementById(id).value = ''; }); + document.getElementById('statusSelect').disabled = true; // Clear the recipient select clearSelectedRecipients(); @@ -492,6 +675,7 @@ document.getElementById('eventOptions')?.classList.add('hidden-section'); document.getElementById('recipientOptions')?.classList.add('hidden-section'); renderTemplatePreview(document.getElementById('templatePreview'), null); + updateTemplateAttachments(''); const modalEl = document.getElementById('notificationModal'); if (modalEl === null) return; @@ -506,7 +690,7 @@ const requiredAlways = ['templateSelect', 'triggerType']; const requiredForDate = ['dateType', 'recipientCategory', 'recipientSelect']; - const requiredForEvent = ['statusSelect', 'recipientCategory', 'recipientSelect']; + const requiredForEvent = ['moduleSelect', 'statusSelect', 'recipientCategory', 'recipientSelect']; const fieldsToValidate = [ ...requiredAlways, @@ -571,9 +755,12 @@ console.warn('init() called again but already initialized, returning early'); return; } - - console.debug('init() starting'); - + + const modalEl = document.getElementById('notificationModal'); + if (modalEl && modalEl.parentElement !== document.body) { + document.body.appendChild(modalEl); + } + formId = document.getElementById('applicationFormId')?.value; if (!formId) { console.warn('formId not found, returning from init()'); @@ -585,10 +772,12 @@ configureSubmitOnlyValidation(); - const modalEl = document.getElementById('notificationModal'); if (modalEl) { // Always reset validation when modal is fully closed - modalEl.addEventListener('hidden.bs.modal', () => resetValidationState()); + modalEl.addEventListener('hidden.bs.modal', () => { + resetValidationState(); + updateTemplateAttachments(''); + }); // Also reset when modal starts opening modalEl.addEventListener('show.bs.modal', () => resetValidationState()); // Refresh select2 when modal is shown @@ -610,6 +799,9 @@ fetchTemplates().then(populateTemplates); initNotificationsTable(); + bindTabVisibilityResize(); + // In case the widget is already visible on load (not inside a hidden tab pane) + resizeNotificationsScrollBody(); // Attach save button listener (remove old one first to prevent duplicates) const saveBtn = document.getElementById('btn-save-notification'); @@ -628,7 +820,17 @@ e.target.classList.remove('is-invalid'); updatePreview(); }); - ['dateType', 'statusSelect'].forEach(id => { + document.getElementById('templateConfigurationLink')?.addEventListener('click', () => { + localStorage.setItem('ConfigurationManagement_ActiveMenu', 'notifications-menu-item'); + localStorage.setItem('notifications-active-tab', 'nav-template-tab'); + const templateId = document.getElementById('templateSelect')?.value; + if (templateId) { + localStorage.setItem('notifications-template-to-select', templateId); + } else { + localStorage.removeItem('notifications-template-to-select'); + } + }); + ['dateType', 'moduleSelect', 'statusSelect'].forEach(id => { document.getElementById(id)?.addEventListener('change', (e) => { e.target.classList.remove('is-invalid'); }); @@ -651,6 +853,13 @@ dateOptionsEl?.classList.add('hidden-section'); eventOptionsEl?.classList.remove('hidden-section'); recipientOptionsEl?.classList.remove('hidden-section'); + const moduleSelect = document.getElementById('moduleSelect'); + const statusSelect = document.getElementById('statusSelect'); + if (moduleSelect?.value) { + loadStatusesForModule(moduleSelect.value); + } else if (statusSelect) { + statusSelect.disabled = true; + } } else { dateOptionsEl?.classList.add('hidden-section'); eventOptionsEl?.classList.add('hidden-section'); @@ -660,6 +869,14 @@ e.target.classList.remove('is-invalid'); }); + document.getElementById('moduleSelect')?.addEventListener('change', (e) => { + e.target.classList.remove('is-invalid'); + loadStatusesForModule(e.target.value).catch(err => { + console.error('Failed to load module statuses', err); + abp.notify.error('Failed to load status triggers'); + }); + }); + document.getElementById('recipientCategory')?.addEventListener('change', (e) => { const cat = e.target.value; e.target.classList.remove('is-invalid'); @@ -693,19 +910,22 @@ const templateId = (document.getElementById('templateSelect').value || '').trim(); const dateType = document.getElementById('dateType').value; - const applicationStatusId = document.getElementById('statusSelect')?.value; + const module = document.getElementById('moduleSelect')?.value; + const statusValue = document.getElementById('statusSelect')?.value; const recipientCategory = document.getElementById('recipientCategory')?.value; // Collect multiple selected recipients as comma-separated string const recipientIdentifier = getSelectedRecipients().join(','); - const resolvedStatusId = triggerType === 'Event' ? (applicationStatusId || null) : null; + const resolvedStatusId = triggerType === 'Event' && module === 'Application' ? (statusValue || null) : null; const bodyObj = { templateId: templateId, triggerType: triggerType, + module: triggerType === 'Event' ? module : null, dateType: triggerType === 'Date' ? dateType : null, applicationStatusId: resolvedStatusId, + eventStatus: triggerType === 'Event' && module === 'Payment' ? (statusValue || null) : null, recipientCategory: recipientCategory, recipientIdentifier: recipientIdentifier }; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Notifications.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Notifications.js index 1e647c841a..84842f10ed 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Notifications.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Notifications.js @@ -127,12 +127,14 @@ function handleTemplatesList(list) { const sel = document.getElementById('cf_template'); sel.innerHTML = ''; - list.forEach(t => { + [...list] + .sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: 'base' })) + .forEach(t => { const opt = document.createElement('option'); opt.value = String(t.id); opt.text = `${t.name} — ${t.subject}`; sel.appendChild(opt); - }); + }); updatePreview(); return list; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.json b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.json index ec48801416..4a3b8af562 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.json +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.json @@ -157,41 +157,9 @@ "Logging": { "EnablePromptFileLog": false }, - "Operations": { - "Defaults": { - "Provider": "OpenAI", - "Profile": "Gpt5Mini", - "PromptVersion": "v1", - "ExecutionMode": "Sequential" - }, - "AttachmentSummary": { - "MaxCompletionTokens": 2000 - }, - "ApplicationAnalysis": { - "MaxCompletionTokens": 4000 - }, - "ApplicationScoring": { - "MaxCompletionTokens": 8000 - } - }, "OpenAI": { "ApiKey": "", - "Endpoint": "", - "Profiles": { - "Gpt4oMini": { - "DeploymentName": "gpt-4o-mini", - "MaxOutputTokenCountSupported": true, - "Temperature": 0.3 - }, - "Gpt5Mini": { - "DeploymentName": "gpt-5-mini", - "MaxOutputTokenCountSupported": false - }, - "Gpt5Nano": { - "DeploymentName": "gpt-5-nano", - "MaxOutputTokenCountSupported": false - } - } + "Endpoint": "" }, "UNITY_GITHUB_PAT": "" } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/wwwroot/js/formConfiguration/Notifications.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/wwwroot/js/formConfiguration/Notifications.js index 811d084201..4361b778f1 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/wwwroot/js/formConfiguration/Notifications.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/wwwroot/js/formConfiguration/Notifications.js @@ -149,12 +149,14 @@ function handleTemplatesList(list) { const sel = document.getElementById('cf_template'); sel.innerHTML = ''; - list.forEach(t => { + [...list] + .sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: 'base' })) + .forEach(t => { const opt = document.createElement('option'); opt.value = String(t.id); opt.text = `${t.name} — ${t.subject}`; sel.appendChild(opt); - }); + }); updatePreview(); return list; } diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/DataSeed/AIModelDataSeederTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/DataSeed/AIModelDataSeederTests.cs index f0a66345d1..c4a9ed510b 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/DataSeed/AIModelDataSeederTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/DataSeed/AIModelDataSeederTests.cs @@ -16,18 +16,21 @@ namespace Unity.GrantManager.AI.DataSeed; public class AIModelDataSeederTests { [Fact] - public async Task Should_Seed_All_Configured_Profiles() + public async Task Should_Seed_All_Configured_Models() { var modelRepository = Substitute.For>(); var insertedModels = new List(); modelRepository - .FirstOrDefaultAsync(Arg.Any>>()) - .Returns((AIModel?)null); + .GetListAsync( + Arg.Any>>(), + cancellationToken: Arg.Any()) + .Returns(Task.FromResult(new List())); modelRepository .InsertAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(callInfo => { var model = callInfo.Arg(); + ArgumentNullException.ThrowIfNull(model); insertedModels.Add(model); return Task.FromResult(model); }); @@ -37,13 +40,13 @@ public async Task Should_Seed_All_Configured_Profiles() await seeder.SeedAsync(new DataSeedContext()); insertedModels.Count.ShouldBe(3); - insertedModels.ShouldContain(model => model.Name == "Gpt4oMini" && model.IsActive); - insertedModels.ShouldContain(model => model.Name == "Gpt5Mini" && model.IsActive); - insertedModels.ShouldContain(model => model.Name == "Gpt5Nano" && model.IsActive); + insertedModels.ShouldContain(model => model.Name == "gpt-4o-mini" && model.Provider == "OpenAI" && model.IsActive); + insertedModels.ShouldContain(model => model.Name == "gpt-5-mini" && model.Provider == "OpenAI" && model.IsActive); + insertedModels.ShouldContain(model => model.Name == "gpt-5-nano" && model.Provider == "OpenAI" && model.IsActive); - var gpt4oMini = insertedModels.Single(model => model.Name == "Gpt4oMini"); - var gpt5Mini = insertedModels.Single(model => model.Name == "Gpt5Mini"); - var gpt5Nano = insertedModels.Single(model => model.Name == "Gpt5Nano"); + var gpt4oMini = insertedModels.Single(model => model.Name == "gpt-4o-mini"); + var gpt5Mini = insertedModels.Single(model => model.Name == "gpt-5-mini"); + var gpt5Nano = insertedModels.Single(model => model.Name == "gpt-5-nano"); DeserializeSettings(gpt4oMini.SettingsJson).MaxOutputTokenCountSupported.ShouldBeTrue(); DeserializeSettings(gpt4oMini.SettingsJson).Temperature.ShouldBe(0.3); @@ -53,6 +56,42 @@ public async Task Should_Seed_All_Configured_Profiles() DeserializeSettings(gpt5Nano.SettingsJson).Temperature.ShouldBeNull(); } + [Fact] + public async Task Should_Update_Existing_Model_When_Provider_Is_Stale() + { + var modelRepository = Substitute.For>(); + var existingModel = new AIModel(Guid.NewGuid(), "gpt-5-mini", "LegacyProvider") + { + IsActive = false, + SettingsJson = "{}" + }; + modelRepository + .GetListAsync( + Arg.Any>>(), + cancellationToken: Arg.Any()) + .Returns(callInfo => + { + var predicateExpression = callInfo + .Arg>>(); + ArgumentNullException.ThrowIfNull(predicateExpression); + var predicate = predicateExpression.Compile(); + return Task.FromResult(new[] { existingModel }.Where(predicate).ToList()); + }); + + var seeder = new AIModelDataSeeder(modelRepository); + + await seeder.SeedAsync(new DataSeedContext()); + + existingModel.Provider.ShouldBe("OpenAI"); + existingModel.IsActive.ShouldBeTrue(); + DeserializeSettings(existingModel.SettingsJson).MaxOutputTokenCountSupported.ShouldBeFalse(); + await modelRepository.Received(1).UpdateAsync(existingModel, autoSave: true); + await modelRepository.DidNotReceive().InsertAsync( + Arg.Is(model => model != null && model.Name == existingModel.Name), + Arg.Any(), + Arg.Any()); + } + private static AIModelSettings DeserializeSettings(string settingsJson) { var settings = JsonSerializer.Deserialize(settingsJson); diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/DataSeed/AIPromptDataSeederTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/DataSeed/AIPromptDataSeederTests.cs new file mode 100644 index 0000000000..9594190aa4 --- /dev/null +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/DataSeed/AIPromptDataSeederTests.cs @@ -0,0 +1,116 @@ +using NSubstitute; +using Shouldly; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Unity.AI.DataSeed; +using Unity.AI.Domain; +using Unity.AI.Runtime.Prompts; +using Volo.Abp.Data; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.MultiTenancy; +using Xunit; + +namespace Unity.GrantManager.AI.DataSeed; + +public class AIPromptDataSeederTests +{ + [Fact] + public async Task Should_Seed_The_Complete_BuiltIn_Prompt_Matrix() + { + var promptRepository = Substitute.For>(); + var insertedPrompts = new List(); + promptRepository + .FirstOrDefaultAsync(Arg.Any>>()) + .Returns((AIPrompt?)null); + promptRepository + .InsertAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .ReturnsForAnyArgs(callInfo => + { + var prompt = callInfo.Arg(); + ArgumentNullException.ThrowIfNull(prompt); + insertedPrompts.Add(prompt); + return Task.FromResult(prompt); + }); + + var currentTenant = Substitute.For(); + currentTenant.Change(null).Returns(Substitute.For()); + var seeder = new AIPromptDataSeeder(promptRepository, currentTenant); + + await seeder.SeedAsync(new DataSeedContext()); + + insertedPrompts.Count.ShouldBe(12); + AssertVersions(insertedPrompts, AIPromptTypes.ApplicationAnalysis, 0, 1, 2); + AssertVersions(insertedPrompts, AIPromptTypes.AttachmentSummary, 0, 1, 2); + AssertVersions(insertedPrompts, AIPromptTypes.ApplicationScoring, 0, 1, 2); + AssertVersions(insertedPrompts, AIPromptTypes.FormMapping, 2); + AssertVersions(insertedPrompts, AIPromptTypes.FormWorksheet, 2); + AssertVersions(insertedPrompts, AIPromptTypes.FormScoresheet, 2); + insertedPrompts.All(prompt => + prompt.TenantId is null && + prompt.IsActive && + !string.IsNullOrWhiteSpace(prompt.SystemPrompt) && + !string.IsNullOrWhiteSpace(prompt.UserPrompt)).ShouldBeTrue(); + + var worksheetPrompt = insertedPrompts.Single(prompt => prompt.Name == AIPromptTypes.FormWorksheet); + worksheetPrompt.UserPrompt.ShouldContain("all applicable field suggestions"); + worksheetPrompt.UserPrompt.ShouldContain("empty fields array"); + } + + + [Fact] + public async Task Should_Update_Existing_Prompt_To_Current_Definition() + { + var existingPrompt = new AIPrompt(Guid.NewGuid(), AIPromptTypes.FormWorksheet, 2, "old system prompt", "old user prompt") + { + MetadataJson = "old metadata", + IsActive = false + }; + var promptName = existingPrompt.Name; + var promptVersion = existingPrompt.VersionNumber; + var promptRepository = Substitute.For>(); + promptRepository + .FirstOrDefaultAsync(Arg.Any>>()) + .ReturnsForAnyArgs(callInfo => + { + var predicate = callInfo.Arg>>().Compile(); + var probe = new AIPrompt(Guid.NewGuid(), promptName, promptVersion, string.Empty, string.Empty); + return Task.FromResult(predicate(probe) ? existingPrompt : null!); + }); + promptRepository + .FirstOrDefaultAsync(Arg.Any>>(), Arg.Any()) + .ReturnsForAnyArgs(callInfo => + { + var predicate = callInfo.Arg>>().Compile(); + var probe = new AIPrompt(Guid.NewGuid(), promptName, promptVersion, string.Empty, string.Empty); + return Task.FromResult(predicate(probe) ? existingPrompt : null!); + }); + promptRepository + .UpdateAsync(Arg.Any(), true, Arg.Any()) + .ReturnsForAnyArgs(callInfo => Task.FromResult(callInfo.Arg())); + + var currentTenant = Substitute.For(); + currentTenant.Change(null).Returns(Substitute.For()); + var seeder = new AIPromptDataSeeder(promptRepository, currentTenant); + await seeder.SeedAsync(new DataSeedContext()); + + existingPrompt.SystemPrompt.ShouldNotBe("old system prompt"); + existingPrompt.UserPrompt.ShouldContain("WORKSHEET"); + existingPrompt.MetadataJson.ShouldContain("DATA"); + existingPrompt.IsActive.ShouldBeTrue(); + await promptRepository.Received().UpdateAsync(existingPrompt, true, Arg.Any()); + } + private static void AssertVersions( + IEnumerable prompts, + string promptName, + params int[] expectedVersions) + { + prompts + .Where(prompt => prompt.Name == promptName) + .Select(prompt => prompt.VersionNumber) + .OrderBy(version => version) + .ShouldBe(expectedVersions); + } +} diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/GenerateFormWorksheetJobTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/GenerateFormWorksheetJobTests.cs index 96a8975302..557345369f 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/GenerateFormWorksheetJobTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/GenerateFormWorksheetJobTests.cs @@ -2,6 +2,7 @@ using System.Linq; using Shouldly; using Unity.Flex.Worksheets; +using Unity.Flex.Domain.Worksheets; using Unity.GrantManager.GrantApplications.Automation.Operations.FormWorksheet; using Xunit; @@ -66,4 +67,16 @@ public void BuildWorksheet_Should_Create_One_SuggestedFields_Section_With_Defaul field.Order.ShouldBe(1u); field.Definition.ShouldContain("maxLength"); } + + [Fact] + public void EnsureCanonicalSuggestionWorksheetState_Should_Reject_Published_Worksheet() + { + var worksheet = new Worksheet(Guid.NewGuid(), "ai-form-worksheet", "AI Worksheet"); + worksheet.SetPublished(true); + + var exception = Should.Throw(() => + FormWorksheetOperationExecutor.EnsureCanonicalSuggestionWorksheetState(worksheet)); + + exception.Message.ShouldContain("canonical AI suggestion worksheet is published"); + } } diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/ApplicantProfile/AppServices/ApplicantContactAppServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/ApplicantProfile/AppServices/ApplicantContactAppServiceTests.cs index cb89efdba1..fb4f54f65e 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/ApplicantProfile/AppServices/ApplicantContactAppServiceTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/ApplicantProfile/AppServices/ApplicantContactAppServiceTests.cs @@ -90,7 +90,8 @@ await _contactManager.Received(1).UpdateAsync( applicantId, contactId, Arg.Is(ci => - ci.Name == input.Name + ci != null + && ci.Name == input.Name && ci.Title == input.Title && ci.Email == input.Email && ci.HomePhoneNumber == null diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/SubmissionInfoDataProviderTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/SubmissionInfoDataProviderTests.cs index effd117a15..281e5dba69 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/SubmissionInfoDataProviderTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/SubmissionInfoDataProviderTests.cs @@ -117,13 +117,24 @@ private static ApplicationStatus CreateStatus(Guid id, string externalStatus, st return entity; } - private static ApplicationForm CreateForm(Guid id, string formName) + private static ApplicationForm CreateForm(Guid id, string formName, Action? configure = null) { var entity = new ApplicationForm { ApplicationFormName = formName }; EntityHelper.TrySetId(entity, () => id); + configure?.Invoke(entity); return entity; } + private static ExternalLink CreateExternalLink( + ExternalLinkType type, bool published, int order = -1, string uri = "https://example.com") + => new() + { + Uri = uri, + ExternalLinkType = type, + Published = published, + Order = order + }; + [Fact] public async Task GetDataAsync_ShouldChangeTenant() { @@ -504,5 +515,232 @@ public async Task GetDataAsync_ShouldFallBackToCreationTimeWhenSubmissionIsNullO var dto = result.ShouldBeOfType(); dto.Submissions[0].SubmissionTime.ShouldBe(creationTime); } + + [Fact] + public async Task GetDataAsync_ShouldReturnRenewalLink_WhenEligibleAndPublished() + { + // Arrange + var request = CreateRequest(); + var applicationId = Guid.NewGuid(); + var formId = Guid.NewGuid(); + var statusId = Guid.NewGuid(); + + SetupQueryables( + [CreateSubmission(applicationId, "TESTUSER")], + [CreateApplication(applicationId, statusId, a => + { + a.ApplicationFormId = formId; + a.EligibleForRenewal = true; + })], + [CreateForm(formId, "Form", f => f.ExternalLinksConfig = new ExternalLinksConfig + { + Links = [CreateExternalLink(ExternalLinkType.Renewal, published: true, uri: "https://renewal.example.com")] + })], + [CreateStatus(statusId, "Submitted")]); + + // Act + var result = await _provider.GetDataAsync(request); + + // Assert + var dto = result.ShouldBeOfType(); + dto.Submissions[0].RenewalLink.ShouldNotBeNull(); + dto.Submissions[0].RenewalLink!.Uri.ShouldBe("https://renewal.example.com"); + } + + [Fact] + public async Task GetDataAsync_ShouldNotReturnRenewalLink_WhenNotEligibleForRenewal() + { + // Arrange + var request = CreateRequest(); + var applicationId = Guid.NewGuid(); + var formId = Guid.NewGuid(); + var statusId = Guid.NewGuid(); + + SetupQueryables( + [CreateSubmission(applicationId, "TESTUSER")], + [CreateApplication(applicationId, statusId, a => + { + a.ApplicationFormId = formId; + a.EligibleForRenewal = false; + })], + [CreateForm(formId, "Form", f => f.ExternalLinksConfig = new ExternalLinksConfig + { + Links = [CreateExternalLink(ExternalLinkType.Renewal, published: true)] + })], + [CreateStatus(statusId, "Submitted")]); + + // Act + var result = await _provider.GetDataAsync(request); + + // Assert + var dto = result.ShouldBeOfType(); + dto.Submissions[0].RenewalLink.ShouldBeNull(); + } + + [Fact] + public async Task GetDataAsync_ShouldNotReturnRenewalLink_WhenUnpublished() + { + // Arrange + var request = CreateRequest(); + var applicationId = Guid.NewGuid(); + var formId = Guid.NewGuid(); + var statusId = Guid.NewGuid(); + + SetupQueryables( + [CreateSubmission(applicationId, "TESTUSER")], + [CreateApplication(applicationId, statusId, a => + { + a.ApplicationFormId = formId; + a.EligibleForRenewal = true; + })], + [CreateForm(formId, "Form", f => f.ExternalLinksConfig = new ExternalLinksConfig + { + Links = [CreateExternalLink(ExternalLinkType.Renewal, published: false)] + })], + [CreateStatus(statusId, "Submitted")]); + + // Act + var result = await _provider.GetDataAsync(request); + + // Assert + var dto = result.ShouldBeOfType(); + dto.Submissions[0].RenewalLink.ShouldBeNull(); + } + + [Fact] + public async Task GetDataAsync_ShouldExcludeUnpublishedRelatedLinks() + { + // Arrange + var request = CreateRequest(); + var applicationId = Guid.NewGuid(); + var formId = Guid.NewGuid(); + var statusId = Guid.NewGuid(); + + SetupQueryables( + [CreateSubmission(applicationId, "TESTUSER")], + [CreateApplication(applicationId, statusId, a => a.ApplicationFormId = formId)], + [CreateForm(formId, "Form", f => f.ExternalLinksConfig = new ExternalLinksConfig + { + Links = + [ + CreateExternalLink(ExternalLinkType.Related, published: true, order: 1, uri: "https://published.example.com"), + CreateExternalLink(ExternalLinkType.Related, published: false, order: 2, uri: "https://unpublished.example.com") + ] + })], + [CreateStatus(statusId, "Submitted")]); + + // Act + var result = await _provider.GetDataAsync(request); + + // Assert + var dto = result.ShouldBeOfType(); + dto.Submissions[0].RelatedLinks.Count.ShouldBe(1); + dto.Submissions[0].RelatedLinks[0].Uri.ShouldBe("https://published.example.com"); + } + + [Fact] + public async Task GetDataAsync_ShouldReturnRelatedLinksInConfiguredOrder() + { + // Arrange + var request = CreateRequest(); + var applicationId = Guid.NewGuid(); + var formId = Guid.NewGuid(); + var statusId = Guid.NewGuid(); + + SetupQueryables( + [CreateSubmission(applicationId, "TESTUSER")], + [CreateApplication(applicationId, statusId, a => a.ApplicationFormId = formId)], + [CreateForm(formId, "Form", f => f.ExternalLinksConfig = new ExternalLinksConfig + { + Links = + [ + CreateExternalLink(ExternalLinkType.Related, published: true, order: 2, uri: "https://second.example.com"), + CreateExternalLink(ExternalLinkType.Related, published: true, order: 0, uri: "https://first.example.com"), + CreateExternalLink(ExternalLinkType.Related, published: true, order: 1, uri: "https://middle.example.com") + ] + })], + [CreateStatus(statusId, "Submitted")]); + + // Act + var result = await _provider.GetDataAsync(request); + + // Assert + var dto = result.ShouldBeOfType(); + var relatedLinks = dto.Submissions[0].RelatedLinks; + relatedLinks.Count.ShouldBe(3); + relatedLinks[0].Uri.ShouldBe("https://first.example.com"); + relatedLinks[1].Uri.ShouldBe("https://middle.example.com"); + relatedLinks[2].Uri.ShouldBe("https://second.example.com"); + } + + [Fact] + public async Task GetDataAsync_ShouldOrderUnorderedRelatedLinksLast() + { + // Arrange + var request = CreateRequest(); + var applicationId = Guid.NewGuid(); + var formId = Guid.NewGuid(); + var statusId = Guid.NewGuid(); + + SetupQueryables( + [CreateSubmission(applicationId, "TESTUSER")], + [CreateApplication(applicationId, statusId, a => a.ApplicationFormId = formId)], + [CreateForm(formId, "Form", f => f.ExternalLinksConfig = new ExternalLinksConfig + { + Links = + [ + CreateExternalLink(ExternalLinkType.Related, published: true, order: -1, uri: "https://unordered.example.com"), + CreateExternalLink(ExternalLinkType.Related, published: true, order: 0, uri: "https://ordered.example.com") + ] + })], + [CreateStatus(statusId, "Submitted")]); + + // Act + var result = await _provider.GetDataAsync(request); + + // Assert + var dto = result.ShouldBeOfType(); + var relatedLinks = dto.Submissions[0].RelatedLinks; + relatedLinks.Count.ShouldBe(2); + relatedLinks[0].Uri.ShouldBe("https://ordered.example.com"); + relatedLinks[1].Uri.ShouldBe("https://unordered.example.com"); + } + + [Fact] + public async Task GetDataAsync_ShouldNotMixRenewalAndRelatedLinks() + { + // Arrange + var request = CreateRequest(); + var applicationId = Guid.NewGuid(); + var formId = Guid.NewGuid(); + var statusId = Guid.NewGuid(); + + SetupQueryables( + [CreateSubmission(applicationId, "TESTUSER")], + [CreateApplication(applicationId, statusId, a => + { + a.ApplicationFormId = formId; + a.EligibleForRenewal = true; + })], + [CreateForm(formId, "Form", f => f.ExternalLinksConfig = new ExternalLinksConfig + { + Links = + [ + CreateExternalLink(ExternalLinkType.Renewal, published: true, uri: "https://renewal.example.com"), + CreateExternalLink(ExternalLinkType.Related, published: true, uri: "https://related.example.com") + ] + })], + [CreateStatus(statusId, "Submitted")]); + + // Act + var result = await _provider.GetDataAsync(request); + + // Assert + var dto = result.ShouldBeOfType(); + dto.Submissions[0].RenewalLink.ShouldNotBeNull(); + dto.Submissions[0].RenewalLink!.Uri.ShouldBe("https://renewal.example.com"); + dto.Submissions[0].RelatedLinks.Count.ShouldBe(1); + dto.Submissions[0].RelatedLinks[0].Uri.ShouldBe("https://related.example.com"); + } } } diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/ApplicationForms/ApplicationFormVersionAppServiceMappingReviewTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/ApplicationForms/ApplicationFormVersionAppServiceMappingReviewTests.cs new file mode 100644 index 0000000000..c036525370 --- /dev/null +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/ApplicationForms/ApplicationFormVersionAppServiceMappingReviewTests.cs @@ -0,0 +1,312 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using NSubstitute; +using Microsoft.Extensions.Localization; +using Unity.AI.Localization; +using Shouldly; +using Unity.AI.Generation; +using Unity.AI.Operations; +using Unity.AI.Settings; +using Unity.Flex.Domain.Worksheets; +using Unity.Flex.Domain.WorksheetLinks; +using Unity.Flex.Worksheets; +using Unity.Flex.Domain.Scoresheets; +using Unity.GrantManager.ApplicationForms.Mapping; +using Unity.GrantManager.Applications; +using Unity.GrantManager.Forms; +using Unity.GrantManager.Intakes; +using Unity.GrantManager.Integrations.Chefs; +using Unity.GrantManager.Reporting.FieldGenerators; +using Unity.GrantManager.GrantApplications.Automation.Operations.FormMapping; +using Unity.Modules.Shared.Correlation; +using Volo.Abp; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Uow; +using Xunit; +using Xunit.Abstractions; + +namespace Unity.GrantManager.ApplicationForms; + +public class ApplicationFormVersionAppServiceMappingReviewTests(ITestOutputHelper outputHelper) + : GrantManagerApplicationTestBase(outputHelper) +{ + [Fact] + public async Task AcceptMappingSuggestionsAsync_Should_Apply_Selected_Suggestions_And_Keep_Remaining() + { + var formVersionId = Guid.NewGuid(); + var selectedSuggestionId = Guid.NewGuid(); + var remainingSuggestionId = Guid.NewGuid(); + var formVersion = new ApplicationFormVersion { SubmissionHeaderMapping = "{}" }; + var repository = Substitute.For>(); + repository.GetAsync(formVersionId).Returns(formVersion); + var review = CreateReview(formVersionId, [ + new FormMappingSuggestionDto + { + Id = selectedSuggestionId, + SourceField = "ChefsProjectName", + TargetField = "ProjectName" + }, + new FormMappingSuggestionDto + { + Id = remainingSuggestionId, + SourceField = "ChefsAmount", + TargetField = "RequestedAmount" + } + ]); + var reviewRepository = Substitute.For(); + reviewRepository.FindLatestByOperationAndFormVersionAsync( + AIGenerationOperations.FormMapping, + formVersionId) + .Returns(review); + var service = CreateService(repository, reviewRepository); + + var result = await service.AcceptMappingSuggestionsAsync(formVersionId, new AcceptMappingSuggestionsDto + { + SuggestionIds = [selectedSuggestionId] + }); + + result.SubmissionHeaderMapping.ShouldContain("ProjectName"); + formVersion.SubmissionHeaderMapping.ShouldContain("ProjectName"); + formVersion.SubmissionHeaderMapping.ShouldContain("ChefsProjectName"); + var payload = JsonSerializer.Deserialize(review.ReviewData)!; + payload.PendingSuggestions.Select(suggestion => suggestion.Id).ShouldBe([remainingSuggestionId]); + await repository.Received(1).UpdateAsync(formVersion, true); + await reviewRepository.Received(1).UpdateAsync(review, true); + } + + [Fact] + public async Task AcceptMappingSuggestionsAsync_Should_Leave_State_Unchanged_When_A_Suggestion_Is_Stale() + { + var formVersionId = Guid.NewGuid(); + var repository = Substitute.For>(); + var review = CreateReview(formVersionId, [new FormMappingSuggestionDto { Id = Guid.NewGuid() }]); + var reviewRepository = Substitute.For(); + reviewRepository.FindLatestByOperationAndFormVersionAsync( + AIGenerationOperations.FormMapping, + formVersionId) + .Returns(review); + var service = CreateService(repository, reviewRepository); + + await Should.ThrowAsync(async () => + { + await service.AcceptMappingSuggestionsAsync( + formVersionId, + new AcceptMappingSuggestionsDto { SuggestionIds = [Guid.NewGuid()] }); + }); + + await repository.DidNotReceive().UpdateAsync(Arg.Any(), Arg.Any()); + await reviewRepository.DidNotReceive().UpdateAsync(Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task AcceptMappingSuggestionsAsync_Should_Replace_Source_And_Move_Conflicting_Target_In_Final_Review() + { + var formVersionId = Guid.NewGuid(); + var suggestionId = Guid.NewGuid(); + var formVersion = new ApplicationFormVersion + { + SubmissionHeaderMapping = "{\"TargetA\":\"SourceA\",\"TargetB\":\"SourceB\"}" + }; + var repository = Substitute.For>(); + repository.GetAsync(formVersionId).Returns(formVersion); + var review = new GenerationReview(Guid.NewGuid(), AIGenerationOperations.FormMapping, formVersionId, sequence: 2); + review.SetReviewData(JsonSerializer.Serialize(new FormMappingReviewPayload + { + PendingSuggestions = + [ + new FormMappingSuggestionDto + { + Id = suggestionId, + SourceField = "SourceA", + TargetField = "TargetB" + } + ] + })); + var reviewRepository = Substitute.For(); + reviewRepository.FindLatestByOperationAndFormVersionAsync(AIGenerationOperations.FormMapping, formVersionId) + .Returns(review); + var service = CreateService(repository, reviewRepository); + + var result = await service.AcceptMappingSuggestionsAsync(formVersionId, new AcceptMappingSuggestionsDto + { + SuggestionIds = [suggestionId] + }); + + result.SubmissionHeaderMapping.ShouldBe("{\"TargetB\":\"SourceA\"}"); + } + + [Fact] + public void ClassifyFinalSuggestions_Should_Remove_Unchanged_And_Describe_Changes() + { + var suggestions = FormMappingOperationExecutor.ClassifyFinalSuggestions( + "{\"TargetA\":\"SourceA\",\"TargetB\":\"SourceB\"}", + [ + new FormMappingSuggestionDto { SourceField = "SourceA", TargetField = "TargetA" }, + new FormMappingSuggestionDto { SourceField = "SourceA", TargetField = "TargetB" }, + new FormMappingSuggestionDto { SourceField = "SourceC", TargetField = "TargetC" } + ], + out var unchangedCount); + + unchangedCount.ShouldBe(1); + suggestions.Count.ShouldBe(2); + suggestions[0].ChangeType.ShouldBe("Changed"); + suggestions[0].PreviousTargetField.ShouldBe("TargetA"); + suggestions[0].ConflictSourceField.ShouldBe("SourceB"); + suggestions[1].ChangeType.ShouldBe("New"); + } + + [Fact] + public async Task FinalizeMappingReviewAsync_Should_Allow_Final_Mapping_When_One_Of_Multiple_Drafts_Is_Published_And_Assigned() + { + var formVersionId = Guid.NewGuid(); + var formVersion = new ApplicationFormVersion { ApplicationFormId = Guid.NewGuid() }; + var assignedDraft = new Worksheet(Guid.NewGuid(), "ai-assigned", "Assigned AI worksheet"); + assignedDraft.SetPublished(true); + var unassignedDraft = new Worksheet(Guid.NewGuid(), "ai-unassigned", "Unassigned AI worksheet"); + var mappingReview = new GenerationReview(Guid.NewGuid(), AIGenerationOperations.FormMapping, formVersionId); + mappingReview.Complete(); + var worksheetReview = new GenerationReview(Guid.NewGuid(), AIGenerationOperations.FormWorksheet, formVersionId); + worksheetReview.Complete(); + worksheetReview.SetReviewData(JsonSerializer.Serialize(new FormWorksheetReviewPayload + { + DraftWorksheetIds = [assignedDraft.Id, unassignedDraft.Id] + })); + + var repository = Substitute.For>(); + repository.GetAsync(formVersionId).Returns(formVersion); + var generationService = Substitute.For(); + var reviewRepository = Substitute.For(); + reviewRepository.FindLatestByOperationAndFormVersionAsync(AIGenerationOperations.FormMapping, formVersionId) + .Returns(mappingReview); + reviewRepository.FindLatestByOperationAndFormVersionAsync(AIGenerationOperations.FormWorksheet, formVersionId) + .Returns(worksheetReview); + var worksheetRepository = Substitute.For(); + worksheetRepository.FindAsync(assignedDraft.Id).Returns(assignedDraft); + worksheetRepository.FindAsync(unassignedDraft.Id).Returns(unassignedDraft); + var worksheetLinkRepository = Substitute.For(); + worksheetLinkRepository.GetListByCorrelationAsync(formVersionId, CorrelationConsts.FormVersion) + .Returns([ + new WorksheetLink(Guid.NewGuid(), assignedDraft.Id, formVersionId, CorrelationConsts.FormVersion, string.Empty) + ]); + var service = CreateService(repository, reviewRepository, generationService, worksheetRepository, worksheetLinkRepository); + + await service.FinalizeMappingReviewAsync(formVersionId); + + mappingReview.Status.ShouldBe(GenerationReviewStatus.Completed); + await generationService.Received(1).SubmitAsync( + AIGenerationOperations.FormMapping, + Arg.Is(submission => + submission != null && + submission.ApplicationFormVersionId == formVersionId && + submission.ApplicationId == formVersion.ApplicationFormId)); + } + + [Fact] + public async Task GetMappingReviewAsync_Should_Complete_When_No_Worksheet_Suggestions_Were_Generated() + { + var formVersionId = Guid.NewGuid(); + var mappingReview = new GenerationReview(Guid.NewGuid(), AIGenerationOperations.FormMapping, formVersionId); + mappingReview.Complete(); + var worksheetReview = new GenerationReview(Guid.NewGuid(), AIGenerationOperations.FormWorksheet, formVersionId); + worksheetReview.SetReviewData(JsonSerializer.Serialize(new FormWorksheetReviewPayload + { + NoSuggestionsGenerated = true + })); + worksheetReview.Complete(); + + var repository = Substitute.For>(); + var reviewRepository = Substitute.For(); + reviewRepository.FindLatestByOperationAndFormVersionAsync(AIGenerationOperations.FormMapping, formVersionId) + .Returns(mappingReview); + reviewRepository.FindLatestByOperationAndFormVersionAsync(AIGenerationOperations.FormWorksheet, formVersionId) + .Returns(worksheetReview); + var service = CreateService(repository, reviewRepository); + + var result = await service.GetMappingReviewAsync(formVersionId); + + result.State.ShouldBe(FormGenerationWorkflowState.Completed.ToString()); + result.Action.ShouldBe(FormGenerationWorkflowAction.GenerateMapping.ToString()); + result.CanGenerateFinalMapping.ShouldBeFalse(); + } + + [Fact] + public async Task FinalizeMappingReviewAsync_Should_Reject_When_No_Worksheet_Suggestions_Were_Generated() + { + var formVersionId = Guid.NewGuid(); + var formVersion = new ApplicationFormVersion { ApplicationFormId = Guid.NewGuid() }; + var mappingReview = new GenerationReview(Guid.NewGuid(), AIGenerationOperations.FormMapping, formVersionId); + mappingReview.Complete(); + var worksheetReview = new GenerationReview(Guid.NewGuid(), AIGenerationOperations.FormWorksheet, formVersionId); + worksheetReview.SetReviewData(JsonSerializer.Serialize(new FormWorksheetReviewPayload + { + NoSuggestionsGenerated = true + })); + worksheetReview.Complete(); + + var repository = Substitute.For>(); + repository.GetAsync(formVersionId).Returns(formVersion); + var generationService = Substitute.For(); + var reviewRepository = Substitute.For(); + reviewRepository.FindLatestByOperationAndFormVersionAsync(AIGenerationOperations.FormMapping, formVersionId) + .Returns(mappingReview); + reviewRepository.FindLatestByOperationAndFormVersionAsync(AIGenerationOperations.FormWorksheet, formVersionId) + .Returns(worksheetReview); + var service = CreateService(repository, reviewRepository, generationService); + + await Should.ThrowAsync(() => service.FinalizeMappingReviewAsync(formVersionId)); + + await generationService.DidNotReceive().SubmitAsync( + Arg.Any(), Arg.Any()); + } + + private static GenerationReview CreateReview( + Guid formVersionId, + System.Collections.Generic.List suggestions) + { + var review = new GenerationReview( + Guid.NewGuid(), + AIGenerationOperations.FormMapping, + formVersionId); + review.SetReviewData(JsonSerializer.Serialize(new FormMappingReviewPayload + { + PendingSuggestions = suggestions + })); + return review; + } + + private ApplicationFormVersionAppService CreateService( + IRepository repository, + IGenerationReviewRepository reviewRepository, + IAIGenerationAppService? generationService = null, + IWorksheetRepository? worksheetRepository = null, + IWorksheetLinkRepository? worksheetLinkRepository = null) + { + var featureChecker = Substitute.For(); + featureChecker.IsEnabledAsync(Arg.Any()).Returns(true); + var localizer = Substitute.For>(); + var service = new ApplicationFormVersionAppService( + repository, + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + featureChecker, + new AIFeatureGuard(featureChecker, localizer), + localizer, + generationService ?? Substitute.For(), + worksheetRepository ?? Substitute.For(), + Substitute.For>(), + reviewRepository, + worksheetLinkRepository ?? Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For()); + service.LazyServiceProvider = GetRequiredService(); + return service; + } +} diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/ApplicationForms/ApplicationFormVersionAppServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/ApplicationForms/ApplicationFormVersionAppServiceTests.cs index d37a007bc9..2289018d11 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/ApplicationForms/ApplicationFormVersionAppServiceTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/ApplicationForms/ApplicationFormVersionAppServiceTests.cs @@ -1,4 +1,4 @@ -using NSubstitute; +using NSubstitute; using Shouldly; using System; using System.Collections.Generic; @@ -8,12 +8,16 @@ using System.Threading.Tasks; using Unity.AI; using Unity.AI.Features; +using Unity.AI.Localization; +using Unity.AI.Settings; +using Microsoft.Extensions.Localization; using Unity.AI.Generation; using Unity.AI.Operations; using Unity.AI.Requests; using Unity.AI.Responses; using Unity.Flex.Domain.Worksheets; using Unity.Flex.Worksheets; +using Unity.Flex.Domain.Scoresheets; using Unity.GrantManager.ApplicationForms; using Unity.GrantManager.ApplicationForms.Mapping; using Unity.GrantManager.Applications; @@ -61,6 +65,23 @@ await generationService.Received(1).SubmitAsync( && request.ApplicationFormVersionId == formVersionId)); } + [Fact] + public async Task GenerateMappingAsync_Should_Reject_When_Review_Is_Active() + { + var formVersionId = Guid.NewGuid(); + var repository = Substitute.For>(); + repository.GetAsync(formVersionId).Returns(new ApplicationFormVersion { ApplicationFormId = Guid.NewGuid() }); + var generationService = Substitute.For(); + var reviewRepository = Substitute.For(); + reviewRepository.FindLatestByOperationAndFormVersionAsync(AIGenerationOperations.FormMapping, formVersionId) + .Returns(new GenerationReview(Guid.NewGuid(), AIGenerationOperations.FormMapping, formVersionId)); + var service = CreateService(repository, generationService, generationReviewRepository: reviewRepository); + service.LazyServiceProvider = GetRequiredService(); + + await Should.ThrowAsync(() => service.GenerateMappingAsync(formVersionId)); + await generationService.DidNotReceive().SubmitAsync(Arg.Any(), Arg.Any()); + } + [Fact] public void FormMappingPromptData_Should_UseEmptyObject_When_NoExistingMappingIsAvailable() { @@ -69,6 +90,38 @@ public void FormMappingPromptData_Should_UseEmptyObject_When_NoExistingMappingIs promptData.GetProperty("existingMapping").GetRawText().ShouldBe("{}"); } + [Fact] + public void FormMappingPromptData_Should_Include_Linked_Worksheet_Fields() + { + var promptData = FormMappingPromptDataBuilder.Build(new ApplicationFormMappingReadModelDto + { + Worksheets = + [ + new WorksheetMappingFieldsDto + { + WorksheetName = "Project details", + Fields = + [ + new MappingFieldDto + { + Name = "custom_project_name", + Label = "Project name", + Type = "String", + IsCustom = true + } + ] + } + ] + }); + + promptData.GetProperty("unityData") + .GetProperty("customFields")[0] + .GetProperty("Fields")[0] + .GetProperty("Name") + .GetString() + .ShouldBe("custom_project_name"); + } + [Fact] public async Task GetPendingAiWorksheetAsync_Should_Return_Unpublished_Worksheet_Fields() { @@ -83,12 +136,14 @@ public async Task GetPendingAiWorksheetAsync_Should_Return_Unpublished_Worksheet formVersionRepository.GetAsync(formVersionId).Returns(formVersion); var worksheetRepository = Substitute.For(); worksheetRepository.GetByNameAsync(Arg.Any(), true).Returns(worksheet); + var reviewRepository = CreateActiveWorksheetReviewRepository(formVersionId); var service = CreateService( Substitute.For>(), Substitute.For(), formVersionRepository, - worksheetRepository); + worksheetRepository, + generationReviewRepository: reviewRepository); service.LazyServiceProvider = GetRequiredService(); var result = await service.GetPendingAiWorksheetAsync(formVersionId); @@ -138,6 +193,7 @@ public async Task CreateAiWorksheetDraftAsync_Should_Create_Unlinked_Unpublished var worksheetRepository = Substitute.For(); worksheetRepository.GetByNameAsync(Arg.Any(), true).Returns(worksheet); var customFieldRepository = Substitute.For>(); + var reviewRepository = CreateActiveWorksheetReviewRepository(formVersionId); Worksheet? createdDraft = null; worksheetRepository.InsertAsync(Arg.Do(worksheet => createdDraft = worksheet), true) .Returns(Task.FromResult(null!)); @@ -147,7 +203,8 @@ public async Task CreateAiWorksheetDraftAsync_Should_Create_Unlinked_Unpublished Substitute.For(), formVersionRepository, worksheetRepository, - customFieldRepository); + customFieldRepository, + generationReviewRepository: reviewRepository); service.LazyServiceProvider = GetRequiredService(); var selectedFieldId = worksheet.Sections.Single().Fields.First().Id; @@ -189,6 +246,7 @@ public async Task CreateAiWorksheetDraftAsync_Should_Number_Internal_Name_And_De var worksheetRepository = Substitute.For(); worksheetRepository.GetByNameAsync(Arg.Any(), true).Returns(worksheet); var customFieldRepository = Substitute.For>(); + var reviewRepository = CreateActiveWorksheetReviewRepository(formVersionId); Worksheet? createdDraft = null; worksheetRepository.GetByNameAsync("ai-risk-review", false).Returns(new Worksheet(Guid.NewGuid(), "ai-risk-review", "Existing")); worksheetRepository.GetByNameAsync("ai-risk-review-2", false).Returns((Worksheet?)null); @@ -200,7 +258,8 @@ public async Task CreateAiWorksheetDraftAsync_Should_Number_Internal_Name_And_De Substitute.For(), formVersionRepository, worksheetRepository, - customFieldRepository); + customFieldRepository, + generationReviewRepository: reviewRepository); service.LazyServiceProvider = GetRequiredService(); await service.CreateAiWorksheetDraftAsync(formVersionId, new CreateAiWorksheetDraftDto @@ -216,6 +275,60 @@ public async Task CreateAiWorksheetDraftAsync_Should_Number_Internal_Name_And_De await worksheetRepository.Received(1).DeleteAsync(worksheet, true); } + [Fact] + public async Task DiscardAiScoresheetSuggestionsAsync_Should_Preserve_Scoresheet_With_Instances() + { + var formVersionId = Guid.NewGuid(); + var formId = Guid.NewGuid(); + var formVersionRepository = Substitute.For(); + formVersionRepository.GetAsync(formVersionId).Returns(new ApplicationFormVersion { ApplicationFormId = formId }); + var scoresheet = new Scoresheet(Guid.NewGuid(), "AI Scoresheet", $"ai-form-{formId}-version-{formVersionId}-scoresheet"); + scoresheet.Instances.Add(new Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance(Guid.NewGuid(), scoresheet.Id, Guid.NewGuid(), "FormVersion")); + var scoresheetRepository = Substitute.For(); + scoresheetRepository.GetByNameAsync(Arg.Any(), true).Returns(scoresheet); + var reviewRepository = Substitute.For(); + var scoresheetInstanceRepository = Substitute.For(); + scoresheetInstanceRepository.AnyByScoresheetAsync(scoresheet.Id).Returns(true); + var review = new GenerationReview(Guid.NewGuid(), AIGenerationOperations.FormScoresheet, formVersionId); + reviewRepository.FindLatestByOperationAndFormVersionAsync(AIGenerationOperations.FormScoresheet, formVersionId).Returns(review); + + var service = CreateService(Substitute.For>(), Substitute.For(), formVersionRepository, scoresheetRepository: scoresheetRepository, generationReviewRepository: reviewRepository, scoresheetInstanceRepository: scoresheetInstanceRepository); + service.LazyServiceProvider = GetRequiredService(); + + await Should.ThrowAsync(() => service.DiscardAiScoresheetSuggestionsAsync(formVersionId)); + await scoresheetRepository.DidNotReceive().DeleteAsync(Arg.Any(), Arg.Any()); + } + [Theory] + [InlineData(GenerationReviewStatus.Completed)] + [InlineData(GenerationReviewStatus.Discarded)] + public async Task GetPendingAiWorksheetAsync_Should_Return_Null_When_Review_Is_Not_Active(GenerationReviewStatus status) + { + var formVersionId = Guid.NewGuid(); + var formId = Guid.NewGuid(); + var formVersion = new ApplicationFormVersion { ApplicationFormId = formId }; + var worksheet = BuildAiWorksheet(formId, formVersionId, published: false); + var formVersionRepository = Substitute.For(); + formVersionRepository.GetAsync(formVersionId).Returns(formVersion); + var worksheetRepository = Substitute.For(); + worksheetRepository.GetByNameAsync(Arg.Any(), true).Returns(worksheet); + var reviewRepository = CreateActiveWorksheetReviewRepository(formVersionId); + var review = await reviewRepository.FindLatestByOperationAndFormVersionAsync( + AIGenerationOperations.FormWorksheet, formVersionId); + review!.SetStatus(status); + + var service = CreateService( + Substitute.For>(), + Substitute.For(), + formVersionRepository, + worksheetRepository, + generationReviewRepository: reviewRepository); + service.LazyServiceProvider = GetRequiredService(); + + var result = await service.GetPendingAiWorksheetAsync(formVersionId); + + result.ShouldBeNull(); + } + private static Worksheet BuildAiWorksheet(Guid formId, Guid formVersionId, bool published, int fieldCount = 1) { var worksheet = new Worksheet( @@ -309,9 +422,14 @@ private static ApplicationFormVersionAppService CreateService( IAIGenerationAppService aiGenerationAppService, IApplicationFormVersionRepository? formVersionRepository = null, IWorksheetRepository? worksheetRepository = null, - IRepository? customFieldRepository = null) + IRepository? customFieldRepository = null, + IGenerationReviewRepository? generationReviewRepository = null, + IScoresheetRepository? scoresheetRepository = null, + Unity.Flex.Domain.ScoresheetInstances.IScoresheetInstanceRepository? scoresheetInstanceRepository = null) { var featureChecker = Substitute.For(); + featureChecker.IsEnabledAsync(Arg.Any()).Returns(true); + var localizer = Substitute.For>(); var service = new ApplicationFormVersionAppService( repository, Substitute.For(), @@ -321,9 +439,30 @@ private static ApplicationFormVersionAppService CreateService( Substitute.For(), Substitute.For(), featureChecker, + new AIFeatureGuard(featureChecker, localizer), + localizer, aiGenerationAppService, worksheetRepository ?? Substitute.For(), - customFieldRepository ?? Substitute.For>()); + customFieldRepository ?? Substitute.For>(), + generationReviewRepository ?? Substitute.For(), + Substitute.For(), + scoresheetRepository ?? Substitute.For(), + Substitute.For(), + scoresheetInstanceRepository ?? Substitute.For()); return service; } + + private static IGenerationReviewRepository CreateActiveWorksheetReviewRepository(Guid formVersionId) + { + var repository = Substitute.For(); + var review = new GenerationReview( + Guid.NewGuid(), + AIGenerationOperations.FormWorksheet, + formVersionId); + repository.FindLatestByOperationAndFormVersionAsync( + AIGenerationOperations.FormWorksheet, + formVersionId) + .Returns(review); + return repository; + } } diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/ApplicationForms/ExternalLinksConfigValidationTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/ApplicationForms/ExternalLinksConfigValidationTests.cs new file mode 100644 index 0000000000..906e533265 --- /dev/null +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/ApplicationForms/ExternalLinksConfigValidationTests.cs @@ -0,0 +1,157 @@ +using Shouldly; +using System; +using System.Linq; +using System.Threading.Tasks; +using Unity.GrantManager.ApplicantProfile; +using Unity.GrantManager.Applications; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Validation; +using Xunit; +using Xunit.Abstractions; + +namespace Unity.GrantManager.ApplicationForms; + +public class ExternalLinksConfigValidationTests : GrantManagerApplicationTestBase +{ + private readonly IApplicationFormAppService _applicationFormAppService; + private readonly IRepository _applicationFormRepository; + + public ExternalLinksConfigValidationTests(ITestOutputHelper outputHelper) : base(outputHelper) + { + _applicationFormAppService = GetRequiredService(); + _applicationFormRepository = GetRequiredService>(); + } + + [Fact] + public async Task PatchExternalLinksConfigAsync_ShouldSaveValidRenewalLinkAndRelatedLinks() + { + await _applicationFormAppService.PatchExternalLinksConfigAsync( + GrantManagerTestData.ApplicationForm1_Id, + new ExternalLinksConfigDto + { + RenewalLink = new ExternalLinkConfigDto + { + Uri = "https://chefs-test.apps.silver.devops.gov.bc.ca/app/form", + Title = "Renew Now", + Description = "Please renew before the deadline.", + Published = true, + ExternalLinkType = ExternalLinkType.Renewal + }, + RelatedLinks = + [ + new ExternalLinkConfigDto + { + Uri = "https://chefs-test.apps.silver.devops.gov.bc.ca/app/related-1", + Title = "Related One", + Description = "First related link.", + Published = true, + ExternalLinkType = ExternalLinkType.Related + }, + new ExternalLinkConfigDto + { + Uri = "https://chefs-test.apps.silver.devops.gov.bc.ca/app/related-2", + Title = "Related Two", + Description = "Second related link.", + Published = false, + ExternalLinkType = ExternalLinkType.Related + } + ], + ApplicantMessage = "Renewal message for applicants." + }); + + var form = await _applicationFormRepository.GetAsync(GrantManagerTestData.ApplicationForm1_Id); + + form.ExternalLinksConfig.Links.Count.ShouldBe(3); + form.ExternalLinksConfig.ApplicantMessage.ShouldBe("Renewal message for applicants."); + var renewalLink = form.ExternalLinksConfig.Links.Single(l => l.ExternalLinkType == ExternalLinkType.Renewal); + renewalLink.Uri.ShouldBe("https://chefs-test.apps.silver.devops.gov.bc.ca/app/form"); + renewalLink.Title.ShouldBe("Renew Now"); + renewalLink.Description.ShouldBe("Please renew before the deadline."); + renewalLink.Published.ShouldBeTrue(); + + var relatedLinks = form.ExternalLinksConfig.Links + .Where(l => l.ExternalLinkType == ExternalLinkType.Related) + .OrderBy(l => l.Order) + .ToList(); + relatedLinks.Count.ShouldBe(2); + relatedLinks[0].Title.ShouldBe("Related One"); + relatedLinks[0].Order.ShouldBe(-1); + relatedLinks[1].Title.ShouldBe("Related Two"); + relatedLinks[1].Order.ShouldBe(-1); + } + + [Fact] + public async Task PatchExternalLinksConfigAsync_ShouldReplaceRelatedLinks_OnSubsequentSave() + { + await _applicationFormAppService.PatchExternalLinksConfigAsync( + GrantManagerTestData.ApplicationForm1_Id, + new ExternalLinksConfigDto + { + RelatedLinks = + [ + new ExternalLinkConfigDto { Uri = "https://chefs-test.apps.silver.devops.gov.bc.ca/app/first" } + ] + }); + + await _applicationFormAppService.PatchExternalLinksConfigAsync( + GrantManagerTestData.ApplicationForm1_Id, + new ExternalLinksConfigDto + { + RelatedLinks = + [ + new ExternalLinkConfigDto { Uri = "https://chefs-test.apps.silver.devops.gov.bc.ca/app/second" }, + new ExternalLinkConfigDto { Uri = "https://chefs-test.apps.silver.devops.gov.bc.ca/app/third" } + ] + }); + + var form = await _applicationFormRepository.GetAsync(GrantManagerTestData.ApplicationForm1_Id); + var relatedLinks = form.ExternalLinksConfig.Links.Where(l => l.ExternalLinkType == ExternalLinkType.Related).ToList(); + + relatedLinks.Count.ShouldBe(2); + relatedLinks.ShouldNotContain(l => l.Uri.EndsWith("first", StringComparison.Ordinal)); + } + + [Fact] + public async Task PatchExternalLinksConfigAsync_ShouldReject_WhenRenewalLinkVisibleWithoutUri() + { + await Should.ThrowAsync( + _applicationFormAppService.PatchExternalLinksConfigAsync( + GrantManagerTestData.ApplicationForm1_Id, + new ExternalLinksConfigDto + { + RenewalLink = new ExternalLinkConfigDto + { + Uri = string.Empty, + Published = true + } + })); + } + + [Fact] + public async Task PatchExternalLinksConfigAsync_ShouldReject_WhenExceedingMaxRelatedLinks() + { + var relatedLinks = Enumerable.Range(1, ApplicationForm.MaxRelatedExternalLinks + 1) + .Select(i => new ExternalLinkConfigDto { Uri = $"https://chefs-test.apps.silver.devops.gov.bc.ca/app/link-{i}" }) + .ToList(); + + await Should.ThrowAsync( + _applicationFormAppService.PatchExternalLinksConfigAsync( + GrantManagerTestData.ApplicationForm1_Id, + new ExternalLinksConfigDto { RelatedLinks = relatedLinks })); + } + + [Fact] + public async Task PatchExternalLinksConfigAsync_ShouldReject_WhenUriIsScriptScheme() + { + var dto = new ExternalLinksConfigDto + { + RenewalLink = new ExternalLinkConfigDto + { + Uri = "javascript:alert(1)" + } + }; + + await Should.ThrowAsync( + _applicationFormAppService.PatchExternalLinksConfigAsync(GrantManagerTestData.ApplicationForm1_Id, dto)); + } +} diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactAppServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactAppServiceTests.cs index e375badf33..42c3501e75 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactAppServiceTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactAppServiceTests.cs @@ -300,7 +300,8 @@ await _contactManager.Received(1).CreateAsync( input.RelatedEntityType, input.RelatedEntityId, Arg.Is(ci => - ci.Name == input.Name + ci != null + && ci.Name == input.Name && ci.Title == input.Title && ci.Email == input.Email && ci.HomePhoneNumber == input.HomePhoneNumber @@ -391,7 +392,8 @@ await _contactManager.Received(1).UpdateAsync( entityId, contactId, Arg.Is(ci => - ci.Name == input.Name + ci != null + && ci.Name == input.Name && ci.Title == input.Title && ci.Email == input.Email && ci.HomePhoneNumber == input.HomePhoneNumber diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/Generation/AIGenerationQueueTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/Generation/AIGenerationQueueTests.cs index 5a4775bc3e..b4fb8e5f13 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/Generation/AIGenerationQueueTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/Generation/AIGenerationQueueTests.cs @@ -106,7 +106,9 @@ private static IBackgroundJobManager CreateBackgroundJobManager(List()) .Returns(callInfo => { - jobs.Add(callInfo.Arg()); + var job = callInfo.Arg(); + ArgumentNullException.ThrowIfNull(job); + jobs.Add(job); return Task.FromResult(string.Empty); }); return backgroundJobManager; @@ -141,11 +143,21 @@ private static ApplicationGenerationQueue CreateQueue( asyncQueryableExecuter.FirstOrDefaultAsync( Arg.Any>(), Arg.Any()) - .Returns(callInfo => Task.FromResult(callInfo.Arg>().FirstOrDefault())); + .Returns(callInfo => + { + var operations = callInfo.Arg>(); + ArgumentNullException.ThrowIfNull(operations); + return Task.FromResult(operations.FirstOrDefault()); + }); asyncQueryableExecuter.FirstOrDefaultAsync( Arg.Any>(), Arg.Any()) - .Returns(callInfo => Task.FromResult(callInfo.Arg>().FirstOrDefault())); + .Returns(callInfo => + { + var requests = callInfo.Arg>(); + ArgumentNullException.ThrowIfNull(requests); + return Task.FromResult(requests.FirstOrDefault()); + }); return new ApplicationGenerationQueue( backgroundJobManager, @@ -221,4 +233,4 @@ public ValueTask DisposeAsync() return ValueTask.CompletedTask; } } -} \ No newline at end of file +} diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Operations/AIExecutionModeResolverTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Operations/AIExecutionModeResolverTests.cs index 84a67f2af3..b49e567b32 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Operations/AIExecutionModeResolverTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Operations/AIExecutionModeResolverTests.cs @@ -1,9 +1,14 @@ -using Microsoft.Extensions.Configuration; +using NSubstitute; using Shouldly; using System; -using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using Unity.AI.Domain; using Unity.AI.Operations; using Unity.AI.Runtime.Prompts; +using Volo.Abp.Domain.Repositories; using Xunit; namespace Unity.GrantManager.AI.Operations; @@ -11,48 +16,45 @@ namespace Unity.GrantManager.AI.Operations; public class AIExecutionModeResolverTests { [Fact] - public void ResolveMode_Uses_Operation_Override_Before_Default() + public async Task ResolveMode_Uses_Persisted_Operation_Mode() { - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary + var repository = Substitute.For>(); + repository.GetListAsync( + Arg.Any>>(), + cancellationToken: Arg.Any()) + .Returns(callInfo => { - ["Azure:Operations:Defaults:ExecutionMode"] = "Parallel", - [$"Azure:Operations:{AIPromptTypes.ApplicationScoring}:ExecutionMode"] = "Batch" - }) - .Build(); - - var resolver = new AIExecutionModeResolver(configuration); - - resolver.ResolveMode(AIPromptTypes.ApplicationScoring).ShouldBe(AIExecutionMode.Batch); - resolver.ResolveMode(AIPromptTypes.AttachmentSummary).ShouldBe(AIExecutionMode.Parallel); + var predicateExpression = callInfo.Arg>>(); + ArgumentNullException.ThrowIfNull(predicateExpression); + var predicate = predicateExpression.Compile(); + return Task.FromResult(new[] + { + new AIOperation(Guid.NewGuid(), AIPromptTypes.ApplicationScoring, Guid.NewGuid()) + { + ExecutionMode = AIExecutionMode.Batch, + IsActive = true + } + }.Where(predicate).ToList()); + }); + + var resolver = new AIExecutionModeResolver(repository); + + (await resolver.ResolveModeAsync(AIPromptTypes.ApplicationScoring)).ShouldBe(AIExecutionMode.Batch); } [Fact] - public void ResolveMode_Should_Throw_When_Default_Is_Missing() + public async Task ResolveMode_Throws_When_Operation_Is_Missing() { - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary()) - .Build(); - - var resolver = new AIExecutionModeResolver(configuration); - - var ex = Should.Throw(() => resolver.ResolveMode(AIPromptTypes.AttachmentSummary)); - ex.Message.ShouldContain(AIPromptTypes.AttachmentSummary); - } - - [Fact] - public void ResolveMode_Should_Throw_When_Configured_Value_Is_Invalid() - { - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - { - ["Azure:Operations:Defaults:ExecutionMode"] = "Fast" - }) - .Build(); + var repository = Substitute.For>(); + repository.GetListAsync( + Arg.Any>>(), + cancellationToken: Arg.Any()) + .Returns(Task.FromResult(new System.Collections.Generic.List())); - var resolver = new AIExecutionModeResolver(configuration); + var resolver = new AIExecutionModeResolver(repository); - var ex = Should.Throw(() => resolver.ResolveMode(AIPromptTypes.AttachmentSummary)); - ex.Message.ShouldContain(AIPromptTypes.AttachmentSummary); + var exception = await Should.ThrowAsync( + () => resolver.ResolveModeAsync(AIPromptTypes.AttachmentSummary)); + exception.Message.ShouldContain(AIPromptTypes.AttachmentSummary); } } diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Operations/ApplicationScoringServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Operations/ApplicationScoringServiceTests.cs index 82f758a5d7..a3e154d85e 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Operations/ApplicationScoringServiceTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Operations/ApplicationScoringServiceTests.cs @@ -1,4 +1,3 @@ -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; using Shouldly; @@ -9,10 +8,13 @@ using System.Threading; using System.Threading.Tasks; using Unity.AI; +using Unity.AI.Domain; using Unity.AI.Models; using Unity.AI.Operations; using Unity.AI.Requests; using Unity.AI.Responses; +using Unity.AI.Runtime.Prompts; +using Volo.Abp.Domain.Repositories; using Xunit; namespace Unity.GrantManager.AI.Operations; @@ -27,16 +29,28 @@ public async Task RegenerateAsync_Sequential_Mode_Uses_Per_Section_Requests() aiService.GenerateApplicationScoringAsync(Arg.Do(request => capturedRequests.Add(request)), Arg.Any()) .Returns(new ApplicationScoringResponse()); - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary + var operationRepository = Substitute.For>(); + operationRepository.GetListAsync( + Arg.Any>>(), + cancellationToken: Arg.Any()) + .Returns(callInfo => { - ["Azure:Operations:Defaults:ExecutionMode"] = "Sequential" - }) - .Build(); + var predicateExpression = callInfo.Arg>>(); + ArgumentNullException.ThrowIfNull(predicateExpression); + var predicate = predicateExpression.Compile(); + return Task.FromResult(new[] + { + new AIOperation(Guid.NewGuid(), AIPromptTypes.ApplicationScoring, Guid.NewGuid()) + { + ExecutionMode = AIExecutionMode.Sequential, + IsActive = true + } + }.Where(predicate).ToList()); + }); var service = new ApplicationScoringService( aiService, - new AIExecutionModeResolver(configuration), + new AIExecutionModeResolver(operationRepository), NullLogger.Instance); var result = await service.RegenerateAsync(new ApplicationScoringOperationInputDto diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Runtime/Execution/AIProviderPayloadValidatorTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Runtime/Execution/AIProviderPayloadValidatorTests.cs index fbb0b5a392..b63570adb1 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Runtime/Execution/AIProviderPayloadValidatorTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Runtime/Execution/AIProviderPayloadValidatorTests.cs @@ -52,7 +52,7 @@ public void ValidateApplicationScoringJson_Should_Return_InvalidOutput_When_Answ result.IsValid.ShouldBeFalse(); result.FailureCategory.ShouldBe(AIFailureCategory.InvalidOutput); - result.Reason.ShouldContain("q1"); + result.Reason!.ShouldContain("q1"); } [Fact] @@ -96,7 +96,7 @@ public void ValidateApplicationAnalysisJson_Should_Return_InvalidOutput_When_Dec result.IsValid.ShouldBeFalse(); result.FailureCategory.ShouldBe(AIFailureCategory.InvalidOutput); - result.Reason.ShouldContain("Expected 'PROCEED' or 'HOLD'"); + result.Reason!.ShouldContain("Expected 'PROCEED' or 'HOLD'"); } [Fact] @@ -223,6 +223,29 @@ public void ValidateFormScoresheetJson_Should_Allow_Empty_Optional_Reporting_Fie result.IsValid.ShouldBeTrue(); } + [Fact] + public void ValidateFormScoresheetJson_Should_Allow_Mixed_Property_Casing() + { + var response = ValidFormScoresheetJson + .Replace("\"Title\"", "\"title\"", StringComparison.Ordinal) + .Replace("\"Name\"", "\"name\"", StringComparison.Ordinal) + .Replace("\"Version\"", "\"version\"", StringComparison.Ordinal) + .Replace("\"Order\"", "\"order\"", StringComparison.Ordinal) + .Replace("\"Published\"", "\"published\"", StringComparison.Ordinal) + .Replace("\"Sections\"", "\"sections\"", StringComparison.Ordinal) + .Replace("\"Fields\"", "\"fields\"", StringComparison.Ordinal) + .Replace("\"ReportColumns\"", "\"reportColumns\"", StringComparison.Ordinal) + .Replace("\"ReportKeys\"", "\"reportKeys\"", StringComparison.Ordinal) + .Replace("\"ReportViewName\"", "\"reportViewName\"", StringComparison.Ordinal) + .Replace("\"Description\"", "\"description\"", StringComparison.Ordinal) + .Replace("\"Type\"", "\"type\"", StringComparison.Ordinal) + .Replace("\"Definition\"", "\"definition\"", StringComparison.Ordinal); + + var result = AIProviderPayloadValidator.ValidateFormScoresheetJson(response); + + result.IsValid.ShouldBeTrue(); + } + [Theory] [InlineData("\"Title\": \"Generated scoresheet\"", "\"Title\": \"\"")] [InlineData("\"Version\": 1", "\"Version\": \"1\"")] @@ -247,7 +270,7 @@ public void ValidateFormScoresheetJson_Should_Return_InvalidOutput_For_Duplicate var result = AIProviderPayloadValidator.ValidateFormScoresheetJson(response); result.IsValid.ShouldBeFalse(); - result.Reason.ShouldContain("duplicate field names"); + result.Reason!.ShouldContain("duplicate field names"); } [Fact] @@ -261,7 +284,7 @@ public void ValidateFormScoresheetJson_Should_Return_InvalidOutput_For_Duplicate var result = AIProviderPayloadValidator.ValidateFormScoresheetJson(response); result.IsValid.ShouldBeFalse(); - result.Reason.ShouldContain("duplicate section names"); + result.Reason!.ShouldContain("duplicate section names"); } private const string ValidFormScoresheetJson = """ diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Runtime/Execution/OpenAIConfigurationResolverTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Runtime/Execution/OpenAIConfigurationResolverTests.cs index 7872860802..1ae5919a2b 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Runtime/Execution/OpenAIConfigurationResolverTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Runtime/Execution/OpenAIConfigurationResolverTests.cs @@ -43,7 +43,7 @@ public async Task Should_Resolve_Operation_Strictly_From_Database() modelRepository .GetAsync(modelId, cancellationToken: Arg.Any()) - .Returns(new AIModel(modelId, "Gpt5Mini") + .Returns(new AIModel(modelId, "gpt-5-mini", "OpenAI") { IsActive = true, SettingsJson = JsonSerializer.Serialize(new AIModelSettings @@ -92,7 +92,6 @@ public async Task Should_Resolve_Operation_Strictly_From_Database() new Dictionary { ["Azure:OpenAI:ApiKey"] = "secret", - ["Azure:OpenAI:Profiles:Gpt5Mini:DeploymentName"] = "gpt-5-mini" }, modelRepository, operationRepository, @@ -101,7 +100,7 @@ public async Task Should_Resolve_Operation_Strictly_From_Database() var settings = await resolver.ResolveOperationSettingsAsync(AIPromptTypes.ApplicationAnalysis); settings.ProviderName.ShouldBe("OpenAI"); - settings.ProfileName.ShouldBe("Gpt5Mini"); + settings.ProfileName.ShouldBe("gpt-5-mini"); settings.Endpoint.ShouldBe(new Uri("https://example.test")); settings.DeploymentName.ShouldBe("gpt-5-mini"); settings.Temperature.ShouldBe(0.25); @@ -121,7 +120,7 @@ public async Task Should_Throw_When_Operation_Is_Missing() modelRepository .GetAsync(modelId, cancellationToken: Arg.Any()) - .Returns(new AIModel(modelId, "Gpt5Mini") + .Returns(new AIModel(modelId, "gpt-5-mini", "OpenAI") { IsActive = true, SettingsJson = JsonSerializer.Serialize(new AIModelSettings @@ -169,7 +168,6 @@ public async Task Should_Throw_When_Operation_Is_Missing() new Dictionary { ["Azure:OpenAI:ApiKey"] = "secret", - ["Azure:OpenAI:Profiles:Gpt5Mini:DeploymentName"] = "gpt-5-mini" }, modelRepository, operationRepository, @@ -193,7 +191,7 @@ public async Task Should_Resolve_Model_Values_Strictly_From_Database() ArgumentNullException.ThrowIfNull(predicate); return Task.FromResult(new List { - new(Guid.NewGuid(), "Gpt5Mini") + new(Guid.NewGuid(), "gpt-5-mini", "OpenAI") { IsActive = true, SettingsJson = JsonSerializer.Serialize(new AIModelSettings @@ -208,10 +206,10 @@ public async Task Should_Resolve_Model_Values_Strictly_From_Database() var resolver = CreateResolver(modelRepository: modelRepository); resolver.ResolveProviderName().ShouldBe("OpenAI"); - (await resolver.ResolveDeploymentNameAsync()).ShouldBe("gpt-5-mini"); - (await resolver.ResolveEndpointAsync()).ShouldBe(new Uri("https://example.test")); - (await resolver.ResolveConfiguredTemperatureAsync()).ShouldBe(0.35); - (await resolver.ResolveMaxOutputTokenCountSupportedAsync()).ShouldBeTrue(); + (await resolver.ResolveDeploymentNameAsync("gpt-5-mini")).ShouldBe("gpt-5-mini"); + (await resolver.ResolveEndpointAsync("gpt-5-mini")).ShouldBe(new Uri("https://example.test")); + (await resolver.ResolveConfiguredTemperatureAsync("gpt-5-mini")).ShouldBe(0.35); + (await resolver.ResolveMaxOutputTokenCountSupportedAsync("gpt-5-mini")).ShouldBeTrue(); } [Fact] @@ -226,7 +224,7 @@ public async Task Should_Resolve_ApiKey_From_Provider_Secret() ArgumentNullException.ThrowIfNull(predicate); return Task.FromResult(new List { - new(Guid.NewGuid(), "Default") + new(Guid.NewGuid(), "Default", "OpenAI") { IsActive = true, SettingsJson = JsonSerializer.Serialize(new AIModelSettings @@ -261,7 +259,7 @@ public async Task Should_Resolve_Operation_Settings_From_Host_Context() modelRepository .GetAsync(modelId, cancellationToken: Arg.Any()) - .Returns(new AIModel(modelId, "Gpt5Mini") + .Returns(new AIModel(modelId, "gpt-5-mini", "OpenAI") { IsActive = true, SettingsJson = JsonSerializer.Serialize(new AIModelSettings @@ -340,7 +338,7 @@ public async Task Should_Select_Newest_Tenant_Prompt_And_Isolate_It_From_Global_ var modelRepository = Substitute.For>(); modelRepository.GetAsync(modelId, cancellationToken: Arg.Any()) - .Returns(new AIModel(modelId, "Gpt5Mini") + .Returns(new AIModel(modelId, "gpt-5-mini", "OpenAI") { IsActive = true, SettingsJson = JsonSerializer.Serialize(new AIModelSettings()) @@ -400,12 +398,8 @@ private static OpenAIConfigurationResolver CreateResolver( { var configurationValues = new Dictionary { - ["Azure:Operations:Defaults:Provider"] = "OpenAI", - ["Azure:Operations:Defaults:Profile"] = "Gpt5Mini", ["Azure:OpenAI:Endpoint"] = "https://example.test", - ["Azure:OpenAI:ApiKey"] = "secret", - ["Azure:OpenAI:Profiles:Gpt5Mini:DeploymentName"] = "gpt-5-mini", - ["Azure:OpenAI:Profiles:Gpt5Mini:MaxOutputTokenCountSupported"] = "true" + ["Azure:OpenAI:ApiKey"] = "secret" }; if (values != null) diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Domain.Tests/ApplicationForms/ApplicationFormTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Domain.Tests/ApplicationForms/ApplicationFormTests.cs index 9675be6a74..228c0dd2b0 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Domain.Tests/ApplicationForms/ApplicationFormTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Domain.Tests/ApplicationForms/ApplicationFormTests.cs @@ -1,6 +1,10 @@ -using Unity.GrantManager.Applications; -using Xunit; +using Shouldly; +using System.Collections.Generic; +using Unity.GrantManager.ApplicantProfile; +using Unity.GrantManager.Applications; using Unity.GrantManager.GrantApplications; +using Volo.Abp; +using Xunit; namespace Unity.GrantManager.ApplicationForms { @@ -33,6 +37,72 @@ public void GetAvailableElectoralDistrictAddressTypesReturnsExpected() x => x.AddressType == AddressType.MailingAddress ); } + + [Fact] + public void SetExternalLinks_ShouldThrow_WhenRenewalLinkPublishedWithoutUri() + { + var form = new ApplicationForm(); + + var exception = Should.Throw(() => + form.SetExternalLinks( + new ExternalLink { Uri = string.Empty, Published = true }, + [])); + + exception.Code.ShouldBe(GrantManagerDomainErrorCodes.RenewalLinkRequiredForVisibility); + } + + [Fact] + public void SetExternalLinks_ShouldThrow_WhenRelatedLinkPublishedWithoutUri() + { + var form = new ApplicationForm(); + + var exception = Should.Throw(() => + form.SetExternalLinks( + null, + [new ExternalLink { Uri = string.Empty, Published = true }])); + + exception.Code.ShouldBe(GrantManagerDomainErrorCodes.RelatedLinkInvalidUri); + } + + [Fact] + public void SetExternalLinks_ShouldThrow_WhenExceedingMaxRelatedLinks() + { + var form = new ApplicationForm(); + var relatedLinks = new List(); + for (var i = 0; i <= ApplicationForm.MaxRelatedExternalLinks; i++) + { + relatedLinks.Add(new ExternalLink { Uri = $"https://example.com/{i}" }); + } + + var exception = Should.Throw(() => + form.SetExternalLinks(null, relatedLinks)); + + exception.Code.ShouldBe(GrantManagerDomainErrorCodes.TooManyRelatedLinks); + } + + [Fact] + public void SetExternalLinks_ShouldAssignOrderAndTypes_WhenValid() + { + var form = new ApplicationForm(); + + form.SetExternalLinks( + new ExternalLink { Uri = "https://example.com/renew", Published = true }, + [ + new ExternalLink { Uri = "https://example.com/one", Order = 2 }, + new ExternalLink { Uri = "https://example.com/two", Order = 1} + ], + "Please renew soon."); + + var links = form.ExternalLinksConfig.Links; + links.Count.ShouldBe(3); + links[0].ExternalLinkType.ShouldBe(ExternalLinkType.Renewal); + links[0].Order.ShouldBe(-1); + links[1].ExternalLinkType.ShouldBe(ExternalLinkType.Related); + links[1].Order.ShouldBe(2); + links[2].ExternalLinkType.ShouldBe(ExternalLinkType.Related); + links[2].Order.ShouldBe(1); + form.ExternalLinksConfig.ApplicantMessage.ShouldBe("Please renew soon."); + } } } diff --git a/documentation/reporting/reporting-configuration.md b/documentation/reporting/reporting-configuration.md index 9d607119b1..3e7e65bf8a 100644 --- a/documentation/reporting/reporting-configuration.md +++ b/documentation/reporting/reporting-configuration.md @@ -119,15 +119,19 @@ This ensures established mappings are preserved and only genuinely new fields re ## Field Column Definitions -The configuration table displays one row per mappable field from the source. The first three column headers change depending on the active provider tab; the remaining three are constant across all providers. +The configuration table displays one row per mappable field from the source. For the `worksheet` / `worksheet_consolidated` providers, **Worksheet Name** is the leftmost column. After that, the next three column headers change depending on the active provider tab; the remaining three are constant across all providers. -**Provider-specific headers (columns 1–3):** +**Worksheet-only header (leftmost column):** **Worksheet Name** — shown only for the `worksheet` / `worksheet_consolidated` providers; absent (and every other column shifts one position left) for `formversion` / `formversion_consolidated` / `scoresheet`. + +**Provider-specific headers (next three columns):** - `formversion` / `formversion_consolidated` — **CHEFS Label**, **CHEFS Property Name**, **CHEFS Type** - `worksheet` / `worksheet_consolidated` — **Worksheet Label**, **Worksheet Property Name**, **Worksheet Type** - `scoresheet` — **Scoresheet Label**, **Scoresheet Property Name**, **Scoresheet Type** -**Constant headers (columns 4–6):** **Path**, **Report Column**, **Type Path** +**Constant headers (following three columns):** **Path**, **Report Column**, **Type Path** + +**Source Order** — present for every provider as a trailing column, hidden by default (see below). ### Label *(CHEFS Label / Worksheet Label / Scoresheet Label)* @@ -184,6 +188,26 @@ Indicates which form versions contain this field. Only present when using `formv - `v1` — field exists only in version 1 - `v1, v3` — field exists in versions 1 and 3 but not in all versions +### Worksheet Name *(worksheet providers only)* + +The name of the source `Worksheet` this field came from, including its version suffix (e.g., `grant_application-v2`). Only present for the `worksheet` and `worksheet_consolidated` providers; blank for `formversion` and `scoresheet`. + +### Source Order + +A numeric column, hidden by default, giving each field's 1-based position in the provider's calculated overall field order. Present for every provider. Not user-editable — for saved configurations it reflects the value stored in the mapping (older mappings are backfilled from live provider metadata) and is not affected by saved Report Column values. Users can reveal it via the column picker to inspect the underlying order, or to manually re-sort back to it after sorting by another column. + +### Default Sort Order + +For the `worksheet` and `worksheet_consolidated` providers, rows are returned pre-sorted (and numbered via **Source Order**) by: + +1. **Worksheet Name**, A–Z (the version suffix means later versions naturally sort after earlier ones) +2. **Section order** within each worksheet +3. **Field layout order** within each section (top to bottom, left to right) +4. **Checkbox group option order** — when a single Checkbox Group field expands into one row per option, the rows follow the order the options are defined on the field (not alphabetical) +5. **Data grid column order** — when a single Data Grid field expands into one row per column, the rows follow the order the columns are defined on the grid. For a mixed dynamic/static grid, CHEFS-extracted dynamic columns are emitted first (in CHEFS's own order), followed by any additional statically-defined columns not already covered + +For the `worksheet` and `worksheet_consolidated` providers, the table's default sort is by **Source Order**, ascending, until the user explicitly sorts by another column, at which point the existing sort/column-visibility persistence takes over. Other providers (`formversion`, `formversion_consolidated`, `scoresheet`) continue to default-sort by Label, unchanged — their **Source Order** column is still populated (reflecting each provider's natural field order) and available via the column picker, but is not the default sort key. + --- ## Column Name Validation