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 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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); + } }