From 3b1bc3f2cb02468821d0e817d5acb711ab31c8e1 Mon Sep 17 00:00:00 2001 From: Darrick Joo Date: Wed, 9 Sep 2026 12:49:20 +0200 Subject: [PATCH 1/3] Fix retention policy run-limit propagation Return EndCurrentRun through the temporary retention policy setup record so background processing stops after the deletion-limit event. Preserve per-policy error isolation and cover limit boundaries, continued processing below the limit, policy errors, and subsequent runs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../ApplyRetentionPolicyImpl.Codeunit.al | 2 + .../RetentionPolicySetup.Table.al | 6 ++ .../src/RetentionPolicyTest.Codeunit.al | 91 +++++++++++++++++++ 3 files changed, 99 insertions(+) diff --git a/src/System Application/App/Retention Policy/src/Apply Retention Policy/ApplyRetentionPolicyImpl.Codeunit.al b/src/System Application/App/Retention Policy/src/Apply Retention Policy/ApplyRetentionPolicyImpl.Codeunit.al index b191b1baf6b..d275e3c0c8f 100644 --- a/src/System Application/App/Retention Policy/src/Apply Retention Policy/ApplyRetentionPolicyImpl.Codeunit.al +++ b/src/System Application/App/Retention Policy/src/Apply Retention Policy/ApplyRetentionPolicyImpl.Codeunit.al @@ -79,6 +79,7 @@ codeunit 3904 "Apply Retention Policy Impl." TotalNumberOfRecordsDeleted := Rec."Number Of Records Deleted"; ApplyRetentionPolicy(RetentionPolicySetup, false, false); Rec."Number Of Records Deleted" := TotalNumberOfRecordsDeleted; + Rec."End Current Run" := EndCurrentRun; end; end; @@ -192,6 +193,7 @@ codeunit 3904 "Apply Retention Policy Impl." exit end; TotalNumberOfRecordsDeleted := TempRetentionPolicySetup."Number Of Records Deleted"; + EndCurrentRun := TempRetentionPolicySetup."End Current Run"; end; local procedure CanApplyRetentionPolicy(var RetentionPolicySetup: Record "Retention Policy Setup"; Manual: Boolean): Boolean diff --git a/src/System Application/App/Retention Policy/src/Retention Policy Setup/RetentionPolicySetup.Table.al b/src/System Application/App/Retention Policy/src/Retention Policy Setup/RetentionPolicySetup.Table.al index 7015a2f4d7d..3d3e3f3fa62 100644 --- a/src/System Application/App/Retention Policy/src/Retention Policy Setup/RetentionPolicySetup.Table.al +++ b/src/System Application/App/Retention Policy/src/Retention Policy Setup/RetentionPolicySetup.Table.al @@ -154,6 +154,12 @@ table 3901 "Retention Policy Setup" Access = Internal; Editable = false; } + field(101; "End Current Run"; Boolean) + { + DataClassification = SystemMetadata; + Access = Internal; + Editable = false; + } } keys diff --git a/src/System Application/Test/Retention Policy/src/RetentionPolicyTest.Codeunit.al b/src/System Application/Test/Retention Policy/src/RetentionPolicyTest.Codeunit.al index 906e4d96aa7..805b8a47963 100644 --- a/src/System Application/Test/Retention Policy/src/RetentionPolicyTest.Codeunit.al +++ b/src/System Application/Test/Retention Policy/src/RetentionPolicyTest.Codeunit.al @@ -1455,6 +1455,53 @@ codeunit 138702 "Retention Policy Test" Assert.RecordIsEmpty(RetentionPolicyTestData3); end; + [Test] + procedure TestApplyRetentionPolicyStopsAtLimitAcrossTables() + begin + VerifyApplyRetentionPolicyRecordLimit(RetentionPolicyTestLibrary.MaxNumberOfRecordsToDelete(), 0, true); + end; + + [Test] + procedure TestApplyRetentionPolicyStopsWithRecordsRemainingAcrossTables() + var + RemainingRecords: Integer; + begin + RemainingRecords := RetentionPolicyTestLibrary.MaxNumberOfRecordsToDeleteBuffer() + 1; + VerifyApplyRetentionPolicyRecordLimit(RetentionPolicyTestLibrary.MaxNumberOfRecordsToDelete() + RemainingRecords, RemainingRecords, true); + end; + + [Test] + procedure TestApplyRetentionPolicyContinuesBelowLimitAcrossTables() + begin + VerifyApplyRetentionPolicyRecordLimit(RetentionPolicyTestLibrary.MaxNumberOfRecordsToDelete() - 2, 0, false); + end; + + [Test] + procedure TestApplyRetentionPolicyContinuesAfterPolicyError() + var + RetentionPeriod: Record "Retention Period"; + RetentionPolicySetup: Record "Retention Policy Setup"; + RetentionPolicyTestData: Record "Retention Policy Test Data"; + RetentionPolicyTestData3: Record "Retention Policy Test Data 3"; + ApplyRetentionPolicy: Codeunit "Apply Retention Policy"; + begin + PermissionsMock.Set('Retention Pol. Admin'); + // Setup + ClearTestData(); + InsertOneMonthRetentionPeriod(RetentionPeriod); + InsertEnabledRetentionPolicySetupForAllRecords(RetentionPolicySetup, RetentionPeriod, 0); + InsertRetentionPolicySetupTable3(RetentionPolicySetup, RetentionPeriod, RetentionPolicyTestData3.FieldNo("Datetime Field")); + InsertRetentionPolicyTestData('<-2M>'); + InsertRetentionPolicyTestData3('<-2M>'); + + // Exercise + ApplyRetentionPolicy.ApplyRetentionPolicy(false); + + // Verify + Assert.AreEqual(1, RetentionPolicyTestData.Count(), 'Records for the invalid policy must not be deleted.'); + Assert.RecordIsEmpty(RetentionPolicyTestData3); + end; + [Test] procedure TestApplyRetentionPolicyTooManyLinesToDeleteOneTableAndReschedule() var @@ -1624,6 +1671,50 @@ codeunit 138702 "Retention Policy Test" RetentionPeriod.DeleteAll(true); end; + local procedure VerifyApplyRetentionPolicyRecordLimit(RecordsTableOne: Integer; RemainingRecordsTableOne: Integer; LimitReached: Boolean) + var + RetentionPeriod: Record "Retention Period"; + RetentionPolicySetup: Record "Retention Policy Setup"; + RetentionPolicyTestData: Record "Retention Policy Test Data"; + RetentionPolicyTestData3: Record "Retention Policy Test Data 3"; + ApplyRetentionPolicy: Codeunit "Apply Retention Policy"; + RetentionPolicyTestLibrarySubs: Codeunit "Retention Policy Test Library"; + i: Integer; + begin + PermissionsMock.Set('Retention Pol. Admin'); + // Setup + ClearTestData(); + InsertOneMonthRetentionPeriod(RetentionPeriod); + InsertEnabledRetentionPolicySetupForAllRecords(RetentionPolicySetup, RetentionPeriod, RetentionPolicyTestData.FieldNo("Date Field")); + InsertRetentionPolicySetupTable3(RetentionPolicySetup, RetentionPeriod, RetentionPolicyTestData3.FieldNo("Datetime Field")); + for i := 1 to RecordsTableOne do + InsertRetentionPolicyTestData('<-2M>'); + InsertRetentionPolicyTestData3('<-2M>'); + + Assert.AreEqual(RecordsTableOne, RetentionPolicyTestData.Count(), 'Incorrect number of records before applying retention policy.'); + Assert.AreEqual(1, RetentionPolicyTestData3.Count(), 'Incorrect number of records before applying retention policy.'); + + // Exercise + BindSubscription(RetentionPolicyTestLibrarySubs); + ApplyRetentionPolicy.ApplyRetentionPolicy(false); + UnbindSubscription(RetentionPolicyTestLibrarySubs); + + // Verify + Assert.AreEqual(RemainingRecordsTableOne, RetentionPolicyTestData.Count(), 'Incorrect number of records after applying retention policy.'); + if LimitReached then begin + Assert.AreEqual(1, RetentionPolicyTestData3.Count(), 'Later policies must not be processed after the record limit is reached.'); + Assert.AreEqual(1, RetentionPolicyTestLibrarySubs.GetRecordLimitExceededSubscriberCount(), 'The record limit event must be raised only once per run.'); + end else begin + Assert.RecordIsEmpty(RetentionPolicyTestData3); + Assert.AreEqual(0, RetentionPolicyTestLibrarySubs.GetRecordLimitExceededSubscriberCount(), 'The record limit event must not be raised below the limit.'); + end; + + // A new background run must process the remaining records. + ApplyRetentionPolicy.Run(); + Assert.RecordIsEmpty(RetentionPolicyTestData); + Assert.RecordIsEmpty(RetentionPolicyTestData3); + end; + local procedure InsertOneWeekRetentionPeriod(var RetentionPeriod: Record "Retention Period") begin InsertRetentionPeriod(RetentionPeriod, RetentionPeriod."Retention Period"::"1 Week"); From e1af5d8fdaa90973e2f1e20a6a81427ce30e6c01 Mon Sep 17 00:00:00 2001 From: Darrick Joo Date: Thu, 10 Sep 2026 09:37:19 +0200 Subject: [PATCH 2/3] Initialize retention policy regression test fixtures Register the retention-policy test tables with the existing allowed-table refresh helper before creating policies in the new regression tests. Fresh unit-test companies do not have the install-time fixture data. Keep all assertions, production behavior, and CI checks unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Test/Retention Policy/src/RetentionPolicyTest.Codeunit.al | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/System Application/Test/Retention Policy/src/RetentionPolicyTest.Codeunit.al b/src/System Application/Test/Retention Policy/src/RetentionPolicyTest.Codeunit.al index 805b8a47963..56d960f0409 100644 --- a/src/System Application/Test/Retention Policy/src/RetentionPolicyTest.Codeunit.al +++ b/src/System Application/Test/Retention Policy/src/RetentionPolicyTest.Codeunit.al @@ -1487,6 +1487,7 @@ codeunit 138702 "Retention Policy Test" begin PermissionsMock.Set('Retention Pol. Admin'); // Setup + RetentionPolicyTestLibrary.RaiseOnRefreshAllowedTables(); ClearTestData(); InsertOneMonthRetentionPeriod(RetentionPeriod); InsertEnabledRetentionPolicySetupForAllRecords(RetentionPolicySetup, RetentionPeriod, 0); @@ -1683,6 +1684,7 @@ codeunit 138702 "Retention Policy Test" begin PermissionsMock.Set('Retention Pol. Admin'); // Setup + RetentionPolicyTestLibrary.RaiseOnRefreshAllowedTables(); ClearTestData(); InsertOneMonthRetentionPeriod(RetentionPeriod); InsertEnabledRetentionPolicySetupForAllRecords(RetentionPolicySetup, RetentionPeriod, RetentionPolicyTestData.FieldNo("Date Field")); From 34fee5182215e20597ee90b4073babf36842ec3c Mon Sep 17 00:00:00 2001 From: Darrick Joo Date: Fri, 11 Sep 2026 14:45:40 +0200 Subject: [PATCH 3/3] Fix permissions for retention regression fixtures Create enabled-policy fixtures with setup-only permissions so Base Application subscribers can schedule the retention job queue entry. Restore Retention Pol. Admin before inserting expired records or applying policies; keep all retention assertions and production behavior unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Retention Policy/src/RetentionPolicyTest.Codeunit.al | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/System Application/Test/Retention Policy/src/RetentionPolicyTest.Codeunit.al b/src/System Application/Test/Retention Policy/src/RetentionPolicyTest.Codeunit.al index 56d960f0409..60dee4dafd5 100644 --- a/src/System Application/Test/Retention Policy/src/RetentionPolicyTest.Codeunit.al +++ b/src/System Application/Test/Retention Policy/src/RetentionPolicyTest.Codeunit.al @@ -1485,13 +1485,15 @@ codeunit 138702 "Retention Policy Test" RetentionPolicyTestData3: Record "Retention Policy Test Data 3"; ApplyRetentionPolicy: Codeunit "Apply Retention Policy"; begin - PermissionsMock.Set('Retention Pol. Admin'); // Setup + // Enabled policy setup can schedule Base Application job queue entries. + PermissionsMock.Set('SUPER'); RetentionPolicyTestLibrary.RaiseOnRefreshAllowedTables(); ClearTestData(); InsertOneMonthRetentionPeriod(RetentionPeriod); InsertEnabledRetentionPolicySetupForAllRecords(RetentionPolicySetup, RetentionPeriod, 0); InsertRetentionPolicySetupTable3(RetentionPolicySetup, RetentionPeriod, RetentionPolicyTestData3.FieldNo("Datetime Field")); + PermissionsMock.Set('Retention Pol. Admin'); InsertRetentionPolicyTestData('<-2M>'); InsertRetentionPolicyTestData3('<-2M>'); @@ -1682,13 +1684,15 @@ codeunit 138702 "Retention Policy Test" RetentionPolicyTestLibrarySubs: Codeunit "Retention Policy Test Library"; i: Integer; begin - PermissionsMock.Set('Retention Pol. Admin'); // Setup + // Enabled policy setup can schedule Base Application job queue entries. + PermissionsMock.Set('SUPER'); RetentionPolicyTestLibrary.RaiseOnRefreshAllowedTables(); ClearTestData(); InsertOneMonthRetentionPeriod(RetentionPeriod); InsertEnabledRetentionPolicySetupForAllRecords(RetentionPolicySetup, RetentionPeriod, RetentionPolicyTestData.FieldNo("Date Field")); InsertRetentionPolicySetupTable3(RetentionPolicySetup, RetentionPeriod, RetentionPolicyTestData3.FieldNo("Datetime Field")); + PermissionsMock.Set('Retention Pol. Admin'); for i := 1 to RecordsTableOne do InsertRetentionPolicyTestData('<-2M>'); InsertRetentionPolicyTestData3('<-2M>');