Skip to content

Commit 72daa9a

Browse files
Merge pull request #8721 from ProcessMaker/task/FOUR-29110
Implement Scheduled Case Retention Evaluation and Automated Case Deletion
2 parents 2755edb + 888d7a9 commit 72daa9a

6 files changed

Lines changed: 519 additions & 0 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
namespace ProcessMaker\Console\Commands;
4+
5+
use Illuminate\Console\Command;
6+
use ProcessMaker\Jobs\EvaluateProcessRetentionJob;
7+
use ProcessMaker\Models\Process;
8+
9+
class EvaluateCaseRetention extends Command
10+
{
11+
/**
12+
* The name and signature of the console command.
13+
*
14+
* @var string
15+
*/
16+
protected $signature = 'cases:retention:evaluate';
17+
18+
/**
19+
* The console command description.
20+
*
21+
* @var string
22+
*/
23+
protected $description = 'Evaluate and delete cases past their retention period';
24+
25+
/**
26+
* Execute the console command.
27+
*/
28+
public function handle()
29+
{
30+
// Only run if case retention policy is enabled
31+
$enabled = config('app.case_retention_policy_enabled', false);
32+
if (!$enabled) {
33+
$this->info('Case retention policy is disabled');
34+
$this->error('Skipping case retention evaluation');
35+
36+
return;
37+
}
38+
39+
$this->info('Case retention policy is enabled');
40+
$this->info('Evaluating and deleting cases past their retention period');
41+
42+
// Process all processes when retention policy is enabled
43+
// Processes without retention_period will default to 1_year
44+
Process::chunkById(100, function ($processes) {
45+
foreach ($processes as $process) {
46+
dispatch(new EvaluateProcessRetentionJob($process->id));
47+
}
48+
});
49+
50+
$this->info('Cases retention evaluation complete');
51+
}
52+
}

ProcessMaker/Console/Kernel.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,13 @@ protected function schedule(Schedule $schedule)
8989
break;
9090
}
9191

92+
// evaluate cases retention policy
93+
$schedule->command('cases:retention:evaluate')
94+
->daily()
95+
->onOneServer()
96+
->withoutOverlapping()
97+
->runInBackground();
98+
9299
// 5 minutes is recommended in https://laravel.com/docs/12.x/horizon#metrics
93100
$schedule->command('horizon:snapshot')->everyFiveMinutes();
94101
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
namespace ProcessMaker\Jobs;
4+
5+
use Carbon\Carbon;
6+
use Illuminate\Contracts\Queue\ShouldQueue;
7+
use Illuminate\Foundation\Queue\Queueable;
8+
use Illuminate\Support\Facades\Log;
9+
use ProcessMaker\Models\CaseNumber;
10+
use ProcessMaker\Models\Process;
11+
use ProcessMaker\Models\ProcessRequest;
12+
13+
class EvaluateProcessRetentionJob implements ShouldQueue
14+
{
15+
use Queueable;
16+
17+
/**
18+
* Create a new job instance.
19+
*/
20+
public function __construct(public int $processId)
21+
{
22+
}
23+
24+
/**
25+
* Execute the job.
26+
*/
27+
public function handle(): void
28+
{
29+
// Only run if case retention policy is enabled
30+
$enabled = config('app.case_retention_policy_enabled', false);
31+
if (!$enabled) {
32+
return;
33+
}
34+
35+
$process = Process::find($this->processId);
36+
if (!$process) {
37+
Log::error('CaseRetentionJob: Process not found', ['process_id' => $this->processId]);
38+
39+
return;
40+
}
41+
42+
// Default to 1_year if retention_period is not set
43+
$retentionPeriod = $process->properties['retention_period'] ?? '1_year';
44+
$retentionMonths = match ($retentionPeriod) {
45+
'6_months' => 6,
46+
'1_year' => 12,
47+
'3_years' => 36,
48+
'5_years' => 60,
49+
default => 12, // Default to 1_year
50+
};
51+
52+
// Default retention_updated_at to now if not set
53+
// This means the retention policy applies from now for processes without explicit retention settings
54+
$retentionUpdatedAt = isset($process->properties['retention_updated_at'])
55+
? Carbon::parse($process->properties['retention_updated_at'])
56+
: Carbon::now();
57+
58+
// Check if there are any process requests for this process
59+
// If not, nothing to delete
60+
if (!ProcessRequest::where('process_id', $this->processId)->exists()) {
61+
return;
62+
}
63+
64+
// Handle two scenarios:
65+
// 1. Cases created BEFORE retention_updated_at: Delete if older than retention period from retention_updated_at
66+
// (These cases were subject to the old retention policy, but we apply current retention from update date)
67+
// 2. Cases created AFTER retention_updated_at: Delete if older than retention period from their creation date
68+
// (These cases are subject to the new retention policy)
69+
70+
$now = Carbon::now();
71+
72+
// For cases created before retention_updated_at: cutoff is retention_updated_at - retention_period
73+
$oldCasesCutoff = $retentionUpdatedAt->copy()->subMonths($retentionMonths);
74+
75+
// For cases created after retention_updated_at: cutoff is now - retention_period
76+
$newCasesCutoff = $now->copy()->subMonths($retentionMonths);
77+
78+
// Use subquery to get process request IDs
79+
$processRequestSubquery = ProcessRequest::where('process_id', $this->processId)->select('id');
80+
81+
CaseNumber::whereIn('process_request_id', $processRequestSubquery)
82+
->where(function ($query) use ($retentionUpdatedAt, $oldCasesCutoff, $newCasesCutoff) {
83+
// Cases created before retention_updated_at: delete if created before (retention_updated_at - retention_period)
84+
$query->where(function ($q) use ($retentionUpdatedAt, $oldCasesCutoff) {
85+
$q->where('created_at', '<', $retentionUpdatedAt)
86+
->where('created_at', '<', $oldCasesCutoff);
87+
})
88+
// Cases created after retention_updated_at: delete if created before (now - retention_period)
89+
->orWhere(function ($q) use ($retentionUpdatedAt, $newCasesCutoff) {
90+
$q->where('created_at', '>=', $retentionUpdatedAt)
91+
->where('created_at', '<', $newCasesCutoff);
92+
});
93+
})
94+
->chunkById(100, function ($cases) {
95+
$caseIds = $cases->pluck('id');
96+
// Delete the cases
97+
CaseNumber::whereIn('id', $caseIds)->delete();
98+
99+
// TODO: Add logs to track the number of cases deleted
100+
// Get deleted timestamp
101+
// $deletedAt = Carbon::now();
102+
// RetentionPolicyLog::record($process->id, $caseIds, $deletedAt);
103+
});
104+
}
105+
}

config/app.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,9 @@
288288
// Enable or disable TCE customization feature
289289
'tce_customization_enable' => env('TCE_CUSTOMIZATION_ENABLED', false),
290290

291+
// Enable or disable case retention policy
292+
'case_retention_policy_enabled' => env('CASE_RETENTION_POLICY_ENABLED', false),
293+
291294
'prometheus_namespace' => env('PROMETHEUS_NAMESPACE', strtolower(preg_replace('/[^a-zA-Z0-9_]+/', '_', env('APP_NAME', 'processmaker')))),
292295

293296
'server_timing' => [
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Database\Factories\ProcessMaker\Models;
4+
5+
use Illuminate\Database\Eloquent\Factories\Factory;
6+
use ProcessMaker\Models\CaseNumber;
7+
use ProcessMaker\Models\ProcessRequest;
8+
9+
class CaseNumberFactory extends Factory
10+
{
11+
protected $model = CaseNumber::class;
12+
13+
public function definition(): array
14+
{
15+
return [
16+
'process_request_id' => function () {
17+
return ProcessRequest::factory()->create()->getKey();
18+
},
19+
];
20+
}
21+
}

0 commit comments

Comments
 (0)