diff --git a/application/Espo/Modules/Crm/Resources/routes.json b/application/Espo/Modules/Crm/Resources/routes.json index fd5bb67e26..47f569872a 100644 --- a/application/Espo/Modules/Crm/Resources/routes.json +++ b/application/Espo/Modules/Crm/Resources/routes.json @@ -54,5 +54,10 @@ "route": "/Campaign/:id/generateMailMerge", "method": "post", "actionClassName": "Espo\\Modules\\Crm\\Tools\\Campaign\\Api\\PostGenerateMailMerge" + }, + { + "route": "/TargetList/:id/optedOut", + "method": "get", + "actionClassName": "Espo\\Modules\\Crm\\Tools\\TargetList\\Api\\GetOptedOut" } ] diff --git a/application/Espo/Modules/Crm/Services/TargetList.php b/application/Espo/Modules/Crm/Services/TargetList.php index 0af26aa9b6..d17b639490 100644 --- a/application/Espo/Modules/Crm/Services/TargetList.php +++ b/application/Espo/Modules/Crm/Services/TargetList.php @@ -31,19 +31,13 @@ namespace Espo\Modules\Crm\Services; use Espo\Core\Acl\Table; use Espo\ORM\Entity; -use Espo\ORM\Query\Select; use Espo\Core\Exceptions\NotFound; use Espo\Core\Exceptions\Forbidden; use Espo\Core\Exceptions\Error; use Espo\Modules\Crm\Entities\TargetList as TargetListEntity; -use Espo\Core\Record\Collection as RecordCollection; use Espo\Services\Record; -use Espo\Core\Select\SearchParams; use Espo\Core\Utils\Metadata; - -use PDO; use Espo\Core\Di; -use RuntimeException; /** * @extends Record @@ -127,117 +121,6 @@ class TargetList extends Record implements $this->hookManager->process('TargetList', 'afterUnlinkAll', $entity, [], ['link' => $link]); } - protected function getOptedOutSelectQueryForLink(string $targetListId, string $link): Select - { - /** @var TargetListEntity $seed */ - $seed = $this->getRepository()->getNew(); - - $entityType = $seed->getRelationParam($link, 'entity'); - - if (!$entityType) { - throw new RuntimeException(); - } - - $linkEntityType = ucfirst( - $seed->getRelationParam($link, 'relationName') ?? '' - ); - - if ($linkEntityType === '') { - throw new RuntimeException(); - } - - $key = $seed->getRelationParam($link, 'midKeys')[1] ?? null; - - if (!$key) { - throw new RuntimeException(); - } - - return $this->entityManager->getQueryBuilder() - ->select() - ->from($entityType) - ->select([ - 'id', - 'name', - 'createdAt', - ["'$entityType'", 'entityType'], - ]) - ->join( - $linkEntityType, - 'j', - [ - "j.$key:" => 'id', - 'j.deleted' => false, - 'j.optedOut' => true, - 'j.targetListId' => $targetListId, - ] - ) - ->order('createdAt', 'DESC') - ->build(); - } - - /** - * @return RecordCollection - * @noinspection PhpUnused - * @todo Move? Use Tools\TargetList\MetadataProvider. - */ - protected function findLinkedOptedOut(string $id, SearchParams $searchParams): RecordCollection - { - $offset = $searchParams->getOffset() ?? 0; - $maxSize = $searchParams->getMaxSize() ?? 0; - - $em = $this->entityManager; - $queryBuilder = $em->getQueryBuilder(); - - $queryList = []; - - foreach ($this->targetLinkList as $link) { - $queryList[] = $this->getOptedOutSelectQueryForLink($id, $link); - } - - $builder = $queryBuilder - ->union() - ->all(); - - foreach ($queryList as $query) { - $builder->query($query); - } - - $countQuery = $queryBuilder - ->select() - ->fromQuery($builder->build(), 'c') - ->select('COUNT:(c.id)', 'count') - ->build(); - - $row = $em->getQueryExecutor() - ->execute($countQuery) - ->fetch(PDO::FETCH_ASSOC); - - $totalCount = $row['count']; - - $unionQuery = $builder - ->limit($offset, $maxSize) - ->order('createdAt', 'DESC') - ->build(); - - $sth = $em->getQueryExecutor()->execute($unionQuery); - - $collection = $this->entityManager - ->getCollectionFactory() - ->create(); - - while ($row = $sth->fetch(PDO::FETCH_ASSOC)) { - $itemEntity = $this->entityManager->getNewEntity($row['entityType']); - - $itemEntity->set($row); - $itemEntity->setAsFetched(); - - $collection[] = $itemEntity; - } - - /** @var RecordCollection */ - return new RecordCollection($collection, $totalCount); - } - /** * @throws NotFound * @throws Error diff --git a/application/Espo/Modules/Crm/Tools/TargetList/Api/GetOptedOut.php b/application/Espo/Modules/Crm/Tools/TargetList/Api/GetOptedOut.php new file mode 100644 index 0000000000..f8a8e8c34d --- /dev/null +++ b/application/Espo/Modules/Crm/Tools/TargetList/Api/GetOptedOut.php @@ -0,0 +1,75 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Modules\Crm\Tools\TargetList\Api; + +use Espo\Core\Acl; +use Espo\Core\Api\Action; +use Espo\Core\Api\Request; +use Espo\Core\Api\Response; +use Espo\Core\Api\ResponseComposer; +use Espo\Core\Exceptions\BadRequest; +use Espo\Core\Exceptions\Forbidden; +use Espo\Core\Record\SearchParamsFetcher; +use Espo\Modules\Crm\Entities\TargetList; +use Espo\Modules\Crm\Tools\TargetList\OptOutService; + +/** + * @noinspection PhpUnused + */ +class GetOptedOut implements Action +{ + public function __construct( + private SearchParamsFetcher $searchParamsFetcher, + private Acl $acl, + private OptOutService $service + ) {} + + public function process(Request $request): Response + { + $id = $request->getRouteParam('id'); + + if (!$id) { + throw new BadRequest(); + } + + if (!$this->acl->check(TargetList::ENTITY_TYPE)) { + throw new Forbidden(); + } + + $searchParams = $this->searchParamsFetcher->fetch($request); + + $collection = $this->service->find($id, $searchParams); + + return ResponseComposer::json((object) [ + 'total' => $collection->getTotal(), + 'list' => $collection->getValueMapList(), + ]); + } +} diff --git a/application/Espo/Modules/Crm/Tools/TargetList/OptOutService.php b/application/Espo/Modules/Crm/Tools/TargetList/OptOutService.php new file mode 100644 index 0000000000..5592e8d677 --- /dev/null +++ b/application/Espo/Modules/Crm/Tools/TargetList/OptOutService.php @@ -0,0 +1,178 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Modules\Crm\Tools\TargetList; + +use Espo\Core\Exceptions\Forbidden; +use Espo\Core\Exceptions\NotFound; +use Espo\Core\Record\Collection; +use Espo\Core\Record\Collection as RecordCollection; +use Espo\Core\Record\EntityProvider; +use Espo\Core\Select\SearchParams; +use Espo\Modules\Crm\Entities\TargetList; +use Espo\ORM\Entity; +use Espo\ORM\EntityManager; +use Espo\ORM\Query\Part\Order; +use Espo\ORM\Query\Select; +use PDO; +use RuntimeException; + +class OptOutService +{ + public function __construct( + private EntityManager $entityManager, + private MetadataProvider $metadataProvider, + private EntityProvider $entityProvider + ) {} + + /** + * Find opted out targets in a target list. + * + * @return Collection + * @throws Forbidden + * @throws NotFound + */ + public function find(string $id, SearchParams $params): Collection + { + $this->checkEntity($id); + + $offset = $params->getOffset() ?? 0; + $maxSize = $params->getMaxSize() ?? 0; + + $em = $this->entityManager; + $queryBuilder = $em->getQueryBuilder(); + + $queryList = []; + + $targetLinkList = $this->metadataProvider->getTargetLinkList(); + + foreach ($targetLinkList as $link) { + $queryList[] = $this->getSelectQueryForLink($id, $link); + } + + $builder = $queryBuilder + ->union() + ->all(); + + foreach ($queryList as $query) { + $builder->query($query); + } + + $countQuery = $queryBuilder + ->select() + ->fromQuery($builder->build(), 'c') + ->select('COUNT:(c.id)', 'count') + ->build(); + + $row = $em->getQueryExecutor() + ->execute($countQuery) + ->fetch(PDO::FETCH_ASSOC); + + $totalCount = $row['count']; + + $unionQuery = $builder + ->limit($offset, $maxSize) + ->order('createdAt', 'DESC') + ->build(); + + $sth = $em->getQueryExecutor()->execute($unionQuery); + + $collection = $this->entityManager + ->getCollectionFactory() + ->create(); + + while ($row = $sth->fetch(PDO::FETCH_ASSOC)) { + $itemEntity = $this->entityManager->getNewEntity($row['entityType']); + + $itemEntity->set($row); + $itemEntity->setAsFetched(); + + $collection[] = $itemEntity; + } + + /** @var RecordCollection */ + return new RecordCollection($collection, $totalCount); + } + + private function getSelectQueryForLink(string $id, string $link): Select + { + $seed = $this->entityManager->getRDBRepositoryByClass(TargetList::class)->getNew(); + + $entityType = $seed->getRelationParam($link, 'entity'); + + if (!$entityType) { + throw new RuntimeException(); + } + + $linkEntityType = ucfirst( + $seed->getRelationParam($link, 'relationName') ?? '' + ); + + if ($linkEntityType === '') { + throw new RuntimeException(); + } + + $key = $seed->getRelationParam($link, 'midKeys')[1] ?? null; + + if (!$key) { + throw new RuntimeException(); + } + + return $this->entityManager->getQueryBuilder() + ->select() + ->from($entityType) + ->select([ + 'id', + 'name', + 'createdAt', + ["'$entityType'", 'entityType'], + ]) + ->join( + $linkEntityType, + 'j', + [ + "j.$key:" => 'id', + 'j.deleted' => false, + 'j.optedOut' => true, + 'j.targetListId' => $id, + ] + ) + ->order('createdAt', Order::DESC) + ->build(); + } + + /** + * @throws Forbidden + * @throws NotFound + */ + private function checkEntity(string $id): void + { + $this->entityProvider->get(TargetList::class, $id); + } +}