Skip to content

Commit c8c44e1

Browse files
Merge pull request #8740 from ProcessMaker/task/FOUR-29113
Add Detailed Logging to Cases Retention Evaluation Job
2 parents b976ed0 + 9579d01 commit c8c44e1

3 files changed

Lines changed: 173 additions & 12 deletions

File tree

ProcessMaker/Console/Commands/EvaluateCaseRetention.php

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Illuminate\Console\Command;
66
use ProcessMaker\Jobs\EvaluateProcessRetentionJob;
77
use ProcessMaker\Models\Process;
8+
use ProcessMaker\Models\ProcessCategory;
89

910
class EvaluateCaseRetention extends Command
1011
{
@@ -37,16 +38,37 @@ public function handle()
3738
}
3839

3940
$this->info('Case retention policy is enabled');
40-
$this->info('Evaluating and deleting cases past their retention period');
41+
$this->info('Dispatching retention evaluation jobs for all processes');
4142

42-
// Process all processes when retention policy is enabled
43-
// Processes without retention_period will default to one_year
44-
Process::chunkById(100, function ($processes) {
43+
// Get system category IDs to exclude
44+
$systemCategoryIds = ProcessCategory::where('is_system', true)->pluck('id');
45+
46+
// Exclude processes that are templates or in system categories
47+
$jobCount = 0;
48+
$query = Process::where('is_template', '!=', 1);
49+
50+
// Exclude processes in system categories
51+
if ($systemCategoryIds->isNotEmpty()) {
52+
$query->where(function ($q) use ($systemCategoryIds) {
53+
$q->where(function ($subQuery) use ($systemCategoryIds) {
54+
$subQuery->whereNotIn('process_category_id', $systemCategoryIds)
55+
->orWhereNull('process_category_id');
56+
});
57+
})
58+
->whereDoesntHave('categories', function ($q) use ($systemCategoryIds) {
59+
// Exclude processes with any category assignment to system categories
60+
$q->whereIn('process_categories.id', $systemCategoryIds);
61+
});
62+
}
63+
64+
$query->chunkById(100, function ($processes) use (&$jobCount) {
4565
foreach ($processes as $process) {
4666
dispatch(new EvaluateProcessRetentionJob($process->id));
67+
$jobCount++;
4768
}
4869
});
4970

50-
$this->info('Cases retention evaluation complete');
71+
$this->info("Dispatched {$jobCount} retention evaluation job(s) to the queue");
72+
$this->info('Jobs will be processed asynchronously by queue workers');
5173
}
5274
}

ProcessMaker/Jobs/EvaluateProcessRetentionJob.php

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,45 @@ public function __construct(public int $processId)
3131
*/
3232
public function handle(): void
3333
{
34+
$startTime = microtime(true);
35+
36+
Log::info('EvaluateProcessRetentionJob: Starting evaluation', [
37+
'process_id' => $this->processId,
38+
]);
39+
3440
// Only run if case retention policy is enabled
3541
$enabled = config('app.case_retention_policy_enabled', false);
3642
if (!$enabled) {
43+
Log::info('EvaluateProcessRetentionJob: Case retention policy is disabled, skipping', [
44+
'process_id' => $this->processId,
45+
]);
46+
3747
return;
3848
}
3949

4050
$process = Process::find($this->processId);
4151
if (!$process) {
42-
Log::error('CaseRetentionJob: Process not found', ['process_id' => $this->processId]);
52+
Log::error('EvaluateProcessRetentionJob: Process not found', [
53+
'process_id' => $this->processId,
54+
]);
55+
56+
return;
57+
}
58+
59+
// Skip template processes
60+
if ($process->is_template) {
61+
Log::info('EvaluateProcessRetentionJob: Skipping template process', [
62+
'process_id' => $this->processId,
63+
]);
64+
65+
return;
66+
}
67+
68+
// Skip processes in system categories
69+
if ($process->categories()->where('is_system', true)->exists()) {
70+
Log::info('EvaluateProcessRetentionJob: Skipping process in system category', [
71+
'process_id' => $this->processId,
72+
]);
4373

4474
return;
4575
}
@@ -54,15 +84,25 @@ public function handle(): void
5484
default => 12, // Default to one_year
5585
};
5686

87+
Log::info('EvaluateProcessRetentionJob: Retention configuration loaded', [
88+
'process_id' => $this->processId,
89+
'process_name' => $process->name,
90+
'retention_period' => $retentionPeriod,
91+
'retention_months' => $retentionMonths,
92+
]);
93+
5794
// Default retention_updated_at to now if not set
5895
// This means the retention policy applies from now for processes without explicit retention settings
5996
$retentionUpdatedAt = isset($process->properties['retention_updated_at'])
6097
? Carbon::parse($process->properties['retention_updated_at'])
6198
: Carbon::now();
6299

63100
// Check if there are any process requests for this process
64-
// If not, nothing to delete
65101
if (!ProcessRequest::where('process_id', $this->processId)->exists()) {
102+
Log::info('EvaluateProcessRetentionJob: No process requests found, nothing to evaluate', [
103+
'process_id' => $this->processId,
104+
]);
105+
66106
return;
67107
}
68108

@@ -80,15 +120,25 @@ public function handle(): void
80120
// For cases created after retention_updated_at: cutoff is now - retention_period
81121
$newCasesCutoff = $now->copy()->subMonths($retentionMonths);
82122

123+
Log::info('EvaluateProcessRetentionJob: Retention cutoff dates calculated', [
124+
'process_id' => $this->processId,
125+
'retention_updated_at' => $retentionUpdatedAt->toIso8601String(),
126+
'old_cases_cutoff' => $oldCasesCutoff->toIso8601String(),
127+
'new_cases_cutoff' => $newCasesCutoff->toIso8601String(),
128+
'current_time' => $now->toIso8601String(),
129+
]);
130+
83131
// Use subquery to get process request IDs
84132
$processRequestSubquery = ProcessRequest::where('process_id', $this->processId)->select('id');
85133

86134
// Collect all ProcessRequest IDs that will be deleted (to delete them after all chunks are processed)
87135
$processRequestIdsToDelete = [];
136+
$totalDeleted = 0;
137+
$chunkCount = 0;
88138

89139
CaseNumber::whereIn('process_request_id', $processRequestSubquery)
90140
->where($this->buildRetentionQuery($retentionUpdatedAt, $oldCasesCutoff, $newCasesCutoff))
91-
->chunkById(100, function ($cases) use (&$processRequestIdsToDelete) {
141+
->chunkById(100, function ($cases) use (&$processRequestIdsToDelete, &$totalDeleted, &$chunkCount) {
92142
$caseIds = $cases->pluck('id')->all();
93143
$processRequestIds = $cases->pluck('process_request_id')->unique()->all();
94144

@@ -116,10 +166,15 @@ public function handle(): void
116166
$this->deleteTaskDraftMedia($draftIds);
117167
$this->deleteTaskDrafts($processRequestTokenIds);
118168

119-
// TODO: Add logs to track the number of cases deleted
120-
// Get deleted timestamp
121-
// $deletedAt = Carbon::now();
122-
// RetentionPolicyLog::record($process->id, $caseIds, $deletedAt);
169+
$chunkCount++;
170+
$chunkSize = count($caseIds);
171+
$totalDeleted += $chunkSize;
172+
173+
Log::info('EvaluateProcessRetentionJob: Deleted chunk of cases', [
174+
'process_id' => $this->processId,
175+
'chunk_number' => $chunkCount,
176+
'cases_deleted' => $chunkSize,
177+
]);
123178
});
124179

125180
// Delete ProcessRequests after all chunks are processed
@@ -142,6 +197,16 @@ public function handle(): void
142197
$this->dispatchSavedSearchRecount();
143198
}
144199
}
200+
201+
$endTime = microtime(true);
202+
$executionTime = round(($endTime - $startTime) * 1000, 2);
203+
204+
Log::info('EvaluateProcessRetentionJob: Evaluation completed', [
205+
'process_id' => $this->processId,
206+
'total_cases_deleted' => $totalDeleted,
207+
'total_chunks_processed' => $chunkCount,
208+
'execution_time_ms' => $executionTime,
209+
]);
145210
}
146211

147212
/**

tests/Jobs/EvaluateProcessRetentionJobTest.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use ProcessMaker\Models\Media;
1414
use ProcessMaker\Models\Notification;
1515
use ProcessMaker\Models\Process;
16+
use ProcessMaker\Models\ProcessCategory;
1617
use ProcessMaker\Models\ProcessRequest;
1718
use ProcessMaker\Models\ProcessRequestToken;
1819
use Tests\TestCase;
@@ -522,4 +523,77 @@ public function testItDeletesAllRelatedRecordsWhenCasesExceedRetentionPeriod()
522523
// Check that notification has been deleted
523524
$this->assertNull(Notification::find($notification->id), 'The notification should be deleted');
524525
}
526+
527+
public function testItDoesNotRunForTemplates()
528+
{
529+
// Create a template process
530+
$process = Process::factory()->create([
531+
'is_template' => 1,
532+
'properties' => [
533+
'retention_period' => self::RETENTION_PERIOD,
534+
],
535+
]);
536+
$process->save();
537+
$process->refresh();
538+
539+
// Create a process request
540+
$processRequest = ProcessRequest::factory()->create();
541+
$processRequest->process_id = $process->id;
542+
$processRequest->save();
543+
$processRequest->refresh();
544+
545+
// Create an old case that should be deleted if this wasn't a template
546+
// 13 months ago is older than 1 year retention period
547+
$oldCaseDate = Carbon::now()->subMonths(13);
548+
$oldCase = CaseNumber::factory()->create([
549+
'process_request_id' => $processRequest->id,
550+
]);
551+
$oldCase->created_at = $oldCaseDate;
552+
$oldCase->save();
553+
554+
// Dispatch the job
555+
EvaluateProcessRetentionJob::dispatchSync($process->id);
556+
557+
// The case should NOT be deleted because the process is a template
558+
$this->assertNotNull(CaseNumber::find($oldCase->id), 'The case should NOT be deleted because the process is a template');
559+
$this->assertDatabaseCount('case_numbers', 2);
560+
}
561+
562+
public function testItDoesNotRunForProcessesInSystemCategories()
563+
{
564+
$category = ProcessCategory::factory()->create([
565+
'is_system' => 1,
566+
]);
567+
// Create a process in a system category
568+
$process = Process::factory()->create([
569+
'process_category_id' => $category->id,
570+
'properties' => [
571+
'retention_period' => self::RETENTION_PERIOD,
572+
],
573+
]);
574+
$process->save();
575+
$process->refresh();
576+
577+
// Create a process request
578+
$processRequest = ProcessRequest::factory()->create();
579+
$processRequest->process_id = $process->id;
580+
$processRequest->save();
581+
$processRequest->refresh();
582+
583+
// Create an old case that should be deleted if this wasn't in a system category
584+
// 13 months ago is older than 1 year retention period
585+
$oldCaseDate = Carbon::now()->subMonths(13);
586+
$oldCase = CaseNumber::factory()->create([
587+
'process_request_id' => $processRequest->id,
588+
]);
589+
$oldCase->created_at = $oldCaseDate;
590+
$oldCase->save();
591+
592+
// Dispatch the job
593+
EvaluateProcessRetentionJob::dispatchSync($process->id);
594+
595+
// The case should NOT be deleted because the process is in a system category
596+
$this->assertNotNull(CaseNumber::find($oldCase->id), 'The case should NOT be deleted because the process is in a system category');
597+
$this->assertDatabaseCount('case_numbers', 2);
598+
}
525599
}

0 commit comments

Comments
 (0)