diff --git a/applications/Unity.GrantManager/common.props b/applications/Unity.GrantManager/common.props index 85e7cf2d80..f461c7ac7d 100644 --- a/applications/Unity.GrantManager/common.props +++ b/applications/Unity.GrantManager/common.props @@ -9,6 +9,7 @@ $(NoWarn);0436 + $(NoWarn);NU1510 @@ -24,7 +25,7 @@ - + \ No newline at end of file diff --git a/applications/Unity.GrantManager/global.json b/applications/Unity.GrantManager/global.json new file mode 100644 index 0000000000..c396020fc7 --- /dev/null +++ b/applications/Unity.GrantManager/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "10.0.303", + "rollForward": "latestPatch", + "allowPrerelease": false + } +} 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 157e19c3fa..61e2236b05 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 @@ -1,4 +1,4 @@ - + net10.0 @@ -10,7 +10,7 @@ - + diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/Components/DataGrid/DataGridWriteService.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/Components/DataGrid/DataGridWriteService.cs index 7489f231e2..8144f245e5 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/Components/DataGrid/DataGridWriteService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/Components/DataGrid/DataGridWriteService.cs @@ -279,7 +279,7 @@ internal async Task UpdateRowAsync(RowInputData rowInputDa }; } - internal async Task DeleteRowAsync(Guid valueId, uint row, Guid worksheetInstanceId) + public async Task DeleteRowAsync(Guid valueId, uint row, Guid worksheetInstanceId) { var currentValue = await customFieldValueAppService.GetAsync(valueId); var dataGridValue = DataGridServiceUtils.DeserializeDataGridValue(currentValue.CurrentValue); diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionSelectListWidget/QuestionSelectListViewModel.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionSelectListWidget/QuestionSelectListViewModel.cs index b61da02057..0f46f4f783 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionSelectListWidget/QuestionSelectListViewModel.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionSelectListWidget/QuestionSelectListViewModel.cs @@ -7,7 +7,7 @@ public class QuestionSelectListViewModel : RequiredFieldViewModel public Guid QuestionId { get; set; } public bool IsDisabled { get; set; } public string Answer { get; set; } = string.Empty; - public string Definition { get; set; } = "{}"; + public new string Definition { get; set; } = "{}"; public bool IsHumanConfirmed { get; set; } = true; public string? AICitation { get; set; } public int? AIConfidence { get; set; } diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Templates/TemplatesService.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Templates/TemplatesService.cs index 1468102240..1935c9b638 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Templates/TemplatesService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Templates/TemplatesService.cs @@ -1,38 +1,37 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; +using Microsoft.EntityFrameworkCore; using Volo.Abp.Application.Services; using Volo.Abp.DependencyInjection; using Volo.Abp.MultiTenancy; +using Volo.Abp.Uow; +using Unity.Notifications.Permissions; namespace Unity.Notifications.Templates; [Dependency(ReplaceServices = false)] [ExposeServices(typeof(TemplateService), typeof(ITemplateService))] -public class TemplateService : ApplicationService, ITemplateService +public class TemplateService( + ITemplatesRepository templatesRepository, + ICurrentTenant currentTenant, + ITemplateVariablesRepository templateVariablesRepository, + IUnitOfWorkManager unitOfWorkManager, + Volo.Abp.Domain.Repositories.IRepository scheduledNotificationsRepository + + ) : ApplicationService, ITemplateService { - private readonly ITemplatesRepository _templatesRepository; - private readonly ICurrentTenant _currentTenant; - private readonly ITemplateVariablesRepository _templateVariablesRepository; + private readonly ITemplatesRepository _templatesRepository = templatesRepository; + private readonly ICurrentTenant _currentTenant = currentTenant; + private readonly ITemplateVariablesRepository _templateVariablesRepository = templateVariablesRepository; + private readonly IUnitOfWorkManager _unitOfWorkManager = unitOfWorkManager; + private readonly Volo.Abp.Domain.Repositories.IRepository _scheduledNotificationsRepository = scheduledNotificationsRepository; - - public TemplateService( - ITemplatesRepository templatesRepository, - ICurrentTenant currentTenant, - ITemplateVariablesRepository templateVariablesRepository - - ) - { - _templatesRepository = templatesRepository; - _currentTenant = currentTenant; - _templateVariablesRepository = templateVariablesRepository; - } - - [Authorize] + [Authorize(NotificationsPermissions.Email.Default)] public async Task CreateAsync(EmailTempateDto templateDto) { var templateType = NormalizeTemplateType(templateDto.TemplateType); @@ -51,16 +50,16 @@ ITemplateVariablesRepository templateVariablesRepository templateType)); } - [Authorize] + [Authorize(NotificationsPermissions.Email.Send)] public async Task UpdateTemplate(Guid id, EmailTempateDto templateDto) { - + EmailTemplate template = await _templatesRepository.GetAsync(id); template.Description = templateDto.Description; template.Subject = templateDto.Subject; template.BodyText = templateDto.BodyText; - template.BodyHTML = templateDto.BodyHTML != null ? templateDto.BodyHTML : ""; + template.BodyHTML = templateDto.BodyHTML ?? ""; template.Name = templateDto.Name; template.SendFrom = templateDto.SendFrom; template.RecipientCategory = templateDto.RecipientCategory; @@ -72,31 +71,46 @@ ITemplateVariablesRepository templateVariablesRepository return updatedTemplate; } - [Authorize] + [Authorize(NotificationsPermissions.Email.Default)] public async Task> GetTemplatesByTenant() { var tenentId = _currentTenant.Id; return await _templatesRepository.GetByTenentIdAsync(tenentId); } - [Authorize] + + // Shared lookup endpoint: accessible to anyone with email/notification access (lookup, schedule, send) + [Authorize(NotificationsPermissions.Email.Default)] public async Task GetTemplateById(Guid id) { return await _templatesRepository.GetAsync(id); } - [Authorize] + [Authorize(NotificationsPermissions.Email.Send)] public async Task DeleteTemplate(Guid id) { - await _templatesRepository.DeleteAsync(id); + using var uow = _unitOfWorkManager.Begin(requiresNew: true, isTransactional: true); + var template = await _templatesRepository.LockActiveAsync(id) + ?? throw new Volo.Abp.Domain.Entities.EntityNotFoundException(typeof(EmailTemplate), id); + + var scheduledQuery = await _scheduledNotificationsRepository.GetQueryableAsync(); + var inUse = await scheduledQuery.AnyAsync(x => x.EmailTemplateId == id); + if (inUse) + { + throw new Volo.Abp.BusinessException("Notifications:TemplateInUse", "Template is referenced by scheduled notifications and cannot be deleted."); + } + + await _templatesRepository.DeleteAsync(template); + await uow.CompleteAsync(); } - [Authorize] + [Authorize(NotificationsPermissions.Email.Send)] public async Task GetTemplateByName(string name) { var data = await _templatesRepository.GetByNameAsync(name); return data; - } - - [Authorize] + } + + // Shared lookup endpoint: accessible to anyone with email/notification access + [Authorize(NotificationsPermissions.Email.Default)] public Task> GetTemplateTypes() { return Task.FromResult(new List @@ -106,15 +120,15 @@ public Task> GetTemplateTypes() }); } - [Authorize] + // Shared lookup endpoint: accessible to anyone with email/notification access (forms, templates, scheduling) + [Authorize(NotificationsPermissions.Email.Default)] public async Task> GetTemplateVariables(string? templateType = null) { var selectedType = NormalizeTemplateType(templateType); var templateVariables = await _templateVariablesRepository.GetQueryableAsync(); - return templateVariables + return [.. templateVariables .Where(x => x.TemplateType == selectedType) - .OrderBy(x => x.Name) - .ToList(); + .OrderBy(x => x.Name)]; } private static string NormalizeTemplateType(string? templateType) diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Templates/ITemplatesRepository.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Templates/ITemplatesRepository.cs index 5e7c38e3bd..6fa6448ddb 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Templates/ITemplatesRepository.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Templates/ITemplatesRepository.cs @@ -8,6 +8,7 @@ namespace Unity.Notifications.Templates public interface ITemplatesRepository : IBasicRepository { Task GetByIdAsync(Guid id, bool includeDetails = false); + Task LockActiveAsync(Guid id); Task> GetByTenentIdAsync(Guid? tenentId); Task GetByNameAsync(string name); } diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/EntityFrameworkCore/NotificationsDbContextModelCreatingExtensions.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/EntityFrameworkCore/NotificationsDbContextModelCreatingExtensions.cs index c09380facb..c74679e5b0 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/EntityFrameworkCore/NotificationsDbContextModelCreatingExtensions.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/EntityFrameworkCore/NotificationsDbContextModelCreatingExtensions.cs @@ -147,7 +147,7 @@ public static void ConfigureNotifications( b.ConfigureByConvention(); b.Property(x => x.TemplateType) .HasMaxLength(64) - .HasDefaultValue(TemplateTypes.Applicant) + .HasDefaultValue(TemplateTypes.Application) .IsRequired(); }); modelBuilder.Entity(b => diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/Repositories/TemplatesRepository.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/Repositories/TemplatesRepository.cs index d65f96c739..bf7ecfc47b 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/Repositories/TemplatesRepository.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/Repositories/TemplatesRepository.cs @@ -13,18 +13,40 @@ namespace Unity.Notifications.Repositories { - public class TemplatesRepository : EfCoreRepository, ITemplatesRepository + public class TemplatesRepository(IDbContextProvider dbContextProvider) : EfCoreRepository(dbContextProvider), ITemplatesRepository { - public TemplatesRepository(IDbContextProvider dbContextProvider) : base(dbContextProvider) - { - } - public async Task GetByIdAsync(Guid id, bool includeDetails = false) { var dbSet = await GetDbSetAsync(); return await dbSet.FirstOrDefaultAsync(s => s.Id == id); } + public async Task LockActiveAsync(Guid id) + { + var dbContext = await GetDbContextAsync(); + if (dbContext.Database.ProviderName == "Npgsql.EntityFrameworkCore.PostgreSQL") + { + if (dbContext.Database.CurrentTransaction == null) + { + throw new InvalidOperationException("Template row locking requires a database transaction."); + } + + return await dbContext.EmailTemplates + .FromSqlInterpolated($"""SELECT * FROM "Notifications"."EmailTemplates" WHERE "Id" = {id} FOR UPDATE""") + .SingleOrDefaultAsync(); + } + + if (dbContext.Database.ProviderName == "Microsoft.EntityFrameworkCore.Sqlite") + { + // SQLite has no SELECT FOR UPDATE; a write serializes access inside a transaction. + await dbContext.Database.ExecuteSqlInterpolatedAsync( + $"""UPDATE "EmailTemplates" SET "IsDeleted" = "IsDeleted" WHERE "Id" = {id}"""); + return await dbContext.EmailTemplates.SingleOrDefaultAsync(x => x.Id == id); + } + + throw new NotSupportedException("Template row locking requires PostgreSQL or SQLite."); + } + public async Task> GetByTenentIdAsync(Guid? tenentId) { var dbSet = await GetDbSetAsync(); diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Pages/Notifications/Index.js b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Pages/Notifications/Index.js index a5843e3442..e317d7921c 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Pages/Notifications/Index.js +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Pages/Notifications/Index.js @@ -247,7 +247,7 @@ $(function () { setDateRangeFilters(selectedRange, range); setDateRangeLocalStorage(selectedRange, range); updateDateInputsVisibility(); - notificationsTable.ajax.reload(null, true); + dataTable.ajax.reload(null, true); } function handleInputFilterChange() { @@ -261,8 +261,7 @@ $(function () { localStorage.setItem(STORAGE_KEYS.quickRange, 'custom'); localStorage.setItem(STORAGE_KEYS.fromDate, notificationTableFilters.dateFrom || ''); localStorage.setItem(STORAGE_KEYS.toDate, notificationTableFilters.dateTo || ''); - - notificationsTable.ajax.reload(null, true); + dataTable.ajax.reload(null, true); } function setDateRangeFilters(quickDateRange, range) { diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.js b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.js index 630dd8bb11..6990c18554 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.js +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.js @@ -844,10 +844,8 @@ $(function () { } function handleDeleteCanCheckError(templateId) { - // If check fails, proceed with deletion - proceedWithDelete(templateId, function () { - PubSub.publish('reload_templates_table_with_close'); - }); + // If check fails, do not proceed with deletion. Let the user know the preflight check failed. + abp.notify.error('Unable to verify whether this template can be safely deleted. Deletion has been cancelled.'); } function checkCanDeleteTemplate(templateId) { diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/test/Unity.Notifications.Application.Tests/ScheduledNotificationHelperTests.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/test/Unity.Notifications.Application.Tests/ScheduledNotificationHelperTests.cs index 8a98d4bb5b..35c35d916d 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/test/Unity.Notifications.Application.Tests/ScheduledNotificationHelperTests.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/test/Unity.Notifications.Application.Tests/ScheduledNotificationHelperTests.cs @@ -63,6 +63,35 @@ public void BuildTokenValues_ForApplicationTemplate_PreservesApplicationValues() values["unity_application_id"].ShouldBe("UNITY-456"); } + [Fact] + public void BuildTokenValues_ForDeclinedApplication_MapsDisplayDeclineRationale() + { + var application = new Application + { + Applicant = new Applicant { ApplicantName = "VelangTest2", OrgName = "DEC DESIGN" }, + ApplicationStatus = new ApplicationStatus { InternalStatus = "Declined" }, + ApplicationForm = new ApplicationForm { Category = "Community" }, + ReferenceNo = "SUB-123", + DeclineRational = "INELIGIBLE_PROJECT", + ProjectName = "Test project", + RequestedAmount = 1000m, + UnityApplicationId = "UNITY-123" + }; + var applicantAgent = new ApplicantAgent { Name = "Test Contact", Title = "Director" }; + + var values = ScheduledNotificationHelper.BuildTokenValues(application, applicantAgent); + + values["applicant_name"].ShouldBe("VelangTest2"); + values["organization_name"].ShouldBe("DEC DESIGN"); + values["decline_rationale"].ShouldBe("Ineligible Project"); + values["status"].ShouldBe("Declined"); + values["contact_full_name"].ShouldBe("Test Contact"); + values["contact_title"].ShouldBe("Director"); + values["category"].ShouldBe("Community"); + values["submission_number"].ShouldBe("SUB-123"); + values["unity_application_id"].ShouldBe("UNITY-123"); + } + [Fact] public void BuildTokenValues_ForApplicantTemplate_RendersMissingOptionalValuesAsEmpty() { diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/test/Unity.Notifications.Application.Tests/TemplatesServiceTests.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/test/Unity.Notifications.Application.Tests/TemplatesServiceTests.cs index 60664aafcd..7e62b2d5a7 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/test/Unity.Notifications.Application.Tests/TemplatesServiceTests.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/test/Unity.Notifications.Application.Tests/TemplatesServiceTests.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using NSubstitute; @@ -52,6 +52,8 @@ private static TemplateService CreateService(IEnumerable varia return new TemplateService( Substitute.For(), Substitute.For(), - variablesRepository); + variablesRepository, + Substitute.For(), +Substitute.For>()); } } \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/test/Unity.Notifications.EntityFrameworkCore.Tests/TemplateLockRepositoryTests.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/test/Unity.Notifications.EntityFrameworkCore.Tests/TemplateLockRepositoryTests.cs new file mode 100644 index 0000000000..79acc8fded --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/test/Unity.Notifications.EntityFrameworkCore.Tests/TemplateLockRepositoryTests.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading.Tasks; +using Shouldly; +using Unity.Notifications.EntityFrameworkCore; +using Unity.Notifications.Templates; +using Volo.Abp.MultiTenancy; +using Xunit; + +namespace Unity.Notifications; + +public class TemplateLockRepositoryTests : NotificationsEntityFrameworkCoreTestBase +{ + [Fact] + public async Task LockActiveAsync_ReturnsOnlyActiveTemplatesForCurrentTenant() + { + var repository = GetRequiredService(); + var tenant = GetRequiredService(); + var tenantId = Guid.NewGuid(); + var template = new EmailTemplate(Guid.NewGuid(), "test", "", "", "", "", "") { TenantId = tenantId }; + + using (tenant.Change(tenantId)) + { + await WithUnitOfWorkAsync(() => repository.InsertAsync(template, autoSave: true)); + await WithUnitOfWorkAsync(async () => + { + (await repository.LockActiveAsync(template.Id)).ShouldNotBeNull(); + }); + } + + using (tenant.Change(Guid.NewGuid())) + { + await WithUnitOfWorkAsync(async () => + { + (await repository.LockActiveAsync(template.Id)).ShouldBeNull(); + }); + } + + using (tenant.Change(tenantId)) + { + await WithUnitOfWorkAsync(() => repository.DeleteAsync(template.Id)); + await WithUnitOfWorkAsync(async () => + { + (await repository.LockActiveAsync(template.Id)).ShouldBeNull(); + }); + } + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/PaymentRequests/PaymentRequest.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/PaymentRequests/PaymentRequest.cs index 4742e2178b..1d1be952b3 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/PaymentRequests/PaymentRequest.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/PaymentRequests/PaymentRequest.cs @@ -163,6 +163,19 @@ public PaymentRequest SetPaymentRequestStatus(PaymentRequestStatus status) return this; } + public void SetLevel3ApprovalRequired(bool required, Guid approvalId) + { + var level3Approval = ExpenseApprovals.FirstOrDefault(x => x.Type == ExpenseApprovalType.Level3); + if (required && level3Approval == null) + { + ExpenseApprovals.Add(new ExpenseApproval(approvalId, ExpenseApprovalType.Level3)); + } + else if (!required && level3Approval != null) + { + ExpenseApprovals.Remove(level3Approval); + } + } + public PaymentRequest SetInvoiceStatus(string status) { InvoiceStatus = status; diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/PaymentsManager.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/PaymentsManager.cs index 0960c1a8fb..e393e06100 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/PaymentsManager.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Domain/Services/PaymentsManager.cs @@ -127,6 +127,7 @@ public async Task TriggerAction(Guid paymentRequestsId, PaymentA { var index = paymentRequest.ExpenseApprovals.FindIndex(i => i.Type == ExpenseApprovalType.Level2); paymentRequest.ExpenseApprovals[index].Approve(currentUserId); + paymentRequest.SetLevel3ApprovalRequired(true, GuidGenerator.Create()); statusChangedTo = PaymentRequestStatus.L3Pending; } @@ -150,6 +151,7 @@ public async Task TriggerAction(Guid paymentRequestsId, PaymentA { var index = paymentRequest.ExpenseApprovals.FindIndex(i => i.Type == ExpenseApprovalType.Level2); paymentRequest.ExpenseApprovals[index].Approve(currentUserId); + paymentRequest.SetLevel3ApprovalRequired(false, GuidGenerator.Create()); } else if (await HasPermissionAsync(PaymentsPermissions.Payments.L3ApproveOrDecline) && paymentRequest.Status == PaymentRequestStatus.L3Pending) { diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/BackgroundJobWorkers/FinancialNotificationSummaryWorker.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/BackgroundJobWorkers/FinancialNotificationSummaryWorker.cs index 84e5e516d0..4b24c4585e 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/BackgroundJobWorkers/FinancialNotificationSummaryWorker.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/BackgroundJobWorkers/FinancialNotificationSummaryWorker.cs @@ -2,7 +2,6 @@ using System; using System.Threading.Tasks; using Unity.Payments.PaymentRequests.Notifications; -using Volo.Abp.BackgroundWorkers.Quartz; using Volo.Abp.SettingManagement; using Microsoft.Extensions.Logging; using Unity.Payments.PaymentRequests.BackgroundJobWorkers; @@ -13,7 +12,7 @@ namespace Unity.Payments.PaymentRequests; [DisallowConcurrentExecution] -public class FinancialNotificationSummaryWorker : QuartzBackgroundWorkerBase +public class FinancialNotificationSummaryWorker : UnityQuartzBackgroundWorkerBase { private readonly FinancialSummaryNotifier _financialSummaryNotifier; private readonly IEnumerable _strategies; @@ -45,10 +44,15 @@ public FinancialNotificationSummaryWorker( .Build(); } - public override async Task Execute(IJobExecutionContext context) + protected override async Task ExecuteInBackgroundAsync(IJobExecutionContext context) { Logger.LogInformation("FinancialNotificationSummary Execute"); - await _financialSummaryNotifier.NotifyFailedPayments(_strategies); + foreach (var tenantId in await _financialSummaryNotifier.GetTenantIdsAsync()) + { + await ExecuteInTenantAuditScopeAsync( + tenantId, + () => _financialSummaryNotifier.NotifyFailedPaymentsForTenantAsync(tenantId, _strategies)); + } } private static string ResolveCronExpression(ISettingManager settingManager, ILogger logger) diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/BackgroundJobWorkers/FinancialSummaryNotifier.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/BackgroundJobWorkers/FinancialSummaryNotifier.cs index 468ae9172c..3e88926f2d 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/BackgroundJobWorkers/FinancialSummaryNotifier.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/BackgroundJobWorkers/FinancialSummaryNotifier.cs @@ -25,13 +25,24 @@ public class FinancialSummaryNotifier( ILocalEventBus localEventBus, ILogger logger) : ISingletonDependency { - public async Task NotifyFailedPayments(IEnumerable strategies) + public async Task> GetTenantIdsAsync() { var tenants = await tenantRepository.GetListAsync(); - foreach (var tenantId in tenants.Select(tenant => tenant.Id)) + return tenants.Select(tenant => tenant.Id).ToList(); + } + + public async Task NotifyFailedPayments(IEnumerable strategies) + { + foreach (var tenantId in await GetTenantIdsAsync()) + { + await NotifyFailedPaymentsForTenantAsync(tenantId, strategies); + } + } + + public async Task NotifyFailedPaymentsForTenantAsync(Guid tenantId, IEnumerable strategies) + { + using (currentTenant.Change(tenantId)) { - using (currentTenant.Change(tenantId)) - { List failedPaymentList = await GetFailedPayments(); if (failedPaymentList != null && failedPaymentList.Count > 0) { @@ -61,7 +72,7 @@ public async Task NotifyFailedPayments(IEnumerable stra if (recipientEmails.Count == 0) { logger.LogWarning("NotifyFailedPayments: No recipients found from any strategy for tenant {TenantId}", tenantId); - continue; + return; } var defaultFromAddress = await settingProvider.GetOrNullAsync(NotificationsSettings.Mailing.DefaultFromAddress); @@ -83,7 +94,6 @@ await localEventBus.PublishAsync( } } - } } } diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/BackgroundJobWorkers/ReconciliationProducer.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/BackgroundJobWorkers/ReconciliationProducer.cs index 73c187eb55..923839903d 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/BackgroundJobWorkers/ReconciliationProducer.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/BackgroundJobWorkers/ReconciliationProducer.cs @@ -1,6 +1,5 @@ using System.Threading.Tasks; using Quartz; -using Volo.Abp.BackgroundWorkers.Quartz; using Volo.Abp.SettingManagement; using Unity.Modules.Shared.Utils; using Unity.Payments.Settings; @@ -9,7 +8,7 @@ namespace Unity.Payments.PaymentRequests; [DisallowConcurrentExecution] -public class ReconciliationProducer : QuartzBackgroundWorkerBase +public class ReconciliationProducer : UnityQuartzBackgroundWorkerBase { private readonly CasPaymentRequestCoordinator _casPaymentRequestCoordinator; @@ -42,7 +41,7 @@ CasPaymentRequestCoordinator casPaymentRequestCoordinator } } - public override async Task Execute(IJobExecutionContext context) + protected override async Task ExecuteInBackgroundAsync(IJobExecutionContext context) { await _casPaymentRequestCoordinator.AddPaymentRequestsToReconciliationQueue(); await Task.CompletedTask; diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/CasPaymentRequestCoordinator.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/CasPaymentRequestCoordinator.cs index 74f126b99a..f8a8c4c710 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/CasPaymentRequestCoordinator.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/CasPaymentRequestCoordinator.cs @@ -13,6 +13,7 @@ using Unity.Payments.Events; using Unity.Payments.RabbitMQ.QueueMessages; using Unity.Notifications.Integrations.RabbitMQ; +using Volo.Abp.Data; using Volo.Abp.EventBus.Local; namespace Unity.Payments.PaymentRequests @@ -129,55 +130,151 @@ public async Task AddPaymentRequestsToReconciliationQueue() return null; } - using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: true); + const int maxConcurrencyAttempts = 3; + CasPaymentValues? originalCasValues = null; - var paymentRequest = await paymentRequestsRepository.GetAsync(PaymentRequestId); - var previousPaymentStatus = paymentRequest.PaymentStatus; - var previousStatus = paymentRequest.Status; + for (var attempt = 1; ; attempt++) + { + try + { + using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: true); + + var paymentRequest = await paymentRequestsRepository.GetAsync(PaymentRequestId); + var previousPaymentStatus = paymentRequest.PaymentStatus; + var previousStatus = paymentRequest.Status; + originalCasValues ??= GetCurrentCasValues(paymentRequest); + + var casValues = GetCasValues(paymentRequest, result); + if (attempt > 1) + { + var currentCasValues = GetCurrentCasValues(paymentRequest); + if (currentCasValues == casValues) + { + return paymentRequest; + } - UpdatePaymentRequestFromCasResult(paymentRequest, result); + if (currentCasValues != originalCasValues) + { + Logger.LogWarning( + "Rejecting stale CAS result for payment request {PaymentRequestId}; the payment has a different CAS state.", + PaymentRequestId); + return paymentRequest; + } + } - await paymentRequestsRepository.UpdateAsync(paymentRequest, autoSave: false); + ApplyCasValues(paymentRequest, casValues); - // CompleteAsync commits the transaction and calls SaveChangesAsync, - // which triggers AbpDbContext to collect entity changes into the active audit log. - // The audit log is then persisted by QueueConsumerHandler after ConsumeAsync returns. - await uow.CompleteAsync(); + await paymentRequestsRepository.UpdateAsync(paymentRequest, autoSave: false); - if (previousPaymentStatus != paymentRequest.PaymentStatus || previousStatus != paymentRequest.Status) - { - await localEventBus.PublishAsync(new PaymentStatusChangedEvent + // CompleteAsync commits the transaction and calls SaveChangesAsync, + // which triggers AbpDbContext to collect entity changes into the active audit log. + // The audit log is then persisted by QueueConsumerHandler after ConsumeAsync returns. + await uow.CompleteAsync(); + + if (previousPaymentStatus != paymentRequest.PaymentStatus || previousStatus != paymentRequest.Status) + { + await localEventBus.PublishAsync(new PaymentStatusChangedEvent + { + PaymentRequestId = paymentRequest.Id, + ApplicationId = paymentRequest.CorrelationId, + Status = paymentRequest.Status, + CasPaymentStatus = paymentRequest.PaymentStatus, + TenantId = TenantId + }); + } + + return paymentRequest; + } + catch (AbpDbConcurrencyException ex) when (attempt < maxConcurrencyAttempts) { - PaymentRequestId = paymentRequest.Id, - ApplicationId = paymentRequest.CorrelationId, - Status = paymentRequest.Status, - CasPaymentStatus = paymentRequest.PaymentStatus, - TenantId = TenantId - }); - } + using var inspectionUow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false); + var currentPaymentRequest = await paymentRequestsRepository.GetAsync(PaymentRequestId); + var currentCasValues = GetCurrentCasValues(currentPaymentRequest); + var desiredCasValues = GetCasValues(currentPaymentRequest, result); - return paymentRequest; + if (currentCasValues == desiredCasValues) + { + return currentPaymentRequest; + } + + if (currentCasValues != originalCasValues) + { + Logger.LogWarning( + ex, + "Rejecting stale CAS result for payment request {PaymentRequestId}; the payment has a different CAS state.", + PaymentRequestId); + return currentPaymentRequest; + } + + Logger.LogWarning( + ex, + "Concurrency conflict updating payment request {PaymentRequestId} from CAS; retrying attempt {Attempt} of {MaxAttempts}.", + PaymentRequestId, + attempt + 1, + maxConcurrencyAttempts); + } + catch (AbpDbConcurrencyException) + { + throw; + } + } } - private static void UpdatePaymentRequestFromCasResult(PaymentRequest paymentRequest, CasPaymentSearchResult result) + private static CasPaymentValues GetCasValues(PaymentRequest paymentRequest, CasPaymentSearchResult result) { - // Handle duplicate NotFound status by appending "2" - if (paymentRequest.InvoiceStatus == CasPaymentRequestStatus.NotFound && - result.InvoiceStatus == CasPaymentRequestStatus.NotFound) + var invoiceStatus = result.InvoiceStatus ?? string.Empty; + if (paymentRequest.InvoiceStatus == CasPaymentRequestStatus.NotFound + "2" && + invoiceStatus == CasPaymentRequestStatus.NotFound) { - result.InvoiceStatus = CasPaymentRequestStatus.NotFound + "2"; + invoiceStatus = paymentRequest.InvoiceStatus; } - paymentRequest.SetInvoiceStatus(result.InvoiceStatus ?? ""); - paymentRequest.SetPaymentStatus(result.PaymentStatus ?? ""); - paymentRequest.SetPaymentDate(result.PaymentDate ?? ""); - paymentRequest.SetPaymentNumber(result.PaymentNumber ?? ""); - - if (result.InvoiceStatus != null) + if (paymentRequest.InvoiceStatus == CasPaymentRequestStatus.NotFound && + invoiceStatus == CasPaymentRequestStatus.NotFound) { - paymentRequest.SetCasHttpStatusCode((int)System.Net.HttpStatusCode.OK); - paymentRequest.SetCasResponse("SUCCEEDED"); + invoiceStatus = CasPaymentRequestStatus.NotFound + "2"; } + + return new CasPaymentValues( + invoiceStatus, + result.PaymentStatus ?? string.Empty, + result.PaymentNumber ?? string.Empty, + result.PaymentDate ?? string.Empty, + result.InvoiceStatus != null ? (int)System.Net.HttpStatusCode.OK : null, + result.InvoiceStatus != null ? "SUCCEEDED" : null); } + + private static CasPaymentValues GetCurrentCasValues(PaymentRequest paymentRequest) + { + return new CasPaymentValues( + paymentRequest.InvoiceStatus ?? string.Empty, + paymentRequest.PaymentStatus ?? string.Empty, + paymentRequest.PaymentNumber ?? string.Empty, + paymentRequest.PaymentDate ?? string.Empty, + paymentRequest.CasHttpStatusCode, + paymentRequest.CasResponse); + } + + private static void ApplyCasValues(PaymentRequest paymentRequest, CasPaymentValues casValues) + { + paymentRequest.SetInvoiceStatus(casValues.InvoiceStatus); + paymentRequest.SetPaymentStatus(casValues.PaymentStatus); + paymentRequest.SetPaymentDate(casValues.PaymentDate); + paymentRequest.SetPaymentNumber(casValues.PaymentNumber); + + if (casValues.CasHttpStatusCode.HasValue) + { + paymentRequest.SetCasHttpStatusCode(casValues.CasHttpStatusCode.Value); + paymentRequest.SetCasResponse(casValues.CasResponse ?? string.Empty); + } + } + + private readonly record struct CasPaymentValues( + string InvoiceStatus, + string PaymentStatus, + string PaymentNumber, + string PaymentDate, + int? CasHttpStatusCode, + string? CasResponse); } } diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentApprovals/UpdatePaymentRequestStatus.cshtml.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentApprovals/UpdatePaymentRequestStatus.cshtml.cs index 0568393a6e..c021fdba7f 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentApprovals/UpdatePaymentRequestStatus.cshtml.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentApprovals/UpdatePaymentRequestStatus.cshtml.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Threading.Tasks; using Unity.GrantManager.ApplicationForms; -using Unity.Payments.Domain.PaymentRequests; using Unity.Payments.Domain.Services; using Unity.Payments.Domain.Shared; using Unity.Payments.Enums; @@ -28,7 +27,6 @@ public class UpdatePaymentRequestStatus( IPaymentsManager paymentsManager, IPaymentRequestAppService paymentRequestAppService, IApplicationFormAppService applicationFormAppService, - IPaymentRequestRepository paymentRepository, IPaymentConfigurationAppService paymentConfigurationAppService, IPermissionCheckerService permissionCheckerService, PaymentIdsCacheService cacheService) : AbpPageModel @@ -113,7 +111,7 @@ private async Task> BuildPaymentApprovalsAsync(List< PaymentThreshold = 0m; } - var approvalModel = await CreateApprovalModel(payment); + var approvalModel = CreateApprovalModel(payment); ValidateApprovalModel(approvalModel); if (await VerifyPermissionsAsync(payment.Status, approvalModel)) @@ -125,10 +123,9 @@ private async Task> BuildPaymentApprovalsAsync(List< return paymentApprovals; } - private async Task CreateApprovalModel(PaymentDetailsDto payment) + private PaymentsApprovalModel CreateApprovalModel(PaymentDetailsDto payment) { bool isL3ApprovalRequired = payment.Amount > PaymentThreshold; - await UpdateExpenseApprovalsAsync(payment, isL3ApprovalRequired); return new PaymentsApprovalModel { @@ -148,29 +145,6 @@ private async Task CreateApprovalModel(PaymentDetailsDto }; } - private async Task UpdateExpenseApprovalsAsync(PaymentDetailsDto payment, bool isL3ApprovalRequired) - { - if (payment.Status == PaymentRequestStatus.L2Pending) - { - var l3Approval = payment.ExpenseApprovals.FirstOrDefault(x => x.Type == ExpenseApprovalType.Level3); - PaymentRequest paymentEntity = await paymentRepository.GetAsync(payment.Id); - if (isL3ApprovalRequired && l3Approval == null) - { - paymentEntity.ExpenseApprovals.Add(new ExpenseApproval(Guid.NewGuid(), ExpenseApprovalType.Level3)); - } - else if (!isL3ApprovalRequired && l3Approval != null) - { - var l3ApprovalEntity = paymentEntity.ExpenseApprovals.FirstOrDefault(x => x.Type == ExpenseApprovalType.Level3); - if (l3ApprovalEntity != null) - { - paymentEntity.ExpenseApprovals.Remove(l3ApprovalEntity); - } - } - - await paymentRepository.UpdateAsync(paymentEntity); - } - } - private void ValidateApprovalModel(PaymentsApprovalModel request) { var validationResults = new List(); diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequest_Constructor_Tests.cs b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequest_Constructor_Tests.cs index 7a646fc933..cbbe6edcf2 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequest_Constructor_Tests.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Domain/PaymentRequests/PaymentRequest_Constructor_Tests.cs @@ -1,9 +1,9 @@ using Shouldly; using System; using System.ComponentModel; +using System.Linq; using Unity.Payments.Codes; using Unity.Payments.Domain.Exceptions; -using Unity.Payments.Domain.PaymentRequests; using Unity.Payments.Enums; using Unity.Payments.PaymentRequests; using Volo.Abp; @@ -51,6 +51,32 @@ public void NormalConstructor_WithValidData_Succeeds() payment.ExpenseApprovals.Count.ShouldBe(2); } + [Fact] + public void SetLevel3ApprovalRequired_AddsOnlyOneLevel3Slot() + { + var payment = new PaymentRequest(Guid.NewGuid(), BuildNormalDto()); + var approvalId = Guid.NewGuid(); + + payment.SetLevel3ApprovalRequired(true, approvalId); + payment.SetLevel3ApprovalRequired(true, Guid.NewGuid()); + + payment.ExpenseApprovals.Count.ShouldBe(3); + payment.ExpenseApprovals.Single(x => x.Type == ExpenseApprovalType.Level3).Id.ShouldBe(approvalId); + } + + [Fact] + public void SetLevel3ApprovalRequired_RemovesOnlyLevel3Slot() + { + var payment = new PaymentRequest(Guid.NewGuid(), BuildNormalDto()); + payment.SetLevel3ApprovalRequired(true, Guid.NewGuid()); + + payment.SetLevel3ApprovalRequired(false, Guid.NewGuid()); + payment.SetLevel3ApprovalRequired(false, Guid.NewGuid()); + + payment.ExpenseApprovals.Count.ShouldBe(2); + payment.ExpenseApprovals.Select(x => x.Type).ShouldBe([ExpenseApprovalType.Level1, ExpenseApprovalType.Level2]); + } + #endregion #region Historical payment constructor diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/PaymentRequests/CasPaymentRequestCoordinatorTests.cs b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/PaymentRequests/CasPaymentRequestCoordinatorTests.cs new file mode 100644 index 0000000000..c22d8869b0 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/PaymentRequests/CasPaymentRequestCoordinatorTests.cs @@ -0,0 +1,190 @@ +using System; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Shouldly; +using Microsoft.Extensions.Logging.Abstractions; +using Unity.Payments.Domain.PaymentRequests; +using Unity.Payments.Integrations.Cas; +using Unity.Notifications.Integrations.RabbitMQ; +using Volo.Abp.Application.Services; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.EventBus.Local; +using Volo.Abp.MultiTenancy; +using Volo.Abp.TenantManagement; +using Volo.Abp.Uow; +using Xunit; + +namespace Unity.Payments.PaymentRequests; + +public class CasPaymentRequestCoordinatorTests +{ + private static readonly Guid TenantId = Guid.NewGuid(); + private static readonly Guid PaymentRequestId = Guid.NewGuid(); + + [Fact] + public async Task UpdatePaymentRequestStatus_RetriesAfterConcurrencyConflict() + { + var repository = Substitute.For(); + repository.GetAsync(PaymentRequestId) + .Returns(CreatePaymentRequest(), CreatePaymentRequest(), CreatePaymentRequest()); + var unitOfWork = Substitute.For(); + unitOfWork.CompleteAsync() + .Returns(Task.FromException(new AbpDbConcurrencyException()), Task.CompletedTask); + var coordinator = CreateCoordinator(repository, unitOfWork); + + var result = await coordinator.UpdatePaymentRequestStatus(TenantId, PaymentRequestId, CreateCasResult()); + + result.ShouldNotBeNull(); + result!.PaymentStatus.ShouldBe("Paid"); + _ = repository.Received(2).UpdateAsync(Arg.Any(), Arg.Any(), Arg.Any()); + _ = unitOfWork.Received(2).CompleteAsync(); + } + + [Fact] + public async Task UpdatePaymentRequestStatus_ReturnsCurrentRequestWhenResultIsAlreadyApplied() + { + var repository = Substitute.For(); + repository.GetAsync(PaymentRequestId) + .Returns(CreatePaymentRequest(), CreatePaymentRequest(paymentStatus: "Paid")); + var unitOfWork = Substitute.For(); + unitOfWork.CompleteAsync().Returns(Task.FromException(new AbpDbConcurrencyException())); + var coordinator = CreateCoordinator(repository, unitOfWork); + + var result = await coordinator.UpdatePaymentRequestStatus(TenantId, PaymentRequestId, CreateCasResult()); + + result!.PaymentStatus.ShouldBe("Paid"); + _ = repository.Received(1).UpdateAsync(Arg.Any(), Arg.Any(), Arg.Any()); + _ = unitOfWork.Received(1).CompleteAsync(); + } + + [Fact] + public async Task UpdatePaymentRequestStatus_RejectsStaleCasResult() + { + var repository = Substitute.For(); + repository.GetAsync(PaymentRequestId) + .Returns(CreatePaymentRequest(), CreatePaymentRequest(invoiceStatus: "Paid")); + var unitOfWork = Substitute.For(); + unitOfWork.CompleteAsync().Returns(Task.FromException(new AbpDbConcurrencyException())); + var coordinator = CreateCoordinator(repository, unitOfWork); + + var result = await coordinator.UpdatePaymentRequestStatus(TenantId, PaymentRequestId, CreateCasResult()); + + result!.InvoiceStatus.ShouldBe("Paid"); + _ = repository.Received(1).UpdateAsync(Arg.Any(), Arg.Any(), Arg.Any()); + _ = unitOfWork.Received(1).CompleteAsync(); + } + + [Fact] + public async Task UpdatePaymentRequestStatus_RejectsStateChangedBeforeRetryLoad() + { + var repository = Substitute.For(); + repository.GetAsync(PaymentRequestId) + .Returns( + CreatePaymentRequest(), + CreatePaymentRequest(), + CreatePaymentRequest(invoiceStatus: "Paid")); + var unitOfWork = Substitute.For(); + unitOfWork.CompleteAsync().Returns(Task.FromException(new AbpDbConcurrencyException())); + var coordinator = CreateCoordinator(repository, unitOfWork); + + var result = await coordinator.UpdatePaymentRequestStatus(TenantId, PaymentRequestId, CreateCasResult()); + + result!.InvoiceStatus.ShouldBe("Paid"); + _ = repository.Received(1).UpdateAsync(Arg.Any(), Arg.Any(), Arg.Any()); + _ = unitOfWork.Received(1).CompleteAsync(); + } + + [Fact] + public async Task UpdatePaymentRequestStatus_ThrowsAfterMaximumConcurrencyAttempts() + { + var repository = Substitute.For(); + repository.GetAsync(PaymentRequestId) + .Returns( + CreatePaymentRequest(), + CreatePaymentRequest(), + CreatePaymentRequest(), + CreatePaymentRequest(), + CreatePaymentRequest()); + var unitOfWork = Substitute.For(); + unitOfWork.CompleteAsync().Returns( + Task.FromException(new AbpDbConcurrencyException()), + Task.FromException(new AbpDbConcurrencyException()), + Task.FromException(new AbpDbConcurrencyException())); + var coordinator = CreateCoordinator(repository, unitOfWork); + + await Should.ThrowAsync(() => + coordinator.UpdatePaymentRequestStatus(TenantId, PaymentRequestId, CreateCasResult())); + + _ = unitOfWork.Received(3).CompleteAsync(); + } + + private static CasPaymentRequestCoordinator CreateCoordinator( + IPaymentRequestRepository repository, + IUnitOfWork unitOfWork) + { + var unitOfWorkManager = Substitute.For(); + unitOfWorkManager.Begin(Arg.Any(), Arg.Any()).Returns(unitOfWork); + + var coordinator = new CasPaymentRequestCoordinator( + paymentQueueService: null!, + paymentRequestsRepository: repository, + unitOfWorkManager: unitOfWorkManager, + tenantRepository: Substitute.For(), + currentTenant: Substitute.For(), + localEventBus: Substitute.For()); + + var lazyServiceProvider = Substitute.For(); + lazyServiceProvider.LazyGetService(Arg.Any>()) + .Returns(NullLogger.Instance); + typeof(ApplicationService) + .GetProperty("LazyServiceProvider", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + ?.SetValue(coordinator, lazyServiceProvider); + return coordinator; + } + + private static PaymentRequest CreatePaymentRequest( + string? invoiceStatus = null, + string? paymentStatus = null) + { + var paymentRequest = new PaymentRequest(PaymentRequestId, new CreatePaymentRequestDto + { + InvoiceNumber = "INV-123", + Amount = 100, + PayeeName = "Supplier", + ContractNumber = "CON-123", + SupplierNumber = "SUP-123", + SupplierName = "Supplier", + CorrelationId = Guid.NewGuid(), + SiteId = Guid.NewGuid(), + AccountCodingId = Guid.NewGuid(), + BatchName = "Batch" + }); + + if (invoiceStatus != null) + { + paymentRequest.SetInvoiceStatus(invoiceStatus); + } + + if (paymentStatus != null) + { + paymentRequest.SetPaymentStatus(paymentStatus); + } + + return paymentRequest; + } + + private static CasPaymentSearchResult CreateCasResult() + { + return new CasPaymentSearchResult + { + InvoiceStatus = "Paid", + PaymentStatus = "Paid", + PaymentNumber = "PAY-123", + PaymentDate = "2026-10-13" + }; + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.SharedKernel/MessageBrokers.RabbitMQ/Constants/QueueingConstants.cs b/applications/Unity.GrantManager/modules/Unity.SharedKernel/MessageBrokers.RabbitMQ/Constants/QueueingConstants.cs index 6e70f6c617..aab9837dba 100644 --- a/applications/Unity.GrantManager/modules/Unity.SharedKernel/MessageBrokers.RabbitMQ/Constants/QueueingConstants.cs +++ b/applications/Unity.GrantManager/modules/Unity.SharedKernel/MessageBrokers.RabbitMQ/Constants/QueueingConstants.cs @@ -1,9 +1,21 @@  +using System; + namespace Unity.Modules.Shared.MessageBrokers.RabbitMQ.Constants { public static class QueueingConstants { public const string DeadletterAddition = "-deadletter"; + public const string RetryAddition = ".retry"; + public const string RetryCountHeader = "x-retry-count"; + public const int MaxMessageRetries = 3; public const int MAX_RABBIT_CONCURRENT_CONSUMERS = 10; + + public static readonly TimeSpan[] RetryDelays = + [ + TimeSpan.FromSeconds(30), + TimeSpan.FromMinutes(2), + TimeSpan.FromMinutes(10) + ]; } } diff --git a/applications/Unity.GrantManager/modules/Unity.SharedKernel/MessageBrokers.RabbitMQ/Interfaces/IQueueChannelProvider.cs b/applications/Unity.GrantManager/modules/Unity.SharedKernel/MessageBrokers.RabbitMQ/Interfaces/IQueueChannelProvider.cs index 1baa6ebc8c..d161313cb3 100644 --- a/applications/Unity.GrantManager/modules/Unity.SharedKernel/MessageBrokers.RabbitMQ/Interfaces/IQueueChannelProvider.cs +++ b/applications/Unity.GrantManager/modules/Unity.SharedKernel/MessageBrokers.RabbitMQ/Interfaces/IQueueChannelProvider.cs @@ -1,6 +1,8 @@ using System; +using System.Threading; using System.Threading.Tasks; using RabbitMQ.Client; +using RabbitMQ.Client.Events; #pragma warning disable CA1005 // Avoid excessive parameters on generic types #pragma warning disable S2326 @@ -16,6 +18,14 @@ public interface IQueueChannelProvider : IDisposable where TQueue /// Task GetChannelAsync(); + /// + /// Republishes a failed delivery to a delayed retry queue. The returned task completes + /// only after RabbitMQ confirms the retry publish. Returns true when the retry handoff + /// was accepted by the broker; false if the original delivery was requeued or channel recovery + /// was arranged due to a publishing failure. + /// + Task PublishRetryAsync(IChannel channel, BasicDeliverEventArgs delivery, int retryCount, TimeSpan delay, CancellationToken cancellationToken = default); + /// /// Returns a channel obtained from so it can be pooled /// or disposed and its throttling permit released. Callers that finish with a channel diff --git a/applications/Unity.GrantManager/modules/Unity.SharedKernel/MessageBrokers.RabbitMQ/QueueChannelProvider.cs b/applications/Unity.GrantManager/modules/Unity.SharedKernel/MessageBrokers.RabbitMQ/QueueChannelProvider.cs index 9be537782d..f2e5395560 100644 --- a/applications/Unity.GrantManager/modules/Unity.SharedKernel/MessageBrokers.RabbitMQ/QueueChannelProvider.cs +++ b/applications/Unity.GrantManager/modules/Unity.SharedKernel/MessageBrokers.RabbitMQ/QueueChannelProvider.cs @@ -1,10 +1,11 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using RabbitMQ.Client; +using RabbitMQ.Client.Events; using Unity.Modules.Shared.MessageBrokers.RabbitMQ.Constants; using Unity.Modules.Shared.MessageBrokers.RabbitMQ.Interfaces; @@ -71,6 +72,92 @@ public async Task GetChannelAsync() } } + public async Task PublishRetryAsync( + IChannel channel, + BasicDeliverEventArgs delivery, + int retryCount, + TimeSpan delay, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(channel); + ArgumentNullException.ThrowIfNull(delivery); + + var retryExchange = $"{_queueName}{QueueingConstants.RetryAddition}"; + var retryQueue = $"{retryExchange}.{GetRetryDelayKey(delay)}"; + var properties = new BasicProperties + { + Persistent = true, + Type = delivery.BasicProperties.Type, + MessageId = delivery.BasicProperties.MessageId, + Headers = new Dictionary + { + [QueueingConstants.RetryCountHeader] = retryCount + } + }; + + // Use mandatory=true so unroutable publishes are reported by publisher-confirms. + // Retry publish failures are handled explicitly: retry with backoff, and if all attempts fail + // requeue the original delivery (or close the channel to force broker redelivery) so the + // message is not lost. + var attempts = 0; + const int maxAttempts = 3; + var backoff = TimeSpan.FromMilliseconds(100); + while (true) + { + attempts++; + try + { + var publisherChannel = await GetChannelAsync(); + try + { + await publisherChannel.BasicPublishAsync( + exchange: retryExchange, + routingKey: retryQueue, + mandatory: true, + basicProperties: properties, + body: delivery.Body, + cancellationToken: cancellationToken); + } + finally + { + ReturnChannel(publisherChannel); + } + + return true; + } + catch (Exception ex) when (attempts < maxAttempts) + { + _logger.LogWarning(ex, "Retry publish attempt {Attempt} failed for message {MessageId}; backing off {Backoff} before retrying.", attempts, delivery.BasicProperties?.MessageId, backoff); + try + { + await Task.Delay(backoff, cancellationToken); + } + catch (OperationCanceledException) { } + backoff = TimeSpan.FromTicks(backoff.Ticks * 2); + continue; + } + catch (Exception ex) + { + _logger.LogError(ex, "Retry publish failed for message {MessageId}; attempting to requeue original delivery.", delivery.BasicProperties?.MessageId); + try + { + // Requeue the original delivery so it will be redelivered when the broker recovers. + await channel.BasicNackAsync(delivery.DeliveryTag, multiple: false, requeue: true); + } + catch (Exception nackEx) + { + // BasicNack failure means the channel is compromised. Do NOT close it here - if it's an active consumer channel, + // closing it will interrupt message delivery. Log the error and return false to signal failure; the consumer + // handler or connection recovery will handle channel state. The broker will eventually redeliver the message + // via its internal recovery mechanisms even if this immediate nack fails. + _logger.LogError(nackEx, "Failed to BasicNack original delivery after retry publish failure for message {MessageId}. Channel may require recovery by consumer handler.", delivery.BasicProperties?.MessageId); + } + + return false; + } + } + } + public void ReturnChannel(IChannel channel) { if (channel?.IsOpen == true && !_disposed) @@ -122,6 +209,7 @@ private async Task DeclareQueueAsync(IChannel channel) { var dlxName = $"{_queueName}.dlx"; var dlqName = $"{_queueName}{QueueingConstants.DeadletterAddition}"; + var retryExchange = $"{_queueName}{QueueingConstants.RetryAddition}"; // Ensure DLX exchange exists await channel.ExchangeDeclareAsync(dlxName, ExchangeType.Direct, durable: true, autoDelete: false, arguments: null); @@ -135,6 +223,27 @@ await channel.QueueDeclareAsync(dlqName, durable: true, exclusive: false, autoDe }); await channel.QueueBindAsync(dlqName, dlxName, dlqName, arguments: null); + await channel.ExchangeDeclareAsync(retryExchange, ExchangeType.Direct, durable: true, autoDelete: false, arguments: null); + foreach (var retryDelay in QueueingConstants.RetryDelays) + { + var retryQueue = $"{retryExchange}.{GetRetryDelayKey(retryDelay)}"; + await channel.QueueDeclareAsync( + retryQueue, + durable: true, + exclusive: false, + autoDelete: false, + arguments: new Dictionary + { + { "x-queue-type", "quorum" }, + { "x-message-ttl", checked((int)retryDelay.TotalMilliseconds) }, + { "x-dead-letter-exchange", $"{_queueName}.exchange" }, + { "x-dead-letter-routing-key", _queueName }, + { "x-overflow", "reject-publish" }, + { "x-dead-letter-strategy", "at-least-once" } + }); + await channel.QueueBindAsync(retryQueue, retryExchange, retryQueue, arguments: null); + } + // Declare main queue with DLX args await channel.QueueDeclareAsync( _queueName, @@ -172,6 +281,9 @@ await channel.QueueDeclareAsync( } } + private static string GetRetryDelayKey(TimeSpan delay) => + $"{(int)delay.TotalSeconds}s"; + private async Task BindToExchangeAsync(IChannel channel) { var mainExchange = $"{_queueName}.exchange"; diff --git a/applications/Unity.GrantManager/modules/Unity.SharedKernel/MessageBrokers.RabbitMQ/QueueConsumerHandler.cs b/applications/Unity.GrantManager/modules/Unity.SharedKernel/MessageBrokers.RabbitMQ/QueueConsumerHandler.cs index 37e07c7048..7be8bc66a7 100644 --- a/applications/Unity.GrantManager/modules/Unity.SharedKernel/MessageBrokers.RabbitMQ/QueueConsumerHandler.cs +++ b/applications/Unity.GrantManager/modules/Unity.SharedKernel/MessageBrokers.RabbitMQ/QueueConsumerHandler.cs @@ -1,12 +1,13 @@ using System; +using System.Collections.Generic; using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Newtonsoft.Json; -using RabbitMQ.Client; using RabbitMQ.Client.Events; using Unity.Modules.Shared.MessageBrokers.RabbitMQ.Exceptions; +using Unity.Modules.Shared.MessageBrokers.RabbitMQ.Constants; using Unity.Modules.Shared.MessageBrokers.RabbitMQ.Interfaces; using Unity.Modules.Shared.Utils; using Volo.Abp.Auditing; @@ -126,9 +127,73 @@ private async Task HandleMessageAsync(object sender, BasicDeliverEventArgs ea) } catch (Exception ex) { - _logger.LogError(ex, "Error processing message on {Queue}", _queueName); - await consumingChannel.BasicRejectAsync(ea.DeliveryTag, requeue: false); + var retryCount = GetRetryCount(ea.BasicProperties.Headers); + if (IsPermanentFailure(ex) || retryCount >= QueueingConstants.MaxMessageRetries) + { + _logger.LogError(ex, + "Permanently rejecting message {MessageId} on {Queue} after {RetryCount} retries", + ea.BasicProperties.MessageId, + _queueName, + retryCount); + await consumingChannel.BasicRejectAsync(ea.DeliveryTag, requeue: false); + return; + } + + var nextRetryCount = retryCount + 1; + var delay = QueueingConstants.RetryDelays[retryCount]; + var channelProvider = consumerScope.ServiceProvider + .GetRequiredService>(); + + _logger.LogWarning(ex, + "Scheduling retry {RetryCount} for message {MessageId} on {Queue} in {Delay}", + nextRetryCount, + ea.BasicProperties.MessageId, + _queueName, + delay); + + var retryAccepted = await channelProvider.PublishRetryAsync(consumingChannel, ea, nextRetryCount, delay); + if (retryAccepted) + { + await consumingChannel.BasicAckAsync(ea.DeliveryTag, multiple: false); + } + else + { + // Retry publish failed; the provider requeued the original delivery or arranged channel recovery. + // Do not acknowledge the original delivery here so it will be redelivered. + } + } + } + + private static int GetRetryCount(IDictionary? headers) + { + if (headers == null || !headers.TryGetValue(QueueingConstants.RetryCountHeader, out var value)) + { + return 0; + } + + return value switch + { + int count => Math.Max(count, 0), + long count => Math.Max((int)count, 0), + byte[] bytes when int.TryParse(Encoding.UTF8.GetString(bytes), out var count) => Math.Max(count, 0), + _ when int.TryParse(value?.ToString(), out var count) => Math.Max(count, 0), + _ => 0 + }; + } + + private static bool IsPermanentFailure(Exception exception) + { + for (var current = exception; current != null; current = current.InnerException) + { + if (current is ArgumentException || + current is Volo.Abp.Validation.AbpValidationException || + current is Volo.Abp.Domain.Entities.EntityNotFoundException) + { + return true; + } } + + return false; } /// @@ -137,7 +202,7 @@ private async Task HandleMessageAsync(object sender, BasicDeliverEventArgs ea) /// audit persistence are handled here so individual consumers stay free of /// infrastructure concerns. /// - private static async Task ConsumeWithAuditingAsync(IServiceScope consumerScope, ITenantedQueueMessage tenantedMessage, TQueueMessage message) + private async Task ConsumeWithAuditingAsync(IServiceScope consumerScope, ITenantedQueueMessage tenantedMessage, TQueueMessage message) { var auditingManager = consumerScope.ServiceProvider.GetRequiredService(); var principalAccessor = consumerScope.ServiceProvider.GetRequiredService(); @@ -157,7 +222,16 @@ private static async Task ConsumeWithAuditingAsync(IServiceScope consumerScope, // so this call happens after the consumer's unit of work completes. if (auditingManager.Current?.Log is { EntityChanges.Count: > 0 } log) { - await auditingStore.SaveAsync(log); + try + { + await auditingStore.SaveAsync(log); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Failed to persist audit log for message {MessageId}; consumer work was already committed.", + message.MessageId); + } } } } diff --git a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Unity.SharedKernel.csproj b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Unity.SharedKernel.csproj index bc3d9950fd..113692d930 100644 --- a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Unity.SharedKernel.csproj +++ b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Unity.SharedKernel.csproj @@ -1,4 +1,4 @@ - + @@ -19,6 +19,7 @@ + diff --git a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Utils/BackgroundExecutionAuthorizationHandler.cs b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Utils/BackgroundExecutionAuthorizationHandler.cs new file mode 100644 index 0000000000..0ffec4ba42 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Utils/BackgroundExecutionAuthorizationHandler.cs @@ -0,0 +1,27 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; + +namespace Unity.Modules.Shared.Utils; + +public static class BackgroundExecutionPolicy +{ + public const string Name = "Unity.BackgroundExecution"; +} + +public sealed class BackgroundExecutionRequirement : IAuthorizationRequirement; + +public sealed class BackgroundExecutionAuthorizationHandler + : AuthorizationHandler +{ + protected override Task HandleRequirementAsync( + AuthorizationHandlerContext context, + BackgroundExecutionRequirement requirement) + { + if (BackgroundJobExecutionContext.IsActive) + { + context.Succeed(requirement); + } + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Utils/BackgroundJobContext.cs b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Utils/BackgroundJobContext.cs index 78ce8c6609..46370e100f 100644 --- a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Utils/BackgroundJobContext.cs +++ b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Utils/BackgroundJobContext.cs @@ -21,21 +21,19 @@ public static class BackgroundJobContext /// The current principal accessor for setting user identity /// The current tenant accessor for setting tenant context /// The tenant ID to set context for - /// Optional user ID. If null, uses BackgroundJobConstants.BackgroundJobPersonId /// IDisposable that restores previous context when disposed (LIFO order) public static IDisposable Set( IAuditingManager auditingManager, ICurrentPrincipalAccessor principalAccessor, ICurrentTenant currentTenant, - Guid? tenantId, - Guid? userId = null) + Guid? tenantId) { - var effectiveUserId = userId ?? BackgroundJobConstants.BackgroundJobPersonId; + var backgroundJobPersonId = BackgroundJobConstants.BackgroundJobPersonId; var claims = new[] { - new Claim(AbpClaimTypes.UserId, effectiveUserId.ToString()), - new Claim(ClaimTypes.NameIdentifier, effectiveUserId.ToString()), // Standard claim for user ID + new Claim(AbpClaimTypes.UserId, backgroundJobPersonId.ToString()), + new Claim(ClaimTypes.NameIdentifier, backgroundJobPersonId.ToString()), // Standard claim for user ID new Claim(AbpClaimTypes.UserName, BackgroundJobConstants.BackgroundJobUserName), new Claim(AbpClaimTypes.Email, BackgroundJobConstants.BackgroundJobEmail), new Claim(AbpClaimTypes.TenantId, tenantId?.ToString() ?? Guid.Empty.ToString()), @@ -52,20 +50,49 @@ public static IDisposable Set( var principalDisposable = principalAccessor.Change(principal); // NOW start auditing - it will see the correct tenant/user context + var auditingDisposable = BeginAuditScope(auditingManager, tenantId); + + // Dispose in LIFO order: last registered is disposed first. + // audit scope closes first (while tenant/user context is still valid), + // then principal, then tenant. + return new CompositeDisposable(tenantDisposable, principalDisposable, auditingDisposable); + } + + public static IDisposable SetExecutionContext( + ICurrentPrincipalAccessor principalAccessor, + ICurrentTenant currentTenant, + Guid? tenantId) + { + var backgroundJobPersonId = BackgroundJobConstants.BackgroundJobPersonId; + var claims = new[] + { + new Claim(AbpClaimTypes.UserId, backgroundJobPersonId.ToString()), + new Claim(ClaimTypes.NameIdentifier, backgroundJobPersonId.ToString()), + new Claim(AbpClaimTypes.UserName, BackgroundJobConstants.BackgroundJobUserName), + new Claim(AbpClaimTypes.Email, BackgroundJobConstants.BackgroundJobEmail), + new Claim(AbpClaimTypes.TenantId, tenantId?.ToString() ?? Guid.Empty.ToString()), + new Claim(AbpClaimTypes.Name, BackgroundJobConstants.BackgroundJobName) + }; + + var principal = new ClaimsPrincipal(new ClaimsIdentity( + claims, "BackgroundJob", AbpClaimTypes.UserName, AbpClaimTypes.Role)); + var tenantDisposable = currentTenant.Change(tenantId); + var principalDisposable = principalAccessor.Change(principal); + return new CompositeDisposable(tenantDisposable, principalDisposable); + } + + public static IDisposable BeginAuditScope(IAuditingManager auditingManager, Guid? tenantId) + { var auditingDisposable = auditingManager.BeginScope(); - - // Ensure the current audit log has the user ID set for entity change tracking + if (auditingManager.Current != null) { - auditingManager.Current.Log.UserId = effectiveUserId; + auditingManager.Current.Log.UserId = BackgroundJobConstants.BackgroundJobPersonId; auditingManager.Current.Log.UserName = BackgroundJobConstants.BackgroundJobUserName; auditingManager.Current.Log.TenantId = tenantId; } - // Dispose in LIFO order: last registered is disposed first. - // audit scope closes first (while tenant/user context is still valid), - // then principal, then tenant. - return new CompositeDisposable(auditingDisposable, principalDisposable, tenantDisposable); + return auditingDisposable; } /// diff --git a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Utils/UnityQuartzBackgroundWorkerBase.cs b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Utils/UnityQuartzBackgroundWorkerBase.cs new file mode 100644 index 0000000000..163d648d59 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Utils/UnityQuartzBackgroundWorkerBase.cs @@ -0,0 +1,109 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Quartz; +using Volo.Abp.BackgroundWorkers.Quartz; +using Volo.Abp.Auditing; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Security.Claims; + +namespace Unity.Modules.Shared.Utils; + +/// +/// Establishes the trusted execution context for every Quartz background worker. +/// +public abstract class UnityQuartzBackgroundWorkerBase : QuartzBackgroundWorkerBase +{ + protected virtual bool UsesTenantAuditScopes => false; + + public sealed override async Task Execute(IJobExecutionContext context) + { + var auditingManager = ServiceProvider.GetRequiredService(); + var auditingStore = ServiceProvider.GetRequiredService(); + var principalAccessor = ServiceProvider.GetRequiredService(); + var currentTenant = ServiceProvider.GetRequiredService(); + + using (BackgroundJobExecutionContext.Use()) + { + if (UsesTenantAuditScopes) + { + using (BackgroundJobContext.SetExecutionContext(principalAccessor, currentTenant, tenantId: null)) + { + await ExecuteInBackgroundAsync(context); + } + return; + } + + using (BackgroundJobContext.Set(auditingManager, principalAccessor, currentTenant, tenantId: null)) + { + AddWorkerAuditAction(auditingManager, context); + await ExecuteInBackgroundAsync(context); + + if (auditingManager.Current?.Log is { EntityChanges.Count: > 0 } log) + { + try + { + using (currentTenant.Change(null)) + { + await auditingStore.SaveAsync(log); + } + } + catch (Exception ex) + { + // Audit persistence failures after transaction commit should not cause worker to retry. + // The business transaction already succeeded; audit logging is non-critical. + Logger.LogWarning(ex, "Failed to persist audit log for background worker {WorkerName}; worker work was already committed (not retrying).", GetType().Name); + } + } + } + } + } + + protected async Task ExecuteInTenantAuditScopeAsync(Guid? tenantId, Func action) + { + var auditingManager = ServiceProvider.GetRequiredService(); + var auditingStore = ServiceProvider.GetRequiredService(); + var currentTenant = ServiceProvider.GetRequiredService(); + var principalAccessor = ServiceProvider.GetRequiredService(); + + using (BackgroundJobContext.Set(auditingManager, principalAccessor, currentTenant, tenantId)) + { + AddWorkerAuditAction(auditingManager, null); + await action(); + + if (auditingManager.Current?.Log is { EntityChanges.Count: > 0 } log) + { + try + { + await auditingStore.SaveAsync(log); + } + catch (Exception ex) + { + // Audit persistence failures after transaction commit should not cause worker to retry. + // The business transaction already succeeded; audit logging is non-critical. + Logger.LogWarning(ex, "Failed to persist audit log for tenant {TenantId}; worker work was already committed (not retrying).", tenantId); + } + } + } + } + + private void AddWorkerAuditAction(IAuditingManager auditingManager, IJobExecutionContext? context) + { + if (auditingManager.Current?.Log == null) + { + return; + } + + auditingManager.Current.Log.Actions.Add(new AuditLogActionInfo + { + ServiceName = GetType().FullName ?? GetType().Name, + MethodName = nameof(ExecuteInBackgroundAsync), + Parameters = context == null ? "{}" : System.Text.Json.JsonSerializer.Serialize(new { context.FireInstanceId }), + ExecutionTime = DateTime.UtcNow + }); + } + + protected abstract Task ExecuteInBackgroundAsync(IJobExecutionContext context); +} \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Components/Toolbar/UserMenu/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Components/Toolbar/UserMenu/Default.cshtml index c6832c41e0..674914101b 100644 --- a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Components/Toolbar/UserMenu/Default.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Components/Toolbar/UserMenu/Default.cshtml @@ -9,14 +9,11 @@ @model ApplicationMenu [DisallowConcurrentExecution] - public class DateBasedScheduledNotificationJob : QuartzBackgroundWorkerBase, ITransientDependency + public class DateBasedScheduledNotificationJob : UnityQuartzBackgroundWorkerBase, ITransientDependency { + protected override bool UsesTenantAuditScopes => true; + private readonly IRepository _scheduledNotificationRepository; private readonly IApplicationRepository _applicationRepository; private readonly IRepository _trackingRepository; @@ -128,7 +129,7 @@ public DateBasedScheduledNotificationJob( } } - public override async Task Execute(IJobExecutionContext context) + protected override async Task ExecuteInBackgroundAsync(IJobExecutionContext context) { try { @@ -148,11 +149,11 @@ public override async Task Execute(IJobExecutionContext context) // Process each tenant separately foreach (var tenant in allTenants) { - using (_currentTenant.Change(tenant.Id)) + await ExecuteInTenantAuditScopeAsync(tenant.Id, async () => { _logger.LogDebug("DateBasedScheduledNotificationJob: Processing notifications for tenant '{TenantName}' ({TenantId}).", tenant.Name, tenant.Id); await ProcessTenantNotificationsAsync(); - } + }); } _logger.LogInformation("DateBasedScheduledNotificationJob: Completed execution."); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs index 21da7aa288..b4ceab4826 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs @@ -40,9 +40,9 @@ using Unity.GrantManager.Infrastructure; using Medallion.Threading; using Unity.GrantManager.Locks; -using JsonSerializerOptions = System.Text.Json.JsonSerializerOptions; using Unity.GrantManager.Integrations.Chefs; using Unity.Modules.Shared.Http; +using Unity.Modules.Shared.Utils; using Unity.GrantManager.Integrations.Geocoder; using Unity.GrantManager.GrantsPortal; using Unity.GrantManager.GrantsPortal.Configuration; @@ -153,6 +153,10 @@ public override void ConfigureServices(ServiceConfigurationContext context) }); context.Services.AddSingleton(); + context.Services.AddSingleton(); + context.Services.AddAuthorizationBuilder() + .AddPolicy(BackgroundExecutionPolicy.Name, policy => + policy.Requirements.Add(new BackgroundExecutionRequirement())); context.Services.AddTransient(); context.Services.AddTransient(); context.Services.AddTransient(); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantsPortal/GrantsPortalMessageCleanupWorker.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantsPortal/GrantsPortalMessageCleanupWorker.cs index e1cd8a10f9..e8a9c6531b 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantsPortal/GrantsPortalMessageCleanupWorker.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantsPortal/GrantsPortalMessageCleanupWorker.cs @@ -6,7 +6,7 @@ using Quartz; using Unity.GrantManager.GrantsPortal.Configuration; using Unity.GrantManager.Messaging; -using Volo.Abp.BackgroundWorkers.Quartz; +using Unity.Modules.Shared.Utils; using Volo.Abp.Uow; namespace Unity.GrantManager.GrantsPortal; @@ -16,7 +16,7 @@ namespace Unity.GrantManager.GrantsPortal; /// Runs against the central host database. /// [DisallowConcurrentExecution] -public class GrantsPortalMessageCleanupWorker : QuartzBackgroundWorkerBase +public class GrantsPortalMessageCleanupWorker : UnityQuartzBackgroundWorkerBase { private readonly IServiceProvider _serviceProvider; private readonly int _retentionDays; @@ -43,7 +43,7 @@ public GrantsPortalMessageCleanupWorker( .Build(); } - public override async Task Execute(IJobExecutionContext context) + protected override async Task ExecuteInBackgroundAsync(IJobExecutionContext context) { Logger.LogDebug("GrantsPortalMessageCleanupWorker executing (retention={RetentionDays} days)...", _retentionDays); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantsPortal/Notifications/ApplicantUpdateEmailRecoveryWorker.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantsPortal/Notifications/ApplicantUpdateEmailRecoveryWorker.cs index 058f75d065..afea577612 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantsPortal/Notifications/ApplicantUpdateEmailRecoveryWorker.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantsPortal/Notifications/ApplicantUpdateEmailRecoveryWorker.cs @@ -7,7 +7,7 @@ using Quartz; using Unity.Notifications.EmailNotifications; using Unity.Notifications.Emails; -using Volo.Abp.BackgroundWorkers.Quartz; +using Unity.Modules.Shared.Utils; using Volo.Abp.Features; using Volo.Abp.Linq; using Volo.Abp.MultiTenancy; @@ -21,8 +21,10 @@ namespace Unity.GrantManager.GrantsPortal.Notifications; /// Recovers the commit-to-queue gap. Delivery failures are retried by the existing EmailConsumer. /// [DisallowConcurrentExecution] -public class ApplicantUpdateEmailRecoveryWorker : QuartzBackgroundWorkerBase +public class ApplicantUpdateEmailRecoveryWorker : UnityQuartzBackgroundWorkerBase { + protected override bool UsesTenantAuditScopes => true; + private readonly IServiceProvider _serviceProvider; public ApplicantUpdateEmailRecoveryWorker(IServiceProvider serviceProvider) @@ -35,7 +37,7 @@ public ApplicantUpdateEmailRecoveryWorker(IServiceProvider serviceProvider) .WithMisfireHandlingInstructionIgnoreMisfires()).Build(); } - public override async Task Execute(IJobExecutionContext context) + protected override async Task ExecuteInBackgroundAsync(IJobExecutionContext context) { using var scope = _serviceProvider.CreateScope(); var currentTenant = scope.ServiceProvider.GetRequiredService(); @@ -52,11 +54,14 @@ public override async Task Execute(IJobExecutionContext context) { try { - using var tenantScope = _serviceProvider.CreateScope(); - using (tenantScope.ServiceProvider.GetRequiredService().Change(tenant.Id)) + await ExecuteInTenantAuditScopeAsync(tenant.Id, async () => { - await RecoverTenantAsync(tenantScope.ServiceProvider); - } + using var tenantScope = _serviceProvider.CreateScope(); + using (tenantScope.ServiceProvider.GetRequiredService().Change(tenant.Id)) + { + await RecoverTenantAsync(tenantScope.ServiceProvider); + } + }); } catch (Exception ex) { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/HealthChecks/BackgroundWorkers/DataHealthCheckWorker.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/HealthChecks/BackgroundWorkers/DataHealthCheckWorker.cs index 23b42e4cdb..0b35a13ea7 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/HealthChecks/BackgroundWorkers/DataHealthCheckWorker.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/HealthChecks/BackgroundWorkers/DataHealthCheckWorker.cs @@ -7,7 +7,6 @@ using Unity.Notifications.EmailNotifications; using Unity.Payments.Domain.PaymentRequests; using Unity.Payments.Enums; -using Volo.Abp.BackgroundWorkers.Quartz; using Volo.Abp.MultiTenancy; using Volo.Abp.SettingManagement; using Volo.Abp.TenantManagement; @@ -15,8 +14,10 @@ namespace Unity.GrantManager.HealthChecks.BackgroundWorkers { [DisallowConcurrentExecution] - public class DataHealthCheckWorker : QuartzBackgroundWorkerBase + public class DataHealthCheckWorker : UnityQuartzBackgroundWorkerBase { + protected override bool UsesTenantAuditScopes => true; + private readonly ICurrentTenant _currentTenant; private readonly ITenantRepository _tenantRepository; private readonly IEmailNotificationService _emailNotificationService; @@ -49,7 +50,7 @@ public DataHealthCheckWorker(ICurrentTenant currentTenant, .Build(); } - public override async Task Execute(IJobExecutionContext context) + protected override async Task ExecuteInBackgroundAsync(IJobExecutionContext context) { Logger.LogInformation("Executing DataHealthCheckWorker..."); @@ -65,27 +66,30 @@ public override async Task Execute(IJobExecutionContext context) foreach (var tenant in tenants) { - using (_currentTenant.Change(tenant.Id, tenant.Name)) + await ExecuteInTenantAuditScopeAsync(tenant.Id, async () => { - // Lookup the missing emails - var missingEmailsCount = await _emailNotificationService.GetEmailsChesWithNoResponseCountAsync(); - if (missingEmailsCount > 0) - { - Logger.LogWarning("Tenant {TenantName} has {MissingEmailsCount} missing email(s) with a status of Initialized or Sent but no CHES Response.", tenant.Name, missingEmailsCount); - string missingEmailBody = $"Unity tenant {tenant.Name} has {missingEmailsCount} email(s) that were sent but have no CHES Response."; - sendEmail = true; - emailBodyBuilder.AppendLine($"{missingEmailBody}
"); - } - // Lookup the missing payments - var missingPayments = await GetPaymentsSentWithoutResponseCountAsync(); - if (missingPayments > 0) + using (_currentTenant.Change(tenant.Id, tenant.Name)) { - Logger.LogWarning("Tenant {TenantName} has {MissingPaymentsCount} payments sent without a response.", tenant.Name, missingPayments); - string missingPaymentBody = $"Unity tenant {tenant.Name} has {missingPayments} payment(s) that are in Submitted status but have no CAS Response."; - sendEmail = true; - emailBodyBuilder.AppendLine($"{missingPaymentBody}
"); + // Lookup the missing emails + var missingEmailsCount = await _emailNotificationService.GetEmailsChesWithNoResponseCountAsync(); + if (missingEmailsCount > 0) + { + Logger.LogWarning("Tenant {TenantName} has {MissingEmailsCount} missing email(s) with a status of Initialized or Sent but no CHES Response.", tenant.Name, missingEmailsCount); + string missingEmailBody = $"Unity tenant {tenant.Name} has {missingEmailsCount} email(s) that were sent but have no CHES Response."; + sendEmail = true; + emailBodyBuilder.AppendLine($"{missingEmailBody}
"); + } + // Lookup the missing payments + var missingPayments = await GetPaymentsSentWithoutResponseCountAsync(); + if (missingPayments > 0) + { + Logger.LogWarning("Tenant {TenantName} has {MissingPaymentsCount} payments sent without a response.", tenant.Name, missingPayments); + string missingPaymentBody = $"Unity tenant {tenant.Name} has {missingPayments} payment(s) that are in Submitted status but have no CAS Response."; + sendEmail = true; + emailBodyBuilder.AppendLine($"{missingPaymentBody}
"); + } } - } + }); } if (sendEmail) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/History/AuditLogCleanupWorker.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/History/AuditLogCleanupWorker.cs index e4c2c6aa60..5cb5230346 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/History/AuditLogCleanupWorker.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/History/AuditLogCleanupWorker.cs @@ -7,7 +7,7 @@ using Unity.GrantManager.Applications; using Unity.GrantManager.Logs; using Unity.GrantManager.Settings; -using Volo.Abp.BackgroundWorkers.Quartz; +using Unity.Modules.Shared.Utils; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories; using Volo.Abp.MultiTenancy; @@ -17,8 +17,10 @@ namespace Unity.GrantManager.History; [DisallowConcurrentExecution] -public class AuditLogCleanupWorker : QuartzBackgroundWorkerBase, ITransientDependency +public class AuditLogCleanupWorker : UnityQuartzBackgroundWorkerBase, ITransientDependency { + protected override bool UsesTenantAuditScopes => true; + private readonly IServiceProvider _serviceProvider; private readonly ITenantRepository _tenantRepository; private readonly ISettingProvider _settingProvider; @@ -45,7 +47,7 @@ public AuditLogCleanupWorker( .Build(); } - public override async Task Execute(IJobExecutionContext context) + protected override async Task ExecuteInBackgroundAsync(IJobExecutionContext context) { var enabled = await _settingProvider.GetAsync( SettingsConstants.Retention.CleanupEnabled, @@ -122,7 +124,7 @@ private async Task CleanupInScopeAsync( { try { - await cleanup(scope.ServiceProvider); + await ExecuteInTenantAuditScopeAsync(tenantId, () => cleanup(scope.ServiceProvider)); } catch (Exception ex) { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/History/HistoryAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/History/HistoryAppService.cs index e5177775a9..4c8b505df5 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/History/HistoryAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/History/HistoryAppService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; using Volo.Abp; using Volo.Abp.AuditLogging; using Volo.Abp.Data; @@ -10,6 +11,7 @@ namespace Unity.GrantManager.History; +[Authorize] public class HistoryAppService( IAuditLogRepository auditLogRepository, IIdentityUserRepository identityUserRepository, diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/BackgroundWorkers/IntakeSyncWorker.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/BackgroundWorkers/IntakeSyncWorker.cs index dd35d8aaf0..b34a9a0964 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/BackgroundWorkers/IntakeSyncWorker.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/BackgroundWorkers/IntakeSyncWorker.cs @@ -6,7 +6,6 @@ using Unity.GrantManager.Settings; using Unity.Modules.Shared.Utils; using Unity.Notifications.EmailNotifications; -using Volo.Abp.BackgroundWorkers.Quartz; using Volo.Abp.MultiTenancy; using Volo.Abp.SettingManagement; using Volo.Abp.TenantManagement; @@ -14,8 +13,10 @@ namespace Unity.GrantManager.Intakes.BackgroundWorkers { [DisallowConcurrentExecution] - public class IntakeSyncWorker : QuartzBackgroundWorkerBase + public class IntakeSyncWorker : UnityQuartzBackgroundWorkerBase { + protected override bool UsesTenantAuditScopes => true; + private readonly ICurrentTenant _currentTenant; private readonly ITenantRepository _tenantRepository; private readonly IApplicationFormSycnronizationService _applicationFormSynchronizationService; @@ -49,7 +50,7 @@ public IntakeSyncWorker(ICurrentTenant currentTenant, .Build(); } - public override async Task Execute(IJobExecutionContext context) + protected override async Task ExecuteInBackgroundAsync(IJobExecutionContext context) { Logger.LogInformation("Executing IntakeSyncWorker..."); var tenants = await _tenantRepository.GetListAsync(); @@ -66,15 +67,18 @@ public override async Task Execute(IJobExecutionContext context) foreach (var tenant in tenants) { - using (_currentTenant.Change(tenant.Id, tenant.Name)) + await ExecuteInTenantAuditScopeAsync(tenant.Id, async () => { - var (missingSubmissions, submissionReport) = await _applicationFormSynchronizationService.GetMissingSubmissions(numberDaysBack); - if (missingSubmissions?.Count > 0) + using (_currentTenant.Change(tenant.Id, tenant.Name)) { - emailBodyBuilder.AppendLine($"

Tenant: {tenant.Name}
{submissionReport}"); - sendEmail = true; + var (missingSubmissions, submissionReport) = await _applicationFormSynchronizationService.GetMissingSubmissions(numberDaysBack); + if (missingSubmissions?.Count > 0) + { + emailBodyBuilder.AppendLine($"

Tenant: {tenant.Name}
{submissionReport}"); + sendEmail = true; + } } - } + }); } emailBodyBuilder.AppendLine("
Bests"); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs index dd02614dac..bb91b61e09 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Integrations/Endpoints/EndpointManagementAppService.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; +using Unity.Modules.Shared.Permissions; using Volo.Abp; using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; @@ -13,6 +14,7 @@ namespace Unity.GrantManager.Integrations.Endpoints { + [Authorize(IdentityConsts.ITAdminOrITOperationsPolicyName)] public class EndpointManagementAppService( IRepository repository, IDistributedCache cache, diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Messaging/InboxWorkerBase.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Messaging/InboxWorkerBase.cs index c7440d1744..49db72b010 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Messaging/InboxWorkerBase.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Messaging/InboxWorkerBase.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Quartz; -using Volo.Abp.BackgroundWorkers.Quartz; +using Unity.Modules.Shared.Utils; using Volo.Abp.MultiTenancy; using Volo.Abp.Uow; @@ -19,8 +19,10 @@ namespace Unity.GrantManager.Messaging; /// Handlers are resolved from DI as filtered by . /// [DisallowConcurrentExecution] -public abstract class InboxWorkerBase : QuartzBackgroundWorkerBase +public abstract class InboxWorkerBase : UnityQuartzBackgroundWorkerBase { + protected override bool UsesTenantAuditScopes => true; + private readonly IServiceProvider _serviceProvider; /// @@ -68,7 +70,7 @@ protected InboxWorkerBase(IServiceProvider serviceProvider) _serviceProvider = serviceProvider; } - public override async Task Execute(IJobExecutionContext context) + protected override async Task ExecuteInBackgroundAsync(IJobExecutionContext context) { try { @@ -114,7 +116,7 @@ private async Task ProcessSingleMessageAsync(IServiceScope scope, InboxMessage i inboxMsg.MessageId, inboxMsg.Source, inboxMsg.DataType, inboxMsg.TenantId); string ackStatus; - string details; + string details = string.Empty; try { @@ -149,12 +151,15 @@ private async Task ProcessSingleMessageAsync(IServiceScope scope, InboxMessage i else { // Switch to tenant context ONLY for the domain handler execution - using (currentTenant.Change(inboxMsg.TenantId)) + await ExecuteInTenantAuditScopeAsync(inboxMsg.TenantId, async () => { - using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: true); - details = await handler.HandleAsync(inboxMsg.Payload); - await uow.CompleteAsync(); - } + using (currentTenant.Change(inboxMsg.TenantId)) + { + using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: true); + details = await handler.HandleAsync(inboxMsg.Payload); + await uow.CompleteAsync(); + } + }); ackStatus = "SUCCESS"; } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Messaging/OutboxWorkerBase.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Messaging/OutboxWorkerBase.cs index f41e23c52a..d351c39d89 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Messaging/OutboxWorkerBase.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Messaging/OutboxWorkerBase.cs @@ -4,7 +4,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Quartz; -using Volo.Abp.BackgroundWorkers.Quartz; +using Unity.Modules.Shared.Utils; using Volo.Abp.Uow; namespace Unity.GrantManager.Messaging; @@ -17,7 +17,7 @@ namespace Unity.GrantManager.Messaging; /// via . /// [DisallowConcurrentExecution] -public abstract class OutboxWorkerBase : QuartzBackgroundWorkerBase +public abstract class OutboxWorkerBase : UnityQuartzBackgroundWorkerBase { private readonly IServiceProvider _serviceProvider; @@ -44,7 +44,7 @@ protected OutboxWorkerBase(IServiceProvider serviceProvider) _serviceProvider = serviceProvider; } - public override async Task Execute(IJobExecutionContext context) + protected override async Task ExecuteInBackgroundAsync(IJobExecutionContext context) { try { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Notifications/AutomatedNotificationAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Notifications/AutomatedNotificationAppService.cs index cf5128eba7..65454bc122 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Notifications/AutomatedNotificationAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Notifications/AutomatedNotificationAppService.cs @@ -1,19 +1,36 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; using Volo.Abp.Domain.Repositories; using Microsoft.EntityFrameworkCore; +using Unity.Notifications.Permissions; +using Unity.Notifications.Templates; +using Volo.Abp.Uow; +using Volo.Abp; namespace Unity.GrantManager.Notifications { - public class AutomatedNotificationAppService(IRepository repository) : ApplicationService, IAutomatedNotificationAppService + [Authorize] + public class AutomatedNotificationAppService( + IRepository repository, + ITemplatesRepository templatesRepository, + IUnitOfWorkManager unitOfWorkManager) : ApplicationService, IAutomatedNotificationAppService { private readonly IRepository _repository = repository; + [Authorize(NotificationsPermissions.Email.ScheduleCreate)] public async Task CreateAsync(CreateUpdateNotificationDto input) { + using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: true); + if (await templatesRepository.LockActiveAsync(input.EmailTemplateId) == null) + { + throw new BusinessException("Notifications:TemplateNotAvailable"); + } + var entity = new ScheduledNotification { FormId = input.FormId, @@ -32,6 +49,7 @@ public async Task CreateAsync(CreateUpdateNotificationDto input }; await _repository.InsertAsync(entity, autoSave: true); + await uow.CompleteAsync(); return new NotificationDto { @@ -51,11 +69,13 @@ public async Task CreateAsync(CreateUpdateNotificationDto input }; } + [Authorize(NotificationsPermissions.Email.ScheduleCancel)] public async Task DeleteAsync(Guid id) { await _repository.DeleteAsync(id, autoSave: true); } + [Authorize(NotificationsPermissions.Email.ScheduleCancel)] public async Task CancelAsync(Guid id) { var entity = await _repository.GetAsync(id); @@ -63,6 +83,7 @@ public async Task CancelAsync(Guid id) await _repository.UpdateAsync(entity, autoSave: true); } + [Authorize(NotificationsPermissions.Email.NotificationsTab)] public async Task GetAsync(Guid id) { var e = await _repository.GetAsync(id); @@ -86,6 +107,7 @@ public async Task GetAsync(Guid id) }; } + [Authorize(NotificationsPermissions.Email.NotificationsTab)] public async Task> GetListAsync(GetNotificationsInput input) { var query = await _repository.GetQueryableAsync(); @@ -125,9 +147,32 @@ public async Task> GetListAsync(GetNotifications return new PagedResultDto(total, items); } + // Limited lookup for template editors (delete/edit preflights); does not expose the schedule list. + [Authorize(NotificationsPermissions.Email.Send)] + public async Task> GetTemplateReferencesAsync(Guid templateId) + { + var query = await _repository.GetQueryableAsync(); + return await AsyncExecuter.ToListAsync(query + .Where(x => x.EmailTemplateId == templateId) + .OrderByDescending(x => x.CreationTime) + .Select(x => new TemplateNotificationReferenceDto + { + TriggerType = x.TriggerType, + TriggerDetail = x.TriggerDetail, + IsActive = x.IsActive + })); + } + + [Authorize(NotificationsPermissions.Email.ScheduleCreate)] public async Task UpdateAsync(Guid id, CreateUpdateNotificationDto input) { + using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: true); var e = await _repository.GetAsync(id); + if (await templatesRepository.LockActiveAsync(input.EmailTemplateId) == null) + { + throw new BusinessException("Notifications:TemplateNotAvailable"); + } + e.EmailTemplateId = input.EmailTemplateId; e.TriggerType = input.TriggerType; e.Module = input.Module; @@ -142,6 +187,7 @@ public async Task UpdateAsync(Guid id, CreateUpdateNotification e.RecipientIdentifier = input.RecipientIdentifier; await _repository.UpdateAsync(e, autoSave: true); + await uow.CompleteAsync(); return new NotificationDto { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Unity.GrantManager.Application.csproj b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Unity.GrantManager.Application.csproj index 9c3ca77020..faf4dc8232 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Unity.GrantManager.Application.csproj +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Unity.GrantManager.Application.csproj @@ -1,4 +1,4 @@ - + diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.DbMigrator/DbMigratorHostedService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.DbMigrator/DbMigratorHostedService.cs index 35e01c2f17..31df11be15 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.DbMigrator/DbMigratorHostedService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.DbMigrator/DbMigratorHostedService.cs @@ -23,25 +23,23 @@ public DbMigratorHostedService(IHostApplicationLifetime hostApplicationLifetime, public async Task StartAsync(CancellationToken cancellationToken) { - using (var application = await AbpApplicationFactory.CreateAsync(options => + using var application = await AbpApplicationFactory.CreateAsync(options => { - options.Services.ReplaceConfiguration(_configuration); - options.UseAutofac(); - options.Services.AddLogging(c => c.AddSerilog()); - options.AddDataMigrationEnvironment(); - })) - { - await application.InitializeAsync(); + options.Services.ReplaceConfiguration(_configuration); + options.UseAutofac(); + options.Services.AddLogging(c => c.AddSerilog()); + options.AddDataMigrationEnvironment(); + }); + await application.InitializeAsync(); - await application - .ServiceProvider - .GetRequiredService() - .MigrateAsync(); + await application + .ServiceProvider + .GetRequiredService() + .MigrateAsync(); - await application.ShutdownAsync(); + await application.ShutdownAsync(); - _hostApplicationLifetime.StopApplication(); - } + _hostApplicationLifetime.StopApplication(); } public Task StopAsync(CancellationToken cancellationToken) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.DbMigrator/Unity.GrantManager.DbMigrator.csproj b/applications/Unity.GrantManager/src/Unity.GrantManager.DbMigrator/Unity.GrantManager.DbMigrator.csproj index 11ae6d5466..a9d56bbc7e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.DbMigrator/Unity.GrantManager.DbMigrator.csproj +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.DbMigrator/Unity.GrantManager.DbMigrator.csproj @@ -1,4 +1,4 @@ - + diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Unity.GrantManager.Domain.Shared.csproj b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Unity.GrantManager.Domain.Shared.csproj index 3647cb4840..67177c2067 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Unity.GrantManager.Domain.Shared.csproj +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Unity.GrantManager.Domain.Shared.csproj @@ -1,4 +1,4 @@ - + diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Unity.GrantManager.Domain.csproj b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Unity.GrantManager.Domain.csproj index 8def4f7e90..c0d2ef2e92 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Unity.GrantManager.Domain.csproj +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Unity.GrantManager.Domain.csproj @@ -1,4 +1,4 @@ - + diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantTenantDbContext.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantTenantDbContext.cs index c80a60b025..c979825ad1 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantTenantDbContext.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantTenantDbContext.cs @@ -17,6 +17,7 @@ using Unity.GrantManager.Intakes; using Unity.GrantManager.Notifications; using Unity.Notifications.EntityFrameworkCore; +using Unity.Notifications.Templates; using Unity.Payments.EntityFrameworkCore; using Unity.Reporting.EntityFrameworkCore; using Volo.Abp.Data; @@ -26,7 +27,7 @@ namespace Unity.GrantManager.EntityFrameworkCore { [ConnectionStringName(GrantManagerConsts.DefaultTenantConnectionStringName)] - public class GrantTenantDbContext : AbpDbContext + public class GrantTenantDbContext(DbContextOptions options) : AbpDbContext(options) { #region Domain Entities public DbSet Intakes { get; set; } @@ -64,11 +65,8 @@ public class GrantTenantDbContext : AbpDbContext public DbSet ReportsHistories { get; set; } public DbSet ScheduledNotifications { get; set; } public DbSet ScheduledNotificationTrackings { get; set; } - #endregion - public GrantTenantDbContext(DbContextOptions options) : base(options) - { - } + #endregion protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -489,6 +487,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.Property(x => x.DateField).HasMaxLength(128); b.Property(x => x.TenantId).HasColumnName("TenantId"); b.HasIndex(x => x.TenantId); + b.HasIndex(x => x.EmailTemplateId); + // Foreign key constraint prevents concurrent template deletion races. + b.HasOne().WithMany().HasForeignKey(x => x.EmailTemplateId).IsRequired().OnDelete(DeleteBehavior.Restrict); b.TryConfigureExtraProperties(); }); @@ -503,7 +504,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.HasIndex(x => x.ApplicationId); b.HasIndex(x => x.ScheduledNotificationId); b.HasIndex(x => x.CreationTime); - b.HasIndex(new[] { nameof(ScheduledNotificationTracking.ApplicationId), nameof(ScheduledNotificationTracking.ScheduledNotificationId), nameof(ScheduledNotificationTracking.DateField), nameof(ScheduledNotificationTracking.TriggerDate) }).IsUnique(); + b.HasIndex([nameof(ScheduledNotificationTracking.ApplicationId), nameof(ScheduledNotificationTracking.ScheduledNotificationId), nameof(ScheduledNotificationTracking.DateField), nameof(ScheduledNotificationTracking.TriggerDate)]).IsUnique(); b.HasOne().WithMany().HasForeignKey(x => x.ApplicationId).IsRequired().OnDelete(DeleteBehavior.Cascade); b.HasOne().WithMany().HasForeignKey(x => x.ScheduledNotificationId).IsRequired().OnDelete(DeleteBehavior.Cascade); }); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20261013120000_AddQuartzMisfireOriginalFireTime.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20261013120000_AddQuartzMisfireOriginalFireTime.cs new file mode 100644 index 0000000000..ce3f6dfc39 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20261013120000_AddQuartzMisfireOriginalFireTime.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Infrastructure; + +using Unity.GrantManager.EntityFrameworkCore; + +#nullable disable + +namespace Unity.GrantManager.Migrations.HostMigrations; + +[DbContext(typeof(GrantManagerDbContext))] +[Migration("20261013120000_AddQuartzMisfireOriginalFireTime")] +public partial class AddQuartzMisfireOriginalFireTime : Migration +{ + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "misfire_orig_fire_time", + table: "qrtz_triggers", + type: "bigint", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "misfire_orig_fire_time", + table: "qrtz_triggers"); + } +} \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20261015221241_AddScheduledNotificationTemplateConstraint.Designer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20261015221241_AddScheduledNotificationTemplateConstraint.Designer.cs new file mode 100644 index 0000000000..fa73efaf3c --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20261015221241_AddScheduledNotificationTemplateConstraint.Designer.cs @@ -0,0 +1,5865 @@ +// +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("20261015221241_AddScheduledNotificationTemplateConstraint")] + partial class AddScheduledNotificationTemplateConstraint + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.PostgreSql) + .HasAnnotation("ProductVersion", "10.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScoresheetId"); + + b.ToTable("ScoresheetInstances", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Answer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("QuestionId") + .HasColumnType("uuid"); + + b.Property("ScoresheetInstanceId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("QuestionId"); + + b.HasIndex("ScoresheetInstanceId"); + + b.ToTable("Answers", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("SectionId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("Questions", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Scoresheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsArchived") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Scoresheets", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ScoresheetId"); + + b.ToTable("ScoresheetSections", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.CustomFieldValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CustomFieldId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("WorksheetInstanceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetInstanceId"); + + b.ToTable("CustomFieldValues", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UiAnchor") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetCorrelationId") + .HasColumnType("uuid"); + + b.Property("WorksheetCorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("WorksheetInstances", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetLinks.WorksheetLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UiAnchor") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetId"); + + b.ToTable("WorksheetLinks", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.CustomField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("SectionId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("CustomFields", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.Worksheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsArchived") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Worksheets", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetId"); + + b.ToTable("WorksheetSections", "Flex"); + }); + + modelBuilder.Entity("Unity.GrantManager.ApplicationForms.GenerationReview", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContextId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Operation") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReviewData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Sequence") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("Operation", "ContextId", "Sequence") + .IsUnique(); + + b.ToTable("GenerationReviews", "AI"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Applicant", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantName") + .IsRequired() + .HasMaxLength(600) + .HasColumnType("character varying(600)"); + + b.Property("ApproxNumberOfEmployees") + .HasColumnType("text"); + + b.Property("AuditComments") + .HasColumnType("text"); + + b.Property("BusinessNumber") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FiscalDay") + .HasColumnType("integer"); + + b.Property("FiscalMonth") + .HasColumnType("text"); + + b.Property("FiscalYearEnd") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("date"); + + b.Property("FundingHistoryComments") + .HasColumnType("text"); + + b.Property("IndigenousOrgInd") + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsDuplicated") + .HasColumnType("boolean"); + + b.Property("IssueTrackingComments") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MatchPercentage") + .HasColumnType("numeric"); + + b.Property("NonRegOrgName") + .HasColumnType("text"); + + b.Property("NonRegisteredBusinessName") + .HasColumnType("text"); + + b.Property("OrgName") + .HasColumnType("text"); + + b.Property("OrgNumber") + .HasColumnType("text"); + + b.Property("OrgStatus") + .HasColumnType("text"); + + b.Property("OrganizationType") + .HasColumnType("text"); + + b.Property("RedStop") + .HasColumnType("boolean"); + + b.Property("ReportsComments") + .HasColumnType("text"); + + b.Property("Sector") + .HasColumnType("text"); + + b.Property("SectorSubSectorIndustryDesc") + .HasColumnType("text"); + + b.Property("StartedOperatingDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("SubSector") + .HasColumnType("text"); + + b.Property("SupplierId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UnityApplicantId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantName"); + + b.HasIndex("OrgName"); + + b.HasIndex("OrgNumber"); + + b.HasIndex("Status"); + + b.HasIndex("SupplierId"); + + b.HasIndex("TenantId"); + + b.HasIndex("UnityApplicantId"); + + b.HasIndex("TenantId", "IsDeleted", "CreationTime") + .HasFilter("\"IsDeleted\" = false"); + + b.ToTable("Applicants", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAddress", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AddressType") + .HasColumnType("integer"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Postal") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("Street") + .HasColumnType("text"); + + b.Property("Street2") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Unit") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicantAddresses", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAgent", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("BceidBusinessGuid") + .HasColumnType("uuid"); + + b.Property("BceidBusinessName") + .HasColumnType("text"); + + b.Property("BceidUserGuid") + .HasColumnType("uuid"); + + b.Property("BceidUserName") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactOrder") + .HasColumnType("integer"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IdentityEmail") + .HasColumnType("text"); + + b.Property("IdentityName") + .HasColumnType("text"); + + b.Property("IdentityProvider") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsConfirmed") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OidcSubUser") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("Phone2") + .HasColumnType("text"); + + b.Property("Phone2Extension") + .HasColumnType("text"); + + b.Property("PhoneExtension") + .HasColumnType("text"); + + b.Property("RoleForApplicant") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationId") + .IsUnique(); + + b.HasIndex("TenantId", "ApplicationId"); + + b.ToTable("ApplicantAgents", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("ApplicantAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantMergeApplicationChange", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantIdAfter") + .HasColumnType("uuid"); + + b.Property("ApplicantIdBefore") + .HasColumnType("uuid"); + + b.Property("ApplicantMergeOperationId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("DefaultSiteIdAfter") + .HasColumnType("uuid"); + + b.Property("DefaultSiteIdBefore") + .HasColumnType("uuid"); + + b.Property("RelatedRecordsSnapshot") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("WasTransferred") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("ApplicantMergeOperationId", "ApplicationId") + .IsUnique(); + + b.HasIndex("TenantId", "ApplicationId"); + + b.ToTable("ApplicantMergeApplicationChanges", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantMergeOperation", 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("MergedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("MergedById") + .HasColumnType("uuid"); + + b.Property("PrincipalApplicantId") + .HasColumnType("uuid"); + + b.Property("PrincipalStateAfter") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("PrincipalStateBefore") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ReversalReason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("ReversedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ReversedById") + .HasColumnType("uuid"); + + b.Property("SecondaryApplicantId") + .HasColumnType("uuid"); + + b.Property("SecondaryStateAfter") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SecondaryStateBefore") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SnapshotVersion") + .HasColumnType("integer"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("PrincipalApplicantId"); + + b.HasIndex("SecondaryApplicantId"); + + b.HasIndex("TenantId", "MergedAt"); + + b.HasIndex("TenantId", "PrincipalApplicantId", "Status"); + + b.HasIndex("TenantId", "SecondaryApplicantId", "Status"); + + b.ToTable("ApplicantMergeOperations", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AIAnalysis") + .HasColumnType("text"); + + b.Property("Acquisition") + .HasColumnType("text"); + + b.Property("ApplicantElectoralDistrict") + .HasColumnType("text"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("ApplicationStatusId") + .HasColumnType("uuid"); + + b.Property("ApprovedAmount") + .HasColumnType("numeric"); + + b.Property("AssessmentResultDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AssessmentResultStatus") + .HasColumnType("text"); + + b.Property("AssessmentStartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Community") + .HasColumnType("text"); + + b.Property("CommunityPopulation") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContractExecutionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ContractNumber") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeclineRational") + .HasColumnType("text"); + + b.Property("DefaultSiteId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("DueDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DueDiligenceStatus") + .HasColumnType("text"); + + b.Property("EconomicRegion") + .HasColumnType("text"); + + b.Property("ElectoralDistrict") + .HasColumnType("text"); + + b.Property("EligibleForRenewal") + .HasColumnType("boolean"); + + b.Property("ExternalStatusVisibility") + .HasColumnType("boolean"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FinalDecisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Forestry") + .HasColumnType("text"); + + b.Property("ForestryFocus") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LikelihoodOfFunding") + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("NotificationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Payload") + .HasColumnType("jsonb"); + + b.Property("PercentageTotalProjectBudget") + .HasColumnType("double precision"); + + b.Property("Place") + .HasColumnType("text"); + + b.Property("ProjectEndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ProjectFundingTotal") + .HasColumnType("numeric"); + + b.Property("ProjectName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProjectStartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ProjectSummary") + .HasColumnType("text"); + + b.Property("ProposalDate") + .HasColumnType("timestamp without time zone"); + + b.Property("RecommendedAmount") + .HasColumnType("numeric"); + + b.Property("ReferenceNo") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegionalDistrict") + .HasColumnType("text"); + + b.Property("RequestedAmount") + .HasColumnType("numeric"); + + b.Property("RiskRanking") + .HasColumnType("text"); + + b.Property("SigningAuthorityBusinessPhone") + .HasColumnType("text"); + + b.Property("SigningAuthorityCellPhone") + .HasColumnType("text"); + + b.Property("SigningAuthorityEmail") + .HasColumnType("text"); + + b.Property("SigningAuthorityFullName") + .HasColumnType("text"); + + b.Property("SigningAuthorityTitle") + .HasColumnType("text"); + + b.Property("SubStatus") + .HasColumnType("text"); + + b.Property("SubmissionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TotalProjectBudget") + .HasColumnType("numeric"); + + b.Property("TotalScore") + .HasColumnType("integer"); + + b.Property("UnityApplicationId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationFormId"); + + b.HasIndex("ApplicationStatusId"); + + b.HasIndex("OwnerId"); + + b.HasIndex("ReferenceNo"); + + b.HasIndex("TenantId", "SubmissionDate") + .HasFilter("\"IsDeleted\" = false"); + + b.ToTable("Applications", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAssignment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AssigneeId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Duty") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("AssigneeId"); + + b.HasIndex("TenantId", "ApplicationId"); + + b.ToTable("ApplicationAssignments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationChefsFileAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AISummary") + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ChefsFileId") + .HasColumnType("text"); + + b.Property("ChefsSubmissionId") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationChefsFileAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationContact", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactEmail") + .HasColumnType("text"); + + b.Property("ContactFullName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactMobilePhone") + .HasColumnType("text"); + + b.Property("ContactTitle") + .HasColumnType("text"); + + b.Property("ContactType") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactWorkPhone") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationContact", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationForm", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountCodingId") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .HasColumnType("text"); + + b.Property("ApplicationFormDescription") + .HasColumnType("text"); + + b.Property("ApplicationFormName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AttemptedConnectionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AutomaticallyGenerateAIAnalysis") + .HasColumnType("boolean"); + + b.Property("AvailableChefsFields") + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("text"); + + b.Property("ChefsApplicationFormGuid") + .HasColumnType("text"); + + b.Property("ChefsCriteriaFormGuid") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ConnectionHttpStatus") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DefaultPaymentGroup") + .HasColumnType("integer"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ElectoralDistrictAddressType") + .HasColumnType("integer"); + + b.Property("ExternalLinksConfig") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("ExternalLinks"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormHierarchy") + .HasColumnType("integer"); + + b.Property("IntakeId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsDirectApproval") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ManuallyInitiateAIAnalysis") + .HasColumnType("boolean"); + + b.Property("ParentFormId") + .HasColumnType("uuid"); + + b.Property("Payable") + .HasColumnType("boolean"); + + b.Property("PaymentApprovalThreshold") + .HasColumnType("numeric"); + + b.Property("Prefix") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PreventPayment") + .HasColumnType("boolean"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("SuffixType") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IntakeId"); + + b.HasIndex("ParentFormId"); + + b.HasIndex("TenantId", "IsDeleted") + .HasFilter("\"IsDeleted\" = false"); + + b.ToTable("ApplicationForms", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormSubmission", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormVersionId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ChefsSubmissionGuid") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormVersionId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OidcSub") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Submission") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormSubmissions", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormVersion", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("AvailableChefsFields") + .HasColumnType("text"); + + b.Property("ChefsApplicationFormGuid") + .HasColumnType("text"); + + b.Property("ChefsFormVersionGuid") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormSchema") + .HasColumnType("jsonb"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SubmissionHeaderMapping") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormVersion", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationLink", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LinkType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("Related"); + + b.Property("LinkedApplicationId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("TenantId", "ApplicationId"); + + b.ToTable("ApplicationLinks", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationScoresheetAnswers", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Answers") + .IsRequired() + .HasColumnType("jsonb"); + + 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("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId") + .IsUnique(); + + b.ToTable("ApplicationScoresheetAnswers", "AI"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationStatus", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExternalStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("InternalStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("NotifiedStatus") + .HasColumnType("text"); + + b.Property("StatusCode") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("StatusCode") + .IsUnique(); + + b.ToTable("ApplicationStatuses", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationTags", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TagId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("TagId"); + + b.HasIndex("TenantId", "ApplicationId"); + + b.ToTable("ApplicationTags", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AssessmentAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AssessmentId"); + + b.ToTable("AssessmentAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AuditHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("AuditDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AuditNote") + .HasColumnType("text"); + + b.Property("AuditStatus") + .HasColumnType("text"); + + b.Property("AuditTrackingNumber") + .HasColumnType("text"); + + b.Property("AuditorName") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("AuditHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.FundingHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApprovedAmount") + .HasColumnType("numeric"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FundingNotes") + .HasColumnType("text"); + + b.Property("FundingYear") + .HasColumnType("text"); + + b.Property("GrantCategory") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OneTimeConsideration") + .HasColumnType("numeric"); + + b.Property("PaidDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ReconsiderationAmount") + .HasColumnType("numeric"); + + b.Property("RenewedFunding") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TotalGrantAmount") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("FundingHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.IssueTracking", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IssueDescription") + .HasColumnType("text"); + + b.Property("IssueHeading") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ResolutionNote") + .HasColumnType("text"); + + b.Property("Resolved") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Year") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("IssueTrackings", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ReportsHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FiscalYear") + .HasColumnType("text"); + + b.Property("IncompleteReport") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("Outstanding") + .HasColumnType("boolean"); + + b.Property("ReportDate") + .HasColumnType("timestamp without time zone"); + + b.Property("SignedOff") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("ReportsHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Assessments.Assessment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ApprovalRecommended") + .HasColumnType("boolean"); + + b.Property("AssessorId") + .HasColumnType("uuid"); + + b.Property("CleanGrowth") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("EconomicImpact") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FinancialAnalysis") + .HasColumnType("integer"); + + b.Property("InclusiveGrowth") + .HasColumnType("integer"); + + b.Property("IsAiAssessment") + .HasColumnType("boolean"); + + b.Property("IsComplete") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("AssessorId"); + + b.ToTable("Assessments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicantComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PinDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("CommenterId"); + + b.ToTable("ApplicantComments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicationComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PinDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("CommenterId"); + + b.ToTable("ApplicationComments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.AssessmentComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PinDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AssessmentId"); + + b.HasIndex("CommenterId"); + + b.ToTable("AssessmentComments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Contacts.Contact", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("HomePhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MobilePhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("WorkPhoneExtension") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("WorkPhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.ToTable("Contacts", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Contacts.ContactLink", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("RelatedEntityId") + .HasColumnType("uuid"); + + b.Property("RelatedEntityType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Role") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("RelatedEntityType", "RelatedEntityId"); + + b.HasIndex("ContactId", "RelatedEntityType", "RelatedEntityId"); + + b.ToTable("ContactLinks", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.GlobalTag.Tag", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Tags", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Identity.Person", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Badge") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FullName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OidcDisplayName") + .IsRequired() + .HasColumnType("text"); + + b.Property("OidcSub") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("OidcSub"); + + b.HasIndex("TenantId"); + + b.ToTable("Persons", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Intakes.Intake", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Budget") + .HasColumnType("double precision"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IntakeName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("StartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Intakes", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Notifications.ScheduledNotification", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationStatus") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ApplicationStatusId") + .HasColumnType("uuid"); + + b.Property("ApplicationStatusIds") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DateField") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EmailTemplateId") + .HasColumnType("uuid"); + + b.Property("EventType") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Module") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("RecipientCategory") + .HasColumnType("text"); + + b.Property("RecipientIdentifier") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TriggerDetail") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("TriggerType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("EmailTemplateId"); + + b.HasIndex("TenantId"); + + b.ToTable("ScheduledNotifications", "Notifications"); + }); + + modelBuilder.Entity("Unity.GrantManager.Notifications.ScheduledNotificationTracking", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatorId") + .HasColumnType("uuid"); + + b.Property("DateField") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("NotificationSentDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ScheduledNotificationId") + .HasColumnType("uuid"); + + b.Property("TriggerDate") + .HasColumnType("timestamp without time zone"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("CreationTime"); + + b.HasIndex("ScheduledNotificationId"); + + b.HasIndex("ApplicationId", "ScheduledNotificationId", "DateField", "TriggerDate") + .IsUnique(); + + b.ToTable("ScheduledNotificationTracking", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.EmailAddresses.EmailAddressConfiguration", 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() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("EmailAddress") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("EmailType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + 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("TenantId") + .IsUnique() + .HasFilter("\"IsDefault\" = true"); + + b.HasIndex("TenantId", "EmailAddress", "EmailType") + .IsUnique(); + + b.ToTable("EmailAddressConfigurations", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EmailGroups", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroupUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.ToTable("EmailGroupUsers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("BCC") + .IsRequired() + .HasColumnType("text"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CC") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChesHttpStatusCode") + .HasColumnType("text"); + + b.Property("ChesMsgId") + .HasColumnType("uuid"); + + b.Property("ChesResponse") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChesStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("EmailType") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FromAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestIds") + .IsRequired() + .HasColumnType("text"); + + b.Property("Priority") + .IsRequired() + .HasColumnType("text"); + + b.Property("Recipient") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("RetryAttempts") + .HasColumnType("integer"); + + b.Property("ScheduledNotificationId") + .HasColumnType("uuid"); + + b.Property("SendOnDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("SentDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("text"); + + b.Property("TemplateName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("ToAddress") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EmailLogs", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLogAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("EmailLogId") + .HasColumnType("uuid"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OriginTemplateId") + .HasColumnType("uuid"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TemplateId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("EmailLogId"); + + b.HasIndex("S3ObjectKey"); + + b.HasIndex("TemplateId"); + + b.ToTable("EmailLogAttachments", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Logs.NotificationLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Channel") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CommitSha") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeliveryTarget") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Environment") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ExceptionMessage") + .HasColumnType("text"); + + b.Property("ExceptionType") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeliveredRealtime") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("NotificationType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PayloadJson") + .HasColumnType("jsonb"); + + b.Property("SenderDisplayName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SenderUserId") + .HasColumnType("uuid"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SourceReference") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("StackExcerpt") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("NotificationType", "CreationTime"); + + b.HasIndex("TenantId", "CreationTime"); + + b.ToTable("NotificationLogs", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.ReadStates.NotificationReadState", 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("LastReadAt") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "UserId") + .IsUnique(); + + b.ToTable("NotificationReadStates", "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() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RecipientCategory") + .HasColumnType("text"); + + b.Property("RecipientIdentifier") + .HasColumnType("text"); + + b.Property("SendFrom") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("TemplateType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasDefaultValue("Application"); + + 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("TemplateType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasDefaultValue("Application"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("TemplateVariables", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.Trigger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("InternalName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Triggers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TriggerSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SubscriptionGroupId") + .HasColumnType("uuid"); + + b.Property("TemplateId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TriggerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionGroupId"); + + b.HasIndex("TemplateId"); + + b.HasIndex("TriggerId"); + + b.ToTable("TriggerSubscriptions", "Notifications"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.AccountCodings.AccountCoding", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Description") + .HasMaxLength(35) + .HasColumnType("character varying(35)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MinistryClient") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("Responsibility") + .IsRequired() + .HasColumnType("text"); + + b.Property("ServiceLine") + .IsRequired() + .HasColumnType("text"); + + b.Property("Stob") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("AccountCodings", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentConfigurations.PaymentConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DefaultAccountCodingId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentIdPrefix") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("PaymentConfigurations", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.ExpenseApproval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DecisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DecisionUserId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PaymentRequestId"); + + b.ToTable("ExpenseApprovals", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountCodingId") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("BatchName") + .IsRequired() + .HasColumnType("text"); + + b.Property("BatchNumber") + .HasColumnType("numeric"); + + b.Property("CancelledBy") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("CancelledBy"); + + b.Property("CancelledById") + .HasColumnType("uuid") + .HasColumnName("CancelledById"); + + b.Property("CancelledOn") + .HasColumnType("timestamp without time zone") + .HasColumnName("CancelledOn"); + + b.Property("CasHttpStatusCode") + .HasColumnType("integer"); + + b.Property("CasResponse") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContractNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FsbApNotified") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("FsbNotificationEmailLogId") + .HasColumnType("uuid"); + + b.Property("FsbNotificationSentDate") + .HasColumnType("timestamp without time zone"); + + b.Property("InvoiceNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("InvoiceStatus") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsRecon") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("PayeeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentDate") + .HasColumnType("text"); + + b.Property("PaymentNumber") + .HasColumnType("text"); + + b.Property("PaymentStatus") + .HasColumnType("text"); + + b.Property("ReferenceNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("RequesterName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SiteId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("SubmissionConfirmationCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SupplierName") + .HasColumnType("text"); + + b.Property("SupplierNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AccountCodingId"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("CreationTime"); + + b.HasIndex("FsbNotificationEmailLogId"); + + b.HasIndex("ReferenceNumber") + .IsUnique(); + + b.HasIndex("SiteId"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId", "CreationTime") + .HasFilter("\"IsDeleted\" = false"); + + b.ToTable("PaymentRequests", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentTags.PaymentTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestId") + .HasColumnType("uuid"); + + b.Property("TagId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("PaymentRequestId"); + + b.HasIndex("TagId"); + + b.ToTable("PaymentTags", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentThresholds.PaymentThreshold", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Threshold") + .HasColumnType("numeric"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("PaymentThresholds", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Site", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddressLine1") + .HasColumnType("text"); + + b.Property("AddressLine2") + .HasColumnType("text"); + + b.Property("AddressLine3") + .HasColumnType("text"); + + b.Property("BankAccount") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EFTAdvicePref") + .HasColumnType("text"); + + b.Property("EmailAddress") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastUpdatedInCas") + .HasColumnType("timestamp without time zone"); + + b.Property("MarkDeletedInUse") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentGroup") + .HasColumnType("integer"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("SiteProtected") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("SupplierId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("SupplierId"); + + b.ToTable("Sites", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Supplier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BusinessNumber") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastUpdatedInCAS") + .HasColumnType("timestamp without time zone"); + + b.Property("MailingAddress") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("text"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("SIN") + .HasColumnType("text"); + + b.Property("StandardIndustryClassification") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("Subcategory") + .HasColumnType("text"); + + b.Property("SupplierProtected") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Suppliers", "Payments"); + }); + + modelBuilder.Entity("Unity.Reporting.Domain.Configuration.ReportColumnsMap", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Mapping") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RoleStatus") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("ViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ViewStatus") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ReportColumnsMaps", "Reporting"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Scoresheet", "Scoresheet") + .WithMany("Instances") + .HasForeignKey("ScoresheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scoresheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Answer", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Question", "Question") + .WithMany("Answers") + .HasForeignKey("QuestionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", null) + .WithMany("Answers") + .HasForeignKey("ScoresheetInstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Question"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.ScoresheetSection", "Section") + .WithMany("Fields") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Scoresheet", "Scoresheet") + .WithMany("Sections") + .HasForeignKey("ScoresheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scoresheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.CustomFieldValue", b => + { + b.HasOne("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", null) + .WithMany("Values") + .HasForeignKey("WorksheetInstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetLinks.WorksheetLink", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.Worksheet", "Worksheet") + .WithMany("Links") + .HasForeignKey("WorksheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Worksheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.CustomField", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.WorksheetSection", "Section") + .WithMany("Fields") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.Worksheet", "Worksheet") + .WithMany("Sections") + .HasForeignKey("WorksheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Worksheet"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAddress", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", "Applicant") + .WithMany("ApplicantAddresses") + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicantAddresses") + .HasForeignKey("ApplicationId"); + + b.Navigation("Applicant"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAgent", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithOne("ApplicantAgent") + .HasForeignKey("Unity.GrantManager.Applications.ApplicantAgent", "ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAttachment", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantMergeApplicationChange", b => + { + b.HasOne("Unity.GrantManager.Applications.ApplicantMergeOperation", null) + .WithMany("ApplicationChanges") + .HasForeignKey("ApplicantMergeOperationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantMergeOperation", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("PrincipalApplicantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("SecondaryApplicantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", "Applicant") + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", "ApplicationForm") + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationStatus", "ApplicationStatus") + .WithMany("Applications") + .HasForeignKey("ApplicationStatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Applicant"); + + b.Navigation("ApplicationForm"); + + b.Navigation("ApplicationStatus"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAssignment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicationAssignments") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", "Assignee") + .WithMany() + .HasForeignKey("AssigneeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Assignee"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAttachment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationChefsFileAttachment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationContact", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationForm", b => + { + b.HasOne("Unity.GrantManager.Intakes.Intake", null) + .WithMany() + .HasForeignKey("IntakeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) + .WithMany() + .HasForeignKey("ParentFormId") + .OnDelete(DeleteBehavior.NoAction); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormSubmission", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormVersion", b => + { + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationLink", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany("ApplicationLinks") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationTags", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicationTags") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.GlobalTag.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AssessmentAttachment", b => + { + b.HasOne("Unity.GrantManager.Assessments.Assessment", null) + .WithMany() + .HasForeignKey("AssessmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AuditHistory", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.FundingHistory", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.IssueTracking", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ReportsHistory", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Assessments.Assessment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("Assessments") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("AssessorId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicantComment", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicationComment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.AssessmentComment", b => + { + b.HasOne("Unity.GrantManager.Assessments.Assessment", null) + .WithMany() + .HasForeignKey("AssessmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Contacts.ContactLink", b => + { + b.HasOne("Unity.GrantManager.Contacts.Contact", null) + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Notifications.ScheduledNotification", b => + { + b.HasOne("Unity.Notifications.Templates.EmailTemplate", null) + .WithMany() + .HasForeignKey("EmailTemplateId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Notifications.ScheduledNotificationTracking", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Notifications.ScheduledNotification", null) + .WithMany() + .HasForeignKey("ScheduledNotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroupUser", b => + { + b.HasOne("Unity.Notifications.EmailGroups.EmailGroup", null) + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLogAttachment", b => + { + b.HasOne("Unity.Notifications.Emails.EmailLog", null) + .WithMany() + .HasForeignKey("EmailLogId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Unity.Notifications.Templates.EmailTemplate", null) + .WithMany() + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroupSubscription", b => + { + b.HasOne("Unity.Notifications.Templates.SubscriptionGroup", "SubscriptionGroup") + .WithMany() + .HasForeignKey("GroupId"); + + b.HasOne("Unity.Notifications.Templates.Subscriber", "Subscriber") + .WithMany() + .HasForeignKey("SubscriberId"); + + b.Navigation("Subscriber"); + + b.Navigation("SubscriptionGroup"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TriggerSubscription", b => + { + b.HasOne("Unity.Notifications.Templates.SubscriptionGroup", "SubscriptionGroup") + .WithMany() + .HasForeignKey("SubscriptionGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Notifications.Templates.EmailTemplate", "EmailTemplate") + .WithMany() + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Notifications.Templates.Trigger", "Trigger") + .WithMany() + .HasForeignKey("TriggerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmailTemplate"); + + b.Navigation("SubscriptionGroup"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.ExpenseApproval", b => + { + b.HasOne("Unity.Payments.Domain.PaymentRequests.PaymentRequest", "PaymentRequest") + .WithMany("ExpenseApprovals") + .HasForeignKey("PaymentRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PaymentRequest"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.HasOne("Unity.Payments.Domain.AccountCodings.AccountCoding", "AccountCoding") + .WithMany() + .HasForeignKey("AccountCodingId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Unity.Payments.Domain.Suppliers.Site", "Site") + .WithMany() + .HasForeignKey("SiteId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("AccountCoding"); + + b.Navigation("Site"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentTags.PaymentTag", b => + { + b.HasOne("Unity.Payments.Domain.PaymentRequests.PaymentRequest", null) + .WithMany("PaymentTags") + .HasForeignKey("PaymentRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.GlobalTag.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Site", b => + { + b.HasOne("Unity.Payments.Domain.Suppliers.Supplier", "Supplier") + .WithMany("Sites") + .HasForeignKey("SupplierId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.Navigation("Answers"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.Navigation("Answers"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Scoresheet", b => + { + b.Navigation("Instances"); + + b.Navigation("Sections"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.Navigation("Fields"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", b => + { + b.Navigation("Values"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.Worksheet", b => + { + b.Navigation("Links"); + + b.Navigation("Sections"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.Navigation("Fields"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Applicant", b => + { + b.Navigation("ApplicantAddresses"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantMergeOperation", b => + { + b.Navigation("ApplicationChanges"); + }); + + 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/20261015221241_AddScheduledNotificationTemplateConstraint.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20261015221241_AddScheduledNotificationTemplateConstraint.cs new file mode 100644 index 0000000000..2d4215d277 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20261015221241_AddScheduledNotificationTemplateConstraint.cs @@ -0,0 +1,77 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + /// + public partial class AddScheduledNotificationTemplateConstraint : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "TemplateType", + schema: "Notifications", + table: "TemplateVariables", + type: "character varying(64)", + maxLength: 64, + nullable: false, + defaultValue: "Application", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldDefaultValue: "Applicant"); + + migrationBuilder.Sql( + @"DELETE FROM ""Notifications"".""ScheduledNotifications"" AS scheduled + WHERE NOT EXISTS ( + SELECT 1 + FROM ""Notifications"".""EmailTemplates"" AS template + WHERE template.""Id"" = scheduled.""EmailTemplateId"");"); + + migrationBuilder.CreateIndex( + name: "IX_ScheduledNotifications_EmailTemplateId", + schema: "Notifications", + table: "ScheduledNotifications", + column: "EmailTemplateId"); + + migrationBuilder.AddForeignKey( + name: "FK_ScheduledNotifications_EmailTemplates_EmailTemplateId", + schema: "Notifications", + table: "ScheduledNotifications", + column: "EmailTemplateId", + principalSchema: "Notifications", + principalTable: "EmailTemplates", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_ScheduledNotifications_EmailTemplates_EmailTemplateId", + schema: "Notifications", + table: "ScheduledNotifications"); + + migrationBuilder.DropIndex( + name: "IX_ScheduledNotifications_EmailTemplateId", + schema: "Notifications", + table: "ScheduledNotifications"); + + migrationBuilder.AlterColumn( + name: "TemplateType", + schema: "Notifications", + table: "TemplateVariables", + type: "character varying(64)", + maxLength: 64, + nullable: false, + defaultValue: "Applicant", + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldDefaultValue: "Application"); + } + } +} 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 62bd3b278e..eed164bddd 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 @@ -3426,6 +3426,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("EmailTemplateId"); + b.HasIndex("TenantId"); b.ToTable("ScheduledNotifications", "Notifications"); @@ -4339,7 +4341,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasMaxLength(64) .HasColumnType("character varying(64)") - .HasDefaultValue("Applicant"); + .HasDefaultValue("Application"); b.Property("TenantId") .HasColumnType("uuid") @@ -5630,6 +5632,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired(); }); + modelBuilder.Entity("Unity.GrantManager.Notifications.ScheduledNotification", b => + { + b.HasOne("Unity.Notifications.Templates.EmailTemplate", null) + .WithMany() + .HasForeignKey("EmailTemplateId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + modelBuilder.Entity("Unity.GrantManager.Notifications.ScheduledNotificationTracking", b => { b.HasOne("Unity.GrantManager.Applications.Application", null) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Unity.GrantManager.EntityFrameworkCore.csproj b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Unity.GrantManager.EntityFrameworkCore.csproj index abdd042926..028fb04d77 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Unity.GrantManager.EntityFrameworkCore.csproj +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Unity.GrantManager.EntityFrameworkCore.csproj @@ -1,4 +1,4 @@ - + diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi.Client/Unity.GrantManager.HttpApi.Client.csproj b/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi.Client/Unity.GrantManager.HttpApi.Client.csproj index 6ed03b7432..26717eb64d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi.Client/Unity.GrantManager.HttpApi.Client.csproj +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi.Client/Unity.GrantManager.HttpApi.Client.csproj @@ -1,4 +1,4 @@ - + diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Unity.GrantManager.HttpApi.csproj b/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Unity.GrantManager.HttpApi.csproj index ae807c1d10..8b179b555c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Unity.GrantManager.HttpApi.csproj +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Unity.GrantManager.HttpApi.csproj @@ -1,4 +1,4 @@ - + diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Controllers/FormNotificationsApiController.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Controllers/FormNotificationsApiController.cs index a084d239e6..8991234cf2 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Controllers/FormNotificationsApiController.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Controllers/FormNotificationsApiController.cs @@ -21,37 +21,21 @@ namespace Unity.GrantManager.Web.Controllers { [Route("api/form-notifications")] [ApiController] - public class FormNotificationsApiController : ControllerBase + public class FormNotificationsApiController(IApplicationStatusService statusService, + IEmailGroupsAppService emailGroupsAppService, + ITemplateService templateService, + Notifications.IAutomatedNotificationAppService automatedNotificationAppService, + EmailAttachmentService emailAttachmentService, ICurrentUser currentUser, + IEmailGroupUsersAppService emailGroupUsersAppService, + IIdentityUserIntegrationService identityUserIntegrationService, + IGrantApplicationAppService grantApplicationAppService, + ScheduledNotificationHelper scheduledNotificationHelper, + IApplicantEmailTemplateAppService applicantEmailTemplateService, + EmailComposerAccessChecker emailAccessChecker) : ControllerBase { - private readonly IApplicationStatusService _statusService; - private readonly IEmailGroupsAppService _emailGroupsAppService; - private readonly ITemplateService _templateService; - private readonly Notifications.IAutomatedNotificationAppService _automatedNotificationAppService; - private readonly EmailAttachmentService _emailAttachmentService; - private readonly ICurrentUser _currentUser; - private readonly IEmailGroupUsersAppService _emailGroupUsersAppService; - private readonly IIdentityUserIntegrationService _identityUserIntegrationService; - private readonly IGrantApplicationAppService _grantApplicationAppService; - private readonly ScheduledNotificationHelper _scheduledNotificationHelper; - private readonly IApplicantEmailTemplateAppService _applicantEmailTemplateService; - private readonly EmailComposerAccessChecker _emailAccessChecker; - - public FormNotificationsApiController(IApplicationStatusService statusService, IEmailGroupsAppService emailGroupsAppService, Unity.Notifications.Templates.ITemplateService templateService, Unity.GrantManager.Notifications.IAutomatedNotificationAppService automatedNotificationAppService, EmailAttachmentService emailAttachmentService, ICurrentUser currentUser, IEmailGroupUsersAppService emailGroupUsersAppService, IIdentityUserIntegrationService identityUserIntegrationService, IGrantApplicationAppService grantApplicationAppService, ScheduledNotificationHelper scheduledNotificationHelper, IApplicantEmailTemplateAppService applicantEmailTemplateService, EmailComposerAccessChecker emailAccessChecker) - { - _statusService = statusService; - _emailGroupsAppService = emailGroupsAppService; - _templateService = templateService; - _automatedNotificationAppService = automatedNotificationAppService; - _emailAttachmentService = emailAttachmentService; - _currentUser = currentUser; - _emailGroupUsersAppService = emailGroupUsersAppService; - _identityUserIntegrationService = identityUserIntegrationService; - _grantApplicationAppService = grantApplicationAppService; - _scheduledNotificationHelper = scheduledNotificationHelper; - _applicantEmailTemplateService = applicantEmailTemplateService; - _emailAccessChecker = emailAccessChecker; - } + + [Authorize(NotificationsPermissions.Email.Send)] [HttpGet("payment-statuses")] public ActionResult> GetPaymentStatuses() { @@ -77,10 +61,11 @@ public ActionResult> GetPaymentStatuses() // In-memory storage removed; persisting to ScheduledNotifications table via IAutomatedNotificationAppService + [Authorize(NotificationsPermissions.Email.Send)] [HttpGet("templates")] public async Task>> GetTemplates([FromQuery] string? templateType = null) { - var templates = await _templateService.GetTemplatesByTenant(); + var templates = await templateService.GetTemplatesByTenant(); if (!string.IsNullOrWhiteSpace(templateType)) { templates = templates @@ -107,13 +92,14 @@ public async Task>> GetTemplates([FromQuery] [HttpGet("templates/{templateId:guid}/applicant-preview")] public async Task> GetApplicantTemplatePreview(Guid templateId, [FromQuery] Guid applicantId) { - return Ok(await _applicantEmailTemplateService.GetPreviewAsync(applicantId, templateId)); + return Ok(await applicantEmailTemplateService.GetPreviewAsync(applicantId, templateId)); } + [Authorize(NotificationsPermissions.Email.Send)] [HttpGet("templates/{templateId:guid}/resolved-recipients")] public async Task> GetResolvedTemplateRecipients(Guid templateId, [FromQuery] Guid? applicationId = null) { - var template = await _templateService.GetTemplateById(templateId); + var template = await templateService.GetTemplateById(templateId); if (template == null) { return NotFound(); @@ -134,11 +120,11 @@ public async Task> GetResolvedTemplateRecipi RecipientIdentifier = recipientIdentifier }; - var emailTo = await _scheduledNotificationHelper.GetInternalRecipientEmailAddressesAsync( + var emailTo = await scheduledNotificationHelper.GetInternalRecipientEmailAddressesAsync( notification, - _emailGroupsAppService, - _emailGroupUsersAppService, - _identityUserIntegrationService); + emailGroupsAppService, + emailGroupUsersAppService, + identityUserIntegrationService); return Ok(new ResolvedRecipientsDto { EmailTo = emailTo }); } @@ -150,7 +136,7 @@ public async Task> GetResolvedTemplateRecipi return Ok(new ResolvedRecipientsDto { EmailTo = string.Empty }); } - var application = await _grantApplicationAppService.GetAsync(applicationId.Value); + var application = await grantApplicationAppService.GetAsync(applicationId.Value); var identifiers = recipientIdentifier .Split(',') .Select(r => r.Trim()) @@ -195,20 +181,21 @@ public async Task> CopyTemplateAttachme if (input.EmailLogId == Guid.Empty) return BadRequest("EmailLogId is required"); - var email = await _emailAccessChecker.CheckEmailAsync(input.EmailLogId, NotificationsPermissions.Email.Send, requireDraft: true); - await _emailAccessChecker.CheckTemplateAsync(templateId, email.ApplicantId); - var copiedCount = await _emailAttachmentService.ReplaceTemplateAttachmentsAsync( + var email = await emailAccessChecker.CheckEmailAsync(input.EmailLogId, NotificationsPermissions.Email.Send, requireDraft: true); + await emailAccessChecker.CheckTemplateAsync(templateId, email.ApplicantId); + var copiedCount = await emailAttachmentService.ReplaceTemplateAttachmentsAsync( templateId, input.EmailLogId, - _currentUser.TenantId); + currentUser.TenantId); return Ok(new CopyAttachmentsResponseDto { AttachmentCount = copiedCount }); } + [Authorize(NotificationsPermissions.Email.Send)] [HttpGet("email-template/{templateId}/validate-attachments")] public async Task ValidateTemplateAttachments(Guid templateId) { - await _emailAttachmentService.ValidateTemplateAttachmentsAsync(templateId); + await emailAttachmentService.ValidateTemplateAttachmentsAsync(templateId); return NoContent(); } @@ -216,26 +203,28 @@ public async Task ValidateTemplateAttachments(Guid templateId) [HttpDelete("email-log/{emailLogId}/origin-attachments")] public async Task> DeleteOriginAttachments(Guid emailLogId) { - await _emailAccessChecker.CheckEmailAsync(emailLogId, NotificationsPermissions.Email.Send, requireDraft: true); - var deletedCount = await _emailAttachmentService.DeleteOriginAttachmentsAsync(emailLogId); + await emailAccessChecker.CheckEmailAsync(emailLogId, NotificationsPermissions.Email.Send, requireDraft: true); + var deletedCount = await emailAttachmentService.DeleteOriginAttachmentsAsync(emailLogId); return Ok(new CopyAttachmentsResponseDto { AttachmentCount = deletedCount }); } + [Authorize(NotificationsPermissions.Email.Send)] [HttpGet("statuses")] public async Task>> GetApplicationStatuses() { - var statuses = await _statusService.GetListAsync(); + var statuses = await statusService.GetListAsync(); var list = statuses.Select(s => new { id = s.Id, internalStatus = s.InternalStatus }).ToList(); return Ok(list); } + [Authorize(NotificationsPermissions.Email.Send)] [HttpGet("recipients")] public async Task>> GetRecipients([FromQuery] string category) { // For internal recipients, load EmailGroups from the notifications module and expose their Name if (string.Equals(category, "Internal", StringComparison.OrdinalIgnoreCase)) { - var groups = await _emailGroupsAppService.GetListAsync(); + var groups = await emailGroupsAppService.GetListAsync(); var list = groups.Select(g => new RecipientDto { Id = g.Name, DisplayName = g.Name }).ToList(); return Ok(list); } @@ -253,21 +242,22 @@ public async Task>> GetRecipients([FromQuery] st } [HttpGet("{formId}")] + [Authorize(NotificationsPermissions.Email.NotificationsTab)] public async Task>> GetForForm(string formId) { if (!Guid.TryParse(formId, out var parsedFormId)) return BadRequest("Invalid form id"); - var listResult = await _automatedNotificationAppService.GetListAsync(new Notifications.GetNotificationsInput { FormId = parsedFormId, MaxResultCount = 1000 }); + var listResult = await automatedNotificationAppService.GetListAsync(new Notifications.GetNotificationsInput { FormId = parsedFormId, MaxResultCount = 1000 }); // Resolve template names and status labels var templateIds = listResult.Items.Select(x => x.EmailTemplateId).Where(id => id != Guid.Empty).Distinct().ToList(); var templateMap = new Dictionary(); foreach (var id in templateIds) { - templateMap[id] = await _templateService.GetTemplateById(id); + templateMap[id] = await templateService.GetTemplateById(id); } - var statuses = await _statusService.GetListAsync(); + var statuses = await statusService.GetListAsync(); var statusMap = statuses.ToDictionary(s => s.Id, s => s.InternalStatus); var items = listResult.Items.Select(e => new ScheduledNotificationDto @@ -293,6 +283,7 @@ public async Task>> GetForForm(strin return Ok(items); } + [Authorize(NotificationsPermissions.Email.ScheduleCreate)] [HttpPost("{formId}")] public async Task> CreateForForm(string formId, [FromBody] CreateScheduledNotificationInput input) { @@ -319,7 +310,7 @@ public async Task> CreateForForm(string f return BadRequest("ApplicationStatusId required for Application event trigger"); } - var template = await _templateService.GetTemplateById(input.TemplateId); + var template = await templateService.GetTemplateById(input.TemplateId); if (template == null) return BadRequest("Template not found"); if (!string.Equals(template.TemplateType, TemplateTypes.Application, StringComparison.OrdinalIgnoreCase)) { @@ -331,7 +322,7 @@ public async Task> CreateForForm(string f string? statusLabel = null; if (input.ApplicationStatusId.HasValue) { - var statuses = await _statusService.GetListAsync(); + var statuses = await statusService.GetListAsync(); var match = statuses.FirstOrDefault(s => s.Id == input.ApplicationStatusId.Value); if (match != null) statusLabel = match.InternalStatus; } @@ -353,7 +344,7 @@ public async Task> CreateForForm(string f RecipientIdentifier = input.RecipientIdentifier }; - var created = await _automatedNotificationAppService.CreateAsync(createDto); + var created = await automatedNotificationAppService.CreateAsync(createDto); var dto = new ScheduledNotificationDto { @@ -375,29 +366,30 @@ public async Task> CreateForForm(string f return CreatedAtAction(nameof(GetForForm), new { formId }, dto); } + [Authorize(NotificationsPermissions.Email.ScheduleCancel)] [HttpDelete("{formId}/{id:guid}")] public async Task Delete(string formId, Guid id) { - await _automatedNotificationAppService.DeleteAsync(id); + await automatedNotificationAppService.DeleteAsync(id); return NoContent(); } + [Authorize(NotificationsPermissions.Email.ScheduleCancel)] [HttpPatch("{formId}/{id:guid}/cancel")] public async Task CancelNotification(string formId, Guid id) { if (!Guid.TryParse(formId, out _)) return BadRequest("Invalid form id"); - await _automatedNotificationAppService.CancelAsync(id); + await automatedNotificationAppService.CancelAsync(id); return NoContent(); } + [Authorize(NotificationsPermissions.Email.Send)] [HttpGet("can-delete-template/{templateId:guid}")] public async Task> CanDeleteTemplate(Guid templateId) { - var result = await _automatedNotificationAppService.GetListAsync( - new Notifications.GetNotificationsInput { MaxResultCount = 1000 }); + var references = await automatedNotificationAppService.GetTemplateReferencesAsync(templateId); - var associatedPlans = result.Items - .Where(n => n.EmailTemplateId == templateId) + var associatedPlans = references .Select(n => string.IsNullOrWhiteSpace(n.TriggerDetail) ? $"{n.TriggerType} notification" @@ -420,16 +412,16 @@ public async Task> CanDeleteTemplate(Guid templateId) return Ok(new { canDelete = true, errorMessage = (string?)null, notificationPlanNames = Array.Empty() }); } + [Authorize(NotificationsPermissions.Email.Send)] [HttpGet("template-notification-plans/{templateId:guid}")] public async Task> GetTemplateNotificationPlans(Guid templateId) { - var result = await _automatedNotificationAppService.GetListAsync( - new Notifications.GetNotificationsInput { MaxResultCount = 1000 }); - var template = await _templateService.GetTemplateById(templateId); + var references = await automatedNotificationAppService.GetTemplateReferencesAsync(templateId); + var template = await templateService.GetTemplateById(templateId); var templateName = template?.Name ?? "Template"; - var planNames = result.Items - .Where(n => n.IsActive && n.EmailTemplateId == templateId) + var planNames = references + .Where(n => n.IsActive) .Select(n => string.IsNullOrWhiteSpace(n.TriggerDetail) ? $"{templateName} - {n.TriggerType} notification" : $"{templateName} - {n.TriggerType} notification - {n.TriggerDetail}") @@ -439,6 +431,7 @@ public async Task> GetTemplateNotificationPlans(Guid templa return Ok(new { notificationPlanNames = planNames }); } + [Authorize(NotificationsPermissions.Email.ScheduleCreate)] [HttpPut("{formId}/{id:guid}")] public async Task> UpdateForForm(string formId, Guid id, [FromBody] CreateScheduledNotificationInput input) { @@ -447,11 +440,11 @@ public async Task> UpdateForForm(string f if (!ValidateModule(input, out var moduleError)) return BadRequest(moduleError); - var template = await _templateService.GetTemplateById(input.TemplateId); + var template = await templateService.GetTemplateById(input.TemplateId); if (template == null) return BadRequest("Template not found"); - var existingNotification = await _automatedNotificationAppService.GetAsync(id); - var existingTemplate = await _templateService.GetTemplateById(existingNotification.EmailTemplateId); + var existingNotification = await automatedNotificationAppService.GetAsync(id); + var existingTemplate = await templateService.GetTemplateById(existingNotification.EmailTemplateId); if (existingTemplate != null && !string.Equals(template.TemplateType, existingTemplate.TemplateType, StringComparison.OrdinalIgnoreCase)) { @@ -465,7 +458,7 @@ public async Task> UpdateForForm(string f string? statusLabel = null; if (input.ApplicationStatusId.HasValue) { - var statuses = await _statusService.GetListAsync(); + var statuses = await statusService.GetListAsync(); var match = statuses.FirstOrDefault(s => s.Id == input.ApplicationStatusId.Value); if (match != null) statusLabel = match.InternalStatus; } @@ -487,7 +480,7 @@ public async Task> UpdateForForm(string f RecipientIdentifier = input.RecipientIdentifier }; - var updated = await _automatedNotificationAppService.UpdateAsync(id, updateDto); + var updated = await automatedNotificationAppService.UpdateAsync(id, updateDto); var dto = new ScheduledNotificationDto { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebModule.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebModule.cs index f5bd9a6963..32ea5f33c5 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebModule.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebModule.cs @@ -506,7 +506,7 @@ private void ConfigureBundles() { Configure(options => { - options.Mode = BundlingMode.Bundle; + options.Mode = BundlingMode.BundleAndMinify; options.MinificationIgnoredFiles.Add("/js/notifications-realtime-client.js"); options.MinificationIgnoredFiles.Add("/Pages/UnityMessaging/Index.js"); options 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 71a46e6347..43837d54f8 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Unity.GrantManager.Applications; using Unity.Modules.Shared; +using Volo.Abp.Domain.Entities; using Volo.Abp.TenantManagement; using Volo.Abp.Users; @@ -78,7 +79,7 @@ public async Task OnGetAsync() var application = await _applicationRepository.WithBasicDetailsAsync(ApplicationId.Value); ApplicantId = application.ApplicantId; } - catch (Exception) + catch (InvalidOperationException) { return RedirectToPage("/Error", new { @@ -113,7 +114,7 @@ public async Task OnGetAsync() return Page(); } - catch (Exception) + catch (EntityNotFoundException) { return RedirectToPage("/Error", new { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/TokenModal.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/TokenModal.js index 7179087f47..62ca6c8a74 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/TokenModal.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/TokenModal.js @@ -23,7 +23,7 @@ $('#CopyApiTokenBtn').click(function (e) { e.preventDefault(); - copyToClipboard($('#ApplicationForm_ApiToken').val()); + void copyToClipboard($('#ApplicationForm_ApiToken').val()); }); $('#ClearApiTokenBtn').click(function (e) { @@ -41,15 +41,14 @@ }); } - function copyToClipboard(value) { - let textToCopy = value; - let tempTextarea = $('