From 40cc5dfe63feaae097d4191167130264a11d3648 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 8 May 2026 09:42:12 -0700 Subject: [PATCH 01/15] WIP add AI generation log context --- .../BackgroundJobs/AIGenerationLogScope.cs | 28 +++++++++++++++++++ .../GenerateApplicationAnalysisJob.cs | 9 ++++++ .../GenerateApplicationScoringJob.cs | 9 ++++++ .../GenerateAttachmentSummaryJob.cs | 9 ++++++ .../RunApplicationAIPipelineJob.cs | 9 ++++++ 5 files changed, 64 insertions(+) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/AIGenerationLogScope.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/AIGenerationLogScope.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/AIGenerationLogScope.cs new file mode 100644 index 0000000000..e5c3f5a97f --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/AIGenerationLogScope.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; + +namespace Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; + +internal static class AIGenerationLogScope +{ + public static IDisposable? Begin( + ILogger logger, + string operationType, + Guid applicationId, + Guid? tenantId, + string requestKey, + string? promptVersion, + Guid? requestedByUserId) + { + return logger.BeginScope(new Dictionary + { + ["AIOperationType"] = operationType, + ["AIApplicationId"] = applicationId, + ["AITenantId"] = tenantId, + ["AIGenerationRequestKey"] = requestKey, + ["AIPromptVersion"] = promptVersion, + ["AIRequestedByUserId"] = requestedByUserId + }); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisJob.cs index 35d3f554c5..e072da2f02 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisJob.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisJob.cs @@ -22,6 +22,15 @@ public class GenerateApplicationAnalysisJob( { public override async Task ExecuteAsync(GenerateApplicationAnalysisBackgroundJobArgs args) { + using var logScope = AIGenerationLogScope.Begin( + logger, + AIGenerationRequestKeyHelper.ApplicationAnalysisOperationType, + args.ApplicationId, + args.TenantId, + args.RequestKey, + args.PromptVersion, + args.RequestedByUserId); + using (currentTenant.Change(args.TenantId)) { await AIGenerationRequestJobHelper.MarkRunningInNewUowAsync(unitOfWorkManager, generationRequestRepository, args.RequestKey); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringJob.cs index 2292324a5e..8a1393ea19 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringJob.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringJob.cs @@ -24,6 +24,15 @@ public class GenerateApplicationScoringJob( { public override async Task ExecuteAsync(GenerateApplicationScoringBackgroundJobArgs args) { + using var logScope = AIGenerationLogScope.Begin( + logger, + AIGenerationRequestKeyHelper.ApplicationScoringOperationType, + args.ApplicationId, + args.TenantId, + args.RequestKey, + args.PromptVersion, + args.RequestedByUserId); + using (currentTenant.Change(args.TenantId)) { await AIGenerationRequestJobHelper.MarkRunningInNewUowAsync(unitOfWorkManager, generationRequestRepository, args.RequestKey); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryJob.cs index 63507cd588..91b17e38a2 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryJob.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryJob.cs @@ -22,6 +22,15 @@ public class GenerateAttachmentSummaryJob( { public override async Task ExecuteAsync(GenerateAttachmentSummaryBackgroundJobArgs args) { + using var logScope = AIGenerationLogScope.Begin( + logger, + AIGenerationRequestKeyHelper.AttachmentSummaryOperationType, + args.ApplicationId, + args.TenantId, + args.RequestKey, + args.PromptVersion, + args.RequestedByUserId); + using (currentTenant.Change(args.TenantId)) { await AIGenerationRequestJobHelper.MarkRunningInNewUowAsync(unitOfWorkManager, generationRequestRepository, args.RequestKey); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJob.cs index 2a46fc3530..20c4c1cc3a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJob.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJob.cs @@ -33,6 +33,15 @@ public class RunApplicationAIPipelineJob( { public override async Task ExecuteAsync(RunApplicationAIPipelineJobArgs args) { + using var logScope = AIGenerationLogScope.Begin( + logger, + AIGenerationRequestKeyHelper.PipelineOperationType, + args.ApplicationId, + args.TenantId, + args.RequestKey, + args.PromptVersion, + args.RequestedByUserId); + if (string.IsNullOrWhiteSpace(args.RequestKey)) { throw new ArgumentException("RequestKey is required.", nameof(args)); From 72872b34d4fc59f7e92d0d6e6e1d43b5b83a7767 Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Fri, 5 Jun 2026 20:12:41 -0700 Subject: [PATCH 02/15] AB#33221: Soft Delete Applicant --- ...ApplicationPermissionDefinitionProvider.cs | 1 + .../Applicants/ApplicantAppService.cs | 45 +- .../Applicants/ApplicantSupplierAppService.cs | 9 +- .../Applicants/IApplicantAppService.cs | 2 + .../Intakes/ApplicantLookupService.cs | 2 +- .../Localization/GrantManager/en.json | 1 + .../GrantApplicationPermissions.cs | 1 + .../Applications/Applicant.cs | 9 + .../Applications/IApplicationRepository.cs | 3 + ...6000444_AddApplicantSoftDelete.Designer.cs | 5046 +++++++++++++++++ .../20260606000444_AddApplicantSoftDelete.cs | 50 + .../GrantTenantDbContextModelSnapshot.cs | 9 + .../Repositories/ApplicantRepository.cs | 10 +- .../Repositories/ApplicationRepository.cs | 6 + .../Pages/Applicants/Details.cshtml | 17 +- .../Pages/Applicants/Details.cshtml.cs | 8 +- .../ApplicantsActionBar/Default.cshtml | 8 + .../Components/ApplicantsActionBar/Default.js | 47 +- .../ApplicantsActionBar/ListMerge.js | 3 +- 19 files changed, 5265 insertions(+), 12 deletions(-) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260606000444_AddApplicantSoftDelete.Designer.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260606000444_AddApplicantSoftDelete.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantApplications/GrantApplicationPermissionDefinitionProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantApplications/GrantApplicationPermissionDefinitionProvider.cs index 1b84ea5227..b9ef4f56e3 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantApplications/GrantApplicationPermissionDefinitionProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantApplications/GrantApplicationPermissionDefinitionProvider.cs @@ -47,6 +47,7 @@ public override void Define(IPermissionDefinitionContext context) applicatPermissions.AddChild(GrantApplicationPermissions.Applicants.ViewList, L("Permission:GrantApplicationManagement.Applicants.ViewList")); applicatPermissions.AddChild(GrantApplicationPermissions.Applicants.Edit, L("Permission:GrantApplicationManagement.Applicants.Edit")); applicatPermissions.AddChild(GrantApplicationPermissions.Applicants.AssignApplicant, L("Permission:GrantApplicationManagement.Applicants.AssignApplicant")); + applicatPermissions.AddChild(GrantApplicationPermissions.Applicants.Delete, L("Permission:GrantApplicationManagement.Applicants.Delete")); var applicantInfoPermissions = applicatPermissions.AddChild( GrantApplicationPermissions.Applicants.ApplicantInfoDefault, L("Permission:GrantApplicationManagement.Applicants.ApplicantInfo")); 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 0746a6744b..273a8c5f06 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantAppService.cs @@ -653,6 +653,15 @@ private async Task UpdateAddress(List applicantAddresses, Addr [RemoteService(true)] public async Task TransferApplicantApplicationsAsync(TransferApplicantApplicationsDto dto) { + var principal = await applicantRepository.GetAsync(dto.PrincipalApplicantId); + var nonPrincipal = await applicantRepository.GetAsync(dto.NonPrincipalApplicantId); + + if ((principal != null && principal.IsDeleted) || (nonPrincipal != null && nonPrincipal.IsDeleted)) + { + throw new UserFriendlyException( + "One or more selected applicants have been deleted. Please refresh the applicant list to update the view."); + } + var applications = await applicationRepository.GetByApplicantIdAsync(dto.NonPrincipalApplicantId); foreach (var application in applications) { @@ -669,6 +678,14 @@ public async Task SetDuplicatedAsync(SetApplicantDuplicateDto dto) { // Set principal as not duplicated var principal = await applicantRepository.GetAsync(dto.PrincipalApplicantId); + var nonPrincipal = await applicantRepository.GetAsync(dto.NonPrincipalApplicantId); + + if ((principal != null && principal.IsDeleted) || (nonPrincipal != null && nonPrincipal.IsDeleted)) + { + throw new UserFriendlyException( + "One or more selected applicants have been deleted. Please refresh the applicant list to update the view."); + } + if (principal != null && principal.IsDuplicated) { principal.IsDuplicated = false; @@ -676,7 +693,6 @@ public async Task SetDuplicatedAsync(SetApplicantDuplicateDto dto) } // Set non-principal as duplicated - var nonPrincipal = await applicantRepository.GetAsync(dto.NonPrincipalApplicantId); if (nonPrincipal != null && !nonPrincipal.IsDuplicated) { nonPrincipal.IsDuplicated = true; @@ -684,6 +700,30 @@ public async Task SetDuplicatedAsync(SetApplicantDuplicateDto dto) } } + [RemoteService(true)] + [Authorize(GrantApplicationPermissions.Applicants.Delete)] + public async Task HasSubmissionsAsync(Guid id) + { + return await applicationRepository.HasApplicationsByApplicantIdAsync(id); + } + + [RemoteService(true)] + [Authorize(GrantApplicationPermissions.Applicants.Delete)] + public async Task DeleteApplicantAsync(Guid id) + { + if (await applicationRepository.HasApplicationsByApplicantIdAsync(id)) + { + throw new UserFriendlyException( + "This applicant cannot be deleted because it is associated with one or more submissions."); + } + + var applicant = await applicantRepository.GetAsync(id); + applicant.IsDeleted = true; + applicant.DeletionTime = Clock.Now; + applicant.DeleterId = CurrentUser.Id; + await applicantRepository.UpdateAsync(applicant, autoSave: true); + } + private async Task UpdateApplicationFormSubmissionsAsync(Guid applicationId, Guid newApplicantId) { try @@ -733,6 +773,9 @@ public async Task> GetListAsync(ApplicantListRe { var query = await applicantRepository.GetQueryableAsync(); + // Exclude soft-deleted applicants from the list + query = query.Where(a => !a.IsDeleted); + // Apply default sorting (client-side DataTable handles search, sorting, and paging) query = query.OrderByDescending(a => a.CreationTime); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantSupplierAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantSupplierAppService.cs index b49bfd1371..daa5c3be42 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantSupplierAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantSupplierAppService.cs @@ -184,10 +184,17 @@ public async Task HandleSupplierAfterMergeAsync(HandleSupplierAfterMergeDto dto) await EnsureNoPendingPaymentsForApplicantAsync(dto.NonPrincipalId); var principal = await applicantRepository.GetAsync(dto.PrincipalId); + var nonPrincipal = await applicantRepository.GetAsync(dto.NonPrincipalId); + + if ((principal != null && principal.IsDeleted) || (nonPrincipal != null && nonPrincipal.IsDeleted)) + { + throw new UserFriendlyException( + "One or more selected applicants have been deleted. Please refresh the applicant list to update the view."); + } + principal.SupplierId = dto.SelectedSupplierId; await applicantRepository.UpdateAsync(principal); - var nonPrincipal = await applicantRepository.GetAsync(dto.NonPrincipalId); nonPrincipal.SupplierId = dto.SelectedSupplierId; await applicantRepository.UpdateAsync(nonPrincipal); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/IApplicantAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/IApplicantAppService.cs index 8a359cc45c..0ac2b2f453 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/IApplicantAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/IApplicantAppService.cs @@ -25,4 +25,6 @@ public interface IApplicantAppService : IApplicationService Task PartialUpdateApplicantSummaryAsync(Guid applicantId, PartialUpdateDto input); Task UpdateApplicantContactAddressesAsync(Guid applicantId, UpdateApplicantContactAddressesDto input); Task UpdateApplicantStatusAsync(Guid applicantId, string status); + Task DeleteApplicantAsync(Guid id); + Task HasSubmissionsAsync(Guid id); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/ApplicantLookupService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/ApplicantLookupService.cs index 33cd8db501..48bdcb431f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/ApplicantLookupService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/ApplicantLookupService.cs @@ -32,7 +32,7 @@ public async Task ApplicantLookupByApplicantId(string unityApplicantId) try { Applicant? applicant = await applicantRepository.GetByUnityApplicantIdAsync(unityApplicantId); - if (applicant == null) + if (applicant == null || applicant.IsDeleted) { throw new KeyNotFoundException("Applicant not found."); } 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 35d17e82b6..77d536ea19 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 @@ -144,6 +144,7 @@ "Permission:GrantApplicationManagement.Applicants.AssignApplicant": "Assign Applicant to an Application", "Permission:GrantApplicationManagement.Applicants.ApplicantInfo": "Applicant Info", "Permission:GrantApplicationManagement.Applicants.ApplicantInfo.EditRedStop": "Edit Red-Stop", + "Permission:GrantApplicationManagement.Applicants.Delete": "Delete Applicant", "Permission:GrantApplicationManagement.Assignments.Default": "Assignment", "Permission:GrantApplicationManagement.Assignments.AssignInitial": "Initial Assignment", "Permission:GrantApplicationManagement.Reviews.Default": "Reviews", diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/GrantApplicationPermissions.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/GrantApplicationPermissions.cs index 73afc90266..da6a031bca 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/GrantApplicationPermissions.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/GrantApplicationPermissions.cs @@ -37,6 +37,7 @@ public static class Applicants public const string Default = GroupName + ".Applicants"; public const string ViewList = Default + ".ViewList"; public const string Edit = Default + Operation.Update; + public const string Delete = Default + Operation.Delete; public const string AssignApplicant = Default + ".AssignApplicant"; public const string ApplicantInfoDefault = Default + ".ApplicantInfo"; public const string EditRedStop = ApplicantInfoDefault + ".EditRedStop"; 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 b69fac3393..feac9027ac 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/Applicant.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/Applicant.cs @@ -36,4 +36,13 @@ public class Applicant : AuditedAggregateRoot, IMultiTenant public string? IssueTrackingComments { get; set; } public string? AuditComments { get; set; } public string? ReportsComments { get; set; } + // Soft-delete fields are declared manually instead of implementing ABP's ISoftDelete. + // ISoftDelete would register a global EF Core query filter that applies to Include() joins — + // Application.Applicant is a required navigation (OnDelete NoAction), so the filter would + // silently drop Application rows whose Applicant is soft-deleted, breaking the application + // and payment list pages. Filtering is applied explicitly only where needed (applicant list, + // lookup, autocomplete) via .Where(a => !a.IsDeleted). + public bool IsDeleted { get; set; } + public DateTime? DeletionTime { get; set; } + public Guid? DeleterId { get; set; } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IApplicationRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IApplicationRepository.cs index 62a57ce6de..fe09b4433b 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IApplicationRepository.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IApplicationRepository.cs @@ -43,5 +43,8 @@ Task> GetApplicationListRecordsAsync( // Get applications whose DefaultSiteId matches the given site id Task> GetApplicationsBySiteIdAsync(Guid siteId); + + // Check whether any applications exist for the given applicant + Task HasApplicationsByApplicantIdAsync(Guid applicantId); } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260606000444_AddApplicantSoftDelete.Designer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260606000444_AddApplicantSoftDelete.Designer.cs new file mode 100644 index 0000000000..99e71a03a2 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260606000444_AddApplicantSoftDelete.Designer.cs @@ -0,0 +1,5046 @@ +// +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("20260606000444_AddApplicantSoftDelete")] + partial class AddApplicantSoftDelete + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.PostgreSql) + .HasAnnotation("ProductVersion", "10.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScoresheetId"); + + b.ToTable("ScoresheetInstances", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Answer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("QuestionId") + .HasColumnType("uuid"); + + b.Property("ScoresheetInstanceId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("QuestionId"); + + b.HasIndex("ScoresheetInstanceId"); + + b.ToTable("Answers", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("SectionId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("Questions", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Scoresheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Scoresheets", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ScoresheetId"); + + b.ToTable("ScoresheetSections", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.CustomFieldValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CustomFieldId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("WorksheetInstanceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetInstanceId"); + + b.ToTable("CustomFieldValues", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UiAnchor") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetCorrelationId") + .HasColumnType("uuid"); + + b.Property("WorksheetCorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("WorksheetInstances", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetLinks.WorksheetLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UiAnchor") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetId"); + + b.ToTable("WorksheetLinks", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.CustomField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("SectionId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("CustomFields", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.Worksheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("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("OrganizationSize") + .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("TenantId"); + + 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("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("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("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("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.Notifications.EmailGroups.EmailGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EmailGroups", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroupUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.ToTable("EmailGroupUsers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("BCC") + .IsRequired() + .HasColumnType("text"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CC") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChesHttpStatusCode") + .HasColumnType("text"); + + b.Property("ChesMsgId") + .HasColumnType("uuid"); + + b.Property("ChesResponse") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChesStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FromAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestIds") + .IsRequired() + .HasColumnType("text"); + + b.Property("Priority") + .IsRequired() + .HasColumnType("text"); + + b.Property("RetryAttempts") + .HasColumnType("integer"); + + b.Property("SendOnDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("SentDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("text"); + + b.Property("TemplateName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("ToAddress") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EmailLogs", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLogAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("EmailLogId") + .HasColumnType("uuid"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("EmailLogId"); + + b.HasIndex("S3ObjectKey"); + + b.ToTable("EmailLogAttachments", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.EmailTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BodyHTML") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyText") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SendFrom") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("EmailTemplates", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.Subscriber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Subscribers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("SubscriptionGroups", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroupSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SubscriberId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.HasIndex("SubscriberId"); + + b.ToTable("SubscriptionGroupSubscribers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TemplateVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MapTo") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("TemplateVariables", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.Trigger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("InternalName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Triggers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TriggerSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SubscriptionGroupId") + .HasColumnType("uuid"); + + b.Property("TemplateId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TriggerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionGroupId"); + + b.HasIndex("TemplateId"); + + b.HasIndex("TriggerId"); + + b.ToTable("TriggerSubscriptions", "Notifications"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.AccountCodings.AccountCoding", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Description") + .HasMaxLength(35) + .HasColumnType("character varying(35)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MinistryClient") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("Responsibility") + .IsRequired() + .HasColumnType("text"); + + b.Property("ServiceLine") + .IsRequired() + .HasColumnType("text"); + + b.Property("Stob") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("AccountCodings", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentConfigurations.PaymentConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DefaultAccountCodingId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentIdPrefix") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("PaymentConfigurations", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.ExpenseApproval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DecisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DecisionUserId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PaymentRequestId"); + + b.ToTable("ExpenseApprovals", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountCodingId") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("BatchName") + .IsRequired() + .HasColumnType("text"); + + b.Property("BatchNumber") + .HasColumnType("numeric"); + + b.Property("CasHttpStatusCode") + .HasColumnType("integer"); + + b.Property("CasResponse") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContractNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FsbApNotified") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("FsbNotificationEmailLogId") + .HasColumnType("uuid"); + + b.Property("FsbNotificationSentDate") + .HasColumnType("timestamp without time zone"); + + b.Property("InvoiceNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("InvoiceStatus") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsRecon") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("PayeeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentDate") + .HasColumnType("text"); + + b.Property("PaymentNumber") + .HasColumnType("text"); + + b.Property("PaymentStatus") + .HasColumnType("text"); + + b.Property("ReferenceNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("RequesterName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SiteId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("SubmissionConfirmationCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SupplierName") + .HasColumnType("text"); + + b.Property("SupplierNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AccountCodingId"); + + b.HasIndex("FsbNotificationEmailLogId"); + + b.HasIndex("ReferenceNumber") + .IsUnique(); + + b.HasIndex("SiteId"); + + b.ToTable("PaymentRequests", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentTags.PaymentTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestId") + .HasColumnType("uuid"); + + b.Property("TagId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("PaymentRequestId"); + + b.HasIndex("TagId"); + + b.ToTable("PaymentTags", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentThresholds.PaymentThreshold", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Threshold") + .HasColumnType("numeric"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("PaymentThresholds", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Site", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddressLine1") + .HasColumnType("text"); + + b.Property("AddressLine2") + .HasColumnType("text"); + + b.Property("AddressLine3") + .HasColumnType("text"); + + b.Property("BankAccount") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EFTAdvicePref") + .HasColumnType("text"); + + b.Property("EmailAddress") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastUpdatedInCas") + .HasColumnType("timestamp without time zone"); + + b.Property("MarkDeletedInUse") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentGroup") + .HasColumnType("integer"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("SiteProtected") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("SupplierId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("SupplierId"); + + b.ToTable("Sites", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Supplier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BusinessNumber") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("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.Notifications.EmailGroups.EmailGroupUser", b => + { + b.HasOne("Unity.Notifications.EmailGroups.EmailGroup", null) + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLogAttachment", b => + { + b.HasOne("Unity.Notifications.Emails.EmailLog", null) + .WithMany() + .HasForeignKey("EmailLogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroupSubscription", b => + { + b.HasOne("Unity.Notifications.Templates.SubscriptionGroup", "SubscriptionGroup") + .WithMany() + .HasForeignKey("GroupId"); + + b.HasOne("Unity.Notifications.Templates.Subscriber", "Subscriber") + .WithMany() + .HasForeignKey("SubscriberId"); + + b.Navigation("Subscriber"); + + b.Navigation("SubscriptionGroup"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TriggerSubscription", b => + { + b.HasOne("Unity.Notifications.Templates.SubscriptionGroup", "SubscriptionGroup") + .WithMany() + .HasForeignKey("SubscriptionGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Notifications.Templates.EmailTemplate", "EmailTemplate") + .WithMany() + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Notifications.Templates.Trigger", "Trigger") + .WithMany() + .HasForeignKey("TriggerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmailTemplate"); + + b.Navigation("SubscriptionGroup"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.ExpenseApproval", b => + { + b.HasOne("Unity.Payments.Domain.PaymentRequests.PaymentRequest", "PaymentRequest") + .WithMany("ExpenseApprovals") + .HasForeignKey("PaymentRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PaymentRequest"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.HasOne("Unity.Payments.Domain.AccountCodings.AccountCoding", "AccountCoding") + .WithMany() + .HasForeignKey("AccountCodingId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Unity.Payments.Domain.Suppliers.Site", "Site") + .WithMany() + .HasForeignKey("SiteId") + .OnDelete(DeleteBehavior.NoAction); + + 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/20260606000444_AddApplicantSoftDelete.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260606000444_AddApplicantSoftDelete.cs new file mode 100644 index 0000000000..c568581f32 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260606000444_AddApplicantSoftDelete.cs @@ -0,0 +1,50 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + /// + public partial class AddApplicantSoftDelete : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DeleterId", + table: "Applicants", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "DeletionTime", + table: "Applicants", + type: "timestamp without time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "IsDeleted", + table: "Applicants", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DeleterId", + table: "Applicants"); + + migrationBuilder.DropColumn( + name: "DeletionTime", + table: "Applicants"); + + migrationBuilder.DropColumn( + name: "IsDeleted", + table: "Applicants"); + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/GrantTenantDbContextModelSnapshot.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/GrantTenantDbContextModelSnapshot.cs index 62fbafaadd..698a9e3994 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 @@ -825,6 +825,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid") .HasColumnName("CreatorId"); + b.Property("DeleterId") + .HasColumnType("uuid"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone"); + b.Property("ExtraProperties") .IsRequired() .HasColumnType("text") @@ -842,6 +848,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IndigenousOrgInd") .HasColumnType("text"); + b.Property("IsDeleted") + .HasColumnType("boolean"); + b.Property("IsDuplicated") .HasColumnType("boolean"); 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 7f16fec56a..00facf5f20 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicantRepository.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicantRepository.cs @@ -28,14 +28,14 @@ public async Task> GetUnmatchedApplicantsAsync() { var dbContext = await GetDbContextAsync(); return await dbContext.Applicants - .Where(x => x.MatchPercentage == null) + .Where(x => x.MatchPercentage == null && !x.IsDeleted) .ToListAsync(); } public async Task GetByUnityApplicantIdAsync(string unityApplicantId) { var dbContext = await GetDbContextAsync(); - return await dbContext.Applicants.FirstOrDefaultAsync(x => x.UnityApplicantId == unityApplicantId); + return await dbContext.Applicants.FirstOrDefaultAsync(x => x.UnityApplicantId == unityApplicantId && !x.IsDeleted); } public async Task GetByUnityApplicantNameAsync(string unityApplicantName) @@ -47,7 +47,8 @@ public async Task> GetUnmatchedApplicantsAsync() // EF Core cannot translate StringComparison overloads to SQL, so we use ToLower() for database compatibility return await dbContext.Applicants .FirstOrDefaultAsync(a => a.ApplicantName != null && - a.ApplicantName.ToLower() == unityApplicantNameNormalized); + a.ApplicantName.ToLower() == unityApplicantNameNormalized && + !a.IsDeleted); #pragma warning restore CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons } @@ -55,7 +56,7 @@ public async Task> GetApplicantsWithUnityApplicantIdAsync() { var dbContext = await GetDbContextAsync(); return await dbContext.Applicants - .Where(x => x.UnityApplicantId != null) + .Where(x => x.UnityApplicantId != null && !x.IsDeleted) .ToListAsync(); } @@ -72,6 +73,7 @@ public async Task GetApplicantAutocompleteQueryAsync(string? appli var applicants = await dbContext.Applicants .AsNoTracking() + .Where(a => !a.IsDeleted) .ToListAsync(); var filtered = applicants 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 4133f618b6..b0b10699ec 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicationRepository.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicationRepository.cs @@ -223,6 +223,12 @@ public async Task> GetByApplicantIdAsync(Guid applicantId) .ToListAsync(); } + public async Task HasApplicationsByApplicantIdAsync(Guid applicantId) + { + var query = await GetQueryableAsync(); + return await query.AnyAsync(a => a.ApplicantId == applicantId); + } + public async Task> GetApplicationListRecordsAsync( int skipCount, int maxResultCount, diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml index c15d87e43f..5f8a17be6c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml @@ -31,7 +31,7 @@ @await Component.InvokeAsync("ApplicantBreadcrumbWidget", new { applicantId = @Model.ApplicantId }) -@if (await PermissionChecker.IsGrantedAsync(GrantApplicationPermissions.Applicants.Edit)) +@if (!Model.ApplicantIsDeleted && await PermissionChecker.IsGrantedAsync(GrantApplicationPermissions.Applicants.Edit)) {
@@ -68,6 +68,19 @@ +@if (Model.ApplicantIsDeleted) +{ +
+ +
+} +else +{ +
unity-applicant-loading @@ -234,3 +247,5 @@
+ +} @* end else (applicant not deleted) *@ 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 261fac752d..998b75cc1c 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 @@ -22,6 +22,7 @@ public class DetailsModel : GrantManagerPageModel public Guid? ApplicationId { get; set; } = null; public Applicant? Applicant { get; set; } + public bool ApplicantIsDeleted { get; set; } public string ApplicantDisplayName { get; set; } = string.Empty; public string UnityApplicantId { get; set; } = string.Empty; public string Status { get; set; } = string.Empty; @@ -69,11 +70,16 @@ public async Task OnGetAsync() { Applicant = await _applicantRepository.GetAsync(ApplicantId); - // Set properties for breadcrumb and display ApplicantDisplayName = !string.IsNullOrEmpty(Applicant.ApplicantName) ? Applicant.ApplicantName : "Applicant Name"; + if (Applicant.IsDeleted) + { + ApplicantIsDeleted = true; + return Page(); + } + UnityApplicantId = Applicant.UnityApplicantId ?? "N/A"; Status = Applicant.Status ?? string.Empty; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantsActionBar/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantsActionBar/Default.cshtml index 52e9348106..e3cd75cd24 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantsActionBar/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantsActionBar/Default.cshtml @@ -29,6 +29,14 @@ class="custom-table-btn flex-none btn btn-secondary d-none" button-type="Secondary" /> } + @if (await PermissionChecker.IsGrantedAsync(GrantApplicationPermissions.Applicants.Delete)) + { + + }
@* Spacer to align buttons properly *@ diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantsActionBar/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantsActionBar/Default.js index ef5f2c8064..2f6eef7d9d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantsActionBar/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantsActionBar/Default.js @@ -37,10 +37,12 @@ $(function () { $('*[data-selector="applicants-table-actions"]').removeClass('action-bar-btn-unavailable'); $('.action-bar').addClass('active'); - // OPEN button is only visible when exactly 1 applicant is selected + // OPEN and DELETE buttons are only visible when exactly 1 applicant is selected $('#openApplicant').addClass('action-bar-btn-unavailable'); + $('#deleteApplicant').addClass('action-bar-btn-unavailable'); if (selectedApplicantIds.length === 1) { $('#openApplicant').removeClass('action-bar-btn-unavailable'); + $('#deleteApplicant').removeClass('action-bar-btn-unavailable'); } } @@ -69,6 +71,47 @@ $(function () { } }); + // DELETE button click — pre-check submissions, then show the correct modal + $('#deleteApplicant').on('click', function () { + if (selectedApplicantIds.length !== 1) return; + var applicantId = selectedApplicantIds[0]; + var applicantName = (selectedApplicants[0].applicantName || 'this applicant'); + + unity.grantManager.applicants.applicant + .hasSubmissions(applicantId) + .then(function (hasSubmissions) { + if (hasSubmissions) { + abp.message.warn( + 'This applicant cannot be deleted because it is associated with one or more submissions.', + 'Cannot Delete Applicant' + ); + } else { + abp.message.confirm( + 'Are you sure you want to delete the applicant "' + applicantName + '"?', + 'Delete Applicant', + function (confirmed) { + if (!confirmed) return; + unity.grantManager.applicants.applicant + .deleteApplicant(applicantId) + .then(function () { + PubSub.publish('deselect_applicant', 'reset_data'); + $('#ApplicantsTable').DataTable().ajax.reload(); + abp.notify.success('Applicant "' + applicantName + '" has been deleted.'); + }) + .catch(function (e) { + console.warn('deleteApplicant error:', e); + const msg = e?.responseJSON?.error?.message || 'An error occurred while deleting the applicant. Please try again.'; + abp.message.error(msg, 'Delete Failed'); + }); + } + ); + } + }) + .catch(function (e) { + console.warn('hasSubmissions error:', e); + }); + }); + // Handle search input $('#search').on('input', function () { let table = $('#ApplicantsTable').DataTable(); @@ -77,4 +120,4 @@ $(function () { // Initialize button states manageActionButtons(); -}); \ No newline at end of file +}); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantsActionBar/ListMerge.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantsActionBar/ListMerge.js index 79d5aa11f1..32655619e9 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantsActionBar/ListMerge.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantsActionBar/ListMerge.js @@ -216,7 +216,8 @@ abp.notify.success('Applicants merged successfully.'); }).catch(err => { console.warn('Merge failed:', err); - abp.notify.error('Merge failed. Please try again.'); + const msg = err?.responseJSON?.error?.message || 'Merge failed. Please try again.'; + abp.message.error(msg, 'Merge Failed'); }).always(() => { $('#listMergeSpinner').addClass('d-none'); $('#listMergeMergeBtn').prop('disabled', false); From 638817b09dce5a1b8ad14a15fe4374375cee72ff Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Mon, 8 Jun 2026 12:58:21 -0700 Subject: [PATCH 03/15] AB#33221: Fix sonarqube issues --- .../Components/ApplicantsActionBar/Default.js | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantsActionBar/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantsActionBar/Default.js index 2f6eef7d9d..b61776c93f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantsActionBar/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantsActionBar/Default.js @@ -71,11 +71,26 @@ $(function () { } }); + function executeDelete(applicantId, applicantName) { + unity.grantManager.applicants.applicant + .deleteApplicant(applicantId) + .then(function () { + PubSub.publish('deselect_applicant', 'reset_data'); + $('#ApplicantsTable').DataTable().ajax.reload(); + abp.notify.success('Applicant "' + applicantName + '" has been deleted.'); + }) + .catch(function (e) { + console.warn('deleteApplicant error:', e); + const msg = e?.responseJSON?.error?.message || 'An error occurred while deleting the applicant. Please try again.'; + abp.message.error(msg, 'Delete Failed'); + }); + } + // DELETE button click — pre-check submissions, then show the correct modal $('#deleteApplicant').on('click', function () { if (selectedApplicantIds.length !== 1) return; - var applicantId = selectedApplicantIds[0]; - var applicantName = (selectedApplicants[0].applicantName || 'this applicant'); + const applicantId = selectedApplicantIds[0]; + const applicantName = (selectedApplicants[0].applicantName || 'Applicant Name'); unity.grantManager.applicants.applicant .hasSubmissions(applicantId) @@ -90,19 +105,7 @@ $(function () { 'Are you sure you want to delete the applicant "' + applicantName + '"?', 'Delete Applicant', function (confirmed) { - if (!confirmed) return; - unity.grantManager.applicants.applicant - .deleteApplicant(applicantId) - .then(function () { - PubSub.publish('deselect_applicant', 'reset_data'); - $('#ApplicantsTable').DataTable().ajax.reload(); - abp.notify.success('Applicant "' + applicantName + '" has been deleted.'); - }) - .catch(function (e) { - console.warn('deleteApplicant error:', e); - const msg = e?.responseJSON?.error?.message || 'An error occurred while deleting the applicant. Please try again.'; - abp.message.error(msg, 'Delete Failed'); - }); + if (confirmed) executeDelete(applicantId, applicantName); } ); } From 512e05512c2bd7edae5269c79d79bb68fd5b43bd Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Mon, 8 Jun 2026 16:21:30 -0700 Subject: [PATCH 04/15] AB#33252: Add Paid Date Column in Funding History --- .../wwwroot/themes/ux2/table-utils.js | 5 + .../History/CreateUpdateFundingHistoryDto.cs | 1 + .../History/FundingHistoryDto.cs | 1 + .../Applications/FundingHistory.cs | 1 + ...44_AddPaidDateToFundingHistory.Designer.cs | 5043 +++++++++++++++++ ...60608222844_AddPaidDateToFundingHistory.cs | 29 + .../GrantTenantDbContextModelSnapshot.cs | 3 + .../CreateFundingHistoryModal.cshtml | 7 + .../CreateFundingHistoryModal.cshtml.cs | 1 + .../EditFundingHistoryModal.cshtml | 7 + .../EditFundingHistoryModal.cshtml.cs | 2 + .../FundingHistoryModalViewModel.cs | 4 + .../Components/ApplicantHistory/Default.js | 14 +- 13 files changed, 5117 insertions(+), 1 deletion(-) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260608222844_AddPaidDateToFundingHistory.Designer.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260608222844_AddPaidDateToFundingHistory.cs diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js index 0b00bdc5e8..09363e3316 100644 --- a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js +++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js @@ -350,6 +350,11 @@ function initializeDataTable(options) { } }, stateLoadParams: function (settings, data) { + // Discard stale state when column count has changed so defaultVisibleColumns applies cleanly + if (data?.columns && data.columns.length !== settings.aoColumns.length) { + return false; + } + if (data?.externalSearch) { let externalSearch = $(settings.oInit.externalSearchInputId); if (externalSearch.length) externalSearch.val(data.externalSearch); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/CreateUpdateFundingHistoryDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/CreateUpdateFundingHistoryDto.cs index 9de866e604..326d83fc08 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/CreateUpdateFundingHistoryDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/CreateUpdateFundingHistoryDto.cs @@ -12,5 +12,6 @@ public class CreateUpdateFundingHistoryDto public decimal? OneTimeConsideration { get; set; } public decimal? ReconsiderationAmount { get; set; } public decimal? TotalGrantAmount { get; set; } + public DateTime? PaidDate { get; set; } public string? FundingNotes { get; set; } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/FundingHistoryDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/FundingHistoryDto.cs index dfdf5afaad..d95cd64eb6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/FundingHistoryDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/FundingHistoryDto.cs @@ -13,5 +13,6 @@ public class FundingHistoryDto : AuditedEntityDto public decimal? OneTimeConsideration { get; set; } public decimal? ReconsiderationAmount { get; set; } public decimal? TotalGrantAmount { get; set; } + public DateTime? PaidDate { get; set; } public string? FundingNotes { get; set; } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/FundingHistory.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/FundingHistory.cs index 5a601ac7c8..ebba8a6390 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/FundingHistory.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/FundingHistory.cs @@ -14,6 +14,7 @@ public class FundingHistory : AuditedAggregateRoot, IMultiTenant public decimal? OneTimeConsideration { get; set; } public decimal? ReconsiderationAmount { get; set; } public decimal? TotalGrantAmount { get; set; } + public DateTime? PaidDate { get; set; } public string? FundingNotes { get; set; } public Guid? TenantId { get; set; } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260608222844_AddPaidDateToFundingHistory.Designer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260608222844_AddPaidDateToFundingHistory.Designer.cs new file mode 100644 index 0000000000..ad77bee640 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260608222844_AddPaidDateToFundingHistory.Designer.cs @@ -0,0 +1,5043 @@ +// +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("20260608222844_AddPaidDateToFundingHistory")] + partial class AddPaidDateToFundingHistory + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.PostgreSql) + .HasAnnotation("ProductVersion", "10.0.3") + .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("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FiscalDay") + .HasColumnType("integer"); + + b.Property("FiscalMonth") + .HasColumnType("text"); + + b.Property("FundingHistoryComments") + .HasColumnType("text"); + + b.Property("IndigenousOrgInd") + .HasColumnType("text"); + + b.Property("IsDuplicated") + .HasColumnType("boolean"); + + b.Property("IssueTrackingComments") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MatchPercentage") + .HasColumnType("numeric"); + + b.Property("NonRegOrgName") + .HasColumnType("text"); + + b.Property("NonRegisteredBusinessName") + .HasColumnType("text"); + + b.Property("OrgName") + .HasColumnType("text"); + + b.Property("OrgNumber") + .HasColumnType("text"); + + b.Property("OrgStatus") + .HasColumnType("text"); + + b.Property("OrganizationSize") + .HasColumnType("text"); + + b.Property("OrganizationType") + .HasColumnType("text"); + + b.Property("RedStop") + .HasColumnType("boolean"); + + b.Property("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("TenantId"); + + 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("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("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("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.Notifications.EmailGroups.EmailGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EmailGroups", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroupUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.ToTable("EmailGroupUsers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("BCC") + .IsRequired() + .HasColumnType("text"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CC") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChesHttpStatusCode") + .HasColumnType("text"); + + b.Property("ChesMsgId") + .HasColumnType("uuid"); + + b.Property("ChesResponse") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChesStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FromAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestIds") + .IsRequired() + .HasColumnType("text"); + + b.Property("Priority") + .IsRequired() + .HasColumnType("text"); + + b.Property("RetryAttempts") + .HasColumnType("integer"); + + b.Property("SendOnDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("SentDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("text"); + + b.Property("TemplateName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("ToAddress") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EmailLogs", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLogAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("EmailLogId") + .HasColumnType("uuid"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("EmailLogId"); + + b.HasIndex("S3ObjectKey"); + + b.ToTable("EmailLogAttachments", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.EmailTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BodyHTML") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyText") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SendFrom") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("EmailTemplates", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.Subscriber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Subscribers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("SubscriptionGroups", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroupSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SubscriberId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.HasIndex("SubscriberId"); + + b.ToTable("SubscriptionGroupSubscribers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TemplateVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MapTo") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("TemplateVariables", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.Trigger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("InternalName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Triggers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TriggerSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SubscriptionGroupId") + .HasColumnType("uuid"); + + b.Property("TemplateId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TriggerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionGroupId"); + + b.HasIndex("TemplateId"); + + b.HasIndex("TriggerId"); + + b.ToTable("TriggerSubscriptions", "Notifications"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.AccountCodings.AccountCoding", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Description") + .HasMaxLength(35) + .HasColumnType("character varying(35)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MinistryClient") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("Responsibility") + .IsRequired() + .HasColumnType("text"); + + b.Property("ServiceLine") + .IsRequired() + .HasColumnType("text"); + + b.Property("Stob") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("AccountCodings", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentConfigurations.PaymentConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DefaultAccountCodingId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentIdPrefix") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("PaymentConfigurations", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.ExpenseApproval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DecisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DecisionUserId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PaymentRequestId"); + + b.ToTable("ExpenseApprovals", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountCodingId") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("BatchName") + .IsRequired() + .HasColumnType("text"); + + b.Property("BatchNumber") + .HasColumnType("numeric"); + + b.Property("CasHttpStatusCode") + .HasColumnType("integer"); + + b.Property("CasResponse") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContractNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FsbApNotified") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("FsbNotificationEmailLogId") + .HasColumnType("uuid"); + + b.Property("FsbNotificationSentDate") + .HasColumnType("timestamp without time zone"); + + b.Property("InvoiceNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("InvoiceStatus") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsRecon") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("PayeeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentDate") + .HasColumnType("text"); + + b.Property("PaymentNumber") + .HasColumnType("text"); + + b.Property("PaymentStatus") + .HasColumnType("text"); + + b.Property("ReferenceNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("RequesterName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SiteId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("SubmissionConfirmationCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SupplierName") + .HasColumnType("text"); + + b.Property("SupplierNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AccountCodingId"); + + b.HasIndex("FsbNotificationEmailLogId"); + + b.HasIndex("ReferenceNumber") + .IsUnique(); + + b.HasIndex("SiteId"); + + b.ToTable("PaymentRequests", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentTags.PaymentTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestId") + .HasColumnType("uuid"); + + b.Property("TagId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("PaymentRequestId"); + + b.HasIndex("TagId"); + + b.ToTable("PaymentTags", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentThresholds.PaymentThreshold", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Threshold") + .HasColumnType("numeric"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("PaymentThresholds", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Site", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddressLine1") + .HasColumnType("text"); + + b.Property("AddressLine2") + .HasColumnType("text"); + + b.Property("AddressLine3") + .HasColumnType("text"); + + b.Property("BankAccount") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EFTAdvicePref") + .HasColumnType("text"); + + b.Property("EmailAddress") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastUpdatedInCas") + .HasColumnType("timestamp without time zone"); + + b.Property("MarkDeletedInUse") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentGroup") + .HasColumnType("integer"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("SiteProtected") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("SupplierId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("SupplierId"); + + b.ToTable("Sites", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Supplier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BusinessNumber") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("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.Notifications.EmailGroups.EmailGroupUser", b => + { + b.HasOne("Unity.Notifications.EmailGroups.EmailGroup", null) + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLogAttachment", b => + { + b.HasOne("Unity.Notifications.Emails.EmailLog", null) + .WithMany() + .HasForeignKey("EmailLogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroupSubscription", b => + { + b.HasOne("Unity.Notifications.Templates.SubscriptionGroup", "SubscriptionGroup") + .WithMany() + .HasForeignKey("GroupId"); + + b.HasOne("Unity.Notifications.Templates.Subscriber", "Subscriber") + .WithMany() + .HasForeignKey("SubscriberId"); + + b.Navigation("Subscriber"); + + b.Navigation("SubscriptionGroup"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TriggerSubscription", b => + { + b.HasOne("Unity.Notifications.Templates.SubscriptionGroup", "SubscriptionGroup") + .WithMany() + .HasForeignKey("SubscriptionGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Notifications.Templates.EmailTemplate", "EmailTemplate") + .WithMany() + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Notifications.Templates.Trigger", "Trigger") + .WithMany() + .HasForeignKey("TriggerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmailTemplate"); + + b.Navigation("SubscriptionGroup"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.ExpenseApproval", b => + { + b.HasOne("Unity.Payments.Domain.PaymentRequests.PaymentRequest", "PaymentRequest") + .WithMany("ExpenseApprovals") + .HasForeignKey("PaymentRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PaymentRequest"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.HasOne("Unity.Payments.Domain.AccountCodings.AccountCoding", "AccountCoding") + .WithMany() + .HasForeignKey("AccountCodingId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Unity.Payments.Domain.Suppliers.Site", "Site") + .WithMany() + .HasForeignKey("SiteId") + .OnDelete(DeleteBehavior.NoAction); + + 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/20260608222844_AddPaidDateToFundingHistory.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260608222844_AddPaidDateToFundingHistory.cs new file mode 100644 index 0000000000..35a7d7704e --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260608222844_AddPaidDateToFundingHistory.cs @@ -0,0 +1,29 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + /// + public partial class AddPaidDateToFundingHistory : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PaidDate", + table: "FundingHistories", + type: "timestamp without time zone", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "PaidDate", + table: "FundingHistories"); + } + } +} 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 aeb709113a..b3f0185aed 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 @@ -2304,6 +2304,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("OneTimeConsideration") .HasColumnType("numeric"); + b.Property("PaidDate") + .HasColumnType("timestamp without time zone"); + b.Property("ReconsiderationAmount") .HasColumnType("numeric"); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateFundingHistoryModal.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateFundingHistoryModal.cshtml index 6db3da1854..69619d4e34 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateFundingHistoryModal.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateFundingHistoryModal.cshtml @@ -76,6 +76,13 @@
+
+ + +
+
OnPostAsync() ReconsiderationAmount = FundingHistoryForm.ReconsiderationAmount, OneTimeConsideration = FundingHistoryForm.OneTimeConsideration, TotalGrantAmount = FundingHistoryForm.TotalGrantAmount, + PaidDate = FundingHistoryForm.PaidDate, FundingNotes = FundingHistoryForm.FundingNotes }; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditFundingHistoryModal.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditFundingHistoryModal.cshtml index 983d0d4cef..da0a314b23 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditFundingHistoryModal.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditFundingHistoryModal.cshtml @@ -77,6 +77,13 @@
+
+ + +
+
OnPostAsync() OneTimeConsideration = FundingHistoryForm.OneTimeConsideration, ReconsiderationAmount = FundingHistoryForm.ReconsiderationAmount, TotalGrantAmount = FundingHistoryForm.TotalGrantAmount, + PaidDate = FundingHistoryForm.PaidDate, FundingNotes = FundingHistoryForm.FundingNotes }; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/FundingHistoryModalViewModel.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/FundingHistoryModalViewModel.cs index 814b9ed8f5..f24e59c644 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/FundingHistoryModalViewModel.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/FundingHistoryModalViewModel.cs @@ -35,6 +35,10 @@ public class FundingHistoryModalViewModel [DataType(DataType.Currency)] public decimal? TotalGrantAmount { get; set; } + [DisplayName("Paid Date")] + [DataType(DataType.Date)] + public DateTime? PaidDate { get; set; } + [DisplayName("Funding Notes")] public string? FundingNotes { get; set; } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.js index d26dbd9525..65bf1e705a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.js @@ -52,6 +52,18 @@ $(function () { { title: 'One-Time Consideration', data: 'oneTimeConsideration', name: 'oneTimeConsideration', className: 'data-table-header currency-display', render: (d) => formatCurrency(d) }, { title: 'Reconsideration Amount', data: 'reconsiderationAmount', name: 'reconsiderationAmount', className: 'data-table-header currency-display', render: (d) => formatCurrency(d) }, { title: 'Total Grant Amount', data: 'totalGrantAmount', name: 'totalGrantAmount', className: 'data-table-header currency-display', render: (d) => formatCurrency(d) }, + { + title: 'Paid Date', data: 'paidDate', name: 'paidDate', className: 'data-table-header', width: '90px', + createdCell: function (td) { $(td).css('min-width', '90px'); }, + render: function (d) { + if (!d) return nullPlaceholder; + try { + return luxon.DateTime.fromISO(d, { + locale: abp.localization.currentCulture.name, + }).toUTC().toLocaleString(); + } catch (e) { console.warn('Paid date parse error:', e); return d; } + } + }, { title: 'Notes', data: 'fundingNotes', name: 'fundingNotes', className: 'data-table-header', width: '200px', createdCell: function (td) { $(td).css('min-width', '200px'); }, @@ -224,7 +236,7 @@ $(function () { const fundingHistoryTable = initializeDataTable({ dt: $('#FundingHistoryTable'), - defaultVisibleColumns: ['grantCategory', 'fundingYear', 'renewedFunding', 'approvedAmount', 'reconsiderationAmount', 'oneTimeConsideration', 'totalGrantAmount', 'fundingNotes', 'actions'], + defaultVisibleColumns: ['grantCategory', 'fundingYear', 'renewedFunding', 'approvedAmount', 'reconsiderationAmount', 'oneTimeConsideration', 'totalGrantAmount', 'paidDate', 'fundingNotes', 'actions'], listColumns: getFundingHistoryColumns(), dataEndpoint: () => unity.grantManager.applicantProfile.applicantHistory.getFundingHistoryList(getApplicantId()), data: () => ({}), From ebf71570be89171d57c51bdec9ec48a8d00a502d Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 5 Jun 2026 08:59:24 -0700 Subject: [PATCH 05/15] AB#33009 Replace OpenAI HttpClient transport with Azure OpenAI SDK - Replace manual HTTP request construction with Azure.AI.OpenAI SDK (v2.1.0) - Simplify OpenAITransportService: remove HttpClient, use AzureOpenAIClient - Update configuration resolver with ResolveEndpoint() and ResolveDeploymentName() - Preserve temperature configuration, metadata extraction, and error handling - Remove non-functional reasoning_effort and verbosity HTTP parameters - Update transport service tests to match SDK-based implementation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AI/Runtime/OpenAIConfigurationResolver.cs | 94 +++++------ .../AI/Runtime/OpenAIRuntimeService.cs | 9 +- .../AI/Runtime/OpenAITransportService.cs | 148 ++++++++---------- .../Unity.AI.Application.csproj | 1 + .../OpenAIConfigurationResolverTests.cs | 92 ----------- .../AI/Runtime/OpenAITransportServiceTests.cs | 81 +--------- 6 files changed, 112 insertions(+), 313 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIConfigurationResolver.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIConfigurationResolver.cs index fc12e4cd99..c7da8513da 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIConfigurationResolver.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIConfigurationResolver.cs @@ -7,9 +7,6 @@ namespace Unity.AI.Runtime; public class OpenAIConfigurationResolver(IConfiguration configuration) : ITransientDependency { - private static readonly string[] SupportedReasoningEfforts = ["minimal", "low", "medium", "high"]; - private static readonly string[] SupportedVerbosityValues = ["low", "medium", "high"]; - private readonly IConfiguration _configuration = configuration; public string ResolveProviderName(string? operationName = null) @@ -32,13 +29,6 @@ public string ResolveApiKey(string? operationName = null) return Required($"Azure:{providerName}:ApiKey"); } - public string ResolveMaxTokensParameterNameForOperation(string? operationName = null) - { - var providerName = ResolveProviderName(operationName); - var profileName = ResolveProfileName(operationName); - return RequiredProfile(providerName, profileName, "MaxTokensParameter"); - } - public double? ResolveConfiguredTemperature(string? operationName = null) { var providerName = ResolveProviderName(operationName); @@ -53,16 +43,6 @@ public string ResolveMaxTokensParameterNameForOperation(string? operationName = return null; } - public string? ResolveConfiguredReasoningEffort(string? operationName = null) - { - return ResolveOptionalProfileValue(operationName, "ReasoningEffort", SupportedReasoningEfforts); - } - - public string? ResolveConfiguredVerbosity(string? operationName = null) - { - return ResolveOptionalProfileValue(operationName, "Verbosity", SupportedVerbosityValues); - } - public int ResolveCompletionTokens(string operationName) { var configuredValue = OptionalPositiveInt($"Azure:Operations:{operationName}:MaxCompletionTokens"); @@ -86,13 +66,54 @@ public string ResolvePromptVersion(string operationName) ?? Required("Azure:Operations:Defaults:PromptVersion"); } - public string ResolveApiUrl(string? operationName = null) + public Uri ResolveEndpoint(string? operationName = null) { var providerName = ResolveProviderName(operationName); var endpoint = Required($"Azure:{providerName}:Endpoint"); + return new Uri(endpoint); + } + + public string ResolveDeploymentName(string? operationName = null) + { + var providerName = ResolveProviderName(operationName); + var profileName = ResolveProfileName(operationName); + var profileApiUrl = RequiredProfile(providerName, profileName, "ApiUrl"); + // Extract deployment name from URL like "/openai/deployments/gpt-5-mini/chat/completions?api-version=..." + var parts = profileApiUrl.Split('/', StringSplitOptions.RemoveEmptyEntries); + var deploymentIndex = Array.IndexOf(parts, "deployments"); + if (deploymentIndex >= 0 && deploymentIndex + 1 < parts.Length) + { + var raw = parts[deploymentIndex + 1]; + // Strip query string if present (e.g., "gpt-5-mini?api-version=...") + var deployment = raw.Contains('?') ? raw[..raw.IndexOf('?')] : raw; + return deployment; + } + throw new InvalidOperationException($"Could not extract deployment name from API URL: {profileApiUrl}"); + } + + public string? ResolveApiVersion(string? operationName = null) + { + var providerName = ResolveProviderName(operationName); var profileName = ResolveProfileName(operationName); var profileApiUrl = RequiredProfile(providerName, profileName, "ApiUrl"); - return CombineEndpointAndPath(endpoint, profileApiUrl); + // Extract api-version from query string like "?api-version=2024-10-01-preview" + var queryString = profileApiUrl.Contains('?') ? profileApiUrl.Substring(profileApiUrl.IndexOf('?') + 1) : null; + if (string.IsNullOrEmpty(queryString)) + { + return null; + } + + var parameters = queryString.Split('&'); + foreach (var param in parameters) + { + var keyValue = param.Split('='); + if (keyValue.Length == 2 && keyValue[0] == "api-version") + { + return Uri.UnescapeDataString(keyValue[1]); + } + } + + return null; } private string ResolveProfileName(string? operationName) @@ -141,33 +162,4 @@ private static string ProfileKey(string providerName, string profileName, string { return $"Azure:{providerName}:Profiles:{profileName}:{settingName}"; } - - private string? ResolveOptionalProfileValue(string? operationName, string settingName, string[] supportedValues) - { - var providerName = ResolveProviderName(operationName); - var profileName = ResolveProfileName(operationName); - var configuredValue = OptionalProfile(providerName, profileName, settingName); - if (configuredValue == null) - { - return null; - } - - var trimmedValue = configuredValue.Trim(); - if (Array.Exists(supportedValues, supportedValue => string.Equals(supportedValue, trimmedValue, StringComparison.Ordinal))) - { - return trimmedValue; - } - - throw new InvalidOperationException( - $"AI {settingName} value '{configuredValue}' is not supported. Use one of: {string.Join(", ", supportedValues)}."); - } - - private static string CombineEndpointAndPath(string endpoint, string profilePath) - { - const char UrlPathSeparator = '/'; - - return endpoint.Trim().TrimEnd(UrlPathSeparator) - + UrlPathSeparator - + profilePath.Trim().TrimStart(UrlPathSeparator); - } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIRuntimeService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIRuntimeService.cs index dd3543b66d..0093f0e662 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIRuntimeService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIRuntimeService.cs @@ -27,9 +27,6 @@ public class OpenAIRuntimeService : IAIService, ITransientDependency private const string ApplicationScoringPromptType = AIPromptTypes.ApplicationScoring; private const int MaxAiAttempts = 3; - private int AttachmentSummaryCompletionTokens => _openAIConfigurationResolver.ResolveCompletionTokens(AttachmentSummaryPromptType); - private int ApplicationAnalysisCompletionTokens => _openAIConfigurationResolver.ResolveCompletionTokens(ApplicationAnalysisPromptType); - private int ApplicationScoringCompletionTokens => _openAIConfigurationResolver.ResolveCompletionTokens(ApplicationScoringPromptType); // Optional local debugging sink for prompt payload logs to a local file. // Not intended for deployed/shared environments. private bool IsPromptFileLoggingEnabled => _configuration.GetValue("Azure:Logging:EnablePromptFileLog") ?? false; @@ -92,7 +89,7 @@ public async Task GenerateApplicationAnalysisAsync( () => _openAITransportService.GenerateSummaryAsync( applicationAnalysisContent, systemPrompt, - ApplicationAnalysisCompletionTokens, + _openAIConfigurationResolver.ResolveCompletionTokens(ApplicationAnalysisPromptType), operationName: ApplicationAnalysisPromptType, promptVersion: promptVersion, cancellationToken: cancellationToken), @@ -146,7 +143,7 @@ public async Task GenerateAttachmentSummaryAsync(Atta () => _openAITransportService.GenerateSummaryAsync( contentToAnalyze, prompt, - AttachmentSummaryCompletionTokens, + _openAIConfigurationResolver.ResolveCompletionTokens(AttachmentSummaryPromptType), operationName: AttachmentSummaryPromptType, promptVersion: promptVersion, fileName: fileName, @@ -223,7 +220,7 @@ public async Task GenerateApplicationScoringAsync(Ap () => _openAITransportService.GenerateSummaryAsync( applicationScoringContent, systemPrompt, - ApplicationScoringCompletionTokens, + _openAIConfigurationResolver.ResolveCompletionTokens(ApplicationScoringPromptType), operationName: ApplicationScoringPromptType, promptVersion: promptVersion, cancellationToken: cancellationToken), diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAITransportService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAITransportService.cs index 1428408375..0cf9b374bc 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAITransportService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAITransportService.cs @@ -1,9 +1,10 @@ +using Azure.AI.OpenAI; using Microsoft.Extensions.Logging; +using OpenAI.Chat; using System; +using System.ClientModel; using System.Collections.Generic; using System.Net; -using System.Net.Http; -using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -12,11 +13,9 @@ namespace Unity.AI.Runtime; public class OpenAITransportService( - HttpClient httpClient, OpenAIConfigurationResolver configurationResolver, ILogger logger) : ITransientDependency { - private readonly HttpClient _httpClient = httpClient; private readonly OpenAIConfigurationResolver _configurationResolver = configurationResolver; private readonly ILogger _logger = logger; @@ -30,105 +29,67 @@ public async Task GenerateSummaryAsync( string? fileName = null, CancellationToken cancellationToken = default) { - var providerName = _configurationResolver.ResolveProviderName(operationName); - if (!string.Equals(providerName, "OpenAI", StringComparison.Ordinal)) - { - _logger.LogWarning("Provider {ProviderName} is not supported by OpenAI transport.", providerName); - return AIOperationResult.PermanentFailure(new AIProviderResult($"Unsupported provider: {providerName}")); - } - - var apiKey = _configurationResolver.ResolveApiKey(operationName); try { - var resolvedSystemPrompt = string.IsNullOrWhiteSpace(systemPrompt) - ? "You are a professional grant analyst for the BC Government." - : systemPrompt; - - var requestPayload = new Dictionary + var providerName = _configurationResolver.ResolveProviderName(operationName); + if (!string.Equals(providerName, "OpenAI", StringComparison.Ordinal)) { - ["messages"] = new[] - { - new { role = "system", content = resolvedSystemPrompt }, - new { role = "user", content = content ?? string.Empty } - }, - [_configurationResolver.ResolveMaxTokensParameterNameForOperation(operationName)] = maxTokens - }; - - var profileTemperature = _configurationResolver.ResolveConfiguredTemperature(operationName); - var resolvedTemperature = profileTemperature.HasValue ? temperature ?? profileTemperature : null; - if (resolvedTemperature.HasValue) - { - requestPayload["temperature"] = resolvedTemperature.Value; + _logger.LogWarning("Provider {ProviderName} is not supported by OpenAI transport.", providerName); + return AIOperationResult.PermanentFailure(new AIProviderResult($"Unsupported provider: {providerName}")); } - var reasoningEffort = _configurationResolver.ResolveConfiguredReasoningEffort(operationName); - if (!string.IsNullOrWhiteSpace(reasoningEffort)) - { - requestPayload["reasoning_effort"] = reasoningEffort; - } + var apiKey = _configurationResolver.ResolveApiKey(operationName); + var resolvedSystemPrompt = string.IsNullOrWhiteSpace(systemPrompt) + ? "You are a professional grant analyst for the BC Government." + : systemPrompt; - var verbosity = _configurationResolver.ResolveConfiguredVerbosity(operationName); - if (!string.IsNullOrWhiteSpace(verbosity)) + var clientOptions = new AzureOpenAIClientOptions(); + var resolvedApiVersion = _configurationResolver.ResolveApiVersion(operationName); + // Note: Azure SDK uses its default API version. Configured version is parsed for documentation/future use. + if (!string.IsNullOrEmpty(resolvedApiVersion)) { - requestPayload["verbosity"] = verbosity; + _logger.LogDebug("Profile specifies API version {ApiVersion}; SDK will use its default", resolvedApiVersion); } - var json = JsonSerializer.Serialize(requestPayload); - var httpContent = new StringContent(json, Encoding.UTF8, "application/json"); + var client = new AzureOpenAIClient( + _configurationResolver.ResolveEndpoint(operationName), + new ApiKeyCredential(apiKey), + clientOptions) + .GetChatClient(_configurationResolver.ResolveDeploymentName(operationName)); - using var request = new HttpRequestMessage(HttpMethod.Post, _configurationResolver.ResolveApiUrl(operationName)) + var options = new ChatCompletionOptions { - Content = httpContent + MaxOutputTokenCount = maxTokens }; - request.Headers.TryAddWithoutValidation("Authorization", apiKey); - using var response = await _httpClient.SendAsync(request, cancellationToken); - var responseContent = await response.Content.ReadAsStringAsync(cancellationToken); - var metadata = TryExtractProviderMetadata(responseContent); - var providerResponse = BuildProviderResponseFromMetadata( - string.Empty, - responseContent, - metadata, - (int)response.StatusCode); - - if (!response.IsSuccessStatusCode) - { - _logger.LogError( - "OpenAI API request failed with status {StatusCode}. Response body length: {ResponseLength}.", - response.StatusCode, - responseContent.Length); - return MapFailureOutcome(response.StatusCode, providerResponse); - } - - if (string.IsNullOrWhiteSpace(responseContent)) + var profileTemperature = _configurationResolver.ResolveConfiguredTemperature(operationName); + var resolvedTemperature = temperature ?? profileTemperature; + if (resolvedTemperature.HasValue) { - return AIOperationResult.InvalidOutput(providerResponse); + options.Temperature = (float)resolvedTemperature.Value; } - try - { - using var jsonDoc = JsonDocument.Parse(responseContent); - var choices = jsonDoc.RootElement.GetProperty("choices"); - if (choices.GetArrayLength() > 0) - { - var message = choices[0].GetProperty("message"); - var modelOutput = message.GetProperty("content").GetString(); - return string.IsNullOrWhiteSpace(modelOutput) - ? AIOperationResult.InvalidOutput(providerResponse) - : AIOperationResult.Success(BuildProviderResponseFromMetadata( - modelOutput, - responseContent, - metadata, - (int)response.StatusCode)); - } + var result = await client.CompleteChatAsync( + [ + new SystemChatMessage(resolvedSystemPrompt), + new UserChatMessage(content ?? string.Empty) + ], + options, + cancellationToken); + + var completion = result.Value; + var rawResponse = result.GetRawResponse(); + var responseContent = rawResponse.Content.ToString(); + var modelOutput = completion.Content.Count > 0 ? completion.Content[0].Text : null; + var providerResponse = BuildProviderResponseFromMetadata( + modelOutput ?? string.Empty, + responseContent, + TryExtractProviderMetadata(responseContent), + rawResponse.Status); - return AIOperationResult.InvalidOutput(providerResponse); - } - catch (Exception ex) when (ex is JsonException || ex is KeyNotFoundException || ex is InvalidOperationException) - { - _logger.LogWarning(ex, "AI response payload had an invalid output shape"); - return AIOperationResult.InvalidOutput(providerResponse); - } + return string.IsNullOrWhiteSpace(modelOutput) + ? AIOperationResult.InvalidOutput(providerResponse) + : AIOperationResult.Success(providerResponse); } catch (OperationCanceledException) { @@ -139,6 +100,21 @@ public async Task GenerateSummaryAsync( _logger.LogError(ex, "AI request configuration is invalid."); return AIOperationResult.PermanentFailure(new AIProviderResult(ex.Message)); } + catch (ClientResultException ex) + { + int? statusCode = ex.Status > 0 ? ex.Status : null; + var responseContent = ex.GetRawResponse()?.Content?.ToString() ?? ex.Message; + var providerResponse = BuildProviderResponseFromMetadata( + string.Empty, + responseContent, + TryExtractProviderMetadata(responseContent), + statusCode); + + _logger.LogError(ex, "OpenAI API request failed: {StatusCode} - {Content}", statusCode, responseContent); + return statusCode.HasValue + ? MapFailureOutcome((HttpStatusCode)statusCode.Value, providerResponse) + : AIOperationResult.TransientFailure(providerResponse); + } catch (Exception ex) { _logger.LogError(ex, "Error generating AI summary"); 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 78d3c1414f..83d79cee4d 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 @@ -7,6 +7,7 @@ + diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/OpenAIConfigurationResolverTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/OpenAIConfigurationResolverTests.cs index 98b8aa1fb9..d833942dec 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/OpenAIConfigurationResolverTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/OpenAIConfigurationResolverTests.cs @@ -9,25 +9,6 @@ namespace Unity.GrantManager.AI.Runtime; public class OpenAIConfigurationResolverTests { - [Fact] - public void ResolveApiUrl_Should_CombineEndpointWithLeadingSlashProfilePath() - { - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - { - ["Azure:Operations:Defaults:Provider"] = "OpenAI", - ["Azure:Operations:Defaults:Profile"] = "Gpt4oMini", - ["Azure:OpenAI:Endpoint"] = "https://d837ad-test-recap-webapp.azurewebsites.net", - ["Azure:OpenAI:Profiles:Gpt4oMini:ApiUrl"] = "/openai/deployments/gpt-4o-mini/chat/completions?api-version=2024-02-01" - }) - .Build(); - - var resolver = new OpenAIConfigurationResolver(configuration); - - resolver.ResolveApiUrl().ShouldBe( - "https://d837ad-test-recap-webapp.azurewebsites.net/openai/deployments/gpt-4o-mini/chat/completions?api-version=2024-02-01"); - } - [Fact] public void ResolveProviderName_Should_Throw_When_DefaultProvider_Is_Missing() { @@ -41,41 +22,6 @@ public void ResolveProviderName_Should_Throw_When_DefaultProvider_Is_Missing() ex.Message.ShouldContain("Azure:Operations:Defaults:Provider"); } - [Fact] - public void ResolveApiUrl_Should_Throw_When_Endpoint_Is_Missing() - { - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - { - ["Azure:Operations:Defaults:Provider"] = "OpenAI", - ["Azure:Operations:Defaults:Profile"] = "Gpt4oMini", - ["Azure:OpenAI:Profiles:Gpt4oMini:ApiUrl"] = "/openai/deployments/gpt-4o-mini/chat/completions?api-version=2024-02-01" - }) - .Build(); - - var resolver = new OpenAIConfigurationResolver(configuration); - - var ex = Should.Throw(() => resolver.ResolveApiUrl()); - ex.Message.ShouldContain("Azure:OpenAI:Endpoint"); - } - - [Fact] - public void ResolveMaxTokensParameterNameForOperation_Should_Return_Configured_Profile_Parameter() - { - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - { - ["Azure:Operations:Defaults:Provider"] = "OpenAI", - ["Azure:Operations:Defaults:Profile"] = "Gpt4oMini", - ["Azure:OpenAI:Profiles:Gpt4oMini:MaxTokensParameter"] = "max_tokens" - }) - .Build(); - - var resolver = new OpenAIConfigurationResolver(configuration); - - resolver.ResolveMaxTokensParameterNameForOperation().ShouldBe("max_tokens"); - } - [Fact] public void ResolveCompletionTokens_Should_Throw_When_Operation_And_Default_Are_Missing() { @@ -176,44 +122,6 @@ public void ResolveConfiguredTemperature_Should_Return_Null_When_Profile_Tempera resolver.ResolveConfiguredTemperature().ShouldBeNull(); } - [Fact] - public void ResolveConfiguredReasoningEffort_Should_Return_ProfileValue() - { - var configuration = BuildProfileConfiguration("ReasoningEffort", "minimal"); - var resolver = new OpenAIConfigurationResolver(configuration); - - resolver.ResolveConfiguredReasoningEffort().ShouldBe("minimal"); - } - - [Fact] - public void ResolveConfiguredVerbosity_Should_Return_ProfileValue() - { - var configuration = BuildProfileConfiguration("Verbosity", "low"); - var resolver = new OpenAIConfigurationResolver(configuration); - - resolver.ResolveConfiguredVerbosity().ShouldBe("low"); - } - - [Fact] - public void ResolveConfiguredReasoningEffort_Should_ReturnNull_WhenProfileSettingMissing() - { - var configuration = BuildProfileConfiguration("Temperature", "0.3"); - var resolver = new OpenAIConfigurationResolver(configuration); - - resolver.ResolveConfiguredReasoningEffort().ShouldBeNull(); - } - - [Fact] - public void ResolveConfiguredVerbosity_Should_RejectUnsupportedValue() - { - var configuration = BuildProfileConfiguration("Verbosity", "verbose"); - var resolver = new OpenAIConfigurationResolver(configuration); - - var exception = Should.Throw(() => resolver.ResolveConfiguredVerbosity()); - exception.Message.ShouldContain("Verbosity"); - exception.Message.ShouldContain("low, medium, high"); - } - private static IConfiguration BuildProfileConfiguration(string settingName, string settingValue) { return new ConfigurationBuilder() diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/OpenAITransportServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/OpenAITransportServiceTests.cs index 8a0c272301..28b25a6e56 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/OpenAITransportServiceTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/OpenAITransportServiceTests.cs @@ -2,11 +2,7 @@ using Microsoft.Extensions.Logging.Abstractions; using Shouldly; using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Text; using System.Text.Json; -using System.Threading; using System.Threading.Tasks; using Unity.AI.Runtime; using Xunit; @@ -16,59 +12,21 @@ namespace Unity.GrantManager.AI.Runtime; public class OpenAITransportServiceTests { [Fact] - public async Task GenerateSummaryAsync_Should_Include_ProfileReasoningEffortAndVerbosity() + public async Task GenerateSummaryAsync_Should_ReturnPermanentFailure_When_ProviderNotSupported() { - var handler = new CaptureRequestHandler(); var resolver = CreateResolver(new Dictionary { - ["Azure:OpenAI:Profiles:Gpt5Mini:ReasoningEffort"] = "minimal", - ["Azure:OpenAI:Profiles:Gpt5Mini:Verbosity"] = "low" + ["Azure:Operations:Defaults:Provider"] = "UnsupportedProvider" }); var service = new OpenAITransportService( - new HttpClient(handler), resolver, NullLogger.Instance); - await service.GenerateSummaryAsync("content", "system", 100); - - using var payload = JsonDocument.Parse(handler.RequestContent); - payload.RootElement.GetProperty("reasoning_effort").GetString().ShouldBe("minimal"); - payload.RootElement.GetProperty("verbosity").GetString().ShouldBe("low"); - payload.RootElement.TryGetProperty("temperature", out _).ShouldBeFalse(); - } - - [Fact] - public async Task GenerateSummaryAsync_Should_Omit_RequestTemperature_When_ProfileDoesNotSupportTemperature() - { - var handler = new CaptureRequestHandler(); - var service = new OpenAITransportService( - new HttpClient(handler), - CreateResolver(), - NullLogger.Instance); - - await service.GenerateSummaryAsync("content", "system", 100, temperature: 0.2); - - using var payload = JsonDocument.Parse(handler.RequestContent); - payload.RootElement.TryGetProperty("temperature", out _).ShouldBeFalse(); - } - - [Fact] - public async Task GenerateSummaryAsync_Should_ReturnPermanentFailure_When_ProfileOptionIsInvalid() - { - var handler = new CaptureRequestHandler(); - var service = new OpenAITransportService( - new HttpClient(handler), - CreateResolver(new Dictionary - { - ["Azure:OpenAI:Profiles:Gpt5Mini:Verbosity"] = "verbose" - }), - NullLogger.Instance); - var result = await service.GenerateSummaryAsync("content", "system", 100); result.Outcome.ShouldBe(AIOperationOutcome.PermanentFailure); - handler.SendCount.ShouldBe(0); + result.Response?.Content.ShouldContain("Unsupported provider"); } private static OpenAIConfigurationResolver CreateResolver(Dictionary? overrides = null) @@ -95,37 +53,4 @@ private static OpenAIConfigurationResolver CreateResolver(Dictionary SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - SendCount++; - RequestContent = request.Content == null - ? string.Empty - : await request.Content.ReadAsStringAsync(cancellationToken); - - const string response = """ - { - "choices": [ - { - "message": { - "content": "generated" - }, - "finish_reason": "stop" - } - ] - } - """; - - return new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(response, Encoding.UTF8, "application/json") - }; - } - } } From a82eb9f1450b9bbda3a825e197eae7d0e77b3c9a Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 5 Jun 2026 15:10:22 -0700 Subject: [PATCH 06/15] AB#33009 sdk migration and CHEFS download refactor --- .../AI/Operations/AttachmentSummaryService.cs | 110 +++++++++++++-- .../AI/Runtime/OpenAIChatClientFactory.cs | 20 +++ .../AI/Runtime/OpenAIConfigurationResolver.cs | 46 ++----- .../AI/Runtime/OpenAITransportService.cs | 126 +++++++++++++++--- .../IChefsAttachmentDownloadService.cs | 10 ++ .../Intakes/ChefsAttachmentDownloadService.cs | 91 +++++++++++++ .../ChefsFileAttachmentStreamProvider.cs | 103 +++----------- .../Intakes/SubmissionAppService.cs | 74 ++-------- .../Unity.GrantManager.Web/appsettings.json | 25 ++-- .../AttachmentSummaryServiceTests.cs | 124 +++++++++++++++++ .../OpenAIConfigurationResolverTests.cs | 101 ++++++++++++-- .../AI/Runtime/OpenAITransportServiceTests.cs | 5 +- 12 files changed, 604 insertions(+), 231 deletions(-) create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIChatClientFactory.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Intakes/IChefsAttachmentDownloadService.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/ChefsAttachmentDownloadService.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryService.cs index bef40bc051..7b3a2e562f 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryService.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Localization; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -12,6 +13,7 @@ using Unity.GrantManager.Intakes; using Volo.Abp; using Volo.Abp.DependencyInjection; +using Volo.Abp.Uow; namespace Unity.AI.Operations; @@ -22,18 +24,26 @@ public class AttachmentSummaryService( IAIService aiService, IAIGenerationPrerequisiteValidator aiGenerationPrerequisiteValidator, AIExecutionModeResolver executionModeResolver, + IUnitOfWorkManager unitOfWorkManager, ILogger logger, IStringLocalizer localizer) : IAttachmentSummaryService, ITransientDependency { private const string SummaryGenerationFailedMessage = "AI summary generation failed."; + private const string TextExtractionFailedSummary = "Attachment text could not be extracted for AI summary generation."; public async Task GenerateAndSaveAsync(Guid attachmentId, string? promptVersion = null, CancellationToken cancellationToken = default) { - var attachment = await applicationChefsFileAttachmentRepository.GetAsync(attachmentId); + var attachment = await LoadAttachmentAsync(attachmentId); var fileName = string.IsNullOrWhiteSpace(attachment.FileName) ? "unknown" : attachment.FileName; await using var attachmentStream = await OpenAttachmentStreamAsync(attachment, fileName, cancellationToken); var extractedText = await textExtractionService.ExtractTextAsync(fileName, attachmentStream.Content, attachmentStream.ContentType, cancellationToken); + if (ShouldStopOnEmptyExtraction(fileName, extractedText)) + { + LogEmptyExtraction(attachmentId, fileName, attachmentStream); + await SaveSummaryAsync(attachmentId, TextExtractionFailedSummary); + return TextExtractionFailedSummary; + } var summaryResponse = await aiService.GenerateAttachmentSummaryAsync(new AttachmentSummaryRequest { @@ -43,8 +53,7 @@ public async Task GenerateAndSaveAsync(Guid attachmentId, string? prompt PromptVersion = promptVersion, }, cancellationToken); - attachment.AISummary = summaryResponse.Summary; - await applicationChefsFileAttachmentRepository.UpdateAsync(attachment); + await SaveSummaryAsync(attachmentId, summaryResponse.Summary); return summaryResponse.Summary; } @@ -113,11 +122,9 @@ public async Task> GenerateForApplicationAsync( IReadOnlyCollection? attachmentIds = null, CancellationToken cancellationToken = default) { - await aiGenerationPrerequisiteValidator.EnsureAttachmentSummaryAvailableAsync(applicationId); + await WithUnitOfWorkAsync(() => aiGenerationPrerequisiteValidator.EnsureAttachmentSummaryAvailableAsync(applicationId)); - var applicationAttachmentIds = (await applicationChefsFileAttachmentRepository.GetListAsync(a => a.ApplicationId == applicationId)) - .Select(a => a.Id) - .ToList(); + var applicationAttachmentIds = await LoadApplicationAttachmentIdsAsync(applicationId); if (attachmentIds is not { Count: > 0 }) { @@ -135,8 +142,47 @@ public async Task> GenerateForApplicationAsync( return await GenerateAndSaveAsync(selectedIds, promptVersion, cancellationToken); } + private async Task LoadAttachmentAsync(Guid attachmentId) + { + using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false); + var attachment = await applicationChefsFileAttachmentRepository.GetAsync(attachmentId); + var source = new AttachmentSummarySource( + attachment.Id, + attachment.FileName, + attachment.ChefsSubmissionId, + attachment.ChefsFileId); + await uow.CompleteAsync(); + return source; + } + + private async Task SaveSummaryAsync(Guid attachmentId, string summary) + { + using var uow = unitOfWorkManager.Begin(requiresNew: true); + var attachment = await applicationChefsFileAttachmentRepository.GetAsync(attachmentId); + attachment.AISummary = summary; + await applicationChefsFileAttachmentRepository.UpdateAsync(attachment); + await uow.CompleteAsync(); + } + + private async Task> LoadApplicationAttachmentIdsAsync(Guid applicationId) + { + using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false); + var ids = (await applicationChefsFileAttachmentRepository.GetListAsync(a => a.ApplicationId == applicationId)) + .Select(a => a.Id) + .ToList(); + await uow.CompleteAsync(); + return ids; + } + + private async Task WithUnitOfWorkAsync(Func operation) + { + using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false); + await operation(); + await uow.CompleteAsync(); + } + private async Task OpenAttachmentStreamAsync( - ApplicationChefsFileAttachment attachment, + AttachmentSummarySource attachment, string fileName, CancellationToken cancellationToken) { @@ -168,4 +214,52 @@ private async Task OpenAttachmentStreamAsync( return ChefsFileAttachmentStream.Empty; } } + + private static bool ShouldStopOnEmptyExtraction(string fileName, string extractedText) + { + return string.IsNullOrWhiteSpace(extractedText) && IsSupportedOfficeOrPdf(fileName); + } + + private static bool IsSupportedOfficeOrPdf(string fileName) + { + var extension = Path.GetExtension(fileName)?.ToLowerInvariant(); + return extension is ".pdf" or ".docx" or ".xlsx" or ".xls" or ".pptx"; + } + + private void LogEmptyExtraction( + Guid attachmentId, + string fileName, + ChefsFileAttachmentStream attachmentStream) + { + logger.LogWarning( + "No text extracted for supported attachment {AttachmentId} ({FileName}). Skipping AI summary generation. ContentType: {ContentType}; StreamCanSeek: {StreamCanSeek}; StreamLength: {StreamLength}.", + attachmentId, + fileName, + attachmentStream.ContentType, + attachmentStream.Content.CanSeek, + TryGetStreamLength(attachmentStream.Content)); + } + + private static long? TryGetStreamLength(Stream stream) + { + if (!stream.CanSeek) + { + return null; + } + + try + { + return stream.Length; + } + catch + { + return null; + } + } + + private sealed record AttachmentSummarySource( + Guid Id, + string? FileName, + string? ChefsSubmissionId, + string? ChefsFileId); } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIChatClientFactory.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIChatClientFactory.cs new file mode 100644 index 0000000000..c3e9b0ae57 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIChatClientFactory.cs @@ -0,0 +1,20 @@ +using Azure.AI.OpenAI; +using OpenAI.Chat; +using System.ClientModel; +using Volo.Abp.DependencyInjection; + +namespace Unity.AI.Runtime; + +public class OpenAIChatClientFactory(OpenAIConfigurationResolver configurationResolver) : ITransientDependency +{ + private readonly OpenAIConfigurationResolver _configurationResolver = configurationResolver; + + public ChatClient Create(string? operationName = null) + { + return new AzureOpenAIClient( + _configurationResolver.ResolveEndpoint(operationName), + new ApiKeyCredential(_configurationResolver.ResolveApiKey(operationName)), + new AzureOpenAIClientOptions()) + .GetChatClient(_configurationResolver.ResolveDeploymentName(operationName)); + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIConfigurationResolver.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIConfigurationResolver.cs index c7da8513da..955d05953a 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIConfigurationResolver.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIConfigurationResolver.cs @@ -43,6 +43,14 @@ public string ResolveApiKey(string? operationName = null) return null; } + public bool ResolveMaxOutputTokenCountSupported(string? operationName = null) + { + var providerName = ResolveProviderName(operationName); + var profileName = ResolveProfileName(operationName); + var configuredValue = OptionalProfile(providerName, profileName, "MaxOutputTokenCountSupported"); + return configuredValue == null || bool.Parse(configuredValue); + } + public int ResolveCompletionTokens(string operationName) { var configuredValue = OptionalPositiveInt($"Azure:Operations:{operationName}:MaxCompletionTokens"); @@ -77,43 +85,7 @@ public string ResolveDeploymentName(string? operationName = null) { var providerName = ResolveProviderName(operationName); var profileName = ResolveProfileName(operationName); - var profileApiUrl = RequiredProfile(providerName, profileName, "ApiUrl"); - // Extract deployment name from URL like "/openai/deployments/gpt-5-mini/chat/completions?api-version=..." - var parts = profileApiUrl.Split('/', StringSplitOptions.RemoveEmptyEntries); - var deploymentIndex = Array.IndexOf(parts, "deployments"); - if (deploymentIndex >= 0 && deploymentIndex + 1 < parts.Length) - { - var raw = parts[deploymentIndex + 1]; - // Strip query string if present (e.g., "gpt-5-mini?api-version=...") - var deployment = raw.Contains('?') ? raw[..raw.IndexOf('?')] : raw; - return deployment; - } - throw new InvalidOperationException($"Could not extract deployment name from API URL: {profileApiUrl}"); - } - - public string? ResolveApiVersion(string? operationName = null) - { - var providerName = ResolveProviderName(operationName); - var profileName = ResolveProfileName(operationName); - var profileApiUrl = RequiredProfile(providerName, profileName, "ApiUrl"); - // Extract api-version from query string like "?api-version=2024-10-01-preview" - var queryString = profileApiUrl.Contains('?') ? profileApiUrl.Substring(profileApiUrl.IndexOf('?') + 1) : null; - if (string.IsNullOrEmpty(queryString)) - { - return null; - } - - var parameters = queryString.Split('&'); - foreach (var param in parameters) - { - var keyValue = param.Split('='); - if (keyValue.Length == 2 && keyValue[0] == "api-version") - { - return Uri.UnescapeDataString(keyValue[1]); - } - } - - return null; + return RequiredProfile(providerName, profileName, "DeploymentName"); } private string ResolveProfileName(string? operationName) diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAITransportService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAITransportService.cs index 0cf9b374bc..2dd136cba6 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAITransportService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAITransportService.cs @@ -1,4 +1,3 @@ -using Azure.AI.OpenAI; using Microsoft.Extensions.Logging; using OpenAI.Chat; using System; @@ -14,9 +13,11 @@ namespace Unity.AI.Runtime; public class OpenAITransportService( OpenAIConfigurationResolver configurationResolver, + OpenAIChatClientFactory chatClientFactory, ILogger logger) : ITransientDependency { private readonly OpenAIConfigurationResolver _configurationResolver = configurationResolver; + private readonly OpenAIChatClientFactory _chatClientFactory = chatClientFactory; private readonly ILogger _logger = logger; public async Task GenerateSummaryAsync( @@ -38,30 +39,16 @@ public async Task GenerateSummaryAsync( return AIOperationResult.PermanentFailure(new AIProviderResult($"Unsupported provider: {providerName}")); } - var apiKey = _configurationResolver.ResolveApiKey(operationName); var resolvedSystemPrompt = string.IsNullOrWhiteSpace(systemPrompt) ? "You are a professional grant analyst for the BC Government." : systemPrompt; - var clientOptions = new AzureOpenAIClientOptions(); - var resolvedApiVersion = _configurationResolver.ResolveApiVersion(operationName); - // Note: Azure SDK uses its default API version. Configured version is parsed for documentation/future use. - if (!string.IsNullOrEmpty(resolvedApiVersion)) + var options = new ChatCompletionOptions(); + if (_configurationResolver.ResolveMaxOutputTokenCountSupported(operationName)) { - _logger.LogDebug("Profile specifies API version {ApiVersion}; SDK will use its default", resolvedApiVersion); + options.MaxOutputTokenCount = maxTokens; } - var client = new AzureOpenAIClient( - _configurationResolver.ResolveEndpoint(operationName), - new ApiKeyCredential(apiKey), - clientOptions) - .GetChatClient(_configurationResolver.ResolveDeploymentName(operationName)); - - var options = new ChatCompletionOptions - { - MaxOutputTokenCount = maxTokens - }; - var profileTemperature = _configurationResolver.ResolveConfiguredTemperature(operationName); var resolvedTemperature = temperature ?? profileTemperature; if (resolvedTemperature.HasValue) @@ -69,7 +56,7 @@ public async Task GenerateSummaryAsync( options.Temperature = (float)resolvedTemperature.Value; } - var result = await client.CompleteChatAsync( + var result = await _chatClientFactory.Create(operationName).CompleteChatAsync( [ new SystemChatMessage(resolvedSystemPrompt), new UserChatMessage(content ?? string.Empty) @@ -80,13 +67,18 @@ public async Task GenerateSummaryAsync( var completion = result.Value; var rawResponse = result.GetRawResponse(); var responseContent = rawResponse.Content.ToString(); - var modelOutput = completion.Content.Count > 0 ? completion.Content[0].Text : null; + var modelOutput = ExtractModelOutput(completion, responseContent); var providerResponse = BuildProviderResponseFromMetadata( modelOutput ?? string.Empty, responseContent, TryExtractProviderMetadata(responseContent), rawResponse.Status); + if (string.IsNullOrWhiteSpace(modelOutput)) + { + LogEmptyModelOutput(completion, providerResponse, rawResponse.Status); + } + return string.IsNullOrWhiteSpace(modelOutput) ? AIOperationResult.InvalidOutput(providerResponse) : AIOperationResult.Success(providerResponse); @@ -110,7 +102,11 @@ public async Task GenerateSummaryAsync( TryExtractProviderMetadata(responseContent), statusCode); - _logger.LogError(ex, "OpenAI API request failed: {StatusCode} - {Content}", statusCode, responseContent); + _logger.LogError( + ex, + "OpenAI API request failed with status {StatusCode}. Response body length: {ResponseLength}.", + statusCode, + responseContent.Length); return statusCode.HasValue ? MapFailureOutcome((HttpStatusCode)statusCode.Value, providerResponse) : AIOperationResult.TransientFailure(providerResponse); @@ -151,6 +147,94 @@ private static AIProviderResult BuildProviderResponseFromMetadata( metadata?.ReasoningTokens); } + private void LogEmptyModelOutput(ChatCompletion completion, AIProviderResult response, int statusCode) + { + _logger.LogWarning( + "AI model output was empty. StatusCode: {StatusCode}; FinishReason: {FinishReason}; ContentPartCount: {ContentPartCount}; PromptTokens: {PromptTokens}; CompletionTokens: {CompletionTokens}; TotalTokens: {TotalTokens}; ReasoningTokens: {ReasoningTokens}.", + statusCode, + response.FinishReason, + completion.Content.Count, + response.PromptTokens, + response.CompletionTokens, + response.TotalTokens, + response.ReasoningTokens); + } + + private static string? ExtractModelOutput(ChatCompletion completion, string? responseContent) + { + if (completion.Content.Count > 0 && !string.IsNullOrWhiteSpace(completion.Content[0].Text)) + { + return completion.Content[0].Text; + } + + return TryExtractMessageContent(responseContent); + } + + private static string? TryExtractMessageContent(string? responseContent) + { + if (string.IsNullOrWhiteSpace(responseContent)) + { + return null; + } + + try + { + using var jsonDoc = JsonDocument.Parse(responseContent); + if (!TryGetFirstChoice(jsonDoc.RootElement, out var firstChoice) + || !firstChoice.TryGetProperty("message", out var message) + || !message.TryGetProperty("content", out var content)) + { + return null; + } + + if (content.ValueKind == JsonValueKind.String) + { + return content.GetString(); + } + + if (content.ValueKind == JsonValueKind.Array) + { + return ExtractTextContentParts(content); + } + } + catch (JsonException) + { + return null; + } + + return null; + } + + private static bool TryGetFirstChoice(JsonElement root, out JsonElement firstChoice) + { + firstChoice = default; + if (!root.TryGetProperty("choices", out var choices) + || choices.ValueKind != JsonValueKind.Array + || choices.GetArrayLength() == 0) + { + return false; + } + + firstChoice = choices[0]; + return true; + } + + private static string? ExtractTextContentParts(JsonElement content) + { + var parts = new List(); + foreach (var part in content.EnumerateArray()) + { + if (part.ValueKind == JsonValueKind.Object + && part.TryGetProperty("text", out var text) + && text.ValueKind == JsonValueKind.String) + { + parts.Add(text.GetString() ?? string.Empty); + } + } + + return parts.Count > 0 ? string.Concat(parts) : null; + } + private static AIProviderResponseMetadata? TryExtractProviderMetadata(string? responseContent) { if (string.IsNullOrWhiteSpace(responseContent)) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Intakes/IChefsAttachmentDownloadService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Intakes/IChefsAttachmentDownloadService.cs new file mode 100644 index 0000000000..6ff49c8198 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Intakes/IChefsAttachmentDownloadService.cs @@ -0,0 +1,10 @@ +using System; +using System.Threading.Tasks; +using Unity.GrantManager.Attachments; + +namespace Unity.GrantManager.Intakes; + +public interface IChefsAttachmentDownloadService +{ + Task DownloadAsync(Guid? formSubmissionId, Guid? chefsFileAttachmentId, string name); +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/ChefsAttachmentDownloadService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/ChefsAttachmentDownloadService.cs new file mode 100644 index 0000000000..a9d086f24d --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/ChefsAttachmentDownloadService.cs @@ -0,0 +1,91 @@ +using System; +using System.Net.Http; +using System.Linq; +using System.Threading.Tasks; +using Unity.GrantManager.Applications; +using Unity.GrantManager.Attachments; +using Unity.GrantManager.Integrations; +using Unity.Modules.Shared.Http; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Linq; +using Volo.Abp.Security.Encryption; + +namespace Unity.GrantManager.Intakes; + +public class ChefsAttachmentDownloadService( + IApplicationFormSubmissionRepository applicationFormSubmissionRepository, + IRepository applicationFormRepository, + IAsyncQueryableExecuter asyncExecuter, + IEndpointManagementAppService endpointManagementAppService, + IResilientHttpRequest resilientRestClient, + IStringEncryptionService stringEncryptionService) : IChefsAttachmentDownloadService, ITransientDependency +{ + public async Task DownloadAsync(Guid? formSubmissionId, Guid? chefsFileAttachmentId, string name) + { + if (formSubmissionId == null) + { + throw new ApiException(400, "Missing required parameter 'formId' when calling GetSubmission"); + } + + if (chefsFileAttachmentId == null) + { + throw new ApiException(400, "Missing required parameter 'chefsFileAttachmentId' when calling GetFileAttachment"); + } + + ApplicationForm? applicationForm = await GetApplicationFormBySubmissionId(formSubmissionId) + ?? throw new ApiException(400, "Missing Form configuration"); + + if (applicationForm.ChefsApplicationFormGuid == null) + { + throw new ApiException(400, "Missing CHEFS form Id"); + } + + if (applicationForm.ApiKey == null) + { + throw new ApiException(400, "Missing CHEFS Api Key"); + } + + string chefsApi = await endpointManagementAppService.GetChefsApiBaseUrlAsync(); + string url = $"{chefsApi}/files/{chefsFileAttachmentId}"; + var decryptedApiKey = stringEncryptionService.Decrypt(applicationForm.ApiKey!); + + var response = await resilientRestClient.HttpAsync( + HttpMethod.Get, + url, + null, + null, + basicAuth: (applicationForm.ChefsApplicationFormGuid!, decryptedApiKey ?? string.Empty) + ); + + if (((int)response.StatusCode) != 200) + { + var errorContent = response.Content != null ? await response.Content.ReadAsStringAsync() : string.Empty; + throw new ApiException((int)response.StatusCode, "Error calling GetChefsFileAttachment: " + errorContent, response.ReasonPhrase ?? $"{response.StatusCode}"); + } + + var contentBytes = response.Content != null ? await response.Content.ReadAsByteArrayAsync() : []; + var contentType = response.Content?.Headers?.ContentType?.MediaType ?? "application/octet-stream"; + if (!string.IsNullOrEmpty(name) && name.Contains('%')) + { + name = Uri.UnescapeDataString(name); + } + + return new BlobDto { Name = name, Content = contentBytes, ContentType = contentType }; + } + + private async Task GetApplicationFormBySubmissionId(Guid? formSubmissionId) + { + ApplicationForm? applicationFormData = new(); + + if (formSubmissionId != null) + { + var query = from applicationSubmission in await applicationFormSubmissionRepository.GetQueryableAsync() + join applicationForm in await applicationFormRepository.GetQueryableAsync() on applicationSubmission.ApplicationFormId equals applicationForm.Id + where applicationSubmission.ChefsSubmissionGuid == formSubmissionId.ToString() + select applicationForm; + applicationFormData = await asyncExecuter.FirstOrDefaultAsync(query); + } + return applicationFormData; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/ChefsFileAttachmentStreamProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/ChefsFileAttachmentStreamProvider.cs index d043403326..07e9587f77 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/ChefsFileAttachmentStreamProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/ChefsFileAttachmentStreamProvider.cs @@ -1,102 +1,41 @@ +using Microsoft.Extensions.Logging; using System; using System.IO; -using System.Net.Http; using System.Threading.Tasks; -using Unity.GrantManager.Applications; -using Unity.GrantManager.Integrations; -using Unity.Modules.Shared.Http; using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; -using Volo.Abp.Security.Encryption; namespace Unity.GrantManager.Intakes; public class ChefsFileAttachmentStreamProvider( - IApplicationFormSubmissionRepository applicationFormSubmissionRepository, - IRepository applicationFormRepository, - IEndpointManagementAppService endpointManagementAppService, - IResilientHttpRequest resilientRestClient, - IStringEncryptionService stringEncryptionService) + IChefsAttachmentDownloadService chefsAttachmentDownloadService, + ILogger logger) : IChefsFileAttachmentStreamProvider, ITransientDependency { public async Task OpenAsync(Guid formSubmissionId, Guid chefsFileAttachmentId, string name) { - var applicationForm = await GetApplicationFormBySubmissionId(formSubmissionId) - ?? throw new ApiException(400, "Missing Form configuration"); - - if (applicationForm.ChefsApplicationFormGuid == null) - { - throw new ApiException(400, "Missing CHEFS form Id"); - } - - if (applicationForm.ApiKey == null) - { - throw new ApiException(400, "Missing CHEFS Api Key"); - } - - var chefsApi = await endpointManagementAppService.GetChefsApiBaseUrlAsync(); - var url = $"{chefsApi}/files/{chefsFileAttachmentId}"; - var decryptedApiKey = stringEncryptionService.Decrypt(applicationForm.ApiKey); - - using var response = await resilientRestClient.HttpAsync( - HttpMethod.Get, - url, - null, - null, - basicAuth: (applicationForm.ChefsApplicationFormGuid, decryptedApiKey ?? string.Empty), - completionOption: HttpCompletionOption.ResponseHeadersRead - ); - - if (((int)response.StatusCode) != 200) - { - var errorContent = response.Content != null ? await response.Content.ReadAsStringAsync() : string.Empty; - throw new ApiException((int)response.StatusCode, "Error calling GetChefsFileAttachment: " + errorContent, response.ReasonPhrase ?? $"{response.StatusCode}"); - } - - var contentType = response.Content?.Headers?.ContentType?.MediaType ?? "application/octet-stream"; - var extension = !string.IsNullOrEmpty(name) ? Path.GetExtension(Uri.UnescapeDataString(name)) : string.Empty; - var tempPath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}{extension}"); - try { - await using (var writeStream = new FileStream(tempPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, 81920, FileOptions.Asynchronous | FileOptions.SequentialScan)) - await using (var contentStream = await response.Content!.ReadAsStreamAsync()) - { - await contentStream.CopyToAsync(writeStream); - } - - var readStream = new FileStream(tempPath, FileMode.Open, FileAccess.Read, FileShare.Read, 81920, FileOptions.Asynchronous | FileOptions.SequentialScan | FileOptions.DeleteOnClose); - return new ChefsFileAttachmentStream(readStream, contentType); + var file = await chefsAttachmentDownloadService.DownloadAsync(formSubmissionId, chefsFileAttachmentId, name); + var content = file.Content ?? []; + var stream = new MemoryStream(content, writable: false); + + logger.LogInformation( + "Opened CHEFS attachment {ChefsFileAttachmentId} for submission {FormSubmissionId}. ContentType: {ContentType}; DownloadedLength: {DownloadedLength}.", + chefsFileAttachmentId, + formSubmissionId, + file.ContentType, + content.Length); + + return new ChefsFileAttachmentStream(stream, file.ContentType); } - catch + catch (Exception ex) { - TryDeleteTempFile(tempPath); + logger.LogWarning( + ex, + "Failed to open CHEFS attachment {ChefsFileAttachmentId} for submission {FormSubmissionId}.", + chefsFileAttachmentId, + formSubmissionId); throw; } } - - private async Task GetApplicationFormBySubmissionId(Guid formSubmissionId) - { - var submission = await applicationFormSubmissionRepository.FirstOrDefaultAsync( - x => x.ChefsSubmissionGuid == formSubmissionId.ToString()); - - return submission == null - ? null - : await applicationFormRepository.FirstOrDefaultAsync(x => x.Id == submission.ApplicationFormId); - } - - private static void TryDeleteTempFile(string tempPath) - { - try - { - if (File.Exists(tempPath)) - { - File.Delete(tempPath); - } - } - catch - { - // Best-effort cleanup; never throw from cleanup path. - } - } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/SubmissionAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/SubmissionAppService.cs index 66efab4511..56fcf4a3bb 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/SubmissionAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/SubmissionAppService.cs @@ -21,15 +21,16 @@ namespace Unity.GrantManager.Intakes; [Authorize] -public class SubmissionAppService( - IApplicationFormSubmissionRepository applicationFormSubmissionRepository, - IRepository applicationFormRepository, - IEndpointManagementAppService endpointManagementAppService, - IResilientHttpRequest resilientRestClient, - IStringEncryptionService stringEncryptionService, - IApplicationRepository applicationRepository, - ITenantRepository tenantRepository - ) : GrantManagerAppService, ISubmissionAppService +public class SubmissionAppService( + IApplicationFormSubmissionRepository applicationFormSubmissionRepository, + IRepository applicationFormRepository, + IEndpointManagementAppService endpointManagementAppService, + IResilientHttpRequest resilientRestClient, + IStringEncryptionService stringEncryptionService, + IChefsAttachmentDownloadService chefsAttachmentDownloadService, + IApplicationRepository applicationRepository, + ITenantRepository tenantRepository + ) : GrantManagerAppService, ISubmissionAppService { protected new ILogger Logger => LazyServiceProvider.LazyGetService(provider => LoggerFactory?.CreateLogger(GetType().FullName!) ?? NullLogger.Instance); @@ -59,57 +60,10 @@ ITenantRepository tenantRepository } [AllowAnonymous] - public async Task GetChefsFileAttachment(Guid? formSubmissionId, Guid? chefsFileAttachmentId, string name) - { - if (formSubmissionId == null) - { - throw new ApiException(400, "Missing required parameter 'formId' when calling GetSubmission"); - } - - if (chefsFileAttachmentId == null) - { - throw new ApiException(400, "Missing required parameter 'chefsFileAttachmentId' when calling GetFileAttachment"); - } - - ApplicationForm? applicationForm = await GetApplicationFormBySubmissionId(formSubmissionId) ?? throw new ApiException(400, "Missing Form configuration"); - if (applicationForm.ChefsApplicationFormGuid == null) - { - throw new ApiException(400, "Missing CHEFS form Id"); - } - - if (applicationForm.ApiKey == null) - { - throw new ApiException(400, "Missing CHEFS Api Key"); - } - - string chefsApi = await endpointManagementAppService.GetChefsApiBaseUrlAsync(); - string url = $"{chefsApi}/files/{chefsFileAttachmentId}"; - var decryptedApiKey = stringEncryptionService.Decrypt(applicationForm.ApiKey!); - - var response = await resilientRestClient.HttpAsync( - HttpMethod.Get, - url, - null, - null, - basicAuth: (applicationForm.ChefsApplicationFormGuid!, decryptedApiKey ?? string.Empty) - ); - - if (((int)response.StatusCode) != 200) - { - var errorContent = response.Content != null ? await response.Content.ReadAsStringAsync() : string.Empty; - throw new ApiException((int)response.StatusCode, "Error calling GetChefsFileAttachment: " + errorContent, response.ReasonPhrase ?? $"{response.StatusCode}"); - } - - var contentBytes = response.Content != null ? await response.Content.ReadAsByteArrayAsync() : []; - var contentType = response.Content?.Headers?.ContentType?.MediaType ?? "application/octet-stream"; - // Check and decode the file name if it is URL encoded - if (!string.IsNullOrEmpty(name) && name.Contains('%')) - { - name = Uri.UnescapeDataString(name); - } - - return new BlobDto { Name = name, Content = contentBytes, ContentType = contentType }; - } + public async Task GetChefsFileAttachment(Guid? formSubmissionId, Guid? chefsFileAttachmentId, string name) + { + return await chefsAttachmentDownloadService.DownloadAsync(formSubmissionId, chefsFileAttachmentId, name); + } public async Task GetApplicationFormBySubmissionId(Guid? formSubmissionId) { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.json b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.json index aca4528f2b..54b7f51937 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.json +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.json @@ -174,18 +174,19 @@ "ApiKey": "", "Endpoint": "", "Profiles": { - "Gpt4oMini": { - "ApiUrl": "/openai/deployments/gpt-4o-mini/chat/completions?api-version=2024-02-01", - "MaxTokensParameter": "max_tokens", - "Temperature": 0.3 - }, - "Gpt5Mini": { - "ApiUrl": "/openai/deployments/gpt-5-mini/chat/completions?api-version=2024-10-01-preview", - "MaxTokensParameter": "max_completion_tokens", - "ReasoningEffort": "minimal", - "Verbosity": "low" - } - } + "Gpt4oMini": { + "DeploymentName": "gpt-4o-mini", + "Temperature": 0.3 + }, + "Gpt5Mini": { + "DeploymentName": "gpt-5-mini", + "MaxOutputTokenCountSupported": false + }, + "Gpt5Nano": { + "DeploymentName": "gpt-5-nano", + "MaxOutputTokenCountSupported": false + } + } }, "Logging": { "EnablePromptFileLog": false diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/AttachmentSummaryServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/AttachmentSummaryServiceTests.cs index 81574bb842..bef3cf2ade 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/AttachmentSummaryServiceTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/AttachmentSummaryServiceTests.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; +using NPOI.XWPF.UserModel; using Shouldly; using System; using System.IO; @@ -16,6 +17,7 @@ using Unity.GrantManager.Applications; using Unity.GrantManager.Intakes; using Volo.Abp; +using Volo.Abp.Uow; using Xunit; namespace Unity.GrantManager.AI.Operations; @@ -55,6 +57,7 @@ public async Task GenerateAndSaveAsync_Uses_Streamed_Attachment_Text() .Returns(new AttachmentSummaryResponse { Summary = "summary text" }); var prerequisiteValidator = Substitute.For(); + var unitOfWorkManager = CreateUnitOfWorkManager(); var service = new AttachmentSummaryService( attachmentRepository, @@ -63,6 +66,7 @@ public async Task GenerateAndSaveAsync_Uses_Streamed_Attachment_Text() aiService, prerequisiteValidator, new AIExecutionModeResolver(new ConfigurationBuilder().Build()), + unitOfWorkManager, NullLogger.Instance, Substitute.For>()); @@ -104,6 +108,7 @@ public async Task GenerateAndSaveAsync_Should_Propagate_Cancellation() var streamProvider = Substitute.For(); streamProvider.OpenAsync(submissionId, fileId, "test.txt") .Returns(new ChefsFileAttachmentStream(stream, "text/plain")); + var unitOfWorkManager = CreateUnitOfWorkManager(); var service = new AttachmentSummaryService( attachmentRepository, @@ -112,6 +117,7 @@ public async Task GenerateAndSaveAsync_Should_Propagate_Cancellation() Substitute.For(), Substitute.For(), new AIExecutionModeResolver(new ConfigurationBuilder().Build()), + unitOfWorkManager, NullLogger.Instance, Substitute.For>()); @@ -127,6 +133,7 @@ public async Task GenerateAndSaveAsync_Should_Reject_Empty_Attachment_List() var textExtractionService = Substitute.For(); var aiService = Substitute.For(); var prerequisiteValidator = Substitute.For(); + var unitOfWorkManager = CreateUnitOfWorkManager(); var service = new AttachmentSummaryService( attachmentRepository, @@ -135,6 +142,7 @@ public async Task GenerateAndSaveAsync_Should_Reject_Empty_Attachment_List() aiService, prerequisiteValidator, new AIExecutionModeResolver(new ConfigurationBuilder().Build()), + unitOfWorkManager, NullLogger.Instance, Substitute.For>()); @@ -142,4 +150,120 @@ public async Task GenerateAndSaveAsync_Should_Reject_Empty_Attachment_List() await aiService.DidNotReceive().GenerateAttachmentSummaryAsync(Arg.Any()); } + + [Fact] + public async Task GenerateAndSaveAsync_Should_Not_Call_AI_When_Supported_File_Extraction_Is_Empty() + { + var attachmentId = Guid.NewGuid(); + var submissionId = Guid.NewGuid(); + var fileId = Guid.NewGuid(); + var stream = new MemoryStream([1, 2, 3]); + + var attachment = new ApplicationChefsFileAttachment + { + ApplicationId = Guid.NewGuid(), + FileName = "test.docx", + ChefsSubmissionId = submissionId.ToString(), + ChefsFileId = fileId.ToString() + }; + + var attachmentRepository = Substitute.For(); + attachmentRepository.GetAsync(attachmentId).Returns(attachment); + + var streamProvider = Substitute.For(); + streamProvider.OpenAsync(submissionId, fileId, "test.docx") + .Returns(new ChefsFileAttachmentStream(stream, "application/vnd.openxmlformats-officedocument.wordprocessingml.document")); + + var textExtractionService = Substitute.For(); + textExtractionService.ExtractTextAsync("test.docx", stream, "application/vnd.openxmlformats-officedocument.wordprocessingml.document") + .Returns(string.Empty); + + var aiService = Substitute.For(); + var service = new AttachmentSummaryService( + attachmentRepository, + streamProvider, + textExtractionService, + aiService, + Substitute.For(), + new AIExecutionModeResolver(new ConfigurationBuilder().Build()), + CreateUnitOfWorkManager(), + NullLogger.Instance, + Substitute.For>()); + + var result = await service.GenerateAndSaveAsync(attachmentId, "v1"); + + result.ShouldBe("Attachment text could not be extracted for AI summary generation."); + attachment.AISummary.ShouldBe(result); + await aiService.DidNotReceive().GenerateAttachmentSummaryAsync(Arg.Any()); + await attachmentRepository.Received(1).UpdateAsync(attachment); + } + + [Fact] + public async Task GenerateAndSaveAsync_Should_Pass_Extracted_Docx_Text_To_AI() + { + var attachmentId = Guid.NewGuid(); + var submissionId = Guid.NewGuid(); + var fileId = Guid.NewGuid(); + var stream = CreateDocxStream("Riverside mock attachment content"); + AttachmentSummaryRequest? capturedRequest = null; + + var attachment = new ApplicationChefsFileAttachment + { + ApplicationId = Guid.NewGuid(), + FileName = "riverside-profile.docx", + ChefsSubmissionId = submissionId.ToString(), + ChefsFileId = fileId.ToString() + }; + + var attachmentRepository = Substitute.For(); + attachmentRepository.GetAsync(attachmentId).Returns(attachment); + + var streamProvider = Substitute.For(); + streamProvider.OpenAsync(submissionId, fileId, "riverside-profile.docx") + .Returns(new ChefsFileAttachmentStream(stream, "application/vnd.openxmlformats-officedocument.wordprocessingml.document")); + + var aiService = Substitute.For(); + aiService.GenerateAttachmentSummaryAsync(Arg.Do(request => capturedRequest = request)) + .Returns(new AttachmentSummaryResponse { Summary = "summary text" }); + + var service = new AttachmentSummaryService( + attachmentRepository, + streamProvider, + new TextExtractionService(NullLogger.Instance), + aiService, + Substitute.For(), + new AIExecutionModeResolver(new ConfigurationBuilder().Build()), + CreateUnitOfWorkManager(), + NullLogger.Instance, + Substitute.For>()); + + var result = await service.GenerateAndSaveAsync(attachmentId, "v1"); + + result.ShouldBe("summary text"); + capturedRequest.ShouldNotBeNull(); + capturedRequest.ExtractedText.ShouldNotBeNull(); + capturedRequest.ExtractedText.ShouldContain("Riverside mock attachment content"); + await aiService.Received(1).GenerateAttachmentSummaryAsync(Arg.Any()); + } + + private static MemoryStream CreateDocxStream(string paragraphText) + { + var writeStream = new MemoryStream(); + using (var document = new XWPFDocument()) + { + document.CreateParagraph().CreateRun().SetText(paragraphText); + document.Write(writeStream); + } + + return new MemoryStream(writeStream.ToArray()); + } + + private static IUnitOfWorkManager CreateUnitOfWorkManager() + { + var unitOfWork = Substitute.For(); + var unitOfWorkManager = Substitute.For(); + unitOfWorkManager.Begin(Arg.Any(), Arg.Any()) + .Returns(unitOfWork); + return unitOfWorkManager; + } } diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/OpenAIConfigurationResolverTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/OpenAIConfigurationResolverTests.cs index d833942dec..2c62d8dc35 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/OpenAIConfigurationResolverTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/OpenAIConfigurationResolverTests.cs @@ -80,6 +80,75 @@ public void ResolveApiKey_Should_Throw_When_Configured_Key_Is_Missing() ex.Message.ShouldContain("Azure:OpenAI:ApiKey"); } + [Fact] + public void ResolveEndpoint_Should_Return_Configured_Provider_Endpoint() + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["Azure:Operations:Defaults:Provider"] = "OpenAI", + ["Azure:OpenAI:Endpoint"] = "https://example.test" + }) + .Build(); + + var resolver = new OpenAIConfigurationResolver(configuration); + + resolver.ResolveEndpoint().ShouldBe(new Uri("https://example.test")); + } + + [Fact] + public void ResolveDeploymentName_Should_Return_Profile_DeploymentName() + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["Azure:Operations:Defaults:Provider"] = "OpenAI", + ["Azure:Operations:Defaults:Profile"] = "Gpt5Mini", + ["Azure:OpenAI:Profiles:Gpt5Mini:DeploymentName"] = "gpt-5-mini" + }) + .Build(); + + var resolver = new OpenAIConfigurationResolver(configuration); + + resolver.ResolveDeploymentName().ShouldBe("gpt-5-mini"); + } + + [Fact] + public void ResolveDeploymentName_Should_Use_Operation_Profile_Override() + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["Azure:Operations:Defaults:Provider"] = "OpenAI", + ["Azure:Operations:Defaults:Profile"] = "Gpt5Mini", + ["Azure:Operations:ApplicationAnalysis:Profile"] = "Gpt4oMini", + ["Azure:OpenAI:Profiles:Gpt5Mini:DeploymentName"] = "gpt-5-mini", + ["Azure:OpenAI:Profiles:Gpt4oMini:DeploymentName"] = "gpt-4o-mini" + }) + .Build(); + + var resolver = new OpenAIConfigurationResolver(configuration); + + resolver.ResolveDeploymentName("ApplicationAnalysis").ShouldBe("gpt-4o-mini"); + } + + [Fact] + public void ResolveDeploymentName_Should_Throw_When_Profile_DeploymentName_Is_Missing() + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["Azure:Operations:Defaults:Provider"] = "OpenAI", + ["Azure:Operations:Defaults:Profile"] = "Gpt5Mini" + }) + .Build(); + + var resolver = new OpenAIConfigurationResolver(configuration); + + var ex = Should.Throw(() => resolver.ResolveDeploymentName()); + ex.Message.ShouldContain("Azure:OpenAI:Profiles:Gpt5Mini:DeploymentName"); + } + [Fact] public void ResolveConfiguredTemperature_Should_Return_Profile_Temperature() { @@ -122,15 +191,31 @@ public void ResolveConfiguredTemperature_Should_Return_Null_When_Profile_Tempera resolver.ResolveConfiguredTemperature().ShouldBeNull(); } - private static IConfiguration BuildProfileConfiguration(string settingName, string settingValue) + [Theory] + [InlineData(null, true)] + [InlineData("true", true)] + [InlineData("false", false)] + public void ResolveMaxOutputTokenCountSupported_Should_Use_Profile_Setting_Or_Default_True( + string? configuredValue, + bool expected) { - return new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - { - ["Azure:Operations:Defaults:Provider"] = "OpenAI", - ["Azure:Operations:Defaults:Profile"] = "Gpt5Mini", - [$"Azure:OpenAI:Profiles:Gpt5Mini:{settingName}"] = settingValue - }) + var values = new Dictionary + { + ["Azure:Operations:Defaults:Provider"] = "OpenAI", + ["Azure:Operations:Defaults:Profile"] = "Gpt5Mini" + }; + + if (configuredValue != null) + { + values["Azure:OpenAI:Profiles:Gpt5Mini:MaxOutputTokenCountSupported"] = configuredValue; + } + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(values) .Build(); + + var resolver = new OpenAIConfigurationResolver(configuration); + + resolver.ResolveMaxOutputTokenCountSupported().ShouldBe(expected); } } diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/OpenAITransportServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/OpenAITransportServiceTests.cs index 28b25a6e56..18636b3c60 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/OpenAITransportServiceTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/OpenAITransportServiceTests.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.Logging.Abstractions; using Shouldly; using System.Collections.Generic; -using System.Text.Json; using System.Threading.Tasks; using Unity.AI.Runtime; using Xunit; @@ -21,6 +20,7 @@ public async Task GenerateSummaryAsync_Should_ReturnPermanentFailure_When_Provid var service = new OpenAITransportService( resolver, + new OpenAIChatClientFactory(resolver), NullLogger.Instance); var result = await service.GenerateSummaryAsync("content", "system", 100); @@ -37,8 +37,7 @@ private static OpenAIConfigurationResolver CreateResolver(Dictionary Date: Fri, 5 Jun 2026 15:48:18 -0700 Subject: [PATCH 07/15] AB#33009 make GPT4oMini token support explicit --- .../src/Unity.GrantManager.Web/appsettings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.json b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.json index 54b7f51937..4dcd3cf17d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.json +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.json @@ -176,6 +176,7 @@ "Profiles": { "Gpt4oMini": { "DeploymentName": "gpt-4o-mini", + "MaxOutputTokenCountSupported": true, "Temperature": 0.3 }, "Gpt5Mini": { From e96688017f4c617cb6bbf9f2c75bc515f590f4d0 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 5 Jun 2026 16:21:49 -0700 Subject: [PATCH 08/15] AB#33009 address SDK review feedback --- .../AI/Runtime/OpenAIConfigurationResolver.cs | 15 +++++++++++++-- .../AI/Runtime/OpenAITransportService.cs | 13 +++++++++++-- .../Intakes/ChefsAttachmentDownloadService.cs | 3 ++- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIConfigurationResolver.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIConfigurationResolver.cs index 955d05953a..ff8a6298b5 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIConfigurationResolver.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIConfigurationResolver.cs @@ -47,8 +47,19 @@ public bool ResolveMaxOutputTokenCountSupported(string? operationName = null) { var providerName = ResolveProviderName(operationName); var profileName = ResolveProfileName(operationName); - var configuredValue = OptionalProfile(providerName, profileName, "MaxOutputTokenCountSupported"); - return configuredValue == null || bool.Parse(configuredValue); + var key = ProfileKey(providerName, profileName, "MaxOutputTokenCountSupported"); + var configuredValue = Optional(key); + if (configuredValue == null) + { + return true; + } + + if (bool.TryParse(configuredValue, out var parsedValue)) + { + return parsedValue; + } + + throw new InvalidOperationException($"{key} must be 'true' or 'false'."); } public int ResolveCompletionTokens(string operationName) diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAITransportService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAITransportService.cs index 2dd136cba6..1a5555d95e 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAITransportService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAITransportService.cs @@ -162,9 +162,18 @@ private void LogEmptyModelOutput(ChatCompletion completion, AIProviderResult res private static string? ExtractModelOutput(ChatCompletion completion, string? responseContent) { - if (completion.Content.Count > 0 && !string.IsNullOrWhiteSpace(completion.Content[0].Text)) + var contentParts = new List(); + foreach (var part in completion.Content) { - return completion.Content[0].Text; + if (!string.IsNullOrWhiteSpace(part.Text)) + { + contentParts.Add(part.Text!); + } + } + + if (contentParts.Count > 0) + { + return string.Concat(contentParts); } return TryExtractMessageContent(responseContent); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/ChefsAttachmentDownloadService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/ChefsAttachmentDownloadService.cs index a9d086f24d..7a33c5335a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/ChefsAttachmentDownloadService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/ChefsAttachmentDownloadService.cs @@ -21,6 +21,7 @@ public class ChefsAttachmentDownloadService( IResilientHttpRequest resilientRestClient, IStringEncryptionService stringEncryptionService) : IChefsAttachmentDownloadService, ITransientDependency { + [Volo.Abp.Uow.UnitOfWork(isTransactional: false)] public async Task DownloadAsync(Guid? formSubmissionId, Guid? chefsFileAttachmentId, string name) { if (formSubmissionId == null) @@ -50,7 +51,7 @@ public async Task DownloadAsync(Guid? formSubmissionId, Guid? chefsFile string url = $"{chefsApi}/files/{chefsFileAttachmentId}"; var decryptedApiKey = stringEncryptionService.Decrypt(applicationForm.ApiKey!); - var response = await resilientRestClient.HttpAsync( + using var response = await resilientRestClient.HttpAsync( HttpMethod.Get, url, null, From ed7fe95a4ed7a39c32a286891f5ffbc0049a6cd9 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Mon, 8 Jun 2026 16:56:26 -0700 Subject: [PATCH 09/15] AB#33009 tighten AI runtime boundary --- .../AI/Runtime/OpenAIChatClientFactory.cs | 12 +- .../AI/Runtime/OpenAIConfigurationResolver.cs | 22 ++ .../AI/Runtime/OpenAIOperationSettings.cs | 14 + .../AI/Runtime/OpenAIPromptFileLogger.cs | 215 +++++++++++++++ .../AI/Runtime/OpenAIRuntimeService.cs | 249 +++--------------- .../AI/Runtime/OpenAITransportService.cs | 22 +- .../ai-generation-button-state.js | 6 +- .../AI/Runtime/OpenAITransportServiceTests.cs | 47 +--- 8 files changed, 311 insertions(+), 276 deletions(-) create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIOperationSettings.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIPromptFileLogger.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIChatClientFactory.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIChatClientFactory.cs index c3e9b0ae57..d97311e3fb 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIChatClientFactory.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIChatClientFactory.cs @@ -5,16 +5,14 @@ namespace Unity.AI.Runtime; -public class OpenAIChatClientFactory(OpenAIConfigurationResolver configurationResolver) : ITransientDependency +public class OpenAIChatClientFactory : ITransientDependency { - private readonly OpenAIConfigurationResolver _configurationResolver = configurationResolver; - - public ChatClient Create(string? operationName = null) + public ChatClient Create(OpenAIOperationSettings settings) { return new AzureOpenAIClient( - _configurationResolver.ResolveEndpoint(operationName), - new ApiKeyCredential(_configurationResolver.ResolveApiKey(operationName)), + settings.Endpoint, + new ApiKeyCredential(settings.ApiKey), new AzureOpenAIClientOptions()) - .GetChatClient(_configurationResolver.ResolveDeploymentName(operationName)); + .GetChatClient(settings.DeploymentName); } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIConfigurationResolver.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIConfigurationResolver.cs index ff8a6298b5..7e0bb0126f 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIConfigurationResolver.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIConfigurationResolver.cs @@ -29,6 +29,28 @@ public string ResolveApiKey(string? operationName = null) return Required($"Azure:{providerName}:ApiKey"); } + public OpenAIOperationSettings ResolveOperationSettings(string operationName) + { + var providerName = ResolveProviderName(operationName); + var profileName = ResolveProfileName(operationName); + var apiKey = Required($"Azure:{providerName}:ApiKey"); + var endpoint = new Uri(Required($"Azure:{providerName}:Endpoint")); + var deploymentName = RequiredProfile(providerName, profileName, "DeploymentName"); + var promptVersion = Optional($"Azure:Operations:{operationName}:PromptVersion") + ?? Required("Azure:Operations:Defaults:PromptVersion"); + + return new OpenAIOperationSettings( + providerName, + profileName, + apiKey, + endpoint, + deploymentName, + ResolveMaxOutputTokenCountSupported(operationName), + ResolveConfiguredTemperature(operationName), + ResolveCompletionTokens(operationName), + promptVersion); + } + public double? ResolveConfiguredTemperature(string? operationName = null) { var providerName = ResolveProviderName(operationName); diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIOperationSettings.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIOperationSettings.cs new file mode 100644 index 0000000000..e98b6ca104 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIOperationSettings.cs @@ -0,0 +1,14 @@ +using System; + +namespace Unity.AI.Runtime; + +public sealed record OpenAIOperationSettings( + string ProviderName, + string ProfileName, + string ApiKey, + Uri Endpoint, + string DeploymentName, + bool MaxOutputTokenCountSupported, + double? Temperature, + int CompletionTokens, + string PromptVersion); diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIPromptFileLogger.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIPromptFileLogger.cs new file mode 100644 index 0000000000..a7317c13c2 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIPromptFileLogger.cs @@ -0,0 +1,215 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace Unity.AI.Runtime; + +public class OpenAIPromptFileLogger( + IConfiguration configuration, + ILogger logger) : ITransientDependency +{ + private readonly IConfiguration _configuration = configuration; + private readonly ILogger _logger = logger; + private const string PromptLogDirectoryName = "logs"; + private static readonly string PromptLogFileName = $"ai-prompts-{DateTime.UtcNow:yyyyMMdd-HHmmss}-{Environment.ProcessId}.log"; + + public Task LogPromptInputAsync(string promptType, string promptVersion, string? systemPrompt, string userPrompt, CancellationToken cancellationToken = default) + { + return WritePromptLogFileAsync(promptType, promptVersion, "INPUT", FormatPromptInputForLog(systemPrompt, userPrompt), cancellationToken); + } + + public Task LogPromptOutputAsync(string promptType, string promptVersion, string output, CancellationToken cancellationToken = default) + { + return WritePromptLogFileAsync(promptType, promptVersion, "OUTPUT", FormatPromptOutputForLog(output), cancellationToken); + } + + private bool CanWritePromptFileLog() + { + return _configuration.GetValue("Azure:Logging:EnablePromptFileLog") ?? false; + } + + private async Task WritePromptLogFileAsync(string promptType, string promptVersion, string payloadType, string payload, CancellationToken cancellationToken = default) + { + if (!CanWritePromptFileLog()) + { + return; + } + + try + { + var now = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss zzz"); + var logDirectory = Path.Combine(AppContext.BaseDirectory, PromptLogDirectoryName); + Directory.CreateDirectory(logDirectory); + + var logPath = Path.Combine(logDirectory, PromptLogFileName); + var entry = $"{now} [{promptType}] [{promptVersion}] {payloadType}\n{payload}\n\n"; + await File.AppendAllTextAsync(logPath, entry, cancellationToken); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to write AI prompt log file."); + } + } + + private static string FormatPromptInputForLog(string? systemPrompt, string userPrompt) + { + var normalizedSystemPrompt = string.IsNullOrWhiteSpace(systemPrompt) ? string.Empty : systemPrompt.Trim(); + var normalizedUserPrompt = string.IsNullOrWhiteSpace(userPrompt) ? string.Empty : userPrompt.Trim(); + return $"SYSTEM_PROMPT\n{normalizedSystemPrompt}\n\nUSER_PROMPT\n{normalizedUserPrompt}"; + } + + private static string FormatPromptOutputForLog(string output) + { + if (string.IsNullOrWhiteSpace(output)) + { + return string.Empty; + } + + if (TryFormatProviderOutput(output, out var formattedProviderOutput)) + { + return formattedProviderOutput; + } + + if (TryParseJsonObjectFromResponse(output, out var jsonObject)) + { + return JsonSerializer.Serialize(jsonObject, AIJsonDefaults.Indented); + } + + return output.Trim(); + } + + private static bool TryFormatProviderOutput(string output, out string formattedOutput) + { + formattedOutput = string.Empty; + + try + { + using var doc = JsonDocument.Parse(output); + var root = doc.RootElement; + if (root.ValueKind != JsonValueKind.Object + || !root.TryGetProperty("choices", out var choices) + || choices.ValueKind != JsonValueKind.Array + || choices.GetArrayLength() == 0) + { + return false; + } + + var firstChoice = choices[0]; + var content = TryGetChoiceContent(firstChoice); + if (string.IsNullOrWhiteSpace(content)) + { + return false; + } + + var lines = new List(); + + if (root.TryGetProperty("usage", out var usage) && usage.ValueKind == JsonValueKind.Object) + { + var promptTokens = TryGetInt32(usage, "prompt_tokens"); + var completionTokens = TryGetInt32(usage, "completion_tokens"); + int? reasoningTokens = null; + + if (usage.TryGetProperty("completion_tokens_details", out var completionTokenDetails) + && completionTokenDetails.ValueKind == JsonValueKind.Object) + { + reasoningTokens = TryGetInt32(completionTokenDetails, "reasoning_tokens"); + } + + if (promptTokens.HasValue) + { + lines.Add($"PromptTokens: {promptTokens.Value}"); + } + + if (completionTokens.HasValue) + { + lines.Add($"CompletionTokens: {completionTokens.Value}"); + } + + if (reasoningTokens.HasValue) + { + lines.Add($"ReasoningTokens: {reasoningTokens.Value}"); + } + } + + var normalizedContent = FormatPromptOutputContent(content); + lines.Add("Output:"); + lines.Add(normalizedContent); + formattedOutput = string.Join(Environment.NewLine, lines); + return true; + } + catch (JsonException) + { + return false; + } + } + + private static string? TryGetChoiceContent(JsonElement firstChoice) + { + if (!firstChoice.TryGetProperty("message", out var message) || message.ValueKind != JsonValueKind.Object) + { + return null; + } + + if (!message.TryGetProperty("content", out var contentProp) || contentProp.ValueKind != JsonValueKind.String) + { + return null; + } + + return contentProp.GetString(); + } + + private static string FormatPromptOutputContent(string content) + { + if (TryParseJsonObjectFromResponse(content, out var contentObject)) + { + return JsonSerializer.Serialize(contentObject, AIJsonDefaults.Indented); + } + + return content.Trim(); + } + + private static bool TryParseJsonObjectFromResponse(string response, out JsonElement objectElement) + { + objectElement = default; + var cleaned = AIResponseJson.CleanJsonResponse(response); + if (string.IsNullOrWhiteSpace(cleaned)) + { + return false; + } + + try + { + using var doc = JsonDocument.Parse(cleaned); + if (doc.RootElement.ValueKind != JsonValueKind.Object) + { + return false; + } + + objectElement = doc.RootElement.Clone(); + return true; + } + catch (JsonException) + { + return false; + } + } + + private static int? TryGetInt32(JsonElement element, string propertyName) + { + return element.TryGetProperty(propertyName, out var prop) + && prop.ValueKind == JsonValueKind.Number + && prop.TryGetInt32(out var value) + ? value + : null; + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIRuntimeService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIRuntimeService.cs index 0093f0e662..8befd007d2 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIRuntimeService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIRuntimeService.cs @@ -1,8 +1,6 @@ -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Text.Json; using System.Threading; @@ -18,33 +16,27 @@ namespace Unity.AI.Runtime [ExposeServices(typeof(IAIService))] public class OpenAIRuntimeService : IAIService, ITransientDependency { - private readonly IConfiguration _configuration; private readonly ILogger _logger; private readonly OpenAITransportService _openAITransportService; private readonly OpenAIConfigurationResolver _openAIConfigurationResolver; + private readonly OpenAIPromptFileLogger _promptFileLogger; private const string ApplicationAnalysisPromptType = AIPromptTypes.ApplicationAnalysis; private const string AttachmentSummaryPromptType = AIPromptTypes.AttachmentSummary; private const string ApplicationScoringPromptType = AIPromptTypes.ApplicationScoring; private const int MaxAiAttempts = 3; - // Optional local debugging sink for prompt payload logs to a local file. - // Not intended for deployed/shared environments. - private bool IsPromptFileLoggingEnabled => _configuration.GetValue("Azure:Logging:EnablePromptFileLog") ?? false; - private const string PromptLogDirectoryName = "logs"; - private static readonly string PromptLogFileName = $"ai-prompts-{DateTime.UtcNow:yyyyMMdd-HHmmss}-{Environment.ProcessId}.log"; - public OpenAIRuntimeService( - IConfiguration configuration, ILogger logger, OpenAITransportService openAITransportService, - OpenAIConfigurationResolver openAIConfigurationResolver) + OpenAIConfigurationResolver openAIConfigurationResolver, + OpenAIPromptFileLogger promptFileLogger) { - _configuration = configuration; _logger = logger; _openAITransportService = openAITransportService; _openAIConfigurationResolver = openAIConfigurationResolver; + _promptFileLogger = promptFileLogger; } public Task IsAvailableAsync() @@ -64,8 +56,8 @@ public Task IsAvailableAsync() public async Task GenerateApplicationAnalysisAsync(ApplicationAnalysisRequest request, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); - var promptVersion = OpenAIPromptRenderer.ResolvePromptVersion( - request.PromptVersion ?? _openAIConfigurationResolver.ResolvePromptVersion(ApplicationAnalysisPromptType)); + var settings = _openAIConfigurationResolver.ResolveOperationSettings(ApplicationAnalysisPromptType); + var promptVersion = OpenAIPromptRenderer.ResolvePromptVersion(request.PromptVersion ?? settings.PromptVersion); var data = JsonSerializer.Serialize(request.Data, AIJsonDefaults.Indented); var schema = JsonSerializer.Serialize(request.Schema, AIJsonDefaults.Indented); @@ -84,25 +76,25 @@ public async Task GenerateApplicationAnalysisAsync( schema, data, attachments); - await LogPromptInputAsync(ApplicationAnalysisPromptType, promptVersion, systemPrompt, applicationAnalysisContent, cancellationToken); - var result = await GenerateWithRetryAsync( - () => _openAITransportService.GenerateSummaryAsync( - applicationAnalysisContent, - systemPrompt, - _openAIConfigurationResolver.ResolveCompletionTokens(ApplicationAnalysisPromptType), - operationName: ApplicationAnalysisPromptType, - promptVersion: promptVersion, - cancellationToken: cancellationToken), - AIProviderPayloadValidator.IsValidApplicationAnalysisJson, - "application analysis", - cancellationToken); + await _promptFileLogger.LogPromptInputAsync(ApplicationAnalysisPromptType, promptVersion, systemPrompt, applicationAnalysisContent, cancellationToken); + var result = await GenerateWithRetryAsync( + () => _openAITransportService.GenerateSummaryAsync( + applicationAnalysisContent, + systemPrompt, + settings, + settings.CompletionTokens, + promptVersion: promptVersion, + cancellationToken: cancellationToken), + AIProviderPayloadValidator.IsValidApplicationAnalysisJson, + "application analysis", + cancellationToken); + await _promptFileLogger.LogPromptOutputAsync(ApplicationAnalysisPromptType, promptVersion, result.CaptureOutput, cancellationToken); if (result.Outcome != AIOperationOutcome.Success) { return new ApplicationAnalysisResponse(); } - await LogPromptOutputAsync(ApplicationAnalysisPromptType, promptVersion, result.CaptureOutput, cancellationToken); return OpenAIResponseParser.ParseApplicationAnalysisResponse(result.Content); } @@ -111,8 +103,8 @@ public async Task GenerateAttachmentSummaryAsync(Atta ArgumentNullException.ThrowIfNull(request); var fileName = request.FileName ?? string.Empty; var contentType = request.ContentType ?? "application/octet-stream"; - var promptVersion = OpenAIPromptRenderer.ResolvePromptVersion( - request.PromptVersion ?? _openAIConfigurationResolver.ResolvePromptVersion(AttachmentSummaryPromptType)); + var settings = _openAIConfigurationResolver.ResolveOperationSettings(AttachmentSummaryPromptType); + var promptVersion = OpenAIPromptRenderer.ResolvePromptVersion(request.PromptVersion ?? settings.PromptVersion); try { @@ -138,19 +130,20 @@ public async Task GenerateAttachmentSummaryAsync(Atta var attachment = JsonSerializer.Serialize(attachmentPayload, AIJsonDefaults.Indented); var contentToAnalyze = OpenAIPromptRenderer.BuildAttachmentSummaryUserPrompt(promptVersion, attachment); - await LogPromptInputAsync(AttachmentSummaryPromptType, promptVersion, prompt, contentToAnalyze, cancellationToken); + await _promptFileLogger.LogPromptInputAsync(AttachmentSummaryPromptType, promptVersion, prompt, contentToAnalyze, cancellationToken); var result = await GenerateWithRetryAsync( () => _openAITransportService.GenerateSummaryAsync( contentToAnalyze, prompt, - _openAIConfigurationResolver.ResolveCompletionTokens(AttachmentSummaryPromptType), - operationName: AttachmentSummaryPromptType, + settings, + settings.CompletionTokens, promptVersion: promptVersion, fileName: fileName, cancellationToken: cancellationToken), AIProviderPayloadValidator.IsValidAttachmentSummaryText, "attachment summary", cancellationToken); + await _promptFileLogger.LogPromptOutputAsync(AttachmentSummaryPromptType, promptVersion, result.CaptureOutput, cancellationToken); if (result.Outcome != AIOperationOutcome.Success) { @@ -160,7 +153,6 @@ public async Task GenerateAttachmentSummaryAsync(Atta }; } - await LogPromptOutputAsync(AttachmentSummaryPromptType, promptVersion, result.CaptureOutput, cancellationToken); return new AttachmentSummaryResponse { Summary = ExtractSummaryFromJson(result.Content) @@ -183,8 +175,8 @@ public async Task GenerateAttachmentSummaryAsync(Atta public async Task GenerateApplicationScoringAsync(ApplicationScoringRequest request, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); - var promptVersion = OpenAIPromptRenderer.ResolvePromptVersion( - request.PromptVersion ?? _openAIConfigurationResolver.ResolvePromptVersion(ApplicationScoringPromptType)); + var settings = _openAIConfigurationResolver.ResolveOperationSettings(ApplicationScoringPromptType); + var promptVersion = OpenAIPromptRenderer.ResolvePromptVersion(request.PromptVersion ?? settings.PromptVersion); var dataJson = JsonSerializer.Serialize(request.Data, AIJsonDefaults.Indented); var sectionJson = JsonSerializer.Serialize(request.SectionSchema, AIJsonDefaults.Indented); @@ -215,25 +207,25 @@ public async Task GenerateApplicationScoringAsync(Ap response); var systemPrompt = OpenAIPromptRenderer.BuildApplicationScoringSystemPrompt(promptVersion); - await LogPromptInputAsync(ApplicationScoringPromptType, promptVersion, systemPrompt, applicationScoringContent, cancellationToken); + await _promptFileLogger.LogPromptInputAsync(ApplicationScoringPromptType, promptVersion, systemPrompt, applicationScoringContent, cancellationToken); var result = await GenerateWithRetryAsync( () => _openAITransportService.GenerateSummaryAsync( applicationScoringContent, systemPrompt, - _openAIConfigurationResolver.ResolveCompletionTokens(ApplicationScoringPromptType), - operationName: ApplicationScoringPromptType, + settings, + settings.CompletionTokens, promptVersion: promptVersion, cancellationToken: cancellationToken), content => AIProviderPayloadValidator.IsValidApplicationScoringJson(content, section), $"application scoring section {request.SectionName}", cancellationToken); + await _promptFileLogger.LogPromptOutputAsync(ApplicationScoringPromptType, promptVersion, result.CaptureOutput, cancellationToken); if (result.Outcome != AIOperationOutcome.Success) { return new ApplicationScoringResponse(); } - await LogPromptOutputAsync(ApplicationScoringPromptType, promptVersion, result.CaptureOutput, cancellationToken); return OpenAIResponseParser.ParseApplicationScoringResponse(result.Content, questionIdAliasMap); } catch (OperationCanceledException) @@ -303,185 +295,6 @@ private async Task GenerateWithRetryAsync( return lastResult; } - private static int? TryGetInt32(JsonElement element, string propertyName) - { - return element.TryGetProperty(propertyName, out var property) - && property.ValueKind == JsonValueKind.Number - && property.TryGetInt32(out var value) - ? value - : null; - } - - private async Task LogPromptInputAsync(string promptType, string promptVersion, string? systemPrompt, string userPrompt, CancellationToken cancellationToken = default) - { - if (!CanWritePromptFileLog()) - { - return; - } - - var formattedInput = FormatPromptInputForLog(systemPrompt, userPrompt); - await WritePromptLogFileAsync(promptType, promptVersion, "INPUT", formattedInput, cancellationToken); - } - - private async Task LogPromptOutputAsync(string promptType, string promptVersion, string output, CancellationToken cancellationToken = default) - { - if (!CanWritePromptFileLog()) - { - return; - } - - var formattedOutput = FormatPromptOutputForLog(output); - await WritePromptLogFileAsync(promptType, promptVersion, "OUTPUT", formattedOutput, cancellationToken); - } - - private async Task WritePromptLogFileAsync(string promptType, string promptVersion, string payloadType, string payload, CancellationToken cancellationToken = default) - { - if (!CanWritePromptFileLog()) - { - return; - } - - try - { - var now = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss zzz"); - var logDirectory = Path.Combine(AppContext.BaseDirectory, PromptLogDirectoryName); - Directory.CreateDirectory(logDirectory); - - var logPath = Path.Combine(logDirectory, PromptLogFileName); - var entry = $"{now} [{promptType}] [{promptVersion}] {payloadType}\n{payload}\n\n"; - await File.AppendAllTextAsync(logPath, entry, cancellationToken); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to write AI prompt log file."); - } - } - - private bool CanWritePromptFileLog() - { - return IsPromptFileLoggingEnabled; - } - private static string FormatPromptInputForLog(string? systemPrompt, string userPrompt) - { - var normalizedSystemPrompt = string.IsNullOrWhiteSpace(systemPrompt) ? string.Empty : systemPrompt.Trim(); - var normalizedUserPrompt = string.IsNullOrWhiteSpace(userPrompt) ? string.Empty : userPrompt.Trim(); - return $"SYSTEM_PROMPT\n{normalizedSystemPrompt}\n\nUSER_PROMPT\n{normalizedUserPrompt}"; - } - - private static string FormatPromptOutputForLog(string output) - { - if (string.IsNullOrWhiteSpace(output)) - { - return string.Empty; - } - - if (TryFormatProviderOutput(output, out var formattedProviderOutput)) - { - return formattedProviderOutput; - } - - if (TryParseJsonObjectFromResponse(output, out var jsonObject)) - { - return JsonSerializer.Serialize(jsonObject, AIJsonDefaults.Indented); - } - - return output.Trim(); - } - - private static bool TryFormatProviderOutput(string output, out string formattedOutput) - { - formattedOutput = string.Empty; - - try - { - using var doc = JsonDocument.Parse(output); - var root = doc.RootElement; - if (root.ValueKind != JsonValueKind.Object - || !root.TryGetProperty("choices", out var choices) - || choices.ValueKind != JsonValueKind.Array - || choices.GetArrayLength() == 0) - { - return false; - } - - var firstChoice = choices[0]; - var content = TryGetChoiceContent(firstChoice); - if (string.IsNullOrWhiteSpace(content)) - { - return false; - } - - var lines = new List(); - - if (root.TryGetProperty("usage", out var usage) && usage.ValueKind == JsonValueKind.Object) - { - var promptTokens = TryGetInt32(usage, "prompt_tokens"); - var completionTokens = TryGetInt32(usage, "completion_tokens"); - int? reasoningTokens = null; - - if (usage.TryGetProperty("completion_tokens_details", out var completionTokenDetails) - && completionTokenDetails.ValueKind == JsonValueKind.Object) - { - reasoningTokens = TryGetInt32(completionTokenDetails, "reasoning_tokens"); - } - - if (promptTokens.HasValue) - { - lines.Add($"PromptTokens: {promptTokens.Value}"); - } - - if (completionTokens.HasValue) - { - lines.Add($"CompletionTokens: {completionTokens.Value}"); - } - - if (reasoningTokens.HasValue) - { - lines.Add($"ReasoningTokens: {reasoningTokens.Value}"); - } - } - - var normalizedContent = FormatPromptOutputContent(content); - lines.Add("Output:"); - lines.Add(normalizedContent); - formattedOutput = string.Join(Environment.NewLine, lines); - return true; - } - catch (JsonException) - { - return false; - } - } - - private static string? TryGetChoiceContent(JsonElement firstChoice) - { - if (!firstChoice.TryGetProperty("message", out var message) || message.ValueKind != JsonValueKind.Object) - { - return null; - } - - if (!message.TryGetProperty("content", out var contentProp) || contentProp.ValueKind != JsonValueKind.String) - { - return null; - } - - return contentProp.GetString(); - } - - private static string FormatPromptOutputContent(string content) - { - if (TryParseJsonObjectFromResponse(content, out var contentObject)) - { - return JsonSerializer.Serialize(contentObject, AIJsonDefaults.Indented); - } - - return content.Trim(); - } - private static bool TryParseJsonObjectFromResponse(string response, out JsonElement objectElement) { objectElement = default; diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAITransportService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAITransportService.cs index 1a5555d95e..fe3a73527c 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAITransportService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAITransportService.cs @@ -12,31 +12,27 @@ namespace Unity.AI.Runtime; public class OpenAITransportService( - OpenAIConfigurationResolver configurationResolver, OpenAIChatClientFactory chatClientFactory, ILogger logger) : ITransientDependency { - private readonly OpenAIConfigurationResolver _configurationResolver = configurationResolver; private readonly OpenAIChatClientFactory _chatClientFactory = chatClientFactory; private readonly ILogger _logger = logger; public async Task GenerateSummaryAsync( string content, string? systemPrompt, + OpenAIOperationSettings settings, int maxTokens = 150, - double? temperature = null, - string? operationName = null, string? promptVersion = null, string? fileName = null, CancellationToken cancellationToken = default) { try { - var providerName = _configurationResolver.ResolveProviderName(operationName); - if (!string.Equals(providerName, "OpenAI", StringComparison.Ordinal)) + if (!string.Equals(settings.ProviderName, "OpenAI", StringComparison.Ordinal)) { - _logger.LogWarning("Provider {ProviderName} is not supported by OpenAI transport.", providerName); - return AIOperationResult.PermanentFailure(new AIProviderResult($"Unsupported provider: {providerName}")); + _logger.LogWarning("Provider {ProviderName} is not supported by OpenAI transport.", settings.ProviderName); + return AIOperationResult.PermanentFailure(new AIProviderResult($"Unsupported provider: {settings.ProviderName}")); } var resolvedSystemPrompt = string.IsNullOrWhiteSpace(systemPrompt) @@ -44,19 +40,17 @@ public async Task GenerateSummaryAsync( : systemPrompt; var options = new ChatCompletionOptions(); - if (_configurationResolver.ResolveMaxOutputTokenCountSupported(operationName)) + if (settings.MaxOutputTokenCountSupported) { options.MaxOutputTokenCount = maxTokens; } - var profileTemperature = _configurationResolver.ResolveConfiguredTemperature(operationName); - var resolvedTemperature = temperature ?? profileTemperature; - if (resolvedTemperature.HasValue) + if (settings.Temperature.HasValue) { - options.Temperature = (float)resolvedTemperature.Value; + options.Temperature = (float)settings.Temperature.Value; } - var result = await _chatClientFactory.Create(operationName).CompleteChatAsync( + var result = await _chatClientFactory.Create(settings).CompleteChatAsync( [ new SystemChatMessage(resolvedSystemPrompt), new UserChatMessage(content ?? string.Empty) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js index 1ffd85a1c6..bf7cc3a8b4 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js @@ -52,9 +52,9 @@ applyRateLimitState(generationStatus, { pollWhenGenerating: true }); }, monitor(options) { - const intervalMs = options.intervalMs || 15000; - const activeIntervalMs = options.activeIntervalMs || 2000; - const activePollCount = options.activePollCount || 10; + const intervalMs = options.intervalMs || 5000; + const activeIntervalMs = options.activeIntervalMs || 1000; + const activePollCount = options.activePollCount || 30; const maxFailures = options.maxFailures || 3; let timeoutId = null; let failures = 0; diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/OpenAITransportServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/OpenAITransportServiceTests.cs index 18636b3c60..6193f8abc7 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/OpenAITransportServiceTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/OpenAITransportServiceTests.cs @@ -1,7 +1,5 @@ -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using Shouldly; -using System.Collections.Generic; using System.Threading.Tasks; using Unity.AI.Runtime; using Xunit; @@ -13,43 +11,24 @@ public class OpenAITransportServiceTests [Fact] public async Task GenerateSummaryAsync_Should_ReturnPermanentFailure_When_ProviderNotSupported() { - var resolver = CreateResolver(new Dictionary - { - ["Azure:Operations:Defaults:Provider"] = "UnsupportedProvider" - }); - var service = new OpenAITransportService( - resolver, - new OpenAIChatClientFactory(resolver), + new OpenAIChatClientFactory(), NullLogger.Instance); - var result = await service.GenerateSummaryAsync("content", "system", 100); + var settings = new OpenAIOperationSettings( + "UnsupportedProvider", + "Gpt5Mini", + "test-key", + new System.Uri("https://example.test"), + "gpt-5-mini", + true, + null, + 100, + "v1"); + + var result = await service.GenerateSummaryAsync("content", "system", settings, 100); result.Outcome.ShouldBe(AIOperationOutcome.PermanentFailure); result.Response?.Content.ShouldContain("Unsupported provider"); } - - private static OpenAIConfigurationResolver CreateResolver(Dictionary? overrides = null) - { - var values = new Dictionary - { - ["Azure:Operations:Defaults:Provider"] = "OpenAI", - ["Azure:Operations:Defaults:Profile"] = "Gpt5Mini", - ["Azure:OpenAI:ApiKey"] = "test-key", - ["Azure:OpenAI:Endpoint"] = "https://example.test", - ["Azure:OpenAI:Profiles:Gpt5Mini:DeploymentName"] = "gpt-5-mini" - }; - - if (overrides != null) - { - foreach (var item in overrides) - { - values[item.Key] = item.Value; - } - } - - return new OpenAIConfigurationResolver(new ConfigurationBuilder() - .AddInMemoryCollection(values) - .Build()); - } } From 75c6de469a9c04a5b6f03e16fa6581130fedcbd5 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Tue, 9 Jun 2026 09:35:25 -0700 Subject: [PATCH 10/15] [AB#32697] Update PaymentInfo layout to 3-column format --- .../Components/PaymentInfo/Default.cshtml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/Default.cshtml index e85e4c364c..d186ef5f8b 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/Default.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/Default.cshtml @@ -33,34 +33,36 @@ @* Zone Section : Payment.Summary *@ @if (await PermissionChecker.IsGrantedAsync(UnitySelector.Payment.Summary.Default)) { - +
@L["PaymentInfoView:PaymentInfoTitle"].Value
- +
$
$ - +
+
+
$ - +
-
-
$ - +
+
+
$ - +
$ From 3b692197c22a2a51c2a8707c16b5af64e4128f14 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Tue, 9 Jun 2026 09:46:23 -0700 Subject: [PATCH 11/15] AB#33395 define AI operation input contracts --- .../ApplicationAnalysisOperationInputDto.cs | 26 +++ .../ApplicationScoringOperationInputDto.cs | 35 ++++ .../Operations/ApplicationAnalysisService.cs | 54 ++----- .../Operations/ApplicationScoringService.cs | 143 +++-------------- .../Operations/IApplicationAnalysisService.cs | 2 +- .../Operations/IApplicationScoringService.cs | 2 +- .../ApplicationAnalysisAppService.cs | 58 ++++++- .../ApplicationScoringAppService.cs | 151 +++++++++++++++++- 8 files changed, 297 insertions(+), 174 deletions(-) create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/ApplicationAnalysisOperationInputDto.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/ApplicationScoringOperationInputDto.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/ApplicationAnalysisOperationInputDto.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/ApplicationAnalysisOperationInputDto.cs new file mode 100644 index 0000000000..17deda9348 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/ApplicationAnalysisOperationInputDto.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using Unity.AI.Models; + +namespace Unity.AI.Operations +{ + public class ApplicationAnalysisOperationInputDto + { + [JsonPropertyName("applicationId")] + public Guid ApplicationId { get; set; } + + [JsonPropertyName("schema")] + public JsonElement Schema { get; set; } + + [JsonPropertyName("data")] + public JsonElement Data { get; set; } + + [JsonPropertyName("attachments")] + public List Attachments { get; set; } = new(); + + [JsonPropertyName("promptVersion")] + public string? PromptVersion { get; set; } + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/ApplicationScoringOperationInputDto.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/ApplicationScoringOperationInputDto.cs new file mode 100644 index 0000000000..d2c5946dcf --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/ApplicationScoringOperationInputDto.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using Unity.AI.Models; + +namespace Unity.AI.Operations +{ + public class ApplicationScoringOperationInputDto + { + [JsonPropertyName("applicationId")] + public Guid ApplicationId { get; set; } + + [JsonPropertyName("data")] + public JsonElement Data { get; set; } + + [JsonPropertyName("attachments")] + public List Attachments { get; set; } = new(); + + [JsonPropertyName("sections")] + public List Sections { get; set; } = new(); + + [JsonPropertyName("promptVersion")] + public string? PromptVersion { get; set; } + } + + public class ApplicationScoringSectionOperationInputDto + { + [JsonPropertyName("sectionName")] + public string SectionName { get; set; } = string.Empty; + + [JsonPropertyName("sectionSchema")] + public JsonElement SectionSchema { get; set; } + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationAnalysisService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationAnalysisService.cs index 1d56edc349..9767997c87 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationAnalysisService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationAnalysisService.cs @@ -1,11 +1,10 @@ -using Microsoft.Extensions.Logging; using System; using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Unity.AI.Prompts; +using Unity.AI; +using Unity.AI.Models; using Unity.AI.Requests; -using Unity.AI.Runtime; using Unity.GrantManager.Applications; using Volo.Abp.DependencyInjection; @@ -13,60 +12,27 @@ namespace Unity.AI.Operations { public class ApplicationAnalysisService( IApplicationRepository applicationRepository, - IApplicationFormSubmissionRepository applicationFormSubmissionRepository, - IApplicationFormVersionRepository applicationFormVersionRepository, - IApplicationChefsFileAttachmentRepository applicationChefsFileAttachmentRepository, IAIService aiService, - IAIGenerationPrerequisiteValidator aiGenerationPrerequisiteValidator, - ILogger logger) : IApplicationAnalysisService, ITransientDependency + IAIGenerationPrerequisiteValidator aiGenerationPrerequisiteValidator) : IApplicationAnalysisService, ITransientDependency { - public async Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null, CancellationToken cancellationToken = default) + public async Task RegenerateAndSaveAsync(ApplicationAnalysisOperationInputDto input, CancellationToken cancellationToken = default) { - await aiGenerationPrerequisiteValidator.EnsureApplicationAnalysisAvailableAsync(applicationId); - - var application = await applicationRepository.GetAsync(applicationId); - var formSubmission = await applicationFormSubmissionRepository.GetByApplicationAsync(applicationId); - var attachments = await applicationChefsFileAttachmentRepository.GetListAsync(a => a.ApplicationId == applicationId); - var formSchema = await GetFormSchemaAsync(formSubmission?.ApplicationFormVersionId); - - var attachmentSummaries = PromptDataPayloadBuilder.BuildAttachmentSummaries(attachments); - var formFieldConfiguration = await PromptDataPayloadBuilder.BuildFormFieldConfigurationAsync( - applicationFormVersionRepository, - formSubmission?.ApplicationFormVersionId, - logger); + await aiGenerationPrerequisiteValidator.EnsureApplicationAnalysisAvailableAsync(input.ApplicationId); var analysis = await aiService.GenerateApplicationAnalysisAsync(new ApplicationAnalysisRequest { - Schema = JsonSerializer.SerializeToElement(formFieldConfiguration), - Data = PromptDataPayloadBuilder.BuildPromptDataPayload(application, formSubmission, formSchema, logger), - Attachments = attachmentSummaries, - PromptVersion = promptVersion, + Schema = input.Schema, + Data = input.Data, + Attachments = input.Attachments, + PromptVersion = input.PromptVersion, }, cancellationToken); var analysisJson = JsonSerializer.Serialize(analysis, AIJsonDefaults.Indented); + var application = await applicationRepository.GetAsync(input.ApplicationId); application.AIAnalysis = analysisJson; await applicationRepository.UpdateAsync(application); return analysisJson; } - private async Task GetFormSchemaAsync(Guid? formVersionId) - { - if (formVersionId == null) - { - return null; - } - - try - { - var formVersion = await applicationFormVersionRepository.GetAsync(formVersionId.Value); - return string.IsNullOrWhiteSpace(formVersion?.FormSchema) ? null : formVersion.FormSchema; - } - catch (Exception ex) - { - logger.LogWarning(ex, "Unable to load form schema for prompt data generation for form version {FormVersionId}.", formVersionId); - return null; - } - } - } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationScoringService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationScoringService.cs index 3a4ee6f7d2..780a7d1098 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationScoringService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationScoringService.cs @@ -5,63 +5,31 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Unity.Flex.Domain.Scoresheets; +using Unity.AI; using Unity.AI.Models; -using Unity.AI.Prompts; using Unity.AI.Requests; using Unity.AI.Runtime; -using Unity.AI.Localization; using Unity.GrantManager.Applications; -using Volo.Abp; using Volo.Abp.DependencyInjection; -using Microsoft.Extensions.Localization; namespace Unity.AI.Operations { public class ApplicationScoringService( IApplicationRepository applicationRepository, - IApplicationFormRepository applicationFormRepository, - IApplicationFormSubmissionRepository applicationFormSubmissionRepository, - IApplicationFormVersionRepository applicationFormVersionRepository, - IApplicationChefsFileAttachmentRepository applicationChefsFileAttachmentRepository, - IScoresheetRepository scoresheetRepository, IAIService aiService, AIExecutionModeResolver executionModeResolver, - ILogger logger, - IStringLocalizer localizer) : IApplicationScoringService, ITransientDependency + ILogger logger) : IApplicationScoringService, ITransientDependency { - public async Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null, CancellationToken cancellationToken = default) + public async Task RegenerateAndSaveAsync(ApplicationScoringOperationInputDto input, CancellationToken cancellationToken = default) { - 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]); - } - - var attachments = await applicationChefsFileAttachmentRepository.GetListAsync(a => a.ApplicationId == applicationId); - var attachmentSummaries = PromptDataPayloadBuilder.BuildAttachmentSummaries( - attachments, - excludeWhitespaceOnlySummaries: false); - - var formSubmission = await applicationFormSubmissionRepository.GetByApplicationAsync(applicationId); - var formSchema = await GetFormSchemaAsync(formSubmission?.ApplicationFormVersionId); - var promptData = PromptDataPayloadBuilder.BuildPromptDataPayload(application, formSubmission, formSchema, logger); - - var sections = scoresheet.Sections.OrderBy(s => s.Order).ToList(); + var sections = input.Sections; var mode = executionModeResolver.ResolveMode(AIExecutionModeResolver.ApplicationScoringOperation); var perSectionResults = await AIExecutionStrategy.RunAsync( sections, mode, - section => ProcessSectionAsync(applicationId, section, promptData, attachmentSummaries, promptVersion, cancellationToken), - batch => ProcessSectionsAsync(applicationId, batch, promptData, attachmentSummaries, promptVersion, cancellationToken)); + section => ProcessSectionAsync(input.ApplicationId, section, input.Data, input.Attachments, input.PromptVersion, cancellationToken), + batch => ProcessSectionsAsync(input.ApplicationId, batch, input.Data, input.Attachments, input.PromptVersion, cancellationToken)); var allSectionResults = new Dictionary(); foreach (var sectionResult in perSectionResults) @@ -74,6 +42,7 @@ public async Task RegenerateAndSaveAsync(Guid applicationId, string? pro var combinedResults = JsonSerializer.Serialize(allSectionResults, AIJsonDefaults.Indented); var validatedJson = ValidateApplicationScoringJson(combinedResults); + var application = await applicationRepository.GetAsync(input.ApplicationId); application.AIScoresheetAnswers = validatedJson; await applicationRepository.UpdateAsync(application); return validatedJson; @@ -81,7 +50,7 @@ public async Task RegenerateAndSaveAsync(Guid applicationId, string? pro private async Task> ProcessSectionAsync( Guid applicationId, - ScoresheetSection section, + ApplicationScoringSectionOperationInputDto section, JsonElement promptData, List attachmentSummaries, string? promptVersion, @@ -94,8 +63,8 @@ private async Task> ProcessSectionAsync( { Data = promptData, Attachments = attachmentSummaries, - SectionName = section.Name, - SectionSchema = JsonSerializer.SerializeToElement(BuildSectionQuestionsData(section), AIJsonDefaults.IndentedCamelCase), + SectionName = section.SectionName, + SectionSchema = section.SectionSchema, PromptVersion = promptVersion, }; var applicationScoringResponse = await aiService.GenerateApplicationScoringAsync(applicationScoringRequest, cancellationToken); @@ -111,14 +80,15 @@ private async Task> ProcessSectionAsync( } catch (Exception ex) { - logger.LogError(ex, "Error processing AI application scoring section {SectionName} for application {ApplicationId}", section.Name, applicationId); + logger.LogError(ex, "Error processing AI application scoring section {SectionName} for application {ApplicationId}", section.SectionName, applicationId); } + return sectionResults; } private async Task>> ProcessSectionsAsync( Guid applicationId, - IReadOnlyCollection sections, + IReadOnlyCollection sections, JsonElement promptData, List attachmentSummaries, string? promptVersion, @@ -127,17 +97,14 @@ private async Task>> ProcessSectionsAsync( var sectionResults = new Dictionary(); try { - var questions = sections - .OrderBy(s => s.Order) - .SelectMany(BuildSectionQuestionsData) - .ToList(); + var questions = BuildBatchSectionSchema(sections); var applicationScoringRequest = new ApplicationScoringRequest { Data = promptData, Attachments = attachmentSummaries, SectionName = "All Sections", - SectionSchema = JsonSerializer.SerializeToElement(questions, AIJsonDefaults.IndentedCamelCase), + SectionSchema = questions, PromptVersion = promptVersion, }; var applicationScoringResponse = await aiService.GenerateApplicationScoringAsync(applicationScoringRequest, cancellationToken); @@ -159,25 +126,18 @@ private async Task>> ProcessSectionsAsync( return [sectionResults]; } - private static List BuildSectionQuestionsData(ScoresheetSection section) + private static JsonElement BuildBatchSectionSchema(IReadOnlyCollection sections) { - var sectionQuestionsData = new List(); - foreach (var field in section.Fields.OrderBy(f => f.Order)) + var questions = new List(); + foreach (var section in sections) { - var options = ExtractSelectListOptions(field); - sectionQuestionsData.Add(new + foreach (var question in section.SectionSchema.EnumerateArray()) { - id = field.Id.ToString(), - section = section.Name, - question = field.Label, - description = field.Description, - type = field.Type.ToString(), - options, - allowed_answers = ExtractSelectListOptionNumbers(options) - }); + questions.Add(question.Clone()); + } } - return sectionQuestionsData; + return JsonSerializer.SerializeToElement(questions, AIJsonDefaults.IndentedCamelCase); } private void CopyAnswers(Dictionary answers, Dictionary results) @@ -190,25 +150,6 @@ private void CopyAnswers(Dictionary answers, D } } - private async Task GetFormSchemaAsync(Guid? formVersionId) - { - if (formVersionId == null) - { - return null; - } - - try - { - var formVersion = await applicationFormVersionRepository.GetAsync(formVersionId.Value); - return string.IsNullOrWhiteSpace(formVersion?.FormSchema) ? null : formVersion.FormSchema; - } - catch (Exception ex) - { - logger.LogWarning(ex, "Unable to load form schema for application scoring prompt data generation for form version {FormVersionId}.", formVersionId); - return null; - } - } - private static string ValidateApplicationScoringJson(string scoresheetAnswers) { try @@ -226,45 +167,5 @@ private static string ValidateApplicationScoringJson(string scoresheetAnswers) return "{}"; } - - private static object[]? ExtractSelectListOptions(Question field) - { - if (field.Type != Unity.Flex.Scoresheets.Enums.QuestionType.SelectList || string.IsNullOrEmpty(field.Definition)) - return null; - - try - { - var definition = JsonSerializer.Deserialize(field.Definition); - if (definition?.Options != null && definition.Options.Count > 0) - { - return definition.Options - .Select((option, index) => - (object)new - { - number = index + 1, - value = option.Value - }) - .ToArray(); - } - } - catch (JsonException) - { - // Ignore malformed definition and return null options. - } - - return null; - } - - private static string[]? ExtractSelectListOptionNumbers(object[]? options) - { - if (options == null || options.Length == 0) - { - return null; - } - - return options - .Select((_, index) => (index + 1).ToString()) - .ToArray(); - } } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationAnalysisService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationAnalysisService.cs index ac02842761..333817e90d 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationAnalysisService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationAnalysisService.cs @@ -6,6 +6,6 @@ namespace Unity.AI.Operations { public interface IApplicationAnalysisService { - Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null, CancellationToken cancellationToken = default); + Task RegenerateAndSaveAsync(ApplicationAnalysisOperationInputDto input, CancellationToken cancellationToken = default); } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationScoringService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationScoringService.cs index 2b2bc1796f..9e22e8623f 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationScoringService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationScoringService.cs @@ -6,6 +6,6 @@ namespace Unity.AI.Operations { public interface IApplicationScoringService { - Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null, CancellationToken cancellationToken = default); + Task RegenerateAndSaveAsync(ApplicationScoringOperationInputDto input, CancellationToken cancellationToken = default); } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationAnalysisAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationAnalysisAppService.cs index 843d4a89fa..75add8d8d3 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationAnalysisAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationAnalysisAppService.cs @@ -1,12 +1,18 @@ using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Logging; using System; +using System.Linq; +using System.Text.Json; using System.Threading.Tasks; using Unity.AI; using Unity.AI.Automation; using Unity.AI.Features; using Unity.AI.Localization; +using Unity.AI.Models; +using Unity.AI.Prompts; using Unity.AI.Permissions; using Unity.AI.Settings; +using Unity.GrantManager.Applications; using Volo.Abp; using Volo.Abp.MultiTenancy; @@ -16,8 +22,13 @@ namespace Unity.GrantManager.GrantApplications; public class ApplicationAnalysisAppService( Unity.AI.Operations.IApplicationAnalysisService applicationAnalysisService, IApplicationAIGenerationQueue aiGenerationQueue, + IApplicationRepository applicationRepository, + IApplicationFormSubmissionRepository applicationFormSubmissionRepository, + IApplicationFormVersionRepository applicationFormVersionRepository, + IApplicationChefsFileAttachmentRepository applicationChefsFileAttachmentRepository, AIFeatureGuard featureGuard, - ICurrentTenant currentTenant) + ICurrentTenant currentTenant, + ILogger logger) : AIAppService, IApplicationAnalysisAppService { public virtual async Task GenerateApplicationAnalysisAsync(Guid applicationId, string? promptVersion = null) @@ -35,7 +46,50 @@ await featureGuard.EnsureEnabledAsync( [RemoteService(IsEnabled = false)] public virtual async Task GenerateApplicationAnalysisForPipelineAsync(Guid applicationId, string? promptVersion = null) { - await applicationAnalysisService.RegenerateAndSaveAsync(applicationId, promptVersion); + var input = await BuildInputAsync(applicationId, promptVersion); + await applicationAnalysisService.RegenerateAndSaveAsync(input); return new ApplicationAnalysisResultDto { Completed = true }; } + + private async Task BuildInputAsync(Guid applicationId, string? promptVersion) + { + var application = await applicationRepository.GetAsync(applicationId); + var formSubmission = await applicationFormSubmissionRepository.GetByApplicationAsync(applicationId); + var attachments = await applicationChefsFileAttachmentRepository.GetListAsync(a => a.ApplicationId == applicationId); + var formSchema = await GetFormSchemaAsync(formSubmission?.ApplicationFormVersionId); + + var attachmentSummaries = PromptDataPayloadBuilder.BuildAttachmentSummaries(attachments); + var formFieldConfiguration = await PromptDataPayloadBuilder.BuildFormFieldConfigurationAsync( + applicationFormVersionRepository, + formSubmission?.ApplicationFormVersionId, + logger); + + return new Unity.AI.Operations.ApplicationAnalysisOperationInputDto + { + ApplicationId = applicationId, + Schema = JsonSerializer.SerializeToElement(formFieldConfiguration), + Data = PromptDataPayloadBuilder.BuildPromptDataPayload(application, formSubmission, formSchema, logger), + Attachments = attachmentSummaries, + PromptVersion = promptVersion + }; + } + + private async Task GetFormSchemaAsync(Guid? formVersionId) + { + if (formVersionId == null) + { + return null; + } + + try + { + var formVersion = await applicationFormVersionRepository.GetAsync(formVersionId.Value); + return string.IsNullOrWhiteSpace(formVersion?.FormSchema) ? null : formVersion.FormSchema; + } + catch (Exception ex) + { + logger.LogWarning(ex, "Unable to load form schema for prompt data generation for form version {FormVersionId}.", formVersionId); + return null; + } + } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationScoringAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationScoringAppService.cs index 9c65c1d1f4..b9426dc70c 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationScoringAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationScoringAppService.cs @@ -1,13 +1,19 @@ using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Localization; using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; using System.Threading.Tasks; using Unity.AI; -using Unity.AI.Features; using Unity.AI.Localization; using Unity.AI.Operations; +using Unity.AI.Prompts; using Unity.AI.Permissions; using Unity.AI.Settings; -using Unity.GrantManager.GrantApplications.Automation.Events; +using Unity.Flex.Domain.Scoresheets; +using Unity.GrantManager.Applications; using Volo.Abp; using Volo.Abp.EventBus.Local; @@ -16,8 +22,16 @@ namespace Unity.GrantManager.GrantApplications; [Authorize(AIPermissions.Analysis.GenerateScoring)] public class ApplicationScoringAppService( IApplicationScoringService applicationScoringService, + IApplicationRepository applicationRepository, + IApplicationFormRepository applicationFormRepository, + IApplicationFormSubmissionRepository applicationFormSubmissionRepository, + IApplicationFormVersionRepository applicationFormVersionRepository, + IApplicationChefsFileAttachmentRepository applicationChefsFileAttachmentRepository, + IScoresheetRepository scoresheetRepository, AIFeatureGuard featureGuard, - ILocalEventBus localEventBus) + ILocalEventBus localEventBus, + ILogger logger, + IStringLocalizer localizer) : AIAppService, IApplicationScoringAppService { public virtual async Task GenerateApplicationScoringAsync(Guid applicationId, string? promptVersion = null) @@ -26,7 +40,8 @@ await featureGuard.EnsureEnabledAsync( AIFeatures.Scoring, AILocalizationKeys.ScoringDisabled); - await applicationScoringService.RegenerateAndSaveAsync(applicationId, promptVersion); + var input = await BuildInputAsync(applicationId, promptVersion); + await applicationScoringService.RegenerateAndSaveAsync(input); if (UnitOfWorkManager.Current != null) { @@ -58,7 +73,133 @@ await localEventBus.PublishAsync(new ApplicationAIScoringGeneratedEvent [RemoteService(IsEnabled = false)] public virtual async Task GenerateApplicationScoringForPipelineAsync(Guid applicationId, string? promptVersion = null) { - await applicationScoringService.RegenerateAndSaveAsync(applicationId, promptVersion); + var input = await BuildInputAsync(applicationId, promptVersion); + await applicationScoringService.RegenerateAndSaveAsync(input); return new ApplicationScoringResultDto { Completed = true }; } + + private async Task BuildInputAsync(Guid applicationId, string? promptVersion) + { + 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]); + } + + var attachments = await applicationChefsFileAttachmentRepository.GetListAsync(a => a.ApplicationId == applicationId); + var attachmentSummaries = PromptDataPayloadBuilder.BuildAttachmentSummaries( + attachments, + excludeWhitespaceOnlySummaries: false); + + var formSubmission = await applicationFormSubmissionRepository.GetByApplicationAsync(applicationId); + var formSchema = await GetFormSchemaAsync(formSubmission?.ApplicationFormVersionId); + var promptData = PromptDataPayloadBuilder.BuildPromptDataPayload(application, formSubmission, formSchema, logger); + + var sections = scoresheet.Sections + .OrderBy(s => s.Order) + .Select(section => new ApplicationScoringSectionOperationInputDto + { + SectionName = section.Name, + SectionSchema = JsonSerializer.SerializeToElement(BuildSectionQuestionsData(section), AIJsonDefaults.IndentedCamelCase) + }) + .ToList(); + + return new ApplicationScoringOperationInputDto + { + ApplicationId = applicationId, + Data = promptData, + Attachments = attachmentSummaries, + Sections = sections, + PromptVersion = promptVersion + }; + } + + private async Task GetFormSchemaAsync(Guid? formVersionId) + { + if (formVersionId == null) + { + return null; + } + + try + { + var formVersion = await applicationFormVersionRepository.GetAsync(formVersionId.Value); + return string.IsNullOrWhiteSpace(formVersion?.FormSchema) ? null : formVersion.FormSchema; + } + catch (Exception ex) + { + logger.LogWarning(ex, "Unable to load form schema for application scoring prompt data generation for form version {FormVersionId}.", formVersionId); + return null; + } + } + + private static List BuildSectionQuestionsData(ScoresheetSection section) + { + var sectionQuestionsData = new List(); + foreach (var field in section.Fields.OrderBy(f => f.Order)) + { + var options = ExtractSelectListOptions(field); + sectionQuestionsData.Add(new + { + id = field.Id.ToString(), + section = section.Name, + question = field.Label, + description = field.Description, + type = field.Type.ToString(), + options, + allowed_answers = ExtractSelectListOptionNumbers(options) + }); + } + + return sectionQuestionsData; + } + + private static object[]? ExtractSelectListOptions(Question field) + { + if (field.Type != Unity.Flex.Scoresheets.Enums.QuestionType.SelectList || string.IsNullOrEmpty(field.Definition)) + { + return null; + } + + try + { + var definition = JsonSerializer.Deserialize(field.Definition); + if (definition?.Options != null && definition.Options.Count > 0) + { + return definition.Options + .Select((option, index) => + (object)new + { + number = index + 1, + value = option.Value + }) + .ToArray(); + } + } + catch (JsonException) + { + // Ignore malformed definition and return null options. + } + + return null; + } + + private static string[]? ExtractSelectListOptionNumbers(object[]? options) + { + if (options == null || options.Length == 0) + { + return null; + } + + return options + .Select((_, index) => (index + 1).ToString()) + .ToArray(); + } } From b5346bcdc2e79e95ec8ebde0ace88013eabf4fac Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Tue, 9 Jun 2026 09:51:50 -0700 Subject: [PATCH 12/15] [AB#32697] Update PaymentInfo layout to 3-column format - fix padding --- .../Views/Shared/Components/PaymentInfo/Default.cshtml | 8 ++++---- .../Unity.GrantManager.Web/Pages/Applicants/Details.css | 1 + .../Pages/GrantApplications/Details.css | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/Default.cshtml index d186ef5f8b..d2cb20c092 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/Default.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/Default.cshtml @@ -33,13 +33,13 @@ @* Zone Section : Payment.Summary *@ @if (await PermissionChecker.IsGrantedAsync(UnitySelector.Payment.Summary.Default)) { - +
@L["PaymentInfoView:PaymentInfoTitle"].Value
- +
$ @@ -49,7 +49,7 @@
- +
$ @@ -59,7 +59,7 @@
- +
$ diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.css index b305b80ba5..01349a8888 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.css +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.css @@ -76,6 +76,7 @@ #main-divider { width: 10px; + min-width: 10px; flex: 0 0 10px; background-color: #ccc; cursor: col-resize; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.css index 73828d06ba..eecf30ac21 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.css +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.css @@ -397,6 +397,7 @@ form label.error { #main-divider { width: 10px; + min-width: 10px; background-color: #ccc; cursor: col-resize; border-radius: 10px; From 7ca474a8ad18df2ddcfcfc9d150b85a400ac4663 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Tue, 9 Jun 2026 10:48:16 -0700 Subject: [PATCH 13/15] AB#33395 fix input contract branch build --- .../Operations/ApplicationAnalysisService.cs | 1 + .../ApplicationScoringAppService.cs | 3 + .../GenerateApplicationAnalysisJob.cs | 10 +- .../ApplicationAnalysisServiceTests.cs | 142 +++++------------- 4 files changed, 46 insertions(+), 110 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationAnalysisService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationAnalysisService.cs index 9767997c87..46277c0ac8 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationAnalysisService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationAnalysisService.cs @@ -5,6 +5,7 @@ using Unity.AI; using Unity.AI.Models; using Unity.AI.Requests; +using Unity.AI.Runtime; using Unity.GrantManager.Applications; using Volo.Abp.DependencyInjection; diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationScoringAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationScoringAppService.cs index b9426dc70c..2597a7c466 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationScoringAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationScoringAppService.cs @@ -7,13 +7,16 @@ using System.Text.Json; using System.Threading.Tasks; using Unity.AI; +using Unity.AI.Features; using Unity.AI.Localization; using Unity.AI.Operations; using Unity.AI.Prompts; +using Unity.AI.Runtime; using Unity.AI.Permissions; using Unity.AI.Settings; using Unity.Flex.Domain.Scoresheets; using Unity.GrantManager.Applications; +using Unity.GrantManager.GrantApplications.Automation.Events; using Volo.Abp; using Volo.Abp.EventBus.Local; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisJob.cs index 2094abad65..1348623d59 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisJob.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisJob.cs @@ -1,7 +1,6 @@ using Microsoft.Extensions.Logging; using System; using System.Threading.Tasks; -using Unity.AI.Operations; using Unity.AI.RateLimit; using Unity.GrantManager.GrantApplications; using Volo.Abp.BackgroundJobs; @@ -13,7 +12,7 @@ namespace Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; public class GenerateApplicationAnalysisJob( - IApplicationAnalysisService applicationAnalysisService, + IApplicationAnalysisAppService applicationAnalysisAppService, IRepository generationRequestRepository, ICurrentTenant currentTenant, IUnitOfWorkManager unitOfWorkManager, @@ -28,8 +27,11 @@ public override async Task ExecuteAsync(GenerateApplicationAnalysisBackgroundJob try { logger.LogInformation("Executing AI application analysis job for application {ApplicationId}.", args.ApplicationId); - await applicationAnalysisService.RegenerateAndSaveAsync(args.ApplicationId, args.PromptVersion); - logger.LogInformation("Completed AI application analysis job for application {ApplicationId}.", args.ApplicationId); + var result = await applicationAnalysisAppService.GenerateApplicationAnalysisForPipelineAsync(args.ApplicationId, args.PromptVersion); + if (result.Completed) + { + logger.LogInformation("Completed AI application analysis job for application {ApplicationId}.", args.ApplicationId); + } await AIGenerationRequestJobHelper.StampRateLimitBestEffortAsync(aiRateLimiter, logger, args.RequestedByUserId, args.ApplicationId, args.RequestKey); await AIGenerationRequestJobHelper.MarkCompletedInNewUowAsync(unitOfWorkManager, generationRequestRepository, args.RequestKey); diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/ApplicationAnalysisServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/ApplicationAnalysisServiceTests.cs index 42c27d35e6..75979a9701 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/ApplicationAnalysisServiceTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/ApplicationAnalysisServiceTests.cs @@ -3,10 +3,10 @@ using Shouldly; using System; using System.Collections.Generic; -using System.Linq.Expressions; using System.Text.Json; using System.Threading.Tasks; using Unity.AI; +using Unity.AI.Models; using Unity.AI.Operations; using Unity.AI.Requests; using Unity.AI.Responses; @@ -24,90 +24,41 @@ public ApplicationAnalysisServiceTests(ITestOutputHelper outputHelper) : base(ou } [Fact] - public async Task RegenerateAndSaveAsync_Builds_Filtered_Prompt_Payload_From_Form_Submission() + public async Task RegenerateAndSaveAsync_Uses_Input_Dto_And_Persists_Analysis() { var applicationId = Guid.NewGuid(); - var formVersionId = Guid.NewGuid(); + var application = WithId(new Application(), applicationId); ApplicationAnalysisRequest? capturedRequest = null; - var application = WithId(new Application - { - ApplicationFormId = Guid.NewGuid(), - ProjectName = "Fallback project", - ReferenceNo = "REF-001", - RequestedAmount = 100, - TotalProjectBudget = 200, - SubmissionDate = new DateTime(2026, 1, 2) - }, applicationId); - var submission = new ApplicationFormSubmission - { - ApplicationId = applicationId, - ApplicationFormVersionId = formVersionId, - Submission = """ - { - "data": { - "projectName": "Submitted project", - "requestedAmount": 12345, - "metadata": { "timezone": "utc" }, - "files": [{ "name": "large.pdf" }], - "ignoredField": "not in schema" - } - } - """ - }; - var formVersion = new ApplicationFormVersion - { - FormSchema = """ - { - "components": [ - { "key": "projectName", "label": "Project Name", "type": "textfield", "input": true, "validate": { "required": true } }, - { "key": "requestedAmount", "label": "Requested Amount", "type": "number", "input": true }, - { "key": "applicantAgent", "label": "Applicant Agent", "type": "textfield", "input": true }, - { "key": "ignoredField", "label": "Ignored Field", "type": "html", "input": false } - ] - } - """ - }; var applicationRepository = Substitute.For(); applicationRepository.GetAsync(applicationId).Returns(application); - var submissionRepository = Substitute.For(); - submissionRepository.GetByApplicationAsync(applicationId).Returns(submission); - - var formVersionRepository = Substitute.For(); - formVersionRepository.GetAsync(formVersionId).Returns(formVersion); - - var attachmentRepository = Substitute.For(); - attachmentRepository - .GetListAsync(Arg.Any>>()) - .Returns([ - new ApplicationChefsFileAttachment - { - FileName = " summary.pdf ", - AISummary = " Summary text " - }, - new ApplicationChefsFileAttachment - { - FileName = "empty.txt", - AISummary = " " - } - ]); - var aiService = Substitute.For(); aiService.GenerateApplicationAnalysisAsync(Arg.Do(request => capturedRequest = request)) .Returns(new ApplicationAnalysisResponse { Decision = "ok" }); + var prerequisiteValidator = Substitute.For(); var service = new ApplicationAnalysisService( applicationRepository, - submissionRepository, - formVersionRepository, - attachmentRepository, aiService, - prerequisiteValidator, - NullLogger.Instance); + prerequisiteValidator); - var result = await service.RegenerateAndSaveAsync(applicationId, "v1"); + var result = await service.RegenerateAndSaveAsync(new ApplicationAnalysisOperationInputDto + { + ApplicationId = applicationId, + Schema = JsonSerializer.SerializeToElement(new { projectName = "Project Name" }), + Data = JsonSerializer.SerializeToElement(new { projectName = "Submitted project" }), + Attachments = new List + { + new() + { + Name = "summary.pdf", + Summary = "Summary text" + } + }, + PromptVersion = "v1" + }); result.ShouldContain("\"Decision\": \"ok\""); capturedRequest.ShouldNotBeNull(); @@ -115,68 +66,47 @@ public async Task RegenerateAndSaveAsync_Builds_Filtered_Prompt_Payload_From_For capturedRequest.Attachments.Count.ShouldBe(1); capturedRequest.Attachments[0].Name.ShouldBe("summary.pdf"); capturedRequest.Attachments[0].Summary.ShouldBe("Summary text"); - capturedRequest.Data.GetProperty("projectName").GetString().ShouldBe("Submitted project"); - capturedRequest.Data.GetProperty("requestedAmount").GetInt32().ShouldBe(12345); - capturedRequest.Data.TryGetProperty("metadata", out _).ShouldBeFalse(); - capturedRequest.Data.TryGetProperty("files", out _).ShouldBeFalse(); - capturedRequest.Data.TryGetProperty("ignoredField", out _).ShouldBeFalse(); - - var schemaJson = JsonSerializer.Serialize(capturedRequest.Schema); - schemaJson.ShouldContain("Project Name (projectName)"); - schemaJson.ShouldContain("Requested Amount (requestedAmount)"); - schemaJson.ShouldNotContain("Applicant Agent (applicantAgent)"); + capturedRequest.Schema.GetProperty("projectName").GetString().ShouldBe("Project Name"); + + await prerequisiteValidator.Received(1).EnsureApplicationAnalysisAvailableAsync(applicationId); await applicationRepository.Received(1).UpdateAsync(application); } [Fact] - public async Task RegenerateAndSaveAsync_Falls_Back_To_Application_Data_When_Submission_Is_Missing() + public async Task RegenerateAndSaveAsync_Flows_Without_Repository_Loading_Inputs() { var applicationId = Guid.NewGuid(); - var application = WithId(new Application - { - ProjectName = "Fallback project", - ReferenceNo = "REF-002", - RequestedAmount = 1500, - TotalProjectBudget = 3000, - ProjectSummary = "Fallback summary", - SubmissionDate = new DateTime(2026, 2, 3) - }, applicationId); + var application = WithId(new Application(), applicationId); ApplicationAnalysisRequest? capturedRequest = null; var applicationRepository = Substitute.For(); applicationRepository.GetAsync(applicationId).Returns(application); - var submissionRepository = Substitute.For(); - submissionRepository.GetByApplicationAsync(applicationId).Returns((ApplicationFormSubmission?)null); - - var formVersionRepository = Substitute.For(); - var attachmentRepository = Substitute.For(); - attachmentRepository - .GetListAsync(Arg.Any>>()) - .Returns([]); - var aiService = Substitute.For(); aiService.GenerateApplicationAnalysisAsync(Arg.Do(request => capturedRequest = request)) .Returns(new ApplicationAnalysisResponse()); + var prerequisiteValidator = Substitute.For(); var service = new ApplicationAnalysisService( applicationRepository, - submissionRepository, - formVersionRepository, - attachmentRepository, aiService, - prerequisiteValidator, - NullLogger.Instance); + prerequisiteValidator); - await service.RegenerateAndSaveAsync(applicationId); + await service.RegenerateAndSaveAsync(new ApplicationAnalysisOperationInputDto + { + ApplicationId = applicationId, + Schema = JsonSerializer.SerializeToElement(new { }), + Data = JsonSerializer.SerializeToElement(new { project_name = "Fallback project" }), + Attachments = [], + PromptVersion = null + }); capturedRequest.ShouldNotBeNull(); capturedRequest.Data.GetProperty("project_name").GetString().ShouldBe("Fallback project"); - capturedRequest.Data.GetProperty("reference_number").GetString().ShouldBe("REF-002"); - capturedRequest.Data.GetProperty("requested_amount").GetDecimal().ShouldBe(1500); capturedRequest.Attachments.ShouldBeEmpty(); + capturedRequest.PromptVersion.ShouldBeNull(); } private static T WithId(T entity, Guid id) where T : Entity From 909ac880ef17a16ca803175af2b4be6fe7ab7b93 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Tue, 9 Jun 2026 11:29:03 -0700 Subject: [PATCH 14/15] AB#33009 address SDK review feedback --- .../AI/Runtime/OpenAIConfigurationResolver.cs | 20 +++++- .../AI/Runtime/OpenAIRuntimeService.cs | 68 +++++++++++-------- .../AI/Runtime/OpenAITransportService.cs | 2 - .../Intakes/ChefsAttachmentDownloadService.cs | 2 +- 4 files changed, 57 insertions(+), 35 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIConfigurationResolver.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIConfigurationResolver.cs index 7e0bb0126f..d608869f30 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIConfigurationResolver.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIConfigurationResolver.cs @@ -34,7 +34,7 @@ public OpenAIOperationSettings ResolveOperationSettings(string operationName) var providerName = ResolveProviderName(operationName); var profileName = ResolveProfileName(operationName); var apiKey = Required($"Azure:{providerName}:ApiKey"); - var endpoint = new Uri(Required($"Azure:{providerName}:Endpoint")); + var endpoint = ResolveEndpointUri(providerName); var deploymentName = RequiredProfile(providerName, profileName, "DeploymentName"); var promptVersion = Optional($"Azure:Operations:{operationName}:PromptVersion") ?? Required("Azure:Operations:Defaults:PromptVersion"); @@ -110,8 +110,7 @@ public string ResolvePromptVersion(string operationName) public Uri ResolveEndpoint(string? operationName = null) { var providerName = ResolveProviderName(operationName); - var endpoint = Required($"Azure:{providerName}:Endpoint"); - return new Uri(endpoint); + return ResolveEndpointUri(providerName); } public string ResolveDeploymentName(string? operationName = null) @@ -151,6 +150,21 @@ private string Required(string key) return Optional(key) ?? throw new InvalidOperationException($"{key} is not configured."); } + private Uri ResolveEndpointUri(string providerName) + { + var key = $"Azure:{providerName}:Endpoint"; + var endpoint = Required(key); + + try + { + return new Uri(endpoint); + } + catch (UriFormatException ex) + { + throw new InvalidOperationException($"{key} must be a valid absolute URI.", ex); + } + } + private string? Optional(string key) { var value = _configuration[key]; diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIRuntimeService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIRuntimeService.cs index 8befd007d2..e96efd23c7 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIRuntimeService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIRuntimeService.cs @@ -55,47 +55,60 @@ public Task IsAvailableAsync() public async Task GenerateApplicationAnalysisAsync(ApplicationAnalysisRequest request, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(request); - var settings = _openAIConfigurationResolver.ResolveOperationSettings(ApplicationAnalysisPromptType); - var promptVersion = OpenAIPromptRenderer.ResolvePromptVersion(request.PromptVersion ?? settings.PromptVersion); - var data = JsonSerializer.Serialize(request.Data, AIJsonDefaults.Indented); - var schema = JsonSerializer.Serialize(request.Schema, AIJsonDefaults.Indented); + try + { + ArgumentNullException.ThrowIfNull(request); + var settings = _openAIConfigurationResolver.ResolveOperationSettings(ApplicationAnalysisPromptType); + var promptVersion = OpenAIPromptRenderer.ResolvePromptVersion(request.PromptVersion ?? settings.PromptVersion); + var data = JsonSerializer.Serialize(request.Data, AIJsonDefaults.Indented); + var schema = JsonSerializer.Serialize(request.Schema, AIJsonDefaults.Indented); + + var attachmentsPayload = request.Attachments + .Select(a => new + { + name = string.IsNullOrWhiteSpace(a.Name) ? "attachment" : a.Name.Trim(), + summary = string.IsNullOrWhiteSpace(a.Summary) ? string.Empty : a.Summary.Trim() + }) + .Cast(); + + var attachments = JsonSerializer.Serialize(attachmentsPayload, AIJsonDefaults.Indented); + var systemPrompt = OpenAIPromptRenderer.BuildApplicationAnalysisSystemPrompt(promptVersion); + var applicationAnalysisContent = OpenAIPromptRenderer.BuildApplicationAnalysisUserPrompt( + promptVersion, + schema, + data, + attachments); - var attachmentsPayload = request.Attachments - .Select(a => new - { - name = string.IsNullOrWhiteSpace(a.Name) ? "attachment" : a.Name.Trim(), - summary = string.IsNullOrWhiteSpace(a.Summary) ? string.Empty : a.Summary.Trim() - }) - .Cast(); - - var attachments = JsonSerializer.Serialize(attachmentsPayload, AIJsonDefaults.Indented); - var systemPrompt = OpenAIPromptRenderer.BuildApplicationAnalysisSystemPrompt(promptVersion); - var applicationAnalysisContent = OpenAIPromptRenderer.BuildApplicationAnalysisUserPrompt( - promptVersion, - schema, - data, - attachments); - await _promptFileLogger.LogPromptInputAsync(ApplicationAnalysisPromptType, promptVersion, systemPrompt, applicationAnalysisContent, cancellationToken); + await _promptFileLogger.LogPromptInputAsync(ApplicationAnalysisPromptType, promptVersion, systemPrompt, applicationAnalysisContent, cancellationToken); var result = await GenerateWithRetryAsync( () => _openAITransportService.GenerateSummaryAsync( applicationAnalysisContent, systemPrompt, settings, settings.CompletionTokens, - promptVersion: promptVersion, cancellationToken: cancellationToken), AIProviderPayloadValidator.IsValidApplicationAnalysisJson, "application analysis", cancellationToken); - await _promptFileLogger.LogPromptOutputAsync(ApplicationAnalysisPromptType, promptVersion, result.CaptureOutput, cancellationToken); - if (result.Outcome != AIOperationOutcome.Success) + await _promptFileLogger.LogPromptOutputAsync(ApplicationAnalysisPromptType, promptVersion, result.CaptureOutput, cancellationToken); + + if (result.Outcome != AIOperationOutcome.Success) + { + return new ApplicationAnalysisResponse(); + } + + return OpenAIResponseParser.ParseApplicationAnalysisResponse(result.Content); + } + catch (OperationCanceledException) { + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error generating application analysis."); return new ApplicationAnalysisResponse(); } - - return OpenAIResponseParser.ParseApplicationAnalysisResponse(result.Content); } public async Task GenerateAttachmentSummaryAsync(AttachmentSummaryRequest request, CancellationToken cancellationToken = default) @@ -137,8 +150,6 @@ public async Task GenerateAttachmentSummaryAsync(Atta prompt, settings, settings.CompletionTokens, - promptVersion: promptVersion, - fileName: fileName, cancellationToken: cancellationToken), AIProviderPayloadValidator.IsValidAttachmentSummaryText, "attachment summary", @@ -214,7 +225,6 @@ public async Task GenerateApplicationScoringAsync(Ap systemPrompt, settings, settings.CompletionTokens, - promptVersion: promptVersion, cancellationToken: cancellationToken), content => AIProviderPayloadValidator.IsValidApplicationScoringJson(content, section), $"application scoring section {request.SectionName}", diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAITransportService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAITransportService.cs index fe3a73527c..db1cf6fdd2 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAITransportService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAITransportService.cs @@ -23,8 +23,6 @@ public async Task GenerateSummaryAsync( string? systemPrompt, OpenAIOperationSettings settings, int maxTokens = 150, - string? promptVersion = null, - string? fileName = null, CancellationToken cancellationToken = default) { try diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/ChefsAttachmentDownloadService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/ChefsAttachmentDownloadService.cs index 7a33c5335a..cc8f1116c7 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/ChefsAttachmentDownloadService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/ChefsAttachmentDownloadService.cs @@ -77,7 +77,7 @@ public async Task DownloadAsync(Guid? formSubmissionId, Guid? chefsFile private async Task GetApplicationFormBySubmissionId(Guid? formSubmissionId) { - ApplicationForm? applicationFormData = new(); + ApplicationForm? applicationFormData = null; if (formSubmissionId != null) { From 9d6377facecfb58c79a4303360f8fe26bb9c96f1 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Tue, 9 Jun 2026 13:23:31 -0700 Subject: [PATCH 15/15] AB#33395 refine AI input contracts --- .../AI/Operations/ApplicationAnalysisService.cs | 1 - .../AI/Operations/ApplicationScoringService.cs | 9 +++++++++ .../AI/Prompts/PromptDataPayloadBuilder.cs | 17 +++++------------ .../ApplicationAnalysisAppService.cs | 6 ++---- .../ApplicationAnalysisServiceTests.cs | 1 - 5 files changed, 16 insertions(+), 18 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationAnalysisService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationAnalysisService.cs index 46277c0ac8..8f8a1e5c94 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationAnalysisService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationAnalysisService.cs @@ -1,4 +1,3 @@ -using System; using System.Text.Json; using System.Threading; using System.Threading.Tasks; diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationScoringService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationScoringService.cs index 780a7d1098..34856d162a 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationScoringService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationScoringService.cs @@ -128,6 +128,15 @@ private async Task>> ProcessSectionsAsync( private static JsonElement BuildBatchSectionSchema(IReadOnlyCollection sections) { + foreach (var section in sections) + { + if (section.SectionSchema.ValueKind != JsonValueKind.Array) + { + throw new InvalidOperationException( + $"Section schema for '{section.SectionName}' must be a JSON array."); + } + } + var questions = new List(); foreach (var section in sections) { diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/PromptDataPayloadBuilder.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/PromptDataPayloadBuilder.cs index 66e5cf93e6..9788b9cd3f 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/PromptDataPayloadBuilder.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/PromptDataPayloadBuilder.cs @@ -91,25 +91,18 @@ public static List BuildAttachmentSummaries( .ToList(); } - public static async Task BuildFormFieldConfigurationAsync( - IApplicationFormVersionRepository applicationFormVersionRepository, - Guid? formVersionId, + public static object BuildFormFieldConfigurationAsync( + string? formSchema, ILogger logger) { - if (formVersionId == null) + if (string.IsNullOrWhiteSpace(formSchema)) { return new { message = "Form configuration not available." }; } try { - var formVersion = await applicationFormVersionRepository.GetAsync(formVersionId.Value); - if (formVersion == null || string.IsNullOrWhiteSpace(formVersion.FormSchema)) - { - return new { message = "Form configuration not available." }; - } - - var schema = JObject.Parse(formVersion.FormSchema); + var schema = JObject.Parse(formSchema); var components = schema[ComponentsKey] as JArray; if (components == null || components.Count == 0) { @@ -128,7 +121,7 @@ public static async Task BuildFormFieldConfigurationAsync( } catch (Exception ex) { - logger.LogError(ex, "Error extracting form field configuration for form version {FormVersionId}", formVersionId); + logger.LogError(ex, "Error extracting form field configuration from form schema."); return new { message = "Form configuration could not be extracted." }; } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationAnalysisAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationAnalysisAppService.cs index 75add8d8d3..61e4f928b9 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationAnalysisAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationAnalysisAppService.cs @@ -1,7 +1,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Logging; using System; -using System.Linq; using System.Text.Json; using System.Threading.Tasks; using Unity.AI; @@ -59,9 +58,8 @@ public virtual async Task GenerateApplicationAnaly var formSchema = await GetFormSchemaAsync(formSubmission?.ApplicationFormVersionId); var attachmentSummaries = PromptDataPayloadBuilder.BuildAttachmentSummaries(attachments); - var formFieldConfiguration = await PromptDataPayloadBuilder.BuildFormFieldConfigurationAsync( - applicationFormVersionRepository, - formSubmission?.ApplicationFormVersionId, + var formFieldConfiguration = PromptDataPayloadBuilder.BuildFormFieldConfigurationAsync( + formSchema, logger); return new Unity.AI.Operations.ApplicationAnalysisOperationInputDto diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/ApplicationAnalysisServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/ApplicationAnalysisServiceTests.cs index 75979a9701..579cdf7b16 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/ApplicationAnalysisServiceTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/ApplicationAnalysisServiceTests.cs @@ -1,4 +1,3 @@ -using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; using Shouldly; using System;