From 01ed082e77f6ea979510da1981ad6c2a1f61b09c Mon Sep 17 00:00:00 2001 From: Eleazar Resendez Date: Fri, 9 Jan 2026 11:46:32 -0600 Subject: [PATCH 1/4] Add case delete endpoint and action with tests --- .../Api/Actions/Cases/DeleteCase.php | 46 ++++++++++++++++++ .../Http/Controllers/Api/CaseController.php | 46 +++++++++++++++--- .../Models/ProcessRequestFactory.php | 16 +++++++ routes/api.php | 1 + tests/Feature/Api/CaseDeleteTest.php | 47 +++++++++++++++++++ 5 files changed, 150 insertions(+), 6 deletions(-) create mode 100644 ProcessMaker/Http/Controllers/Api/Actions/Cases/DeleteCase.php create mode 100644 tests/Feature/Api/CaseDeleteTest.php diff --git a/ProcessMaker/Http/Controllers/Api/Actions/Cases/DeleteCase.php b/ProcessMaker/Http/Controllers/Api/Actions/Cases/DeleteCase.php new file mode 100644 index 0000000000..25e0a00079 --- /dev/null +++ b/ProcessMaker/Http/Controllers/Api/Actions/Cases/DeleteCase.php @@ -0,0 +1,46 @@ +where('case_number', $caseNumber) + ->pluck('id') + ->all(); + + if ($requestIds === []) { + abort(404); + } + + DB::transaction(function () use ($caseNumber, $requestIds) { + CaseStarted::query() + ->where('case_number', $caseNumber) + ->delete(); + + CaseParticipated::query() + ->where('case_number', $caseNumber) + ->delete(); + + CaseNumber::query() + ->whereIn('process_request_id', $requestIds) + ->delete(); + + ProcessRequest::query() + ->whereIn('id', $requestIds) + ->delete(); + }); + } +} diff --git a/ProcessMaker/Http/Controllers/Api/CaseController.php b/ProcessMaker/Http/Controllers/Api/CaseController.php index caaacfc14d..574f13bf04 100644 --- a/ProcessMaker/Http/Controllers/Api/CaseController.php +++ b/ProcessMaker/Http/Controllers/Api/CaseController.php @@ -2,6 +2,8 @@ namespace ProcessMaker\Http\Controllers\Api; +use Illuminate\Http\JsonResponse; +use ProcessMaker\Http\Controllers\Api\Actions\Cases\DeleteCase; use ProcessMaker\Http\Controllers\Controller; use ProcessMaker\Models\Process; use ProcessMaker\Models\ProcessRequest; @@ -12,7 +14,7 @@ class CaseController extends Controller /** * Get stage information for cases */ - public function getStagePerCase($case_number = null) + public function getStagePerCase(?string $case_number = null): JsonResponse { if (!empty($case_number)) { $responseData = $this->getSpecificCaseStages($case_number); @@ -31,12 +33,44 @@ public function getStagePerCase($case_number = null) return response()->json($responseData); } + /** + * Delete a case and its related requests. + * + * @param string $case_number + * @return JsonResponse + * + * @OA\Delete( + * path="/cases/{case_number}", + * summary="Delete a case and its related requests", + * operationId="deleteCase", + * tags={"Cases"}, + * @OA\Parameter( + * description="Case number to delete", + * in="path", + * name="case_number", + * required=true, + * @OA\Schema(type="string") + * ), + * @OA\Response( + * response=204, + * description="success" + * ), + * @OA\Response(response=404, ref="#/components/responses/404"), + * ) + */ + public function destroy(string $case_number): JsonResponse + { + (new DeleteCase)($case_number); + + return response()->json([], 204); + } + /** * Get specific case stages information * @param string $caseNumber The unique identifier of the case to retrieve stages for * @return array */ - private function getSpecificCaseStages($caseNumber) + private function getSpecificCaseStages(string $caseNumber): array { $allRequests = ProcessRequest::where('case_number', $caseNumber)->get(); // Check if any requests were found @@ -75,7 +109,7 @@ private function getSpecificCaseStages($caseNumber) * @param string|null $status The status to set for the stages * @return array */ - private function getDefaultCaseStages($status = null) + private function getDefaultCaseStages(?string $status = null): array { return [ [ @@ -100,7 +134,7 @@ private function getDefaultCaseStages($status = null) * @param string $stageName The name of the stage ('In Progress' or 'Completed') * @return string The mapped status */ - private function mapStatus($status, $stageName) + private function mapStatus(?string $status, string $stageName): string { if ($status === 'COMPLETED') { return 'Done'; @@ -120,11 +154,11 @@ private function mapStatus($status, $stageName) /** * Get the stages summary based on the provided request. * - * @param $requestId + * @param ProcessRequest $request * @return array An array of stage results, each containing the stage ID, name, status, * and completion date. */ - private function getStagesSummary(ProcessRequest $request) + private function getStagesSummary(ProcessRequest $request): array { $requestId = $request->id; $processId = $request->process_id; diff --git a/database/factories/ProcessMaker/Models/ProcessRequestFactory.php b/database/factories/ProcessMaker/Models/ProcessRequestFactory.php index 9c9ab875b3..5a4d2f7464 100644 --- a/database/factories/ProcessMaker/Models/ProcessRequestFactory.php +++ b/database/factories/ProcessMaker/Models/ProcessRequestFactory.php @@ -47,4 +47,20 @@ public function definition() }, ]; } + + public function withCaseNumber(int $caseNumber): self + { + $caseTitle = $this->faker->words(4, true); + + return $this->state([ + 'case_number' => $caseNumber, + 'case_title' => $caseTitle, + 'case_title_formatted' => $caseTitle, + ])->afterCreating(function (ProcessRequest $request) use ($caseNumber, $caseTitle) { + $request->case_number = $caseNumber; + $request->case_title = $caseTitle; + $request->case_title_formatted = $caseTitle; + $request->save(); + }); + } } diff --git a/routes/api.php b/routes/api.php index 2b3ffc38bb..04a8c96f5b 100644 --- a/routes/api.php +++ b/routes/api.php @@ -235,6 +235,7 @@ // Cases Route::get('cases/stages_bar/{case_number?}', [CaseController::class, 'getStagePerCase'])->name('cases.stage'); + Route::delete('cases/{case_number}', [CaseController::class, 'destroy'])->name('cases.destroy'); // TaskDrafts Route::put('drafts/{task}', [TaskDraftController::class, 'update'])->name('taskdraft.update'); diff --git a/tests/Feature/Api/CaseDeleteTest.php b/tests/Feature/Api/CaseDeleteTest.php new file mode 100644 index 0000000000..da4e6689ed --- /dev/null +++ b/tests/Feature/Api/CaseDeleteTest.php @@ -0,0 +1,47 @@ +count(2) + ->withCaseNumber($caseNumber) + ->create(); + + CaseNumber::query()->create(['process_request_id' => $requests->first()->id]); + CaseNumber::query()->create(['process_request_id' => $requests->last()->id]); + CaseStarted::factory()->create(['case_number' => $caseNumber]); + CaseParticipated::factory()->create(['case_number' => $caseNumber]); + + $response = $this->apiCall('DELETE', route('api.cases.destroy', ['case_number' => $caseNumber])); + + $response->assertStatus(204); + $this->assertDatabaseMissing('process_requests', ['case_number' => $caseNumber]); + $this->assertDatabaseMissing('cases_started', ['case_number' => $caseNumber]); + $this->assertDatabaseMissing('cases_participated', ['case_number' => $caseNumber]); + $this->assertDatabaseMissing('case_numbers', ['process_request_id' => $requests->first()->id]); + $this->assertDatabaseMissing('case_numbers', ['process_request_id' => $requests->last()->id]); + } + + public function testDeleteCaseReturnsNotFoundWhenMissing(): void + { + $caseNumber = 99999; + + $response = $this->apiCall('DELETE', route('api.cases.destroy', ['case_number' => $caseNumber])); + + $response->assertStatus(404); + } +} From b8628292e39244d6f6075c06987f450b30ed8960 Mon Sep 17 00:00:00 2001 From: Eleazar Resendez Date: Mon, 12 Jan 2026 12:21:44 -0600 Subject: [PATCH 2/4] Remove case dependencies on delete and expand factories/tests --- .../Api/Actions/Cases/DeleteCase.php | 246 ++++++++++++++++-- .../ProcessMaker/Models/CaseNumberFactory.php | 21 ++ .../Models/InboxRuleLogFactory.php | 23 +- .../Models/ProcessRequestLockFactory.php | 26 ++ .../Models/ScheduledTaskFactory.php | 9 + tests/Feature/Api/CaseDeleteTest.php | 120 ++++++++- 6 files changed, 423 insertions(+), 22 deletions(-) create mode 100644 database/factories/ProcessMaker/Models/CaseNumberFactory.php create mode 100644 database/factories/ProcessMaker/Models/ProcessRequestLockFactory.php diff --git a/ProcessMaker/Http/Controllers/Api/Actions/Cases/DeleteCase.php b/ProcessMaker/Http/Controllers/Api/Actions/Cases/DeleteCase.php index 25e0a00079..265d46257a 100644 --- a/ProcessMaker/Http/Controllers/Api/Actions/Cases/DeleteCase.php +++ b/ProcessMaker/Http/Controllers/Api/Actions/Cases/DeleteCase.php @@ -3,44 +3,258 @@ namespace ProcessMaker\Http\Controllers\Api\Actions\Cases; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; use ProcessMaker\Models\CaseNumber; use ProcessMaker\Models\CaseParticipated; use ProcessMaker\Models\CaseStarted; +use ProcessMaker\Models\Comment; +use ProcessMaker\Models\InboxRule; +use ProcessMaker\Models\InboxRuleLog; +use ProcessMaker\Models\Media; +use ProcessMaker\Models\ProcessAbeRequestToken; use ProcessMaker\Models\ProcessRequest; +use ProcessMaker\Models\ProcessRequestLock; +use ProcessMaker\Models\ProcessRequestToken; +use ProcessMaker\Models\ScheduledTask; +use ProcessMaker\Models\TaskDraft; class DeleteCase { public function __invoke(string $caseNumber): void { - // Delete later dependent records for requests and tokens (process_request_tokens, - // process_request_locks, process_abe_request_tokens, scheduled_tasks, - // inbox_rules, inbox_rule_logs, ellucian_ethos_sync_global_task_list, comments). + $requestIds = $this->getRequestIds($caseNumber); - $requestIds = ProcessRequest::query() + if ($requestIds === []) { + abort(404); + } + + $tokenIds = $this->getRequestTokenIds($requestIds); + + DB::transaction(function () use ($caseNumber, $requestIds, $tokenIds) { + $this->deleteInboxRuleLogs($tokenIds); + $this->deleteInboxRules($tokenIds); + $this->deleteProcessRequestLocks($requestIds, $tokenIds); + $this->deleteProcessAbeRequestTokens($requestIds, $tokenIds); + $this->deleteScheduledTasks($requestIds, $tokenIds); + $this->deleteEllucianEthosSyncTasks($tokenIds); + $draftIds = $this->getTaskDraftIds($tokenIds); + $this->deleteTaskDraftMedia($draftIds); + $this->deleteTaskDrafts($tokenIds); + $this->deleteComments($caseNumber, $requestIds, $tokenIds); + $this->deleteRequestMedia($requestIds); + $this->deleteCaseNumbers($requestIds); + $this->deleteCasesStarted($caseNumber); + $this->deleteCasesParticipated($caseNumber); + $this->deleteProcessRequestTokens($requestIds); + $this->deleteProcessRequests($requestIds); + }); + } + + private function getRequestIds(string $caseNumber): array + { + return ProcessRequest::query() ->where('case_number', $caseNumber) ->pluck('id') ->all(); + } + private function getRequestTokenIds(array $requestIds): array + { if ($requestIds === []) { - abort(404); + return []; } - DB::transaction(function () use ($caseNumber, $requestIds) { - CaseStarted::query() - ->where('case_number', $caseNumber) - ->delete(); + return ProcessRequestToken::query() + ->whereIn('process_request_id', $requestIds) + ->pluck('id') + ->all(); + } + + private function getTaskDraftIds(array $tokenIds): array + { + if ($tokenIds === []) { + return []; + } + + return TaskDraft::query() + ->whereIn('task_id', $tokenIds) + ->pluck('id') + ->all(); + } + + private function deleteCasesStarted(string $caseNumber): void + { + CaseStarted::query() + ->where('case_number', $caseNumber) + ->delete(); + } + + private function deleteCasesParticipated(string $caseNumber): void + { + CaseParticipated::query() + ->where('case_number', $caseNumber) + ->delete(); + } + + private function deleteCaseNumbers(array $requestIds): void + { + if ($requestIds === []) { + return; + } + + CaseNumber::query() + ->whereIn('process_request_id', $requestIds) + ->delete(); + } + + private function deleteProcessRequests(array $requestIds): void + { + if ($requestIds === []) { + return; + } + + ProcessRequest::query() + ->whereIn('id', $requestIds) + ->delete(); + } + + private function deleteProcessRequestTokens(array $requestIds): void + { + if ($requestIds === []) { + return; + } + + ProcessRequestToken::query() + ->whereIn('process_request_id', $requestIds) + ->delete(); + } - CaseParticipated::query() - ->where('case_number', $caseNumber) + private function deleteProcessRequestLocks(array $requestIds, array $tokenIds): void + { + ProcessRequestLock::query() + ->whereIn('process_request_id', $requestIds) + ->delete(); + + if ($tokenIds !== []) { + ProcessRequestLock::query() + ->whereIn('process_request_token_id', $tokenIds) ->delete(); + } + } + + private function deleteProcessAbeRequestTokens(array $requestIds, array $tokenIds): void + { + ProcessAbeRequestToken::query() + ->whereIn('process_request_id', $requestIds) + ->delete(); - CaseNumber::query() - ->whereIn('process_request_id', $requestIds) + if ($tokenIds !== []) { + ProcessAbeRequestToken::query() + ->whereIn('process_request_token_id', $tokenIds) ->delete(); + } + } + + private function deleteScheduledTasks(array $requestIds, array $tokenIds): void + { + ScheduledTask::query() + ->whereIn('process_request_id', $requestIds) + ->delete(); - ProcessRequest::query() - ->whereIn('id', $requestIds) + if ($tokenIds !== []) { + ScheduledTask::query() + ->whereIn('process_request_token_id', $tokenIds) ->delete(); - }); + } + } + + private function deleteInboxRules(array $tokenIds): void + { + if ($tokenIds === []) { + return; + } + + InboxRule::query() + ->whereIn('process_request_token_id', $tokenIds) + ->delete(); + } + + private function deleteInboxRuleLogs(array $tokenIds): void + { + if ($tokenIds === []) { + return; + } + + InboxRuleLog::query() + ->whereIn('process_request_token_id', $tokenIds) + ->delete(); + } + + private function deleteEllucianEthosSyncTasks(array $tokenIds): void + { + if ($tokenIds === [] || !Schema::hasTable('ellucian_ethos_sync_global_task_list')) { + return; + } + + DB::table('ellucian_ethos_sync_global_task_list') + ->whereIn('process_request_token_id', $tokenIds) + ->delete(); + } + + private function deleteTaskDrafts(array $tokenIds): void + { + if ($tokenIds === []) { + return; + } + + TaskDraft::query() + ->whereIn('task_id', $tokenIds) + ->delete(); + } + + private function deleteTaskDraftMedia(array $draftIds): void + { + if ($draftIds === []) { + return; + } + + Media::query() + ->where('model_type', TaskDraft::class) + ->whereIn('model_id', $draftIds) + ->get() + ->each + ->delete(); + } + + private function deleteRequestMedia(array $requestIds): void + { + if ($requestIds === []) { + return; + } + + Media::query() + ->where('model_type', ProcessRequest::class) + ->whereIn('model_id', $requestIds) + ->get() + ->each + ->delete(); + } + + private function deleteComments(string $caseNumber, array $requestIds, array $tokenIds): void + { + Comment::query() + ->where('case_number', $caseNumber) + ->orWhere(function ($query) use ($requestIds, $tokenIds) { + $query->where('commentable_type', ProcessRequest::class) + ->whereIn('commentable_id', $requestIds); + + if ($tokenIds !== []) { + $query->orWhere(function ($nestedQuery) use ($tokenIds) { + $nestedQuery->where('commentable_type', ProcessRequestToken::class) + ->whereIn('commentable_id', $tokenIds); + }); + } + }) + ->delete(); } } diff --git a/database/factories/ProcessMaker/Models/CaseNumberFactory.php b/database/factories/ProcessMaker/Models/CaseNumberFactory.php new file mode 100644 index 0000000000..328293278a --- /dev/null +++ b/database/factories/ProcessMaker/Models/CaseNumberFactory.php @@ -0,0 +1,21 @@ + function () { + return ProcessRequest::factory()->create()->id; + }, + ]; + } +} diff --git a/database/factories/ProcessMaker/Models/InboxRuleLogFactory.php b/database/factories/ProcessMaker/Models/InboxRuleLogFactory.php index 1099e9e308..e94d0f6842 100644 --- a/database/factories/ProcessMaker/Models/InboxRuleLogFactory.php +++ b/database/factories/ProcessMaker/Models/InboxRuleLogFactory.php @@ -2,8 +2,11 @@ namespace Database\Factories\ProcessMaker\Models; -use ProcessMaker\Models\InboxRuleLog; use Illuminate\Database\Eloquent\Factories\Factory; +use ProcessMaker\Models\InboxRule; +use ProcessMaker\Models\InboxRuleLog; +use ProcessMaker\Models\ProcessRequestToken; +use ProcessMaker\Models\User; /** * @extends \Illuminate\Database\Eloquent\Factories\Factory<\ProcessMaker\Models\InboxRule> @@ -15,9 +18,21 @@ class InboxRuleLogFactory extends Factory public function definition() { return [ - 'inbox_rule_id' => null, - 'task_id' => null, - 'inbox_rule_data' => json_encode(['key1' => 'value1', 'key2' => 'value2']), + 'user_id' => function () { + return User::factory()->create()->id; + }, + 'inbox_rule_id' => function () { + return InboxRule::factory()->create()->id; + }, + 'process_request_token_id' => function () { + return ProcessRequestToken::factory()->create()->id; + }, + 'inbox_rule_attributes' => [ + 'make_draft' => false, + 'submit_data' => false, + 'mark_as_priority' => false, + 'reassign_to_user_id' => null, + ], 'created_at' => now(), 'updated_at' => now(), ]; diff --git a/database/factories/ProcessMaker/Models/ProcessRequestLockFactory.php b/database/factories/ProcessMaker/Models/ProcessRequestLockFactory.php new file mode 100644 index 0000000000..2debf1fc26 --- /dev/null +++ b/database/factories/ProcessMaker/Models/ProcessRequestLockFactory.php @@ -0,0 +1,26 @@ + function () { + return ProcessRequest::factory()->create()->id; + }, + 'process_request_token_id' => function () { + return ProcessRequestToken::factory()->create()->id; + }, + 'due_at' => now()->addMinute(), + ]; + } +} diff --git a/database/factories/ProcessMaker/Models/ScheduledTaskFactory.php b/database/factories/ProcessMaker/Models/ScheduledTaskFactory.php index 8c017adaf6..27d47f23b6 100644 --- a/database/factories/ProcessMaker/Models/ScheduledTaskFactory.php +++ b/database/factories/ProcessMaker/Models/ScheduledTaskFactory.php @@ -38,4 +38,13 @@ public function definition() 'configuration' => '{}', ]; } + + public function forToken(ProcessRequestToken $token): self + { + return $this->state([ + 'process_id' => $token->process_id, + 'process_request_id' => $token->process_request_id, + 'process_request_token_id' => $token->getKey(), + ]); + } } diff --git a/tests/Feature/Api/CaseDeleteTest.php b/tests/Feature/Api/CaseDeleteTest.php index da4e6689ed..3673ed926f 100644 --- a/tests/Feature/Api/CaseDeleteTest.php +++ b/tests/Feature/Api/CaseDeleteTest.php @@ -2,10 +2,22 @@ namespace Tests\Feature\Api; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; +use Illuminate\Support\Str; use ProcessMaker\Models\CaseNumber; use ProcessMaker\Models\CaseParticipated; use ProcessMaker\Models\CaseStarted; +use ProcessMaker\Models\Comment; +use ProcessMaker\Models\InboxRule; +use ProcessMaker\Models\InboxRuleLog; +use ProcessMaker\Models\Media; +use ProcessMaker\Models\ProcessAbeRequestToken; use ProcessMaker\Models\ProcessRequest; +use ProcessMaker\Models\ProcessRequestLock; +use ProcessMaker\Models\ProcessRequestToken; +use ProcessMaker\Models\ScheduledTask; +use ProcessMaker\Models\TaskDraft; use Tests\Feature\Shared\RequestHelper; use Tests\TestCase; @@ -21,8 +33,8 @@ public function testDeleteCaseRemovesCoreRecords(): void ->withCaseNumber($caseNumber) ->create(); - CaseNumber::query()->create(['process_request_id' => $requests->first()->id]); - CaseNumber::query()->create(['process_request_id' => $requests->last()->id]); + CaseNumber::factory()->create(['process_request_id' => $requests->first()->id]); + CaseNumber::factory()->create(['process_request_id' => $requests->last()->id]); CaseStarted::factory()->create(['case_number' => $caseNumber]); CaseParticipated::factory()->create(['case_number' => $caseNumber]); @@ -36,6 +48,110 @@ public function testDeleteCaseRemovesCoreRecords(): void $this->assertDatabaseMissing('case_numbers', ['process_request_id' => $requests->last()->id]); } + public function testDeleteCaseRemovesDependentRecords(): void + { + $caseNumber = 24680; + $request = ProcessRequest::factory() + ->withCaseNumber($caseNumber) + ->create(); + $token = ProcessRequestToken::factory()->create([ + 'process_request_id' => $request->id, + 'process_id' => $request->process_id, + 'user_id' => $this->user->id, + ]); + + CaseNumber::factory()->create(['process_request_id' => $request->id]); + CaseStarted::factory()->create(['case_number' => $caseNumber]); + CaseParticipated::factory()->create(['case_number' => $caseNumber]); + + ProcessRequestLock::factory()->create([ + 'process_request_id' => $request->id, + 'process_request_token_id' => $token->id, + ]); + + DB::table('scheduled_tasks')->insert( + ScheduledTask::factory()->forToken($token)->raw([ + 'type' => 'INTERMEDIATE_TIMER_EVENT', + 'configuration' => '{}', + 'created_at' => now(), + 'updated_at' => now(), + ]) + ); + + $inboxRule = InboxRule::factory()->create([ + 'user_id' => $this->user->id, + 'process_request_token_id' => $token->id, + ]); + + InboxRuleLog::factory()->create([ + 'user_id' => $this->user->id, + 'inbox_rule_id' => $inboxRule->id, + 'process_request_token_id' => $token->id, + ]); + + ProcessAbeRequestToken::factory()->create([ + 'process_request_id' => $request->id, + 'process_request_token_id' => $token->id, + ]); + + $draft = TaskDraft::factory()->create([ + 'task_id' => $token->id, + 'data' => ['key1' => 'value1'], + ]); + + Media::factory()->create([ + 'model_type' => TaskDraft::class, + 'model_id' => $draft->id, + ]); + + Media::factory()->create([ + 'model_type' => ProcessRequest::class, + 'model_id' => $request->id, + 'custom_properties' => [ + 'data_name' => 'case/file.txt', + ], + ]); + + Comment::factory()->create([ + 'commentable_type' => ProcessRequest::class, + 'commentable_id' => $request->id, + 'case_number' => $caseNumber, + ]); + Comment::factory()->create([ + 'commentable_type' => ProcessRequestToken::class, + 'commentable_id' => $token->id, + 'case_number' => $caseNumber, + ]); + + if (Schema::hasTable('ellucian_ethos_sync_global_task_list')) { + DB::table('ellucian_ethos_sync_global_task_list')->insert([ + 'user_id' => $this->user->id, + 'global_task_uuid' => (string) Str::uuid(), + 'process_request_token_id' => $token->id, + 'status' => 'open', + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + + $response = $this->apiCall('DELETE', route('api.cases.destroy', ['case_number' => $caseNumber])); + + $response->assertStatus(204); + $this->assertDatabaseMissing('process_request_tokens', ['id' => $token->id]); + $this->assertDatabaseMissing('process_request_locks', ['process_request_id' => $request->id]); + $this->assertDatabaseMissing('scheduled_tasks', ['process_request_id' => $request->id]); + $this->assertDatabaseMissing('inbox_rules', ['id' => $inboxRule->id]); + $this->assertDatabaseMissing('inbox_rule_logs', ['process_request_token_id' => $token->id]); + $this->assertDatabaseMissing('process_abe_request_tokens', ['process_request_token_id' => $token->id]); + $this->assertDatabaseMissing('task_drafts', ['task_id' => $token->id]); + $this->assertDatabaseMissing('media', ['model_type' => TaskDraft::class, 'model_id' => $draft->id]); + $this->assertDatabaseMissing('media', ['model_type' => ProcessRequest::class, 'model_id' => $request->id]); + $this->assertSoftDeleted('comments', ['case_number' => $caseNumber]); + if (Schema::hasTable('ellucian_ethos_sync_global_task_list')) { + $this->assertDatabaseMissing('ellucian_ethos_sync_global_task_list', ['process_request_token_id' => $token->id]); + } + } + public function testDeleteCaseReturnsNotFoundWhenMissing(): void { $caseNumber = 99999; From 642b34ffbefd218edb13c8f9ce8ef9f16bd96c9f Mon Sep 17 00:00:00 2001 From: Eleazar Resendez Date: Mon, 12 Jan 2026 12:31:57 -0600 Subject: [PATCH 3/4] Document case delete endpoint in Swagger --- ProcessMaker/Http/Controllers/Api/CaseController.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ProcessMaker/Http/Controllers/Api/CaseController.php b/ProcessMaker/Http/Controllers/Api/CaseController.php index 574f13bf04..1d4ba3a891 100644 --- a/ProcessMaker/Http/Controllers/Api/CaseController.php +++ b/ProcessMaker/Http/Controllers/Api/CaseController.php @@ -55,7 +55,19 @@ public function getStagePerCase(?string $case_number = null): JsonResponse * response=204, * description="success" * ), + * @OA\Response( + * response=401, + * description="Unauthorized" + * ), * @OA\Response(response=404, ref="#/components/responses/404"), + * @OA\Response( + * response=409, + * description="Conflict" + * ), + * @OA\Response( + * response=500, + * description="Internal Server Error" + * ), * ) */ public function destroy(string $case_number): JsonResponse From 350548bf095fbd68bcacbc9f6697c79c3b506942 Mon Sep 17 00:00:00 2001 From: Eleazar Resendez Date: Mon, 12 Jan 2026 13:12:41 -0600 Subject: [PATCH 4/4] Fix delete cascades for inbox rules and process requests --- .../Http/Controllers/Api/Actions/Cases/DeleteCase.php | 4 ++++ ProcessMaker/Models/InboxRule.php | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/ProcessMaker/Http/Controllers/Api/Actions/Cases/DeleteCase.php b/ProcessMaker/Http/Controllers/Api/Actions/Cases/DeleteCase.php index 265d46257a..eb9dd51813 100644 --- a/ProcessMaker/Http/Controllers/Api/Actions/Cases/DeleteCase.php +++ b/ProcessMaker/Http/Controllers/Api/Actions/Cases/DeleteCase.php @@ -115,6 +115,8 @@ private function deleteProcessRequests(array $requestIds): void ProcessRequest::query() ->whereIn('id', $requestIds) + ->get() + ->each ->delete(); } @@ -176,6 +178,8 @@ private function deleteInboxRules(array $tokenIds): void InboxRule::query() ->whereIn('process_request_token_id', $tokenIds) + ->get() + ->each ->delete(); } diff --git a/ProcessMaker/Models/InboxRule.php b/ProcessMaker/Models/InboxRule.php index 59107b38f9..e83c55f5e3 100644 --- a/ProcessMaker/Models/InboxRule.php +++ b/ProcessMaker/Models/InboxRule.php @@ -35,7 +35,9 @@ class InboxRule extends ProcessMakerModel protected static function booted() { static::deleting(function (InboxRule $inboxRule) { - $inboxRule->savedSearch()->delete(); + if (class_exists(SavedSearch::class)) { + $inboxRule->savedSearch()->delete(); + } }); }