Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions ProcessMaker/Http/Controllers/Api/ProcessRequestController.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
use ProcessMaker\Models\ProcessRequest;
use ProcessMaker\Models\ProcessRequestToken;
use ProcessMaker\Models\User;
use ProcessMaker\Nayra\Contracts\Bpmn\ActivityInterface;
use ProcessMaker\Nayra\Contracts\Bpmn\CatchEventInterface;
use ProcessMaker\Notifications\ProcessCanceledNotification;
use ProcessMaker\Query\SyntaxError;
Expand Down Expand Up @@ -609,6 +610,19 @@ private function cancelRequestToken(ProcessRequest $request)
// Close process request
$request->status = 'CANCELED';
$request->save();

// Close any token still open after status is CANCELED (race: task submit commits after CancelRequest job).
ProcessRequestToken::query()
->where('process_request_id', $request->getKey())
->where('status', '!=', ActivityInterface::TOKEN_STATE_CLOSED)
->update([
'status' => ActivityInterface::TOKEN_STATE_CLOSED,
'completed_at' => now(),
'due_at' => null,
'riskchanges_at' => null,
'user_id' => null,
]);

// Update case status
CaseUpdateStatus::dispatchSync($request);

Expand Down
16 changes: 16 additions & 0 deletions ProcessMaker/Jobs/CancelRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

namespace ProcessMaker\Jobs;

use Carbon\Carbon;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Notification;
use ProcessMaker\Models\ProcessRequest;
use ProcessMaker\Models\ProcessRequestToken;
use ProcessMaker\Nayra\Contracts\Bpmn\ActivityInterface;
use ProcessMaker\Notifications\ProcessCanceledNotification;
use ProcessMaker\Repositories\ExecutionInstanceRepository;
use ProcessMaker\Repositories\TokenRepository;
Expand Down Expand Up @@ -49,5 +52,18 @@ public function action(ProcessRequest $instance)
foreach ($instance->getTokens()->toArray() as $token) {
$tokenRepo->store($token);
}

// Tokens created after the in-memory snapshot (e.g. another user submits a task while
// cancel is confirmed) must still be closed so no ACTIVE task remains on a CANCELED request.
ProcessRequestToken::query()
->where('process_request_id', $instance->getKey())
->where('status', '!=', ActivityInterface::TOKEN_STATE_CLOSED)
->update([
'status' => ActivityInterface::TOKEN_STATE_CLOSED,
'completed_at' => Carbon::now(),
'due_at' => null,
'riskchanges_at' => null,
'user_id' => null,
]);
}
}
2 changes: 1 addition & 1 deletion ProcessMaker/Jobs/RunNayraScriptTask.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class RunNayraScriptTask implements ShouldQueue
/**
* Create a new job instance.
*
* @param \ProcessMaker\Models\ProcessRequestToken $token
* @param ProcessRequestToken $token
* @param array $data
*/
public function __construct(TokenInterface $token)
Expand Down
77 changes: 77 additions & 0 deletions tests/Feature/Api/ProcessRequestsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use ProcessMaker\Models\ProcessRequest;
use ProcessMaker\Models\ProcessRequestToken;
use ProcessMaker\Models\User;
use ProcessMaker\Nayra\Contracts\Bpmn\ActivityInterface;
use Tests\Feature\Shared\RequestHelper;
use Tests\TestCase;

Expand Down Expand Up @@ -524,6 +525,82 @@ public function testCancelRequestWithPermissions()
$response->assertStatus(204);
}

/**
* Canceling a request must leave no non-CLOSED tokens (bulk close in CancelRequest + controller).
*/
public function testCancelProcessRequestClosesAllActiveTokens(): void
{
$request = ProcessRequest::factory()->create([
'status' => 'ACTIVE',
]);

ProcessRequestToken::factory()->count(2)->create([
'process_request_id' => $request->id,
'process_id' => $request->process_id,
'status' => 'ACTIVE',
'user_id' => $this->user->id,
'completed_at' => null,
]);

$route = route('api.requests.update', [$request->id]);
$response = $this->apiCall('PUT', $route, ['status' => 'CANCELED']);

$response->assertStatus(204);

$request->refresh();
$this->assertSame('CANCELED', $request->status);

$openCount = ProcessRequestToken::query()
->where('process_request_id', $request->id)
->where('status', '!=', ActivityInterface::TOKEN_STATE_CLOSED)
->count();

$this->assertSame(0, $openCount);
}

/**
* A token created on an already-canceled request (e.g. race with task completion) is closed on a second cancel.
*/
public function testCancelProcessRequestSecondSweepClosesStrayToken(): void
{
$request = ProcessRequest::factory()->create([
'status' => 'ACTIVE',
]);

ProcessRequestToken::factory()->create([
'process_request_id' => $request->id,
'process_id' => $request->process_id,
'status' => 'ACTIVE',
'user_id' => $this->user->id,
'completed_at' => null,
]);

$route = route('api.requests.update', [$request->id]);
$this->apiCall('PUT', $route, ['status' => 'CANCELED'])->assertStatus(204);

$request->refresh();
$this->assertSame('CANCELED', $request->status);

ProcessRequestToken::factory()->create([
'process_request_id' => $request->id,
'process_id' => $request->process_id,
'status' => 'ACTIVE',
'user_id' => $this->user->id,
'completed_at' => null,
'element_id' => 'stray_after_cancel',
'element_type' => 'task',
]);

$this->apiCall('PUT', $route, ['status' => 'CANCELED'])->assertStatus(204);

$openCount = ProcessRequestToken::query()
->where('process_request_id', $request->id)
->where('status', '!=', ActivityInterface::TOKEN_STATE_CLOSED)
->count();

$this->assertSame(0, $openCount);
}

/**
* Test ability to complete a request if it has the status: ERROR
*/
Expand Down
Loading