Skip to content

Commit c08dc75

Browse files
Merge branch 'epic/FOUR-29101' into task/FOUR-29113
2 parents 343b1f0 + b976ed0 commit c08dc75

5 files changed

Lines changed: 371 additions & 76 deletions

File tree

‎ProcessMaker/Console/Commands/EvaluateCaseRetention.php‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,8 @@ public function handle()
4343
// Get system category IDs to exclude
4444
$systemCategoryIds = ProcessCategory::where('is_system', true)->pluck('id');
4545

46-
// Process all processes when retention policy is enabled
4746
// Exclude processes that are templates or in system categories
48-
// Processes without retention_period will default to 1_year
47+
// Processes without retention_period will default to one_year
4948
$jobCount = 0;
5049
$query = Process::where('is_template', '!=', 1);
5150

@@ -63,6 +62,7 @@ public function handle()
6362
});
6463
}
6564

65+
// Processes without retention_period will default to one_year
6666
$query->chunkById(100, function ($processes) use (&$jobCount) {
6767
foreach ($processes as $process) {
6868
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: 108 additions & 28 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.
@@ -69,14 +74,14 @@ public function handle(): void
6974
return;
7075
}
7176

72-
// Default to 1_year if retention_period is not set
73-
$retentionPeriod = $process->properties['retention_period'] ?? '1_year';
77+
// Default to one_year if retention_period is not set
78+
$retentionPeriod = $process->properties['retention_period'] ?? 'one_year';
7479
$retentionMonths = match ($retentionPeriod) {
75-
'6_months' => 6,
76-
'1_year' => 12,
77-
'3_years' => 36,
78-
'5_years' => 60,
79-
default => 12, // Default to 1_year
80+
'six_months' => 6,
81+
'one_year' => 12,
82+
'three_years' => 36,
83+
'five_years' => 60,
84+
default => 12, // Default to one_year
8085
};
8186

8287
Log::info('EvaluateProcessRetentionJob: Retention configuration loaded', [
@@ -126,41 +131,76 @@ public function handle(): void
126131
// Use subquery to get process request IDs
127132
$processRequestSubquery = ProcessRequest::where('process_id', $this->processId)->select('id');
128133

134+
// Collect all ProcessRequest IDs that will be deleted (to delete them after all chunks are processed)
135+
$processRequestIdsToDelete = [];
129136
$totalDeleted = 0;
130137
$chunkCount = 0;
131-
$processId = $this->processId;
132138

133139
CaseNumber::whereIn('process_request_id', $processRequestSubquery)
134-
->where(function ($query) use ($retentionUpdatedAt, $oldCasesCutoff, $newCasesCutoff) {
135-
// Cases created before retention_updated_at: delete if created before (retention_updated_at - retention_period)
136-
$query->where(function ($q) use ($retentionUpdatedAt, $oldCasesCutoff) {
137-
$q->where('created_at', '<', $retentionUpdatedAt)
138-
->where('created_at', '<', $oldCasesCutoff);
139-
})
140-
// Cases created after retention_updated_at: delete if created before (now - retention_period)
141-
->orWhere(function ($q) use ($retentionUpdatedAt, $newCasesCutoff) {
142-
$q->where('created_at', '>=', $retentionUpdatedAt)
143-
->where('created_at', '<', $newCasesCutoff);
144-
});
145-
})
146-
->chunkById(100, function ($cases) use (&$totalDeleted, &$chunkCount, $processId) {
147-
$caseIds = $cases->pluck('id');
148-
$chunkSize = $caseIds->count();
140+
->where($this->buildRetentionQuery($retentionUpdatedAt, $oldCasesCutoff, $newCasesCutoff))
141+
->chunkById(100, function ($cases) use (&$processRequestIdsToDelete, &$totalDeleted, &$chunkCount) {
142+
$caseIds = $cases->pluck('id')->all();
143+
$processRequestIds = $cases->pluck('process_request_id')->unique()->all();
144+
145+
// Collect ProcessRequest IDs for deletion after all chunks are processed
146+
$processRequestIdsToDelete = array_merge($processRequestIdsToDelete, $processRequestIds);
149147

150-
// Delete the cases
148+
$processRequestTokenIds = ProcessRequestToken::whereIn('process_request_id', $processRequestIds)->pluck('id')->all();
149+
$draftIds = $this->getTaskDraftIds($processRequestTokenIds);
150+
151+
// uses case_number to delete
152+
$this->deleteCasesStarted($caseIds);
153+
$this->deleteCasesParticipated($caseIds);
154+
$this->deleteComments($caseIds, $processRequestIds, $processRequestTokenIds);
155+
156+
// Delete the CaseNumber records that were returned by the query (by their IDs)
151157
CaseNumber::whereIn('id', $caseIds)->delete();
152158

153-
$totalDeleted += $chunkSize;
159+
$this->deleteProcessRequestLocks($processRequestIds, $processRequestTokenIds);
160+
$this->deleteInboxRuleLogs($processRequestTokenIds);
161+
$this->deleteInboxRules($processRequestTokenIds);
162+
$this->deleteProcessAbeRequestTokens($processRequestIds, $processRequestTokenIds);
163+
$this->deleteScheduledTasks($processRequestIds, $processRequestTokenIds);
164+
$this->deleteEllucianEthosSyncTasks($processRequestTokenIds);
165+
166+
$this->deleteTaskDraftMedia($draftIds);
167+
$this->deleteTaskDrafts($processRequestTokenIds);
168+
169+
154170
$chunkCount++;
171+
$chunkSize = count($caseIds);
172+
$totalDeleted += $chunkSize;
155173

156174
Log::info('EvaluateProcessRetentionJob: Deleted chunk of cases', [
157-
'process_id' => $processId,
175+
'process_id' => $this->processId,
158176
'chunk_number' => $chunkCount,
159177
'cases_deleted' => $chunkSize,
160178
]);
161179
});
162180

163-
$executionTime = round((microtime(true) - $startTime) * 1000, 2);
181+
// Delete ProcessRequests after all chunks are processed
182+
// Only delete ProcessRequests that have no remaining cases
183+
if (!empty($processRequestIdsToDelete)) {
184+
$processRequestIdsToDelete = array_unique($processRequestIdsToDelete);
185+
186+
// Filter to only ProcessRequests that have no remaining CaseNumbers
187+
$processRequestIdsWithNoCases = array_filter($processRequestIdsToDelete, function ($requestId) {
188+
return !CaseNumber::where('process_request_id', $requestId)->exists();
189+
});
190+
191+
if (!empty($processRequestIdsWithNoCases)) {
192+
$this->deleteProcessRequests($processRequestIdsWithNoCases);
193+
194+
// Delete any remaining related records
195+
$this->deleteRequestMedia($processRequestIdsWithNoCases);
196+
$this->deleteNotifications($processRequestIdsWithNoCases);
197+
198+
$this->dispatchSavedSearchRecount();
199+
}
200+
}
201+
202+
$endTime = microtime(true);
203+
$executionTime = round(($endTime - $startTime) * 1000, 2);
164204

165205
Log::info('EvaluateProcessRetentionJob: Evaluation completed', [
166206
'process_id' => $this->processId,
@@ -169,4 +209,44 @@ public function handle(): void
169209
'execution_time_ms' => $executionTime,
170210
]);
171211
}
212+
213+
/**
214+
* Build a retention query closure that can be applied to any query builder.
215+
*
216+
* This method encapsulates the retention evaluation logic:
217+
* - Cases created before retention_updated_at: delete if created before (retention_updated_at - retention_period)
218+
* - Cases created after retention_updated_at: delete if created before (now - retention_period)
219+
*
220+
* @param Carbon $retentionUpdatedAt The date when the retention policy was updated
221+
* @param Carbon $oldCasesCutoff The cutoff date for cases created before retention_updated_at
222+
* @param Carbon $newCasesCutoff The cutoff date for cases created after retention_updated_at
223+
* @return \Closure A closure that applies the retention query to a query builder
224+
*/
225+
private function buildRetentionQuery(Carbon $retentionUpdatedAt, Carbon $oldCasesCutoff, Carbon $newCasesCutoff): \Closure
226+
{
227+
return function ($query) use ($retentionUpdatedAt, $oldCasesCutoff, $newCasesCutoff) {
228+
// Cases created before retention_updated_at: delete if created before (retention_updated_at - retention_period)
229+
$query->where(function ($q) use ($retentionUpdatedAt, $oldCasesCutoff) {
230+
$q->where('created_at', '<', $retentionUpdatedAt)
231+
->where('created_at', '<', $oldCasesCutoff);
232+
})
233+
// Cases created after retention_updated_at: delete if created before (now - retention_period)
234+
->orWhere(function ($q) use ($retentionUpdatedAt, $newCasesCutoff) {
235+
$q->where('created_at', '>=', $retentionUpdatedAt)
236+
->where('created_at', '<', $newCasesCutoff);
237+
});
238+
};
239+
}
240+
241+
private function getTaskDraftIds(array $tokenIds): array
242+
{
243+
if ($tokenIds === []) {
244+
return [];
245+
}
246+
247+
return TaskDraft::query()
248+
->whereIn('task_id', $tokenIds)
249+
->pluck('id')
250+
->all();
251+
}
172252
}

0 commit comments

Comments
 (0)