diff --git a/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendControlsPolicyData.cs b/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendControlsPolicyData.cs index 4aa09aa1ce48..dee6f55f79bb 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendControlsPolicyData.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendControlsPolicyData.cs @@ -13,4 +13,6 @@ public class SendControlsPolicyData : IPolicyDataModel [Display(Name = "AllowedDomains")] [StringLength(1000)] public string? AllowedDomains { get; set; } + [Display(Name = "DeletionDays")] + public int? DeletionDays { get; set; } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEvent.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEvent.cs index b33445713ca9..5f78596ce371 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEvent.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEvent.cs @@ -109,6 +109,10 @@ private async Task UpdateSendsByPolicyAsync(Policy postUpsertedPolicyState, Send { await sendRepository.UpdateManyDisabledAsync(disabled, true); } + if (sendControlsPolicyData.DeletionDays != null) + { + await sendRepository.UpdateManyDeletionDatesByIdsAsync(sendIdsChunk, sendControlsPolicyData.DeletionDays.GetValueOrDefault(0)); + } } } } diff --git a/src/Core/Tools/Repositories/ISendRepository.cs b/src/Core/Tools/Repositories/ISendRepository.cs index 4f7ced15df5e..ddd052222765 100644 --- a/src/Core/Tools/Repositories/ISendRepository.cs +++ b/src/Core/Tools/Repositories/ISendRepository.cs @@ -98,4 +98,12 @@ UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId, /// The IDs of the ss to load /// Task> GetManyByIdsAsync(IEnumerable ids); + + /// + /// Update deletion dates in bulk by IDs + /// + /// The IDs of the s to update + /// The number of hours after the s' creation dates to set the deletion date + /// + Task UpdateManyDeletionDatesByIdsAsync(IEnumerable ids, int deletionHours); } diff --git a/src/Infrastructure.Dapper/Tools/Repositories/SendRepository.cs b/src/Infrastructure.Dapper/Tools/Repositories/SendRepository.cs index ea112bfefedb..ef550b26ce83 100644 --- a/src/Infrastructure.Dapper/Tools/Repositories/SendRepository.cs +++ b/src/Infrastructure.Dapper/Tools/Repositories/SendRepository.cs @@ -224,6 +224,15 @@ public async Task> GetManyByIdsAsync(IEnumerable ids) return sends; } + public async Task UpdateManyDeletionDatesByIdsAsync(IEnumerable ids, int deletionHours) + { + using var connection = new SqlConnection(ConnectionString); + await connection.ExecuteAsync( + $"[{Schema}].[Send_UpdateDeletionDatesByIds]", + new { Ids = ids.ToGuidIdArrayTVP(), DeletionHours = deletionHours }, + commandType: CommandType.StoredProcedure); + } + private async Task ProtectDataAndSaveAsync(Send send, Func saveTask) { if (send == null) diff --git a/src/Infrastructure.EntityFramework/Tools/Repositories/SendRepository.cs b/src/Infrastructure.EntityFramework/Tools/Repositories/SendRepository.cs index 133879c45ec5..d03a18e1d0de 100644 --- a/src/Infrastructure.EntityFramework/Tools/Repositories/SendRepository.cs +++ b/src/Infrastructure.EntityFramework/Tools/Repositories/SendRepository.cs @@ -156,6 +156,20 @@ public async Task> GetIdsByOrganizationIdAsync(Guid organizati return Mapper.Map>(orgUserSendIds); } + public async Task UpdateManyDeletionDatesByIdsAsync(IEnumerable ids, int deletionHours) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + var sends = dbContext.Sends.Where(s => ids.Contains(s.Id)); + await sends.ExecuteUpdateAsync(setters => setters + .SetProperty(s => s.DeletionDate, s => s.CreationDate.AddHours(deletionHours)) + .SetProperty(s => s.RevisionDate, DateTime.UtcNow) + ); + var userIds = await sends.Select(s => s.User.Id).ToArrayAsync() ?? []; + await dbContext.UserBumpManyAccountRevisionDatesAsync(userIds); + await dbContext.SaveChangesAsync(); + } + public async Task> GetManyByIdsAsync(IEnumerable ids) { using var scope = ServiceScopeFactory.CreateScope(); diff --git a/src/Sql/dbo/Tools/Stored Procedures/Send_UpdateDeletionDatesByIds.sql b/src/Sql/dbo/Tools/Stored Procedures/Send_UpdateDeletionDatesByIds.sql new file mode 100644 index 000000000000..94be904b61f1 --- /dev/null +++ b/src/Sql/dbo/Tools/Stored Procedures/Send_UpdateDeletionDatesByIds.sql @@ -0,0 +1,27 @@ +CREATE PROCEDURE [dbo].[Send_UpdateDeletionDatesByIds] + @Ids AS [dbo].[GuidIdArray] READONLY, + @DeletionHours INT +AS +BEGIN + SET NOCOUNT ON + + -- Set field + UPDATE + [dbo].[Send] + SET + [DeletionDate] = DATEADD(HOUR, @DeletionHours, [CreationDate]), + [RevisionDate] = GETUTCDATE() + WHERE + [Id] IN (SELECT * FROM @Ids) + + -- Bump account revision dates + EXEC [dbo].[User_BumpManyAccountRevisionDates] + ( + SELECT DISTINCT + UserId + FROM + [dbo].[Send] + WHERE + [Id] IN (SELECT * FROM @Ids) + ) +END \ No newline at end of file diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEventTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEventTests.cs index f77b0eb6bf2a..ae3124df7eb2 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEventTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEventTests.cs @@ -449,4 +449,50 @@ await sutProvider.GetDependency() .Received(1) .UpdateManyDisabledAsync(Arg.Is>(l => l.Count == 3 && l.Contains(nonCompliantSend1.Id) && l.Contains(nonCompliantSend2.Id) && l.Contains(nonCompliantSend3.Id)), true); } + + [Theory, BitAutoData] + public async Task ExecutePostUpsertSideEffectAsync_DeletionDateUpdatesSendDeletionDates( + [PolicyUpdate(PolicyType.SendControls, enabled: true)] PolicyUpdate policyUpdate, + [Policy(PolicyType.SendControls, enabled: true)] Policy postUpsertedPolicy, + [Policy(PolicyType.DisableSend, enabled: false)] Policy existingDisableSendPolicy, + [Policy(PolicyType.SendOptions, enabled: false)] Policy existingSendOptionsPolicy, + SutProvider sutProvider) + { + postUpsertedPolicy.OrganizationId = policyUpdate.OrganizationId; + existingDisableSendPolicy.OrganizationId = policyUpdate.OrganizationId; + existingSendOptionsPolicy.OrganizationId = policyUpdate.OrganizationId; + postUpsertedPolicy.SetDataModel(new SendControlsPolicyData { DeletionDays = 48 }); + + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.DisableSend) + .Returns(existingDisableSendPolicy); + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, PolicyType.SendOptions) + .Returns(existingSendOptionsPolicy); + + var send1 = new Send + { + Id = Guid.NewGuid(), + CreationDate = DateTime.UtcNow + }; + var send2 = new Send + { + Id = Guid.NewGuid(), + CreationDate = DateTime.UtcNow + }; + var sendIds = new List([ send1.Id, send2.Id ]); + sutProvider.GetDependency() + .GetIdsByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns(sendIds); + sutProvider.GetDependency() + .GetManyByIdsAsync(Arg.Any>()) + .Returns([ send1, send2 ]); + + await sutProvider.Sut.ExecutePostUpsertSideEffectAsync( + new SavePolicyModel(policyUpdate), postUpsertedPolicy, null); + + await sutProvider.GetDependency() + .Received(1) + .UpdateManyDeletionDatesByIdsAsync(Arg.Is(l => l.Count() == 2), 48); + } } diff --git a/util/Migrator/DbScripts/2026-04-20_01_SendUpdateDeletionDaysByIds.sql b/util/Migrator/DbScripts/2026-04-20_01_SendUpdateDeletionDaysByIds.sql new file mode 100644 index 000000000000..8087cd09b3fe --- /dev/null +++ b/util/Migrator/DbScripts/2026-04-20_01_SendUpdateDeletionDaysByIds.sql @@ -0,0 +1,27 @@ +CREATE OR ALTER PROCEDURE [dbo].[Send_UpdateDeletionDatesByIds] + @Ids AS [dbo].[GuidIdArray] READONLY, + @DeletionHours INT +AS +BEGIN + SET NOCOUNT ON + + -- Set field + UPDATE + [dbo].[Send] + SET + [DeletionDate] = DATEADD(HOUR, @DeletionHours, [CreationDate]), + [RevisionDate] = GETUTCDATE() + WHERE + [Id] IN (SELECT * FROM @Ids) + + -- Bump account revision dates + EXEC [dbo].[User_BumpManyAccountRevisionDates] + ( + SELECT DISTINCT + UserId + FROM + [dbo].[Send] + WHERE + [Id] IN (SELECT * FROM @Ids) + ) +END \ No newline at end of file