Skip to content

Commit b976ed0

Browse files
Merge pull request #8744 from ProcessMaker/task/FOUR-29112-B
Cascade Deletion of cases_started and cases_participated in Cases Retention Job
2 parents de2fd0b + 0f870bf commit b976ed0

5 files changed

Lines changed: 366 additions & 70 deletions

File tree

ProcessMaker/Console/Commands/EvaluateCaseRetention.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public function handle()
4040
$this->info('Evaluating and deleting cases past their retention period');
4141

4242
// Process all processes when retention policy is enabled
43-
// Processes without retention_period will default to 1_year
43+
// Processes without retention_period will default to one_year
4444
Process::chunkById(100, function ($processes) {
4545
foreach ($processes as $process) {
4646
dispatch(new EvaluateProcessRetentionJob($process->id));

ProcessMaker/Http/Controllers/Api/Actions/Cases/DeleteCase.php

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -99,20 +99,4 @@ private function getTaskDraftIds(array $tokenIds): array
9999
->pluck('id')
100100
->all();
101101
}
102-
103-
private function dispatchSavedSearchRecount(): void
104-
{
105-
if (!config('savedsearch.count', false)) {
106-
return;
107-
}
108-
109-
$jobClass = 'ProcessMaker\\Package\\SavedSearch\\Jobs\\RecountAllSavedSearches';
110-
if (!class_exists($jobClass)) {
111-
return;
112-
}
113-
114-
DB::afterCommit(static function () use ($jobClass): void {
115-
$jobClass::dispatch(['request', 'task']);
116-
});
117-
}
118102
}

ProcessMaker/Http/Controllers/Api/Actions/Cases/DeletesCaseRecords.php

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,30 @@
2121

2222
trait DeletesCaseRecords
2323
{
24-
private function deleteCasesStarted(string $caseNumber): void
24+
private function deleteCasesStarted(string | array $caseNumbers): void
2525
{
26-
CaseStarted::query()
27-
->where('case_number', $caseNumber)
28-
->delete();
26+
if (is_array($caseNumbers) && $caseNumbers !== []) {
27+
CaseStarted::query()
28+
->whereIn('case_number', $caseNumbers)
29+
->delete();
30+
} else {
31+
CaseStarted::query()
32+
->where('case_number', $caseNumbers)
33+
->delete();
34+
}
2935
}
3036

31-
private function deleteCasesParticipated(string $caseNumber): void
37+
private function deleteCasesParticipated(string | array $caseNumbers): void
3238
{
33-
CaseParticipated::query()
34-
->where('case_number', $caseNumber)
35-
->delete();
39+
if (is_array($caseNumbers)) {
40+
CaseParticipated::query()
41+
->whereIn('case_number', $caseNumbers)
42+
->delete();
43+
} else {
44+
CaseParticipated::query()
45+
->where('case_number', $caseNumbers)
46+
->delete();
47+
}
3648
}
3749

3850
private function deleteCaseNumbers(array $requestIds): void
@@ -183,11 +195,18 @@ private function deleteRequestMedia(array $requestIds): void
183195
->delete();
184196
}
185197

186-
private function deleteComments(string $caseNumber, array $requestIds, array $tokenIds): void
198+
private function deleteComments(string | array $caseNumbers, array $requestIds, array $tokenIds): void
187199
{
188-
Comment::query()
189-
->where('case_number', $caseNumber)
190-
->orWhere(function ($query) use ($requestIds, $tokenIds) {
200+
if (is_array($caseNumbers) && $caseNumbers !== []) {
201+
$query = Comment::query()
202+
->whereIn('case_number', $caseNumbers);
203+
} else {
204+
$query = Comment::query()
205+
->where('case_number', $caseNumbers);
206+
}
207+
208+
if ($requestIds !== [] || $tokenIds !== []) {
209+
$query->orWhere(function ($query) use ($requestIds, $tokenIds) {
191210
$query->where('commentable_type', ProcessRequest::class)
192211
->whereIn('commentable_id', $requestIds);
193212

@@ -197,8 +216,10 @@ private function deleteComments(string $caseNumber, array $requestIds, array $to
197216
->whereIn('commentable_id', $tokenIds);
198217
});
199218
}
200-
})
201-
->delete();
219+
});
220+
}
221+
222+
$query->delete();
202223
}
203224

204225
private function deleteNotifications(array $requestIds): void
@@ -220,4 +241,20 @@ private function deleteNotifications(array $requestIds): void
220241
->whereIn('data->type', $notificationTypes)
221242
->delete();
222243
}
244+
245+
private function dispatchSavedSearchRecount(): void
246+
{
247+
if (!config('savedsearch.count', false)) {
248+
return;
249+
}
250+
251+
$jobClass = 'ProcessMaker\\Package\\SavedSearch\\Jobs\\RecountAllSavedSearches';
252+
if (!class_exists($jobClass)) {
253+
return;
254+
}
255+
256+
DB::afterCommit(static function () use ($jobClass): void {
257+
$jobClass::dispatch(['request', 'task']);
258+
});
259+
}
223260
}

ProcessMaker/Jobs/EvaluateProcessRetentionJob.php

Lines changed: 104 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,18 @@
66
use Illuminate\Contracts\Queue\ShouldQueue;
77
use Illuminate\Foundation\Queue\Queueable;
88
use Illuminate\Support\Facades\Log;
9+
use ProcessMaker\Http\Controllers\Api\Actions\Cases\DeletesCaseRecords;
910
use ProcessMaker\Models\CaseNumber;
11+
use ProcessMaker\Models\CaseParticipated;
12+
use ProcessMaker\Models\CaseStarted;
1013
use ProcessMaker\Models\Process;
1114
use ProcessMaker\Models\ProcessRequest;
15+
use ProcessMaker\Models\ProcessRequestToken;
16+
use ProcessMaker\Models\TaskDraft;
1217

1318
class EvaluateProcessRetentionJob implements ShouldQueue
1419
{
15-
use Queueable;
20+
use Queueable, DeletesCaseRecords;
1621

1722
/**
1823
* Create a new job instance.
@@ -39,14 +44,14 @@ public function handle(): void
3944
return;
4045
}
4146

42-
// Default to 1_year if retention_period is not set
43-
$retentionPeriod = $process->properties['retention_period'] ?? '1_year';
47+
// Default to one_year if retention_period is not set
48+
$retentionPeriod = $process->properties['retention_period'] ?? 'one_year';
4449
$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+
'six_months' => 6,
51+
'one_year' => 12,
52+
'three_years' => 36,
53+
'five_years' => 60,
54+
default => 12, // Default to one_year
5055
};
5156

5257
// Default retention_updated_at to now if not set
@@ -78,28 +83,104 @@ public function handle(): void
7883
// Use subquery to get process request IDs
7984
$processRequestSubquery = ProcessRequest::where('process_id', $this->processId)->select('id');
8085

86+
// Collect all ProcessRequest IDs that will be deleted (to delete them after all chunks are processed)
87+
$processRequestIdsToDelete = [];
88+
8189
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
90+
->where($this->buildRetentionQuery($retentionUpdatedAt, $oldCasesCutoff, $newCasesCutoff))
91+
->chunkById(100, function ($cases) use (&$processRequestIdsToDelete) {
92+
$caseIds = $cases->pluck('id')->all();
93+
$processRequestIds = $cases->pluck('process_request_id')->unique()->all();
94+
95+
// Collect ProcessRequest IDs for deletion after all chunks are processed
96+
$processRequestIdsToDelete = array_merge($processRequestIdsToDelete, $processRequestIds);
97+
98+
$processRequestTokenIds = ProcessRequestToken::whereIn('process_request_id', $processRequestIds)->pluck('id')->all();
99+
$draftIds = $this->getTaskDraftIds($processRequestTokenIds);
100+
101+
// uses case_number to delete
102+
$this->deleteCasesStarted($caseIds);
103+
$this->deleteCasesParticipated($caseIds);
104+
$this->deleteComments($caseIds, $processRequestIds, $processRequestTokenIds);
105+
106+
// Delete the CaseNumber records that were returned by the query (by their IDs)
97107
CaseNumber::whereIn('id', $caseIds)->delete();
98108

109+
$this->deleteProcessRequestLocks($processRequestIds, $processRequestTokenIds);
110+
$this->deleteInboxRuleLogs($processRequestTokenIds);
111+
$this->deleteInboxRules($processRequestTokenIds);
112+
$this->deleteProcessAbeRequestTokens($processRequestIds, $processRequestTokenIds);
113+
$this->deleteScheduledTasks($processRequestIds, $processRequestTokenIds);
114+
$this->deleteEllucianEthosSyncTasks($processRequestTokenIds);
115+
116+
$this->deleteTaskDraftMedia($draftIds);
117+
$this->deleteTaskDrafts($processRequestTokenIds);
118+
99119
// TODO: Add logs to track the number of cases deleted
100120
// Get deleted timestamp
101121
// $deletedAt = Carbon::now();
102122
// RetentionPolicyLog::record($process->id, $caseIds, $deletedAt);
103123
});
124+
125+
// Delete ProcessRequests after all chunks are processed
126+
// Only delete ProcessRequests that have no remaining cases
127+
if (!empty($processRequestIdsToDelete)) {
128+
$processRequestIdsToDelete = array_unique($processRequestIdsToDelete);
129+
130+
// Filter to only ProcessRequests that have no remaining CaseNumbers
131+
$processRequestIdsWithNoCases = array_filter($processRequestIdsToDelete, function ($requestId) {
132+
return !CaseNumber::where('process_request_id', $requestId)->exists();
133+
});
134+
135+
if (!empty($processRequestIdsWithNoCases)) {
136+
$this->deleteProcessRequests($processRequestIdsWithNoCases);
137+
138+
// Delete any remaining related records
139+
$this->deleteRequestMedia($processRequestIdsWithNoCases);
140+
$this->deleteNotifications($processRequestIdsWithNoCases);
141+
142+
$this->dispatchSavedSearchRecount();
143+
}
144+
}
145+
}
146+
147+
/**
148+
* Build a retention query closure that can be applied to any query builder.
149+
*
150+
* This method encapsulates the retention evaluation logic:
151+
* - Cases created before retention_updated_at: delete if created before (retention_updated_at - retention_period)
152+
* - Cases created after retention_updated_at: delete if created before (now - retention_period)
153+
*
154+
* @param Carbon $retentionUpdatedAt The date when the retention policy was updated
155+
* @param Carbon $oldCasesCutoff The cutoff date for cases created before retention_updated_at
156+
* @param Carbon $newCasesCutoff The cutoff date for cases created after retention_updated_at
157+
* @return \Closure A closure that applies the retention query to a query builder
158+
*/
159+
private function buildRetentionQuery(Carbon $retentionUpdatedAt, Carbon $oldCasesCutoff, Carbon $newCasesCutoff): \Closure
160+
{
161+
return function ($query) use ($retentionUpdatedAt, $oldCasesCutoff, $newCasesCutoff) {
162+
// Cases created before retention_updated_at: delete if created before (retention_updated_at - retention_period)
163+
$query->where(function ($q) use ($retentionUpdatedAt, $oldCasesCutoff) {
164+
$q->where('created_at', '<', $retentionUpdatedAt)
165+
->where('created_at', '<', $oldCasesCutoff);
166+
})
167+
// Cases created after retention_updated_at: delete if created before (now - retention_period)
168+
->orWhere(function ($q) use ($retentionUpdatedAt, $newCasesCutoff) {
169+
$q->where('created_at', '>=', $retentionUpdatedAt)
170+
->where('created_at', '<', $newCasesCutoff);
171+
});
172+
};
173+
}
174+
175+
private function getTaskDraftIds(array $tokenIds): array
176+
{
177+
if ($tokenIds === []) {
178+
return [];
179+
}
180+
181+
return TaskDraft::query()
182+
->whereIn('task_id', $tokenIds)
183+
->pluck('id')
184+
->all();
104185
}
105186
}

0 commit comments

Comments
 (0)