Skip to content

Commit 2b206a0

Browse files
author
Dominic Tubach
committed
Add finish review tasks
1 parent e50dc55 commit 2b206a0

10 files changed

+942
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
<?php
2+
/*
3+
* Copyright (C) 2025 SYSTOPIA GmbH
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published by
7+
* the Free Software Foundation in version 3.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU Affero General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU Affero General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
declare(strict_types = 1);
19+
20+
namespace Civi\Funding\ApplicationProcess\Task;
21+
22+
use Civi\Funding\ActivityStatusNames;
23+
use Civi\Funding\ApplicationProcess\ActionStatusInfo\ApplicationProcessActionStatusInfoContainer;
24+
use Civi\Funding\ApplicationProcess\ActionStatusInfo\ApplicationProcessActionStatusInfoInterface;
25+
use Civi\Funding\ApplicationProcess\ApplicationProcessPermissions;
26+
use Civi\Funding\Entity\ApplicationProcessEntity;
27+
use Civi\Funding\Entity\ApplicationProcessEntityBundle;
28+
use Civi\Funding\Entity\FundingCaseTypeEntity;
29+
use Civi\Funding\Entity\FundingTaskEntity;
30+
use Civi\Funding\Task\Handler\ApplicationProcessTaskHandlerInterface;
31+
use CRM_Funding_ExtensionUtil as E;
32+
33+
abstract class AbstractApplicationReviewFinishTaskHandler implements ApplicationProcessTaskHandlerInterface {
34+
35+
private const TASK_TYPE = 'review_finish';
36+
37+
private ApplicationProcessActionStatusInfoContainer $infoContainer;
38+
39+
/**
40+
* @phpstan-return list<string>
41+
*/
42+
abstract public static function getSupportedFundingCaseTypes(): array;
43+
44+
public function __construct(ApplicationProcessActionStatusInfoContainer $infoContainer) {
45+
$this->infoContainer = $infoContainer;
46+
}
47+
48+
public function createTasksOnChange(
49+
ApplicationProcessEntityBundle $applicationProcessBundle,
50+
ApplicationProcessEntity $previousApplicationProcess
51+
): iterable {
52+
if ($this->isReviewFinishOutstanding($applicationProcessBundle)) {
53+
yield $this->createReviewFinishTask($applicationProcessBundle);
54+
}
55+
}
56+
57+
/**
58+
* @codeCoverageIgnore
59+
*/
60+
public function createTasksOnNew(ApplicationProcessEntityBundle $applicationProcessBundle): iterable {
61+
return [];
62+
}
63+
64+
public function modifyTask(
65+
FundingTaskEntity $task,
66+
ApplicationProcessEntityBundle $applicationProcessBundle,
67+
ApplicationProcessEntity $previousApplicationProcess
68+
): bool {
69+
if (self::TASK_TYPE === $task->getType()) {
70+
if (!$this->isInReviewStatus($applicationProcessBundle)) {
71+
$task->setStatusName(ActivityStatusNames::COMPLETED);
72+
73+
return TRUE;
74+
}
75+
76+
if (!$this->isCalculativeAndContentReviewFinished($applicationProcessBundle)) {
77+
$task->setStatusName(ActivityStatusNames::CANCELLED);
78+
79+
return TRUE;
80+
}
81+
82+
$applicationProcess = $applicationProcessBundle->getApplicationProcess();
83+
if ($this->areReviewerContactsChanged($applicationProcess, $previousApplicationProcess)) {
84+
$task->setAssigneeContactIds($this->getAssigneeContactIds($applicationProcess));
85+
86+
return TRUE;
87+
}
88+
}
89+
90+
return FALSE;
91+
}
92+
93+
final protected function getInfo(
94+
FundingCaseTypeEntity $fundingCaseType
95+
): ApplicationProcessActionStatusInfoInterface {
96+
return $this->infoContainer->get($fundingCaseType->getName());
97+
}
98+
99+
/**
100+
* @phpstan-return non-empty-list<string>
101+
* One of the returned permissions is required to approve an application.
102+
*/
103+
protected function getRequiredPermissions(): array {
104+
return [
105+
ApplicationProcessPermissions::REVIEW_CALCULATIVE,
106+
ApplicationProcessPermissions::REVIEW_CONTENT,
107+
];
108+
}
109+
110+
protected function getTaskSubject(ApplicationProcessEntityBundle $applicationProcessBundle): string {
111+
return E::ts('Finish Application Review');
112+
}
113+
114+
protected function isReviewFinishOutstanding(
115+
ApplicationProcessEntityBundle $applicationProcessBundle
116+
): bool {
117+
return $this->isInReviewStatus($applicationProcessBundle)
118+
&& $this->isCalculativeAndContentReviewFinished($applicationProcessBundle);
119+
}
120+
121+
final protected function isCalculativeAndContentReviewFinished(
122+
ApplicationProcessEntityBundle $applicationProcessBundle
123+
): bool {
124+
return NULL !== $applicationProcessBundle->getApplicationProcess()->getIsReviewCalculative()
125+
&& NULL !== $applicationProcessBundle->getApplicationProcess()->getIsReviewContent();
126+
}
127+
128+
protected function isInReviewStatus(ApplicationProcessEntityBundle $applicationProcessBundle): bool {
129+
return $this->getInfo($applicationProcessBundle->getFundingCaseType())
130+
->isReviewStatus($applicationProcessBundle->getApplicationProcess()->getStatus());
131+
}
132+
133+
private function areReviewerContactsChanged(
134+
ApplicationProcessEntity $applicationProcess,
135+
ApplicationProcessEntity $previousApplicationProcess
136+
): bool {
137+
return $applicationProcess->getReviewerCalculativeContactId()
138+
!== $previousApplicationProcess->getReviewerCalculativeContactId()
139+
|| $applicationProcess->getReviewerContentContactId()
140+
!== $previousApplicationProcess->getReviewerContentContactId();
141+
}
142+
143+
private function createReviewFinishTask(ApplicationProcessEntityBundle $applicationProcessBundle): FundingTaskEntity {
144+
return FundingTaskEntity::newTask([
145+
'subject' => $this->getTaskSubject($applicationProcessBundle),
146+
'affected_identifier' => $applicationProcessBundle->getApplicationProcess()->getIdentifier(),
147+
'required_permissions' => $this->getRequiredPermissions(),
148+
'type' => self::TASK_TYPE,
149+
'funding_case_id' => $applicationProcessBundle->getFundingCase()->getId(),
150+
'application_process_id' => $applicationProcessBundle->getApplicationProcess()->getId(),
151+
'assignee_contact_ids' => $this->getAssigneeContactIds($applicationProcessBundle->getApplicationProcess()),
152+
]);
153+
}
154+
155+
/**
156+
* @phpstan-return list<int>
157+
*/
158+
private function getAssigneeContactIds(ApplicationProcessEntity $applicationProcess): array {
159+
$assigneeContactIds = [];
160+
if (NULL !== $applicationProcess->getReviewerCalculativeContactId()) {
161+
$assigneeContactIds[] = $applicationProcess->getReviewerCalculativeContactId();
162+
}
163+
if (NULL !== $applicationProcess->getReviewerContentContactId()) {
164+
$assigneeContactIds[] = $applicationProcess->getReviewerContentContactId();
165+
}
166+
167+
return $assigneeContactIds;
168+
}
169+
170+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
<?php
2+
/*
3+
* Copyright (C) 2025 SYSTOPIA GmbH
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published by
7+
* the Free Software Foundation in version 3.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU Affero General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU Affero General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
declare(strict_types = 1);
19+
20+
namespace Civi\Funding\ClearingProcess\Task;
21+
22+
use Civi\Funding\ActivityStatusNames;
23+
use Civi\Funding\ClearingProcess\ClearingProcessPermissions;
24+
use Civi\Funding\Entity\ClearingProcessEntity;
25+
use Civi\Funding\Entity\ClearingProcessEntityBundle;
26+
use Civi\Funding\Entity\FundingTaskEntity;
27+
use Civi\Funding\Task\Handler\ClearingProcessTaskHandlerInterface;
28+
use CRM_Funding_ExtensionUtil as E;
29+
30+
abstract class AbstractClearingReviewFinishTaskHandler implements ClearingProcessTaskHandlerInterface {
31+
32+
private const TASK_TYPE = 'review_finish';
33+
34+
/**
35+
* @phpstan-return list<string>
36+
*/
37+
abstract public static function getSupportedFundingCaseTypes(): array;
38+
39+
public function createTasksOnChange(
40+
ClearingProcessEntityBundle $clearingProcessBundle,
41+
ClearingProcessEntity $previousClearingProcess
42+
): iterable {
43+
if ($this->isReviewFinishOutstanding($clearingProcessBundle)) {
44+
yield $this->createReviewFinishTask($clearingProcessBundle);
45+
}
46+
}
47+
48+
public function createTasksOnNew(ClearingProcessEntityBundle $clearingProcessBundle): iterable {
49+
return [];
50+
}
51+
52+
public function modifyTask(
53+
FundingTaskEntity $task,
54+
ClearingProcessEntityBundle $clearingProcessBundle,
55+
ClearingProcessEntity $previousClearingProcess
56+
): bool {
57+
if (self::TASK_TYPE === $task->getType()) {
58+
if (!$this->isInReviewStatus($clearingProcessBundle)) {
59+
$task->setStatusName(ActivityStatusNames::COMPLETED);
60+
61+
return TRUE;
62+
}
63+
64+
if (!$this->isCalculativeAndContentReviewFinished($clearingProcessBundle)) {
65+
$task->setStatusName(ActivityStatusNames::CANCELLED);
66+
67+
return TRUE;
68+
}
69+
70+
if ($this->areReviewerContactsChanged($clearingProcessBundle->getClearingProcess(), $previousClearingProcess)) {
71+
$task->setAssigneeContactIds($this->getAssigneeContactIds($clearingProcessBundle->getClearingProcess()));
72+
73+
return TRUE;
74+
}
75+
}
76+
77+
return FALSE;
78+
}
79+
80+
/**
81+
* @phpstan-return non-empty-list<string>
82+
* One of the returned permissions is required to approve a clearing.
83+
*/
84+
protected function getRequiredPermissions(): array {
85+
return [
86+
ClearingProcessPermissions::REVIEW_CALCULATIVE,
87+
ClearingProcessPermissions::REVIEW_CONTENT,
88+
];
89+
}
90+
91+
protected function getTaskSubject(ClearingProcessEntityBundle $clearingProcessBundle): string {
92+
return E::ts('Finish Clearing Review');
93+
}
94+
95+
protected function isReviewFinishOutstanding(
96+
ClearingProcessEntityBundle $clearingProcessBundle
97+
): bool {
98+
return $this->isInReviewStatus($clearingProcessBundle)
99+
&& $this->isCalculativeAndContentReviewFinished($clearingProcessBundle);
100+
}
101+
102+
final protected function isCalculativeAndContentReviewFinished(
103+
ClearingProcessEntityBundle $clearingProcessBundle
104+
): bool {
105+
return NULL !== $clearingProcessBundle->getClearingProcess()->getIsReviewCalculative()
106+
&& NULL !== $clearingProcessBundle->getClearingProcess()->getIsReviewContent();
107+
}
108+
109+
final protected function isInReviewStatus(ClearingProcessEntityBundle $clearingProcessBundle): bool {
110+
return 'review' === $clearingProcessBundle->getClearingProcess()->getStatus();
111+
}
112+
113+
private function areReviewerContactsChanged(
114+
ClearingProcessEntity $clearingProcess,
115+
ClearingProcessEntity $previousClearingProcess
116+
): bool {
117+
return $clearingProcess->getReviewerCalculativeContactId()
118+
!== $previousClearingProcess->getReviewerCalculativeContactId()
119+
|| $clearingProcess->getReviewerContentContactId() !== $previousClearingProcess->getReviewerContentContactId();
120+
}
121+
122+
private function createReviewFinishTask(ClearingProcessEntityBundle $clearingProcessBundle): FundingTaskEntity {
123+
return FundingTaskEntity::newTask([
124+
'subject' => $this->getTaskSubject($clearingProcessBundle),
125+
'affected_identifier' => $clearingProcessBundle->getApplicationProcess()->getIdentifier(),
126+
'required_permissions' => $this->getRequiredPermissions(),
127+
'type' => self::TASK_TYPE,
128+
'funding_case_id' => $clearingProcessBundle->getFundingCase()->getId(),
129+
'application_process_id' => $clearingProcessBundle->getApplicationProcess()->getId(),
130+
'clearing_process_id' => $clearingProcessBundle->getClearingProcess()->getId(),
131+
'assignee_contact_ids' => $this->getAssigneeContactIds($clearingProcessBundle->getClearingProcess()),
132+
]);
133+
}
134+
135+
/**
136+
* @phpstan-return list<int>
137+
*/
138+
private function getAssigneeContactIds(ClearingProcessEntity $clearingProcess): array {
139+
$assigneeContactIds = [];
140+
if (NULL !== $clearingProcess->getReviewerCalculativeContactId()) {
141+
$assigneeContactIds[] = $clearingProcess->getReviewerCalculativeContactId();
142+
}
143+
if (NULL !== $clearingProcess->getReviewerContentContactId()) {
144+
$assigneeContactIds[] = $clearingProcess->getReviewerContentContactId();
145+
}
146+
147+
return $assigneeContactIds;
148+
}
149+
150+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
/*
3+
* Copyright (C) 2025 SYSTOPIA GmbH
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published by
7+
* the Free Software Foundation in version 3.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU Affero General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU Affero General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
declare(strict_types = 1);
19+
20+
namespace Civi\Funding\IJB\Task;
21+
22+
use Civi\Funding\ApplicationProcess\Task\AbstractApplicationReviewFinishTaskHandler;
23+
use Civi\Funding\IJB\Traits\IJBSupportedFundingCaseTypesTrait;
24+
25+
final class IJBApplicationReviewFinishTaskHandler extends AbstractApplicationReviewFinishTaskHandler {
26+
27+
use IJBSupportedFundingCaseTypesTrait;
28+
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
/*
3+
* Copyright (C) 2025 SYSTOPIA GmbH
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published by
7+
* the Free Software Foundation in version 3.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU Affero General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU Affero General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
declare(strict_types = 1);
19+
20+
namespace Civi\Funding\IJB\Task;
21+
22+
use Civi\Funding\ClearingProcess\Task\AbstractClearingReviewFinishTaskHandler;
23+
use Civi\Funding\IJB\Traits\IJBSupportedFundingCaseTypesTrait;
24+
25+
final class IJBClearingReviewFinishTaskHandler extends AbstractClearingReviewFinishTaskHandler {
26+
27+
use IJBSupportedFundingCaseTypesTrait;
28+
29+
}

0 commit comments

Comments
 (0)