From 816505bf8972d073de04f9f66df6458686746829 Mon Sep 17 00:00:00 2001 From: sanja <52755494+sanjacornelius@users.noreply.github.com> Date: Thu, 5 Feb 2026 09:08:51 -0800 Subject: [PATCH 01/18] Configure new kernal command for evaluating retention --- ProcessMaker/Console/Kernel.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ProcessMaker/Console/Kernel.php b/ProcessMaker/Console/Kernel.php index 1d6ee38a81..3e090f3765 100644 --- a/ProcessMaker/Console/Kernel.php +++ b/ProcessMaker/Console/Kernel.php @@ -89,6 +89,13 @@ protected function schedule(Schedule $schedule) break; } + // evaluate cases retention policy + $schedule->command('cases:retention:evaluate') + ->daily() + ->onOneServer() + ->withoutOverlapping() + ->runInBackground(); + // 5 minutes is recommended in https://laravel.com/docs/12.x/horizon#metrics $schedule->command('horizon:snapshot')->everyFiveMinutes(); } From 4a87002975b2e36c6949f8d0ae43190caa5de1cf Mon Sep 17 00:00:00 2001 From: sanja <52755494+sanjacornelius@users.noreply.github.com> Date: Thu, 5 Feb 2026 09:09:56 -0800 Subject: [PATCH 02/18] Implement new job to run and delete cases --- .../Jobs/EvaluateProcessRetentionJob.php | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 ProcessMaker/Jobs/EvaluateProcessRetentionJob.php diff --git a/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php b/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php new file mode 100644 index 0000000000..0b780ff466 --- /dev/null +++ b/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php @@ -0,0 +1,55 @@ +processId); + if (!$process) { + Log::error('CaseRetentionJob: Process not found', ['process_id' => $this->processId]); + + return; + } + + $retentionMonths = match ($process->properties['retention_period']) { + '6_months' => 6, + '1_year' => 12, + '3_years' => 36, + '5_years' => 60, + }; + + $cutoffDate = $process->retention_updated_at->addMonths($retentionMonths); + + CaseNumber::where('process_id', $this->processId) + ->where('created_at', '<', $cutoffDate) + ->chunkById(100, function ($cases) { + $caseIds = $cases->pluck('id'); + // Delete the cases + CaseNumber::whereIn('id', $caseIds)->delete(); + + // TODO: Add logs to track the number of cases deleted + // Get deleted timestamp + // $deletedAt = Carbon::now(); + // RetentionPolicyLog::record($process->id, $caseIds, $deletedAt); + }); + } +} From 5875f22257375a35ce133dd2bbd0fd9bf599e321 Mon Sep 17 00:00:00 2001 From: sanja <52755494+sanjacornelius@users.noreply.github.com> Date: Thu, 5 Feb 2026 09:10:49 -0800 Subject: [PATCH 03/18] Implement EvaludateCasesRetention command --- .../Commands/EvaluateCaseRetention.php | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 ProcessMaker/Console/Commands/EvaluateCaseRetention.php diff --git a/ProcessMaker/Console/Commands/EvaluateCaseRetention.php b/ProcessMaker/Console/Commands/EvaluateCaseRetention.php new file mode 100644 index 0000000000..8e16572588 --- /dev/null +++ b/ProcessMaker/Console/Commands/EvaluateCaseRetention.php @@ -0,0 +1,40 @@ +info('Evaluating and deleting cases past their retention period'); + + Process::whereNotNull('properties->retention_period')->chunkById(100, function ($processes) { + foreach ($processes as $process) { + dispatch(new EvaluateProcessRetentionJob($process->id)); + } + }); + + $this->info('Cases retention evaluation complete'); + } +} From 98ab8d8f331da462f12f23128d1595fb9f73cae7 Mon Sep 17 00:00:00 2001 From: sanja <52755494+sanjacornelius@users.noreply.github.com> Date: Thu, 5 Feb 2026 16:57:21 -0800 Subject: [PATCH 04/18] Implement unit tests --- .../Jobs/EvaluateProcessRetentionJobTest.php | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 tests/Jobs/EvaluateProcessRetentionJobTest.php diff --git a/tests/Jobs/EvaluateProcessRetentionJobTest.php b/tests/Jobs/EvaluateProcessRetentionJobTest.php new file mode 100644 index 0000000000..87851117b7 --- /dev/null +++ b/tests/Jobs/EvaluateProcessRetentionJobTest.php @@ -0,0 +1,136 @@ +subMonths(6)->toIso8601String(); + $process = Process::factory()->create([ + 'properties' => [ + 'retention_period' => self::RETENTION_PERIOD, + 'retention_updated_at' => $retentionUpdatedAt, + ], + ]); + + $process->save(); + $process->refresh(); + $this->assertEquals(self::RETENTION_PERIOD, $process->properties['retention_period']); + $this->assertEquals($retentionUpdatedAt, $process->properties['retention_updated_at']); + + // Create a process request + $processRequest = ProcessRequest::factory()->create(); + $processRequest->process_id = $process->id; + $processRequest->save(); + $processRequest->refresh(); + $this->assertEquals($process->id, $processRequest->process_id); + + // Create a case number with a creation date that is past the retention period + $oldCaseCreatedAt = Carbon::now()->subMonths(7)->toIso8601String(); + $caseOld = CaseNumber::factory()->create([ + 'created_at' => $oldCaseCreatedAt, + 'process_request_id' => $processRequest->id, + ]); + $this->assertEquals($processRequest->id, $caseOld->process_request_id); + $this->assertEquals($oldCaseCreatedAt, $caseOld->created_at->toIso8601String()); + + // Dispatch the job to evaluate the retention period + EvaluateProcessRetentionJob::dispatchSync($process->id); + + // Check that the case old has been deleted + $this->assertNull(CaseNumber::find($caseOld->id)); + } + + public function testItDoesNotDeleteCasesThatAreWithinRetentionPeriod() + { + // Create a process with a 6 month retention period + $retentionUpdatedAt = Carbon::now()->subMonths(6)->toIso8601String(); + $process = Process::factory()->create([ + 'properties' => [ + 'retention_period' => self::RETENTION_PERIOD, + 'retention_updated_at' => $retentionUpdatedAt, + ], + ]); + $process->save(); + $process->refresh(); + $this->assertEquals(self::RETENTION_PERIOD, $process->properties['retention_period']); + $this->assertEquals($retentionUpdatedAt, $process->properties['retention_updated_at']); + + // Create a process request + $processRequest = ProcessRequest::factory()->create(); + $processRequest->process_id = $process->id; + $processRequest->save(); + $processRequest->refresh(); + $this->assertEquals($process->id, $processRequest->process_id); + + // Create a case number with a creation date that is within the retention period + $caseCreatedAt = Carbon::now()->subMonths(5)->toIso8601String(); + $case = CaseNumber::factory()->create([ + 'created_at' => $caseCreatedAt, + 'process_request_id' => $processRequest->id, + ]); + $this->assertEquals($processRequest->id, $case->process_request_id); + $this->assertEquals($caseCreatedAt, $case->created_at->toIso8601String()); + + // Dispatch the job to evaluate the retention period + EvaluateProcessRetentionJob::dispatchSync($process->id); + + // Check that the case has not been deleted + $this->assertNotNull(CaseNumber::find($case->id)); + } + + public function testItHandlesMultipleCasesInBatches() + { + // Create a process with a 6 month retention period + $process = Process::factory()->create([ + 'properties' => [ + 'retention_period' => self::RETENTION_PERIOD, + 'retention_updated_at' => Carbon::now()->subMonths(6)->toIso8601String(), + ], + ]); + $process->save(); + $process->refresh(); + $this->assertEquals(self::RETENTION_PERIOD, $process->properties['retention_period']); + $this->assertEquals(Carbon::now()->subMonths(6)->toIso8601String(), $process->properties['retention_updated_at']); + + // Create a process request + $processRequest = ProcessRequest::factory()->create(); + $processRequest->process_id = $process->id; + $processRequest->save(); + $processRequest->refresh(); + $this->assertEquals($process->id, $processRequest->process_id); + + // Create 1200 cases (to test chunking/batch deletion) + // These cases should be deleted because they're older than the retention period + // retention_updated_at is 6 months ago, so cases created 7+ months ago should be deleted + $cases = CaseNumber::factory()->count(1200)->create([ + 'process_request_id' => $processRequest->id, + 'created_at' => Carbon::now()->subMonths(7)->toIso8601String(), + ]); + $this->assertEquals($processRequest->id, $cases->first()->process_request_id); + $this->assertEquals(Carbon::now()->subMonths(7)->toIso8601String(), $cases->first()->created_at->toIso8601String()); + + // Dispatch the job to evaluate the retention period + EvaluateProcessRetentionJob::dispatchSync($process->id); + + // Assert all old cases are deleted + // There should be 1 case left (due to the creation of the process request) because the new case is within the retention period + $this->assertDatabaseCount('case_numbers', 1); + + // TODO: Assert log entry is created + } +} From 9db1e9a18b97361f4512b6a979799929dc2bc4c2 Mon Sep 17 00:00:00 2001 From: sanja <52755494+sanjacornelius@users.noreply.github.com> Date: Thu, 5 Feb 2026 16:58:40 -0800 Subject: [PATCH 05/18] Create caseNumber factory --- ProcessMaker/Models/CaseNumber.php | 1 + .../ProcessMaker/Models/CaseNumberFactory.php | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 database/factories/ProcessMaker/Models/CaseNumberFactory.php diff --git a/ProcessMaker/Models/CaseNumber.php b/ProcessMaker/Models/CaseNumber.php index 7b06ee69e5..721f61b019 100644 --- a/ProcessMaker/Models/CaseNumber.php +++ b/ProcessMaker/Models/CaseNumber.php @@ -2,6 +2,7 @@ namespace ProcessMaker\Models; +use Database\Factories\ProcessMaker\Models\CaseNumberFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; diff --git a/database/factories/ProcessMaker/Models/CaseNumberFactory.php b/database/factories/ProcessMaker/Models/CaseNumberFactory.php new file mode 100644 index 0000000000..1c0adf4198 --- /dev/null +++ b/database/factories/ProcessMaker/Models/CaseNumberFactory.php @@ -0,0 +1,21 @@ + function () { + return ProcessRequest::factory()->create()->getKey(); + }, + ]; + } +} From b1e3c3905fcdc78b4e438c2682b100e02b0c0359 Mon Sep 17 00:00:00 2001 From: sanja <52755494+sanjacornelius@users.noreply.github.com> Date: Thu, 5 Feb 2026 17:25:10 -0800 Subject: [PATCH 06/18] Handle retention policy update deletions --- .../Jobs/EvaluateProcessRetentionJob.php | 42 ++++++- .../Jobs/EvaluateProcessRetentionJobTest.php | 117 ++++++++++++++++-- 2 files changed, 148 insertions(+), 11 deletions(-) diff --git a/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php b/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php index 0b780ff466..a9005bcd1e 100644 --- a/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php +++ b/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php @@ -2,10 +2,13 @@ namespace ProcessMaker\Jobs; +use Carbon\Carbon; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Queue\Queueable; +use Illuminate\Support\Facades\Log; use ProcessMaker\Models\CaseNumber; use ProcessMaker\Models\Process; +use ProcessMaker\Models\ProcessRequest; class EvaluateProcessRetentionJob implements ShouldQueue { @@ -37,10 +40,43 @@ public function handle(): void '5_years' => 60, }; - $cutoffDate = $process->retention_updated_at->addMonths($retentionMonths); + $retentionUpdatedAt = Carbon::parse($process->properties['retention_updated_at']); - CaseNumber::where('process_id', $this->processId) - ->where('created_at', '<', $cutoffDate) + // Get all process request IDs for this process + $processRequestIds = ProcessRequest::where('process_id', $this->processId)->pluck('id'); + + // If there are no process requests, nothing to delete + if ($processRequestIds->isEmpty()) { + return; + } + + // Handle two scenarios: + // 1. Cases created BEFORE retention_updated_at: Delete if older than retention period from retention_updated_at + // (These cases were subject to the old retention policy, but we apply current retention from update date) + // 2. Cases created AFTER retention_updated_at: Delete if older than retention period from their creation date + // (These cases are subject to the new retention policy) + + $now = Carbon::now(); + + // For cases created before retention_updated_at: cutoff is retention_updated_at - retention_period + $oldCasesCutoff = $retentionUpdatedAt->copy()->subMonths($retentionMonths); + + // For cases created after retention_updated_at: cutoff is now - retention_period + $newCasesCutoff = $now->copy()->subMonths($retentionMonths); + + CaseNumber::whereIn('process_request_id', $processRequestIds) + ->where(function ($query) use ($retentionUpdatedAt, $oldCasesCutoff, $newCasesCutoff) { + // Cases created before retention_updated_at: delete if created before (retention_updated_at - retention_period) + $query->where(function ($q) use ($retentionUpdatedAt, $oldCasesCutoff) { + $q->where('created_at', '<', $retentionUpdatedAt) + ->where('created_at', '<', $oldCasesCutoff); + }) + // Cases created after retention_updated_at: delete if created before (now - retention_period) + ->orWhere(function ($q) use ($retentionUpdatedAt, $newCasesCutoff) { + $q->where('created_at', '>=', $retentionUpdatedAt) + ->where('created_at', '<', $newCasesCutoff); + }); + }) ->chunkById(100, function ($cases) { $caseIds = $cases->pluck('id'); // Delete the cases diff --git a/tests/Jobs/EvaluateProcessRetentionJobTest.php b/tests/Jobs/EvaluateProcessRetentionJobTest.php index 87851117b7..c2ae255e01 100644 --- a/tests/Jobs/EvaluateProcessRetentionJobTest.php +++ b/tests/Jobs/EvaluateProcessRetentionJobTest.php @@ -19,6 +19,7 @@ class EvaluateProcessRetentionJobTest extends TestCase public function testItDeletesCasesThatExceedRetentionPeriod() { // Create a process with a 6 month retention period + // retention_updated_at is 6 months ago, so old cases cutoff is 12 months ago (6 months ago - 6 months) $retentionUpdatedAt = Carbon::now()->subMonths(6)->toIso8601String(); $process = Process::factory()->create([ 'properties' => [ @@ -39,8 +40,10 @@ public function testItDeletesCasesThatExceedRetentionPeriod() $processRequest->refresh(); $this->assertEquals($process->id, $processRequest->process_id); - // Create a case number with a creation date that is past the retention period - $oldCaseCreatedAt = Carbon::now()->subMonths(7)->toIso8601String(); + // Create a case number created 13 months ago (before retention_updated_at) + // Old cases cutoff = 6 months ago - 6 months = 12 months ago + // 13 months ago < 12 months ago, so it should be deleted + $oldCaseCreatedAt = Carbon::now()->subMonths(13)->toIso8601String(); $caseOld = CaseNumber::factory()->create([ 'created_at' => $oldCaseCreatedAt, 'process_request_id' => $processRequest->id, @@ -58,6 +61,7 @@ public function testItDeletesCasesThatExceedRetentionPeriod() public function testItDoesNotDeleteCasesThatAreWithinRetentionPeriod() { // Create a process with a 6 month retention period + // retention_updated_at is 6 months ago, so old cases cutoff is 12 months ago (6 months ago - 6 months) $retentionUpdatedAt = Carbon::now()->subMonths(6)->toIso8601String(); $process = Process::factory()->create([ 'properties' => [ @@ -77,7 +81,8 @@ public function testItDoesNotDeleteCasesThatAreWithinRetentionPeriod() $processRequest->refresh(); $this->assertEquals($process->id, $processRequest->process_id); - // Create a case number with a creation date that is within the retention period + // Create a case number created 5 months ago (before retention_updated_at) + // This case is NOT older than the old cases cutoff (12 months ago), so it should NOT be deleted $caseCreatedAt = Carbon::now()->subMonths(5)->toIso8601String(); $case = CaseNumber::factory()->create([ 'created_at' => $caseCreatedAt, @@ -115,22 +120,118 @@ public function testItHandlesMultipleCasesInBatches() $this->assertEquals($process->id, $processRequest->process_id); // Create 1200 cases (to test chunking/batch deletion) - // These cases should be deleted because they're older than the retention period - // retention_updated_at is 6 months ago, so cases created 7+ months ago should be deleted + // These cases are created 13 months ago (before retention_updated_at) + // Old cases cutoff = 6 months ago - 6 months = 12 months ago + // 13 months ago < 12 months ago, so these should be deleted $cases = CaseNumber::factory()->count(1200)->create([ 'process_request_id' => $processRequest->id, - 'created_at' => Carbon::now()->subMonths(7)->toIso8601String(), + 'created_at' => Carbon::now()->subMonths(13)->toIso8601String(), ]); $this->assertEquals($processRequest->id, $cases->first()->process_request_id); - $this->assertEquals(Carbon::now()->subMonths(7)->toIso8601String(), $cases->first()->created_at->toIso8601String()); + $this->assertEquals(Carbon::now()->subMonths(13)->toIso8601String(), $cases->first()->created_at->toIso8601String()); // Dispatch the job to evaluate the retention period EvaluateProcessRetentionJob::dispatchSync($process->id); // Assert all old cases are deleted - // There should be 1 case left (due to the creation of the process request) because the new case is within the retention period + // There should be 1 case left (the auto-created case from ProcessRequestObserver) + // because it was created after retention_updated_at and is within the retention period $this->assertDatabaseCount('case_numbers', 1); // TODO: Assert log entry is created } + + public function testItHandlesRetentionPolicyUpdate() + { + // Create a process with retention updated 6 months ago (was 6 months, now 1 year) + $retentionUpdatedAt = Carbon::now()->subMonths(6)->toIso8601String(); + $process = Process::factory()->create([ + 'properties' => [ + 'retention_period' => '1_year', // Updated to 1 year + 'retention_updated_at' => $retentionUpdatedAt, + ], + ]); + $process->save(); + $process->refresh(); + + // Create a process request + $processRequest = ProcessRequest::factory()->create(); + $processRequest->process_id = $process->id; + $processRequest->save(); + $processRequest->refresh(); + + // Create an old case (7 months ago, before retention_updated_at) + // Old cases cutoff = 6 months ago - 1 year = 18 months ago + // 7 months ago is NOT < 18 months ago, so it should NOT be deleted + $oldCase = CaseNumber::factory()->create([ + 'process_request_id' => $processRequest->id, + 'created_at' => Carbon::now()->subMonths(7)->toIso8601String(), + ]); + + // Create a new case (1 month ago, after retention_updated_at) + // New cases cutoff = now - 1 year = 12 months ago + // 1 month ago is NOT < 12 months ago, so it should NOT be deleted + $newCase = CaseNumber::factory()->create([ + 'process_request_id' => $processRequest->id, + 'created_at' => Carbon::now()->subMonths(1)->toIso8601String(), + ]); + + // Dispatch the job + EvaluateProcessRetentionJob::dispatchSync($process->id); + + // Both cases should still exist (plus the auto-created one = 3 total) + $this->assertNotNull(CaseNumber::find($oldCase->id)); + $this->assertNotNull(CaseNumber::find($newCase->id)); + $this->assertDatabaseCount('case_numbers', 3); + } + + public function testItDeletesOldCasesAfterRetentionPolicyUpdate() + { + // Create a process with retention updated 6 months ago (was 6 months, now 1 year) + $retentionUpdatedAt = Carbon::now()->subMonths(6)->toIso8601String(); + $process = Process::factory()->create([ + 'properties' => [ + 'retention_period' => '1_year', // Updated to 1 year + 'retention_updated_at' => $retentionUpdatedAt, + ], + ]); + $process->save(); + $process->refresh(); + + // Create a process request + $processRequest = ProcessRequest::factory()->create(); + $processRequest->process_id = $process->id; + $processRequest->save(); + $processRequest->refresh(); + + // Create an old case (20 months ago, before retention_updated_at which is 6 months ago) + // Old cases cutoff = 6 months ago - 1 year = 18 months ago + // 20 months ago < 18 months ago (earlier date), so it SHOULD be deleted + $oldCaseDate = Carbon::now()->subMonths(20); + $oldCase = CaseNumber::factory()->create([ + 'process_request_id' => $processRequest->id, + ]); + $oldCase->created_at = $oldCaseDate; + $oldCase->save(); + + // Create a case 7 months ago (before retention_updated_at) that should NOT be deleted + // Old cases cutoff = 6 months ago - 1 year = 18 months ago + // 7 months ago is NOT < 18 months ago (7 months ago is more recent), so it should NOT be deleted + $oldCaseNotDeletedDate = Carbon::now()->subMonths(7); + $oldCaseNotDeleted = CaseNumber::factory()->create([ + 'process_request_id' => $processRequest->id, + ]); + $oldCaseNotDeleted->created_at = $oldCaseNotDeletedDate; + $oldCaseNotDeleted->save(); + + // Dispatch the job + EvaluateProcessRetentionJob::dispatchSync($process->id); + + // The 20-month-old case should be deleted (older than 18 months cutoff) + // The 7-month-old case should NOT be deleted (newer than 18 months cutoff) + // Plus the auto-created case = 2 total + $this->assertNull(CaseNumber::find($oldCase->id), 'The 20-month-old case should be deleted'); + $this->assertNotNull(CaseNumber::find($oldCaseNotDeleted->id), 'The 7-month-old case should NOT be deleted'); + $this->assertDatabaseCount('case_numbers', 2); + } } From 082163cd65aa68c8f84fa62d54e6ae8520c3ecc7 Mon Sep 17 00:00:00 2001 From: sanja <52755494+sanjacornelius@users.noreply.github.com> Date: Thu, 5 Feb 2026 17:26:32 -0800 Subject: [PATCH 07/18] Remove todo --- tests/Jobs/EvaluateProcessRetentionJobTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/Jobs/EvaluateProcessRetentionJobTest.php b/tests/Jobs/EvaluateProcessRetentionJobTest.php index c2ae255e01..8b5dcdb430 100644 --- a/tests/Jobs/EvaluateProcessRetentionJobTest.php +++ b/tests/Jobs/EvaluateProcessRetentionJobTest.php @@ -137,8 +137,6 @@ public function testItHandlesMultipleCasesInBatches() // There should be 1 case left (the auto-created case from ProcessRequestObserver) // because it was created after retention_updated_at and is within the retention period $this->assertDatabaseCount('case_numbers', 1); - - // TODO: Assert log entry is created } public function testItHandlesRetentionPolicyUpdate() From eca7b57af4f05fd24de0d7adc38559a3d29ee9d9 Mon Sep 17 00:00:00 2001 From: sanja <52755494+sanjacornelius@users.noreply.github.com> Date: Thu, 5 Feb 2026 17:33:52 -0800 Subject: [PATCH 08/18] Disable job if feature flag is not enabled --- .../Jobs/EvaluateProcessRetentionJob.php | 7 ++ .../Jobs/EvaluateProcessRetentionJobTest.php | 64 +++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php b/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php index a9005bcd1e..51d4328466 100644 --- a/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php +++ b/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php @@ -26,6 +26,13 @@ public function __construct(public int $processId) */ public function handle(): void { + // Only run if case retention policy is enabled + // Use getenv() to read directly from environment (works better in tests) + $enabled = getenv('CASE_RETENTION_POLICY_ENABLED'); + if ($enabled === false || $enabled === 'false' || $enabled === '0' || $enabled === '') { + return; + } + $process = Process::find($this->processId); if (!$process) { Log::error('CaseRetentionJob: Process not found', ['process_id' => $this->processId]); diff --git a/tests/Jobs/EvaluateProcessRetentionJobTest.php b/tests/Jobs/EvaluateProcessRetentionJobTest.php index 8b5dcdb430..f9f6b5a1a9 100644 --- a/tests/Jobs/EvaluateProcessRetentionJobTest.php +++ b/tests/Jobs/EvaluateProcessRetentionJobTest.php @@ -16,6 +16,24 @@ class EvaluateProcessRetentionJobTest extends TestCase const RETENTION_PERIOD = '6_months'; + protected function setUp(): void + { + parent::setUp(); + // Enable case retention policy for all tests + putenv('CASE_RETENTION_POLICY_ENABLED=true'); + $_ENV['CASE_RETENTION_POLICY_ENABLED'] = 'true'; + $_SERVER['CASE_RETENTION_POLICY_ENABLED'] = 'true'; + } + + protected function tearDown(): void + { + // Clean up environment variable + putenv('CASE_RETENTION_POLICY_ENABLED'); + unset($_ENV['CASE_RETENTION_POLICY_ENABLED']); + unset($_SERVER['CASE_RETENTION_POLICY_ENABLED']); + parent::tearDown(); + } + public function testItDeletesCasesThatExceedRetentionPeriod() { // Create a process with a 6 month retention period @@ -232,4 +250,50 @@ public function testItDeletesOldCasesAfterRetentionPolicyUpdate() $this->assertNotNull(CaseNumber::find($oldCaseNotDeleted->id), 'The 7-month-old case should NOT be deleted'); $this->assertDatabaseCount('case_numbers', 2); } + + public function testItDoesNotRunWhenRetentionPolicyIsDisabled() + { + // Disable case retention policy + putenv('CASE_RETENTION_POLICY_ENABLED=false'); + $_ENV['CASE_RETENTION_POLICY_ENABLED'] = 'false'; + $_SERVER['CASE_RETENTION_POLICY_ENABLED'] = 'false'; + + // Create a process with a 6 month retention period + $retentionUpdatedAt = Carbon::now()->subMonths(6)->toIso8601String(); + $process = Process::factory()->create([ + 'properties' => [ + 'retention_period' => self::RETENTION_PERIOD, + 'retention_updated_at' => $retentionUpdatedAt, + ], + ]); + $process->save(); + $process->refresh(); + + // Create a process request + $processRequest = ProcessRequest::factory()->create(); + $processRequest->process_id = $process->id; + $processRequest->save(); + $processRequest->refresh(); + + // Create an old case that should be deleted if retention was enabled + $oldCaseDate = Carbon::now()->subMonths(13); + $oldCase = CaseNumber::factory()->create([ + 'process_request_id' => $processRequest->id, + ]); + $oldCase->created_at = $oldCaseDate; + $oldCase->save(); + + // Dispatch the job + EvaluateProcessRetentionJob::dispatchSync($process->id); + + // The case should NOT be deleted because retention policy is disabled + // Plus the auto-created case = 2 total + $this->assertNotNull(CaseNumber::find($oldCase->id), 'The case should NOT be deleted when retention policy is disabled'); + $this->assertDatabaseCount('case_numbers', 2); + + // Re-enable for other tests + putenv('CASE_RETENTION_POLICY_ENABLED=true'); + $_ENV['CASE_RETENTION_POLICY_ENABLED'] = 'true'; + $_SERVER['CASE_RETENTION_POLICY_ENABLED'] = 'true'; + } } From f3d25730e182faf33d2f7d9aed30a26076c78d56 Mon Sep 17 00:00:00 2001 From: sanja <52755494+sanjacornelius@users.noreply.github.com> Date: Thu, 5 Feb 2026 17:39:17 -0800 Subject: [PATCH 09/18] Default to 6_month retention period for processes that do not have retention_period configured --- .../Commands/EvaluateCaseRetention.php | 4 +- .../Jobs/EvaluateProcessRetentionJob.php | 11 ++++- .../Jobs/EvaluateProcessRetentionJobTest.php | 45 +++++++++++++++++++ 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/ProcessMaker/Console/Commands/EvaluateCaseRetention.php b/ProcessMaker/Console/Commands/EvaluateCaseRetention.php index 8e16572588..7a69059652 100644 --- a/ProcessMaker/Console/Commands/EvaluateCaseRetention.php +++ b/ProcessMaker/Console/Commands/EvaluateCaseRetention.php @@ -29,7 +29,9 @@ public function handle() { $this->info('Evaluating and deleting cases past their retention period'); - Process::whereNotNull('properties->retention_period')->chunkById(100, function ($processes) { + // Process all processes when retention policy is enabled + // Processes without retention_period will default to 6 months + Process::chunkById(100, function ($processes) { foreach ($processes as $process) { dispatch(new EvaluateProcessRetentionJob($process->id)); } diff --git a/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php b/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php index 51d4328466..760224c68a 100644 --- a/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php +++ b/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php @@ -40,14 +40,21 @@ public function handle(): void return; } - $retentionMonths = match ($process->properties['retention_period']) { + // Default to 6 months if retention_period is not set + $retentionPeriod = $process->properties['retention_period'] ?? '6_months'; + $retentionMonths = match ($retentionPeriod) { '6_months' => 6, '1_year' => 12, '3_years' => 36, '5_years' => 60, + default => 6, // Default to 6 months }; - $retentionUpdatedAt = Carbon::parse($process->properties['retention_updated_at']); + // Default retention_updated_at to now if not set + // This means the retention policy applies from now for processes without explicit retention settings + $retentionUpdatedAt = isset($process->properties['retention_updated_at']) + ? Carbon::parse($process->properties['retention_updated_at']) + : Carbon::now(); // Get all process request IDs for this process $processRequestIds = ProcessRequest::where('process_id', $this->processId)->pluck('id'); diff --git a/tests/Jobs/EvaluateProcessRetentionJobTest.php b/tests/Jobs/EvaluateProcessRetentionJobTest.php index f9f6b5a1a9..50a74d2025 100644 --- a/tests/Jobs/EvaluateProcessRetentionJobTest.php +++ b/tests/Jobs/EvaluateProcessRetentionJobTest.php @@ -296,4 +296,49 @@ public function testItDoesNotRunWhenRetentionPolicyIsDisabled() $_ENV['CASE_RETENTION_POLICY_ENABLED'] = 'true'; $_SERVER['CASE_RETENTION_POLICY_ENABLED'] = 'true'; } + + public function testItDefaultsToSixMonthsForProcessesWithoutRetentionPeriod() + { + // Create a process WITHOUT retention_period property (should default to 6 months) + $process = Process::factory()->create([ + 'properties' => [], // No retention_period set + ]); + $process->save(); + $process->refresh(); + + // Create a process request + $processRequest = ProcessRequest::factory()->create(); + $processRequest->process_id = $process->id; + $processRequest->save(); + $processRequest->refresh(); + + // Create a case created 7 months ago (older than default 6 months retention) + // Since retention_updated_at defaults to now, old cases cutoff = now - 6 months + // 7 months ago < (now - 6 months), so it should be deleted + $oldCaseDate = Carbon::now()->subMonths(7); + $oldCase = CaseNumber::factory()->create([ + 'process_request_id' => $processRequest->id, + ]); + $oldCase->created_at = $oldCaseDate; + $oldCase->save(); + + // Create a case created 5 months ago (within default 6 months retention) + // 5 months ago is NOT < (now - 6 months), so it should NOT be deleted + $newCaseDate = Carbon::now()->subMonths(5); + $newCase = CaseNumber::factory()->create([ + 'process_request_id' => $processRequest->id, + ]); + $newCase->created_at = $newCaseDate; + $newCase->save(); + + // Dispatch the job + EvaluateProcessRetentionJob::dispatchSync($process->id); + + // The 7-month-old case should be deleted (older than 6 months default) + // The 5-month-old case should NOT be deleted (within 6 months default) + // Plus the auto-created case = 2 total + $this->assertNull(CaseNumber::find($oldCase->id), 'The 7-month-old case should be deleted with default 6-month retention'); + $this->assertNotNull(CaseNumber::find($newCase->id), 'The 5-month-old case should NOT be deleted with default 6-month retention'); + $this->assertDatabaseCount('case_numbers', 2); + } } From dbc25366e5d6cbe8f6ddcf13ce17e101c6399785 Mon Sep 17 00:00:00 2001 From: sanja <52755494+sanjacornelius@users.noreply.github.com> Date: Tue, 10 Feb 2026 11:26:03 -0800 Subject: [PATCH 10/18] set default retention period to 1 year --- .../Console/Commands/EvaluateCaseRetention.php | 2 +- ProcessMaker/Jobs/EvaluateProcessRetentionJob.php | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/ProcessMaker/Console/Commands/EvaluateCaseRetention.php b/ProcessMaker/Console/Commands/EvaluateCaseRetention.php index 7a69059652..38b7ba3642 100644 --- a/ProcessMaker/Console/Commands/EvaluateCaseRetention.php +++ b/ProcessMaker/Console/Commands/EvaluateCaseRetention.php @@ -30,7 +30,7 @@ public function handle() $this->info('Evaluating and deleting cases past their retention period'); // Process all processes when retention policy is enabled - // Processes without retention_period will default to 6 months + // Processes without retention_period will default to 1_year Process::chunkById(100, function ($processes) { foreach ($processes as $process) { dispatch(new EvaluateProcessRetentionJob($process->id)); diff --git a/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php b/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php index 760224c68a..9b18b9e7f6 100644 --- a/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php +++ b/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php @@ -27,9 +27,8 @@ public function __construct(public int $processId) public function handle(): void { // Only run if case retention policy is enabled - // Use getenv() to read directly from environment (works better in tests) - $enabled = getenv('CASE_RETENTION_POLICY_ENABLED'); - if ($enabled === false || $enabled === 'false' || $enabled === '0' || $enabled === '') { + $enabled = config('app.case_retention_policy_enabled'); + if (!$enabled) { return; } @@ -40,14 +39,14 @@ public function handle(): void return; } - // Default to 6 months if retention_period is not set - $retentionPeriod = $process->properties['retention_period'] ?? '6_months'; + // Default to 1_year if retention_period is not set + $retentionPeriod = $process->properties['retention_period'] ?? '1_year'; $retentionMonths = match ($retentionPeriod) { '6_months' => 6, '1_year' => 12, '3_years' => 36, '5_years' => 60, - default => 6, // Default to 6 months + default => 12, // Default to 1_year }; // Default retention_updated_at to now if not set From 958cbe50c98c383acddc4154116cc3f45b6c80a1 Mon Sep 17 00:00:00 2001 From: sanja <52755494+sanjacornelius@users.noreply.github.com> Date: Tue, 10 Feb 2026 11:51:56 -0800 Subject: [PATCH 11/18] remove unused import --- ProcessMaker/Models/CaseNumber.php | 1 - 1 file changed, 1 deletion(-) diff --git a/ProcessMaker/Models/CaseNumber.php b/ProcessMaker/Models/CaseNumber.php index 721f61b019..7b06ee69e5 100644 --- a/ProcessMaker/Models/CaseNumber.php +++ b/ProcessMaker/Models/CaseNumber.php @@ -2,7 +2,6 @@ namespace ProcessMaker\Models; -use Database\Factories\ProcessMaker\Models\CaseNumberFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; From dca5de8d8f3bc25de264a8269bf0175f5e148fa2 Mon Sep 17 00:00:00 2001 From: sanja <52755494+sanjacornelius@users.noreply.github.com> Date: Tue, 10 Feb 2026 12:54:27 -0800 Subject: [PATCH 12/18] Update test default retention period --- .../Jobs/EvaluateProcessRetentionJobTest.php | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/Jobs/EvaluateProcessRetentionJobTest.php b/tests/Jobs/EvaluateProcessRetentionJobTest.php index 50a74d2025..052475bb44 100644 --- a/tests/Jobs/EvaluateProcessRetentionJobTest.php +++ b/tests/Jobs/EvaluateProcessRetentionJobTest.php @@ -14,7 +14,7 @@ class EvaluateProcessRetentionJobTest extends TestCase { use RefreshDatabase; - const RETENTION_PERIOD = '6_months'; + const RETENTION_PERIOD = '1_year'; protected function setUp(): void { @@ -297,9 +297,9 @@ public function testItDoesNotRunWhenRetentionPolicyIsDisabled() $_SERVER['CASE_RETENTION_POLICY_ENABLED'] = 'true'; } - public function testItDefaultsToSixMonthsForProcessesWithoutRetentionPeriod() + public function testItDefaultsToOneYearForProcessesWithoutRetentionPeriod() { - // Create a process WITHOUT retention_period property (should default to 6 months) + // Create a process WITHOUT retention_period property (should default to 1 year) $process = Process::factory()->create([ 'properties' => [], // No retention_period set ]); @@ -312,18 +312,18 @@ public function testItDefaultsToSixMonthsForProcessesWithoutRetentionPeriod() $processRequest->save(); $processRequest->refresh(); - // Create a case created 7 months ago (older than default 6 months retention) - // Since retention_updated_at defaults to now, old cases cutoff = now - 6 months - // 7 months ago < (now - 6 months), so it should be deleted - $oldCaseDate = Carbon::now()->subMonths(7); + // Create a case created 13 months ago (older than default 1 year retention) + // Since retention_updated_at defaults to now, old cases cutoff = now - 1 year + // 13 months ago < (now - 1 year), so it should be deleted + $oldCaseDate = Carbon::now()->subMonths(13); $oldCase = CaseNumber::factory()->create([ 'process_request_id' => $processRequest->id, ]); $oldCase->created_at = $oldCaseDate; $oldCase->save(); - // Create a case created 5 months ago (within default 6 months retention) - // 5 months ago is NOT < (now - 6 months), so it should NOT be deleted + // Create a case created 5 months ago (within default 1 year retention) + // 5 months ago is NOT < (now - 1 year), so it should NOT be deleted $newCaseDate = Carbon::now()->subMonths(5); $newCase = CaseNumber::factory()->create([ 'process_request_id' => $processRequest->id, @@ -334,11 +334,11 @@ public function testItDefaultsToSixMonthsForProcessesWithoutRetentionPeriod() // Dispatch the job EvaluateProcessRetentionJob::dispatchSync($process->id); - // The 7-month-old case should be deleted (older than 6 months default) - // The 5-month-old case should NOT be deleted (within 6 months default) + // The 13-month-old case should be deleted (older than 1 year default) + // The 5-month-old case should NOT be deleted (within 1 year default) // Plus the auto-created case = 2 total - $this->assertNull(CaseNumber::find($oldCase->id), 'The 7-month-old case should be deleted with default 6-month retention'); - $this->assertNotNull(CaseNumber::find($newCase->id), 'The 5-month-old case should NOT be deleted with default 6-month retention'); + $this->assertNull(CaseNumber::find($oldCase->id), 'The 13-month-old case should be deleted with default 1 year retention'); + $this->assertNotNull(CaseNumber::find($newCase->id), 'The 5-month-old case should NOT be deleted with default 1 year retention'); $this->assertDatabaseCount('case_numbers', 2); } } From 23c5f1a58ed93a83e5f5e014fafc5814720ad8e4 Mon Sep 17 00:00:00 2001 From: sanja <52755494+sanjacornelius@users.noreply.github.com> Date: Tue, 10 Feb 2026 12:55:35 -0800 Subject: [PATCH 13/18] Update EvaluateProcessRetentionJob.php --- ProcessMaker/Jobs/EvaluateProcessRetentionJob.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php b/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php index 9b18b9e7f6..148352d440 100644 --- a/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php +++ b/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php @@ -27,7 +27,7 @@ public function __construct(public int $processId) public function handle(): void { // Only run if case retention policy is enabled - $enabled = config('app.case_retention_policy_enabled'); + $enabled = getenv('CASE_RETENTION_POLICY_ENABLED'); if (!$enabled) { return; } From 09df9d8233ca8344c928eac4582a3944dd2e2db4 Mon Sep 17 00:00:00 2001 From: sanja <52755494+sanjacornelius@users.noreply.github.com> Date: Tue, 10 Feb 2026 13:21:44 -0800 Subject: [PATCH 14/18] Check if case retention policy is enabled before running job --- .../Console/Commands/EvaluateCaseRetention.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ProcessMaker/Console/Commands/EvaluateCaseRetention.php b/ProcessMaker/Console/Commands/EvaluateCaseRetention.php index 38b7ba3642..2601f550d4 100644 --- a/ProcessMaker/Console/Commands/EvaluateCaseRetention.php +++ b/ProcessMaker/Console/Commands/EvaluateCaseRetention.php @@ -27,6 +27,16 @@ class EvaluateCaseRetention extends Command */ public function handle() { + // Only run if case retention policy is enabled + $enabled = getenv('CASE_RETENTION_POLICY_ENABLED'); + if (!$enabled) { + $this->info('Case retention policy is disabled'); + $this->error('Skipping case retention evaluation'); + + return; + } + + $this->info('Case retention policy is enabled'); $this->info('Evaluating and deleting cases past their retention period'); // Process all processes when retention policy is enabled From da5863a543afc876a9df0346e284031219ac8654 Mon Sep 17 00:00:00 2001 From: sanja <52755494+sanjacornelius@users.noreply.github.com> Date: Tue, 10 Feb 2026 13:25:30 -0800 Subject: [PATCH 15/18] fix truthy statement --- ProcessMaker/Console/Commands/EvaluateCaseRetention.php | 2 +- ProcessMaker/Jobs/EvaluateProcessRetentionJob.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ProcessMaker/Console/Commands/EvaluateCaseRetention.php b/ProcessMaker/Console/Commands/EvaluateCaseRetention.php index 2601f550d4..422ba36367 100644 --- a/ProcessMaker/Console/Commands/EvaluateCaseRetention.php +++ b/ProcessMaker/Console/Commands/EvaluateCaseRetention.php @@ -28,7 +28,7 @@ class EvaluateCaseRetention extends Command public function handle() { // Only run if case retention policy is enabled - $enabled = getenv('CASE_RETENTION_POLICY_ENABLED'); + $enabled = env('CASE_RETENTION_POLICY_ENABLED', false); if (!$enabled) { $this->info('Case retention policy is disabled'); $this->error('Skipping case retention evaluation'); diff --git a/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php b/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php index 148352d440..ec4cecb206 100644 --- a/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php +++ b/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php @@ -27,7 +27,7 @@ public function __construct(public int $processId) public function handle(): void { // Only run if case retention policy is enabled - $enabled = getenv('CASE_RETENTION_POLICY_ENABLED'); + $enabled = env('CASE_RETENTION_POLICY_ENABLED', false); if (!$enabled) { return; } From a24399f37de067f64d77cbec48a00b55a7678ed2 Mon Sep 17 00:00:00 2001 From: sanja <52755494+sanjacornelius@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:39:08 -0800 Subject: [PATCH 16/18] fix issue with cached config --- .../Commands/EvaluateCaseRetention.php | 2 +- .../Jobs/EvaluateProcessRetentionJob.php | 2 +- config/app.php | 3 +++ .../Jobs/EvaluateProcessRetentionJobTest.php | 19 ++++++------------- 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/ProcessMaker/Console/Commands/EvaluateCaseRetention.php b/ProcessMaker/Console/Commands/EvaluateCaseRetention.php index 422ba36367..9404366d25 100644 --- a/ProcessMaker/Console/Commands/EvaluateCaseRetention.php +++ b/ProcessMaker/Console/Commands/EvaluateCaseRetention.php @@ -28,7 +28,7 @@ class EvaluateCaseRetention extends Command public function handle() { // Only run if case retention policy is enabled - $enabled = env('CASE_RETENTION_POLICY_ENABLED', false); + $enabled = config('app.case_retention_policy_enabled', false); if (!$enabled) { $this->info('Case retention policy is disabled'); $this->error('Skipping case retention evaluation'); diff --git a/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php b/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php index ec4cecb206..ca6787121f 100644 --- a/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php +++ b/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php @@ -27,7 +27,7 @@ public function __construct(public int $processId) public function handle(): void { // Only run if case retention policy is enabled - $enabled = env('CASE_RETENTION_POLICY_ENABLED', false); + $enabled = config('app.case_retention_policy_enabled', false); if (!$enabled) { return; } diff --git a/config/app.php b/config/app.php index aa82ceab5d..8cff4c371a 100644 --- a/config/app.php +++ b/config/app.php @@ -288,6 +288,9 @@ // Enable or disable TCE customization feature 'tce_customization_enable' => env('TCE_CUSTOMIZATION_ENABLED', false), + // Enable or disable case retention policy + 'case_retention_policy_enabled' => env('CASE_RETENTION_POLICY_ENABLED', false), + 'prometheus_namespace' => env('PROMETHEUS_NAMESPACE', strtolower(preg_replace('/[^a-zA-Z0-9_]+/', '_', env('APP_NAME', 'processmaker')))), 'server_timing' => [ diff --git a/tests/Jobs/EvaluateProcessRetentionJobTest.php b/tests/Jobs/EvaluateProcessRetentionJobTest.php index 052475bb44..f8ecb47243 100644 --- a/tests/Jobs/EvaluateProcessRetentionJobTest.php +++ b/tests/Jobs/EvaluateProcessRetentionJobTest.php @@ -4,6 +4,7 @@ use Carbon\Carbon; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Config; use ProcessMaker\Jobs\EvaluateProcessRetentionJob; use ProcessMaker\Models\CaseNumber; use ProcessMaker\Models\Process; @@ -20,17 +21,13 @@ protected function setUp(): void { parent::setUp(); // Enable case retention policy for all tests - putenv('CASE_RETENTION_POLICY_ENABLED=true'); - $_ENV['CASE_RETENTION_POLICY_ENABLED'] = 'true'; - $_SERVER['CASE_RETENTION_POLICY_ENABLED'] = 'true'; + Config::set('app.case_retention_policy_enabled', true); } protected function tearDown(): void { - // Clean up environment variable - putenv('CASE_RETENTION_POLICY_ENABLED'); - unset($_ENV['CASE_RETENTION_POLICY_ENABLED']); - unset($_SERVER['CASE_RETENTION_POLICY_ENABLED']); + // Clean up config + Config::set('app.case_retention_policy_enabled', false); parent::tearDown(); } @@ -254,9 +251,7 @@ public function testItDeletesOldCasesAfterRetentionPolicyUpdate() public function testItDoesNotRunWhenRetentionPolicyIsDisabled() { // Disable case retention policy - putenv('CASE_RETENTION_POLICY_ENABLED=false'); - $_ENV['CASE_RETENTION_POLICY_ENABLED'] = 'false'; - $_SERVER['CASE_RETENTION_POLICY_ENABLED'] = 'false'; + Config::set('app.case_retention_policy_enabled', false); // Create a process with a 6 month retention period $retentionUpdatedAt = Carbon::now()->subMonths(6)->toIso8601String(); @@ -292,9 +287,7 @@ public function testItDoesNotRunWhenRetentionPolicyIsDisabled() $this->assertDatabaseCount('case_numbers', 2); // Re-enable for other tests - putenv('CASE_RETENTION_POLICY_ENABLED=true'); - $_ENV['CASE_RETENTION_POLICY_ENABLED'] = 'true'; - $_SERVER['CASE_RETENTION_POLICY_ENABLED'] = 'true'; + Config::set('app.case_retention_policy_enabled', true); } public function testItDefaultsToOneYearForProcessesWithoutRetentionPeriod() From dfa1502cbe277e266eb52c29c829ce7e22d439e9 Mon Sep 17 00:00:00 2001 From: sanja <52755494+sanjacornelius@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:47:42 -0800 Subject: [PATCH 17/18] Resolve failing tests: Cases not being deleted due to improper retention period formatting --- .../Jobs/EvaluateProcessRetentionJobTest.php | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/tests/Jobs/EvaluateProcessRetentionJobTest.php b/tests/Jobs/EvaluateProcessRetentionJobTest.php index f8ecb47243..e8c1706de8 100644 --- a/tests/Jobs/EvaluateProcessRetentionJobTest.php +++ b/tests/Jobs/EvaluateProcessRetentionJobTest.php @@ -33,20 +33,17 @@ protected function tearDown(): void public function testItDeletesCasesThatExceedRetentionPeriod() { - // Create a process with a 6 month retention period - // retention_updated_at is 6 months ago, so old cases cutoff is 12 months ago (6 months ago - 6 months) - $retentionUpdatedAt = Carbon::now()->subMonths(6)->toIso8601String(); + // Create a process with a 1 year retention period + // retention_updated_at defaults to now, so cutoff is 12 months ago (now - 12 months) $process = Process::factory()->create([ 'properties' => [ 'retention_period' => self::RETENTION_PERIOD, - 'retention_updated_at' => $retentionUpdatedAt, ], ]); $process->save(); $process->refresh(); $this->assertEquals(self::RETENTION_PERIOD, $process->properties['retention_period']); - $this->assertEquals($retentionUpdatedAt, $process->properties['retention_updated_at']); // Create a process request $processRequest = ProcessRequest::factory()->create(); @@ -55,8 +52,8 @@ public function testItDeletesCasesThatExceedRetentionPeriod() $processRequest->refresh(); $this->assertEquals($process->id, $processRequest->process_id); - // Create a case number created 13 months ago (before retention_updated_at) - // Old cases cutoff = 6 months ago - 6 months = 12 months ago + // Create a case number created 13 months ago + // Cutoff = now - 12 months = 12 months ago // 13 months ago < 12 months ago, so it should be deleted $oldCaseCreatedAt = Carbon::now()->subMonths(13)->toIso8601String(); $caseOld = CaseNumber::factory()->create([ @@ -75,19 +72,16 @@ public function testItDeletesCasesThatExceedRetentionPeriod() public function testItDoesNotDeleteCasesThatAreWithinRetentionPeriod() { - // Create a process with a 6 month retention period - // retention_updated_at is 6 months ago, so old cases cutoff is 12 months ago (6 months ago - 6 months) - $retentionUpdatedAt = Carbon::now()->subMonths(6)->toIso8601String(); + // Create a process with a 1 year retention period + // retention_updated_at defaults to now, so cutoff is 12 months ago (now - 12 months) $process = Process::factory()->create([ 'properties' => [ 'retention_period' => self::RETENTION_PERIOD, - 'retention_updated_at' => $retentionUpdatedAt, ], ]); $process->save(); $process->refresh(); $this->assertEquals(self::RETENTION_PERIOD, $process->properties['retention_period']); - $this->assertEquals($retentionUpdatedAt, $process->properties['retention_updated_at']); // Create a process request $processRequest = ProcessRequest::factory()->create(); @@ -96,8 +90,9 @@ public function testItDoesNotDeleteCasesThatAreWithinRetentionPeriod() $processRequest->refresh(); $this->assertEquals($process->id, $processRequest->process_id); - // Create a case number created 5 months ago (before retention_updated_at) - // This case is NOT older than the old cases cutoff (12 months ago), so it should NOT be deleted + // Create a case number created 5 months ago + // Cutoff = now - 12 months = 12 months ago + // 5 months ago is NOT < 12 months ago, so it should NOT be deleted $caseCreatedAt = Carbon::now()->subMonths(5)->toIso8601String(); $case = CaseNumber::factory()->create([ 'created_at' => $caseCreatedAt, @@ -115,17 +110,16 @@ public function testItDoesNotDeleteCasesThatAreWithinRetentionPeriod() public function testItHandlesMultipleCasesInBatches() { - // Create a process with a 6 month retention period + // Create a process with a 1 year retention period + // retention_updated_at defaults to now, so cutoff is 12 months ago (now - 12 months) $process = Process::factory()->create([ 'properties' => [ 'retention_period' => self::RETENTION_PERIOD, - 'retention_updated_at' => Carbon::now()->subMonths(6)->toIso8601String(), ], ]); $process->save(); $process->refresh(); $this->assertEquals(self::RETENTION_PERIOD, $process->properties['retention_period']); - $this->assertEquals(Carbon::now()->subMonths(6)->toIso8601String(), $process->properties['retention_updated_at']); // Create a process request $processRequest = ProcessRequest::factory()->create(); @@ -135,8 +129,8 @@ public function testItHandlesMultipleCasesInBatches() $this->assertEquals($process->id, $processRequest->process_id); // Create 1200 cases (to test chunking/batch deletion) - // These cases are created 13 months ago (before retention_updated_at) - // Old cases cutoff = 6 months ago - 6 months = 12 months ago + // These cases are created 13 months ago + // Cutoff = now - 12 months = 12 months ago // 13 months ago < 12 months ago, so these should be deleted $cases = CaseNumber::factory()->count(1200)->create([ 'process_request_id' => $processRequest->id, @@ -150,7 +144,7 @@ public function testItHandlesMultipleCasesInBatches() // Assert all old cases are deleted // There should be 1 case left (the auto-created case from ProcessRequestObserver) - // because it was created after retention_updated_at and is within the retention period + // because it was created recently and is within the retention period $this->assertDatabaseCount('case_numbers', 1); } From 888d7a9acd0a8067e79e045403e9e30cbaf4b702 Mon Sep 17 00:00:00 2001 From: sanja <52755494+sanjacornelius@users.noreply.github.com> Date: Wed, 11 Feb 2026 12:06:21 -0800 Subject: [PATCH 18/18] CusorBot Fix: use subquery instead of loading all IDs into memory --- ProcessMaker/Jobs/EvaluateProcessRetentionJob.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php b/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php index ca6787121f..f5d2415993 100644 --- a/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php +++ b/ProcessMaker/Jobs/EvaluateProcessRetentionJob.php @@ -55,11 +55,9 @@ public function handle(): void ? Carbon::parse($process->properties['retention_updated_at']) : Carbon::now(); - // Get all process request IDs for this process - $processRequestIds = ProcessRequest::where('process_id', $this->processId)->pluck('id'); - - // If there are no process requests, nothing to delete - if ($processRequestIds->isEmpty()) { + // Check if there are any process requests for this process + // If not, nothing to delete + if (!ProcessRequest::where('process_id', $this->processId)->exists()) { return; } @@ -77,7 +75,10 @@ public function handle(): void // For cases created after retention_updated_at: cutoff is now - retention_period $newCasesCutoff = $now->copy()->subMonths($retentionMonths); - CaseNumber::whereIn('process_request_id', $processRequestIds) + // Use subquery to get process request IDs + $processRequestSubquery = ProcessRequest::where('process_id', $this->processId)->select('id'); + + CaseNumber::whereIn('process_request_id', $processRequestSubquery) ->where(function ($query) use ($retentionUpdatedAt, $oldCasesCutoff, $newCasesCutoff) { // Cases created before retention_updated_at: delete if created before (retention_updated_at - retention_period) $query->where(function ($q) use ($retentionUpdatedAt, $oldCasesCutoff) {