diff --git a/application/Espo/Modules/Crm/Controllers/Campaign.php b/application/Espo/Modules/Crm/Controllers/Campaign.php index eae0a28fd1..527e1722a4 100644 --- a/application/Espo/Modules/Crm/Controllers/Campaign.php +++ b/application/Espo/Modules/Crm/Controllers/Campaign.php @@ -29,15 +29,23 @@ namespace Espo\Modules\Crm\Controllers; +use Espo\Core\Acl\Table; +use Espo\Core\Controllers\Record; +use Espo\Core\Exceptions\Error; use Espo\Core\Exceptions\Forbidden; use Espo\Core\Exceptions\BadRequest; use Espo\Core\Api\Request; -use Espo\Modules\Crm\Services\Campaign as Service; - +use Espo\Modules\Crm\Entities\Campaign as CampaignEntity; +use Espo\Modules\Crm\Tools\Campaign\MailMergeService; use stdClass; -class Campaign extends \Espo\Core\Controllers\Record +class Campaign extends Record { + /** + * @throws BadRequest + * @throws Forbidden + * @throws Error + */ public function postActionGenerateMailMergePdf(Request $request): stdClass { $data = $request->getParsedBody(); @@ -50,18 +58,16 @@ class Campaign extends \Espo\Core\Controllers\Record throw new BadRequest(); } - if (!$this->getAcl()->checkScope('Campaign', 'read')) { + if (!$this->acl->checkScope(CampaignEntity::ENTITY_TYPE, Table::ACTION_READ)) { throw new Forbidden(); } + $attachmentId = $this->injectableFactory + ->create(MailMergeService::class) + ->generate($data->campaignId, $data->link); + return (object) [ - 'id' => $this->getCampaignService()->generateMailMergePdf($data->campaignId, $data->link, true) + 'id' => $attachmentId, ]; } - - private function getCampaignService(): Service - { - /** @var Service */ - return $this->getRecordService(); - } } diff --git a/application/Espo/Modules/Crm/Entities/Campaign.php b/application/Espo/Modules/Crm/Entities/Campaign.php index fb276c5886..14736aa322 100644 --- a/application/Espo/Modules/Crm/Entities/Campaign.php +++ b/application/Espo/Modules/Crm/Entities/Campaign.php @@ -32,11 +32,25 @@ namespace Espo\Modules\Crm\Entities; use Espo\Core\Field\Date; use Espo\Core\Field\Link; use Espo\Core\Field\LinkMultiple; +use Espo\Core\ORM\Entity; -class Campaign extends \Espo\Core\ORM\Entity +class Campaign extends Entity { public const ENTITY_TYPE = 'Campaign'; + public const TYPE_EMAIL = 'Email'; + public const TYPE_MAIL = 'Mail'; + + public function getName(): ?string + { + return $this->get('name'); + } + + public function getType(): ?string + { + return $this->get('type'); + } + public function getStartDate(): ?Date { /** @var ?Date */ diff --git a/application/Espo/Modules/Crm/Services/Campaign.php b/application/Espo/Modules/Crm/Services/Campaign.php index e48b1efca5..51a249b877 100644 --- a/application/Espo/Modules/Crm/Services/Campaign.php +++ b/application/Espo/Modules/Crm/Services/Campaign.php @@ -29,20 +29,8 @@ namespace Espo\Modules\Crm\Services; -use Espo\Entities\Template; -use Espo\Modules\Crm\Tools\Campaign\MailMergeGenerator; - -use Espo\Modules\Crm\Entities\Campaign as CampaignEntity; - use Espo\ORM\Entity; - -use Espo\Core\Exceptions\Error; -use Espo\Core\Exceptions\Forbidden; -use Espo\Core\Exceptions\BadRequest; - -use Espo\ORM\EntityCollection; use Espo\Services\Record; - use Espo\Core\Di; /** @@ -54,25 +42,6 @@ class Campaign extends Record implements { use Di\DefaultLanguageSetter; - /** - * @var array - */ - protected $entityTypeAddressFieldListMap = [ - 'Account' => ['billingAddress', 'shippingAddress'], - 'Contact' => ['address'], - 'Lead' => ['address'], - 'User' => [], - ]; - - /** - * @var string[] - */ - protected $targetLinkList = [ - 'accounts', - 'contacts', - 'leads', - 'users', - ]; public function logLeadCreated( string $campaignId, @@ -376,157 +345,5 @@ class Campaign extends Record implements $this->entityManager->saveEntity($logRecord); } - /** - * @throws BadRequest - * @throws Error - * @throws Forbidden - */ - public function generateMailMergePdf(string $campaignId, string $link, bool $checkAcl = false): string - { - /** @var CampaignEntity $campaign */ - $campaign = $this->entityManager->getEntity('Campaign', $campaignId); - if ($checkAcl && !$this->acl->check($campaign, 'read')) { - throw new Forbidden(); - } - - /** @var string $targetEntityType */ - $targetEntityType = $campaign->getRelationParam($link, 'entity'); - - if ($checkAcl) { - if (!$this->acl->check($targetEntityType, 'read')) { - throw new Forbidden("Could not mail merge campaign because access to target entity type is forbidden."); - } - } - - if (!in_array($link, $this->targetLinkList)) { - throw new BadRequest(); - } - - if ($campaign->get('type') !== 'Mail') { - throw new Error("Could not mail merge campaign not of Mail type."); - } - - if ( - !$campaign->get($link . 'TemplateId') - ) { - throw new Error("Could not mail merge campaign w/o specified template."); - } - - /** @var ?Template $template */ - $template = $this->entityManager->getEntity(Template::ENTITY_TYPE, $campaign->get($link . 'TemplateId')); - - if (!$template) { - throw new Error("Template not found."); - } - - if ($template->get('entityType') !== $targetEntityType) { - throw new Error("Template is not of proper entity type."); - } - - $campaign->loadLinkMultipleField('targetLists'); - $campaign->loadLinkMultipleField('excludingTargetLists'); - - if (count($campaign->getLinkMultipleIdList('targetLists') ?? []) === 0) { - throw new Error("Could not mail merge campaign w/o any specified target list."); - } - - $metTargetHash = []; - $targetEntityList = []; - - /** @var iterable<\Espo\Modules\Crm\Entities\TargetList> $excludingTargetListList */ - $excludingTargetListList = $this->entityManager - ->getRDBRepository('Campaign') - ->getRelation($campaign, 'excludingTargetLists') - ->find(); - - foreach ($excludingTargetListList as $excludingTargetList) { - $recordList = $this->entityManager - ->getRDBRepository('TargetList') - ->getRelation($excludingTargetList, $link) - ->find(); - - foreach ($recordList as $excludingTarget) { - $hashId = $excludingTarget->getEntityType() . '-' . $excludingTarget->getId(); - $metTargetHash[$hashId] = true; - } - } - - $addressFieldList = $this->entityTypeAddressFieldListMap[$targetEntityType]; - - /** @var iterable<\Espo\Modules\Crm\Entities\TargetList> $targetListCollection */ - $targetListCollection = $this->entityManager - ->getRDBRepository('Campaign') - ->getRelation($campaign, 'targetLists') - ->find(); - - foreach ($targetListCollection as $targetList) { - if (!$campaign->get($link . 'TemplateId')) { - continue; - } - - $entityList = $this->entityManager - ->getRDBRepository('TargetList') - ->getRelation($targetList, $link) - ->where([ - '@relation.optedOut' => false, - ]) - ->find(); - - foreach ($entityList as $e) { - $hashId = $e->getEntityType() . '-'. $e->getId(); - - if (!empty($metTargetHash[$hashId])) { - continue; - } - - $metTargetHash[$hashId] = true; - - if ($campaign->get('mailMergeOnlyWithAddress')) { - if (empty($addressFieldList)) { - continue; - } - - $hasAddress = false; - - foreach ($addressFieldList as $addressField) { - if ( - $e->get($addressField . 'Street') || - $e->get($addressField . 'PostalCode') - ) { - $hasAddress = true; - break; - } - } - - if (!$hasAddress) { - continue; - } - } - - $targetEntityList[] = $e; - } - } - - if (empty($targetEntityList)) { - throw new Error("No targets available for mail merge."); - } - - $filename = $campaign->get('name') . ' - ' . - $this->defaultLanguage->translateLabel($targetEntityType, 'scopeNamesPlural'); - - /** @var EntityCollection $collection */ - $collection = $this->entityManager - ->getCollectionFactory() - ->create($targetEntityType, $targetEntityList); - - return $this->injectableFactory - ->create(MailMergeGenerator::class) - ->generate( - $collection, - $template, - $campaign->getId(), - $filename - ); - } } diff --git a/application/Espo/Modules/Crm/Tools/Campaign/MailMergeGenerator.php b/application/Espo/Modules/Crm/Tools/Campaign/MailMergeGenerator.php index 02f246b3f9..304c6517b5 100644 --- a/application/Espo/Modules/Crm/Tools/Campaign/MailMergeGenerator.php +++ b/application/Espo/Modules/Crm/Tools/Campaign/MailMergeGenerator.php @@ -75,6 +75,9 @@ class MailMergeGenerator } /** + * Generate a mail-merge PDF. + * + * @return string An attachment ID. * @param EntityCollection $collection * @throws Error */ diff --git a/application/Espo/Modules/Crm/Tools/Campaign/MailMergeService.php b/application/Espo/Modules/Crm/Tools/Campaign/MailMergeService.php new file mode 100644 index 0000000000..d9ae63f708 --- /dev/null +++ b/application/Espo/Modules/Crm/Tools/Campaign/MailMergeService.php @@ -0,0 +1,230 @@ + */ + protected $entityTypeAddressFieldListMap = [ + 'Account' => ['billingAddress', 'shippingAddress'], + 'Contact' => ['address'], + 'Lead' => ['address'], + 'User' => [], + ]; + + /** @var string[] */ + protected $targetLinkList = [ + 'accounts', + 'contacts', + 'leads', + 'users', + ]; + + private EntityManager $entityManager; + private Acl $acl; + private Language $defaultLanguage; + private MailMergeGenerator $generator; + + public function __construct( + EntityManager $entityManager, + Acl $acl, + Language $defaultLanguage, + MailMergeGenerator $generator + ) { + $this->entityManager = $entityManager; + $this->acl = $acl; + $this->defaultLanguage = $defaultLanguage; + $this->generator = $generator; + } + + /** + * @return string An attachment ID. + * @throws BadRequest + * @throws Error + * @throws Forbidden + */ + public function generate(string $campaignId, string $link, bool $checkAcl = true): string + { + /** @var CampaignEntity $campaign */ + $campaign = $this->entityManager->getEntityById(CampaignEntity::ENTITY_TYPE, $campaignId); + + if ($checkAcl && !$this->acl->checkEntityRead($campaign)) { + throw new Forbidden(); + } + + /** @var string $targetEntityType */ + $targetEntityType = $campaign->getRelationParam($link, 'entity'); + + if ($checkAcl && !$this->acl->check($targetEntityType, Acl\Table::ACTION_READ)) { + throw new Forbidden("Could not mail merge campaign because access to target entity type is forbidden."); + } + + if (!in_array($link, $this->targetLinkList)) { + throw new BadRequest(); + } + + if ($campaign->getType() !== CampaignEntity::TYPE_MAIL) { + throw new Error("Could not mail merge campaign not of Mail type."); + } + + $templateId = $campaign->get($link . 'TemplateId'); + + if (!$templateId) { + throw new Error("Could not mail merge campaign w/o specified template."); + } + + /** @var ?Template $template */ + $template = $this->entityManager->getEntityById(Template::ENTITY_TYPE, $templateId); + + if (!$template) { + throw new Error("Template not found."); + } + + if ($template->getTargetEntityType() !== $targetEntityType) { + throw new Error("Template is not of proper entity type."); + } + + $campaign->loadLinkMultipleField('targetLists'); + $campaign->loadLinkMultipleField('excludingTargetLists'); + + if (count($campaign->getLinkMultipleIdList('targetLists') ?? []) === 0) { + throw new Error("Could not mail merge campaign w/o any specified target list."); + } + + $metTargetHash = []; + $targetEntityList = []; + + /** @var Collection $excludingTargetListList */ + $excludingTargetListList = $this->entityManager + ->getRDBRepository(CampaignEntity::ENTITY_TYPE) + ->getRelation($campaign, 'excludingTargetLists') + ->find(); + + foreach ($excludingTargetListList as $excludingTargetList) { + $recordList = $this->entityManager + ->getRDBRepository(TargetList::ENTITY_TYPE) + ->getRelation($excludingTargetList, $link) + ->find(); + + foreach ($recordList as $excludingTarget) { + $hashId = $excludingTarget->getEntityType() . '-' . $excludingTarget->getId(); + $metTargetHash[$hashId] = true; + } + } + + $addressFieldList = $this->entityTypeAddressFieldListMap[$targetEntityType]; + + /** @var Collection $targetListCollection */ + $targetListCollection = $this->entityManager + ->getRDBRepository(CampaignEntity::ENTITY_TYPE) + ->getRelation($campaign, 'targetLists') + ->find(); + + foreach ($targetListCollection as $targetList) { + if (!$campaign->get($link . 'TemplateId')) { + continue; + } + + $entityList = $this->entityManager + ->getRDBRepository(TargetList::ENTITY_TYPE) + ->getRelation($targetList, $link) + ->where([ + '@relation.optedOut' => false, + ]) + ->find(); + + foreach ($entityList as $e) { + $hashId = $e->getEntityType() . '-'. $e->getId(); + + if (!empty($metTargetHash[$hashId])) { + continue; + } + + $metTargetHash[$hashId] = true; + + if ($campaign->get('mailMergeOnlyWithAddress')) { + if (empty($addressFieldList)) { + continue; + } + + $hasAddress = false; + + foreach ($addressFieldList as $addressField) { + if ( + $e->get($addressField . 'Street') || + $e->get($addressField . 'PostalCode') + ) { + $hasAddress = true; + break; + } + } + + if (!$hasAddress) { + continue; + } + } + + $targetEntityList[] = $e; + } + } + + if (empty($targetEntityList)) { + throw new Error("No targets available for mail merge."); + } + + $filename = $campaign->getName() . ' - ' . + $this->defaultLanguage->translateLabel($targetEntityType, 'scopeNamesPlural'); + + /** @var EntityCollection $collection */ + $collection = $this->entityManager + ->getCollectionFactory() + ->create($targetEntityType, $targetEntityList); + + return $this->generator->generate( + $collection, + $template, + $campaign->getId(), + $filename + ); + } +}