From df71ccbea6b8a57cadd33322ae4df06a071ac8f6 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Sun, 5 Jan 2025 15:40:55 +0200 Subject: [PATCH] meeting invitation ref --- .../Espo/Core/Templates/Entities/Event.php | 5 + .../Espo/Modules/Crm/Controllers/Call.php | 4 +- .../Espo/Modules/Crm/Controllers/Meeting.php | 2 +- .../Espo/Modules/Crm/Entities/Meeting.php | 1 - .../Meeting/{ => Invitation}/Invitee.php | 8 +- .../Crm/Tools/Meeting/Invitation/Sender.php | 203 ++++++++++++++++++ .../Meeting/Invitation/SenderFactory.php | 57 +++++ .../Crm/Tools/Meeting/InvitationService.php | 131 ++--------- 8 files changed, 285 insertions(+), 126 deletions(-) rename application/Espo/Modules/Crm/Tools/Meeting/{ => Invitation}/Invitee.php (91%) create mode 100644 application/Espo/Modules/Crm/Tools/Meeting/Invitation/Sender.php create mode 100644 application/Espo/Modules/Crm/Tools/Meeting/Invitation/SenderFactory.php diff --git a/application/Espo/Core/Templates/Entities/Event.php b/application/Espo/Core/Templates/Entities/Event.php index 90cc72fe49..98e2c3188e 100644 --- a/application/Espo/Core/Templates/Entities/Event.php +++ b/application/Espo/Core/Templates/Entities/Event.php @@ -38,4 +38,9 @@ class Event extends Entity public const STATUS_PLANNED = 'Planned'; public const STATUS_HELD = 'Held'; public const STATUS_NOT_HELD = 'Not Held'; + + public function getStatus(): string + { + return $this->get('status'); + } } diff --git a/application/Espo/Modules/Crm/Controllers/Call.php b/application/Espo/Modules/Crm/Controllers/Call.php index bc3c02c247..c0ba9f095d 100644 --- a/application/Espo/Modules/Crm/Controllers/Call.php +++ b/application/Espo/Modules/Crm/Controllers/Call.php @@ -40,7 +40,7 @@ use Espo\Core\Mail\Exceptions\SendingError; use Espo\Core\Utils\Json; use Espo\Modules\Crm\Entities\Call as CallEntity; use Espo\Modules\Crm\Tools\Meeting\InvitationService; -use Espo\Modules\Crm\Tools\Meeting\Invitee; +use Espo\Modules\Crm\Tools\Meeting\Invitation\Invitee; use Espo\Modules\Crm\Tools\Meeting\Service; use stdClass; @@ -96,7 +96,7 @@ class Call extends Record /** * @param Request $request - * @return ?Invitee[] + * @return ?\Espo\Modules\Crm\Tools\Meeting\Invitation\Invitee[] * @throws BadRequest */ private function fetchInvitees(Request $request): ?array diff --git a/application/Espo/Modules/Crm/Controllers/Meeting.php b/application/Espo/Modules/Crm/Controllers/Meeting.php index 03ab7d1aec..74b0186381 100644 --- a/application/Espo/Modules/Crm/Controllers/Meeting.php +++ b/application/Espo/Modules/Crm/Controllers/Meeting.php @@ -43,7 +43,7 @@ use Espo\Core\Mail\Exceptions\SendingError; use Espo\Core\Utils\Json; use Espo\Modules\Crm\Entities\Meeting as MeetingEntity; use Espo\Modules\Crm\Tools\Meeting\InvitationService; -use Espo\Modules\Crm\Tools\Meeting\Invitee; +use Espo\Modules\Crm\Tools\Meeting\Invitation\Invitee; use Espo\Modules\Crm\Tools\Meeting\Service; use stdClass; diff --git a/application/Espo/Modules/Crm/Entities/Meeting.php b/application/Espo/Modules/Crm/Entities/Meeting.php index cd8a3b0359..9d66e3f201 100644 --- a/application/Espo/Modules/Crm/Entities/Meeting.php +++ b/application/Espo/Modules/Crm/Entities/Meeting.php @@ -35,7 +35,6 @@ use Espo\Core\Field\LinkMultiple; use Espo\Core\Field\LinkParent; use Espo\Core\Name\Field; use Espo\Core\ORM\Entity; -use Espo\Entities\Email; use Espo\Entities\User; use Espo\ORM\Entity as OrmEntity; diff --git a/application/Espo/Modules/Crm/Tools/Meeting/Invitee.php b/application/Espo/Modules/Crm/Tools/Meeting/Invitation/Invitee.php similarity index 91% rename from application/Espo/Modules/Crm/Tools/Meeting/Invitee.php rename to application/Espo/Modules/Crm/Tools/Meeting/Invitation/Invitee.php index baffbfb6a8..c5405905a8 100644 --- a/application/Espo/Modules/Crm/Tools/Meeting/Invitee.php +++ b/application/Espo/Modules/Crm/Tools/Meeting/Invitation/Invitee.php @@ -27,12 +27,14 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -namespace Espo\Modules\Crm\Tools\Meeting; +namespace Espo\Modules\Crm\Tools\Meeting\Invitation; class Invitee { - public function __construct(private string $entityType, private string $id) - {} + public function __construct( + private string $entityType, + private string $id + ) {} public function getEntityType(): string { diff --git a/application/Espo/Modules/Crm/Tools/Meeting/Invitation/Sender.php b/application/Espo/Modules/Crm/Tools/Meeting/Invitation/Sender.php new file mode 100644 index 0000000000..2f8fa90a3e --- /dev/null +++ b/application/Espo/Modules/Crm/Tools/Meeting/Invitation/Sender.php @@ -0,0 +1,203 @@ +. + * + * 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\Meeting\Invitation; + +use Espo\Core\Binding\BindingContainerBuilder; +use Espo\Core\Exceptions\Error; +use Espo\Core\Exceptions\Forbidden; +use Espo\Core\InjectableFactory; +use Espo\Core\Mail\Exceptions\SendingError; +use Espo\Core\Mail\SmtpParams; +use Espo\Core\Name\Field; +use Espo\Core\Utils\Config; +use Espo\Core\Utils\Metadata; +use Espo\Entities\User; +use Espo\Modules\Crm\Business\Event\Invitations; +use Espo\Modules\Crm\Entities\Call; +use Espo\Modules\Crm\Entities\Meeting; +use Espo\ORM\Entity; +use Espo\ORM\EntityManager; +use Espo\Tools\Email\SendService; + +/** + * @since 9.0.0 + */ +class Sender +{ + private const TYPE_INVITATION = 'invitation'; + private const TYPE_CANCELLATION = 'cancellation'; + + public function __construct( + private SendService $sendService, + private User $user, + private InjectableFactory $injectableFactory, + private EntityManager $entityManager, + private Config $config, + private Metadata $metadata, + ) {} + + /** + * @param Meeting|Call $entity + * @param ?Invitee[] $targets + * @return Entity[] Entities an invitation was sent to. + * @throws Error + * @throws SendingError + * @throws Forbidden + */ + public function sendInvitation(Meeting|Call $entity, ?array $targets = null): array + { + return $this->sendInternal($entity, self::TYPE_INVITATION, $targets); + } + + /** + * @param Meeting|Call $entity + * @param ?Invitee[] $targets + * @return Entity[] Entities an invitation was sent to. + * @throws Error + * @throws SendingError + * @throws Forbidden + */ + public function sendCancellation(Meeting|Call $entity, ?array $targets = null): array + { + return $this->sendInternal($entity, self::TYPE_CANCELLATION, $targets); + } + + + /** + * @param ?Invitee[] $targets + * @return Entity[] + * @throws Error + * @throws SendingError + * @throws Forbidden + */ + private function sendInternal(Meeting|Call $entity, string $type, ?array $targets): array + { + $this->checkStatus($entity); + + $linkList = [ + 'users', + 'contacts', + 'leads', + ]; + + $sender = $this->getSender(); + + $sentAddressList = []; + $resultEntityList = []; + + foreach ($linkList as $link) { + $builder = $this->entityManager->getRelation($entity, $link); + + if ($targets === null && $type === self::TYPE_INVITATION) { + $builder->where(['@relation.status=' => Meeting::ATTENDEE_STATUS_NONE]); + } + + $collection = $builder->find(); + + foreach ($collection as $attendee) { + if ($targets && !self::isInTargets($attendee, $targets)) { + continue; + } + + $emailAddress = $attendee->get(Field::EMAIL_ADDRESS); + + if (!$emailAddress || in_array($emailAddress, $sentAddressList)) { + continue; + } + + if ($type === self::TYPE_INVITATION) { + $sender->sendInvitation($entity, $attendee, $link); + } + + if ($type === self::TYPE_CANCELLATION) { + $sender->sendCancellation($entity, $attendee, $link); + } + + $sentAddressList[] = $emailAddress; + $resultEntityList[] = $attendee; + + $this->entityManager + ->getRelation($entity, $link) + ->updateColumns($attendee, ['status' => Meeting::ATTENDEE_STATUS_NONE]); + } + } + + return $resultEntityList; + } + + /** + * @param Invitee[] $targets + */ + private static function isInTargets(Entity $entity, array $targets): bool + { + foreach ($targets as $target) { + if ( + $entity->getEntityType() === $target->getEntityType() && + $entity->getId() === $target->getId() + ) { + return true; + } + } + + return false; + } + + private function getSender(): Invitations + { + $smtpParams = !$this->config->get('eventInvitationForceSystemSmtp') ? + $this->sendService->getUserSmtpParams($this->user->getId()) : + null; + + $builder = BindingContainerBuilder::create(); + + if ($smtpParams) { + $builder->bindInstance(SmtpParams::class, $smtpParams); + } + + return $this->injectableFactory->createWithBinding(Invitations::class, $builder->build()); + } + + /** + * @throws Forbidden + */ + private function checkStatus(Meeting|Call $entity): void + { + $entityType = $entity->getEntityType(); + + $notActualStatusList = [ + ...($this->metadata->get("scopes.$entityType.completedStatusList") ?? []), + ...($this->metadata->get("scopes.$entityType.canceledStatusList") ?? []), + ]; + + if (in_array($entity->getStatus(), $notActualStatusList)) { + throw new Forbidden("Can't send invitation for not actual event."); + } + } +} diff --git a/application/Espo/Modules/Crm/Tools/Meeting/Invitation/SenderFactory.php b/application/Espo/Modules/Crm/Tools/Meeting/Invitation/SenderFactory.php new file mode 100644 index 0000000000..fb712485d8 --- /dev/null +++ b/application/Espo/Modules/Crm/Tools/Meeting/Invitation/SenderFactory.php @@ -0,0 +1,57 @@ +. + * + * 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\Meeting\Invitation; + +use Espo\Core\Binding\BindingContainerBuilder; +use Espo\Core\InjectableFactory; +use Espo\Entities\User; + +/** + * @since 9.0.0 + */ +class SenderFactory +{ + public function __construct( + private InjectableFactory $injectableFactory, + ) {} + + /** + * Create a sender for a user. The SMTP of the user will be used (if not disabled in the config). + */ + public function createForUser(User $user): Sender + { + return $this->injectableFactory->createWithBinding( + Sender::class, + BindingContainerBuilder::create() + ->bindInstance(User::class, $user) + ->build() + ); + } +} diff --git a/application/Espo/Modules/Crm/Tools/Meeting/InvitationService.php b/application/Espo/Modules/Crm/Tools/Meeting/InvitationService.php index 6134528c60..a6f8bad1d8 100644 --- a/application/Espo/Modules/Crm/Tools/Meeting/InvitationService.php +++ b/application/Espo/Modules/Crm/Tools/Meeting/InvitationService.php @@ -30,22 +30,16 @@ namespace Espo\Modules\Crm\Tools\Meeting; use Espo\Core\Acl; -use Espo\Core\Binding\BindingContainerBuilder; use Espo\Core\Exceptions\Error; use Espo\Core\Exceptions\Forbidden; use Espo\Core\Exceptions\NotFound; -use Espo\Core\InjectableFactory; use Espo\Core\Mail\Exceptions\SendingError; -use Espo\Core\Mail\SmtpParams; use Espo\Core\Record\ServiceContainer as RecordServiceContainer; -use Espo\Core\Utils\Config; -use Espo\Core\Utils\Metadata; -use Espo\Entities\User; -use Espo\Modules\Crm\Business\Event\Invitations; +use Espo\Modules\Crm\Entities\Call; use Espo\Modules\Crm\Entities\Meeting; +use Espo\Modules\Crm\Tools\Meeting\Invitation\Sender; +use Espo\Modules\Crm\Tools\Meeting\Invitation\Invitee; use Espo\ORM\Entity; -use Espo\ORM\EntityManager; -use Espo\Tools\Email\SendService; class InvitationService { @@ -54,13 +48,8 @@ class InvitationService public function __construct( private RecordServiceContainer $recordServiceContainer, - private SendService $sendService, - private User $user, - private InjectableFactory $injectableFactory, private Acl $acl, - private EntityManager $entityManager, - private Config $config, - private Metadata $metadata + private Sender $invitationSender, ) {} /** @@ -75,7 +64,7 @@ class InvitationService */ public function send(string $entityType, string $id, ?array $targets = null): array { - return $this->sendInternal($entityType, $id, $targets); + return $this->sendInternal($entityType, $id, $targets, self::TYPE_INVITATION); } /** @@ -104,8 +93,8 @@ class InvitationService private function sendInternal( string $entityType, string $id, - ?array $targets = null, - string $type = self::TYPE_INVITATION + ?array $targets, + string $type, ): array { $entity = $this->recordServiceContainer @@ -120,110 +109,14 @@ class InvitationService throw new Forbidden("No edit access."); } - $this->checkStatus($entity); - - $linkList = [ - 'users', - 'contacts', - 'leads', - ]; - - $sender = $this->getSender(); - - $sentAddressList = []; - $resultEntityList = []; - - foreach ($linkList as $link) { - $builder = $this->entityManager - ->getRDBRepository($entityType) - ->getRelation($entity, $link); - - if ($targets === null && $type === self::TYPE_INVITATION) { - $builder->where([ - '@relation.status=' => Meeting::ATTENDEE_STATUS_NONE, - ]); - } - - $collection = $builder->find(); - - foreach ($collection as $attendee) { - if ($targets && !self::isInTargets($attendee, $targets)) { - continue; - } - - $emailAddress = $attendee->get('emailAddress'); - - if (!$emailAddress || in_array($emailAddress, $sentAddressList)) { - continue; - } - - if ($type === self::TYPE_INVITATION) { - $sender->sendInvitation($entity, $attendee, $link); - } - - if ($type === self::TYPE_CANCELLATION) { - $sender->sendCancellation($entity, $attendee, $link); - } - - $sentAddressList[] = $emailAddress; - $resultEntityList[] = $attendee; - - $this->entityManager - ->getRDBRepository($entityType) - ->getRelation($entity, $link) - ->updateColumns($attendee, ['status' => Meeting::ATTENDEE_STATUS_NONE]); - } + if (!$entity instanceof Meeting && !$entity instanceof Call) { + throw new Error("Not supported entity type."); } - return $resultEntityList; - } - - /** - * @param Invitee[] $targets - */ - private static function isInTargets(Entity $entity, array $targets): bool - { - foreach ($targets as $target) { - if ( - $entity->getEntityType() === $target->getEntityType() && - $entity->getId() === $target->getId() - ) { - return true; - } + if ($type === self::TYPE_CANCELLATION) { + return $this->invitationSender->sendCancellation($entity, $targets); } - return false; - } - - private function getSender(): Invitations - { - $smtpParams = !$this->config->get('eventInvitationForceSystemSmtp') ? - $this->sendService->getUserSmtpParams($this->user->getId()) : - null; - - $builder = BindingContainerBuilder::create(); - - if ($smtpParams) { - $builder->bindInstance(SmtpParams::class, $smtpParams); - } - - return $this->injectableFactory->createWithBinding(Invitations::class, $builder->build()); - } - - /** - * @throws Forbidden - */ - private function checkStatus(Entity $entity): void - { - $entityType = $entity->getEntityType(); - - $notActualStatusList = [ - ...($this->metadata->get("scopes.$entityType.completedStatusList") ?? []), - ...($this->metadata->get("scopes.$entityType.canceledStatusList") ?? []), - ]; - - if (in_array($entity->get('status'), $notActualStatusList)) { - throw new Forbidden("Can't send invitation for not actual event."); - } + return $this->invitationSender->sendInvitation($entity, $targets); } }