diff --git a/config/xml/Bitrix24.Lib.ContactPersons.Entity.ContactPerson.dcm.xml b/config/xml/Bitrix24.Lib.ContactPersons.Entity.ContactPerson.dcm.xml index cfd06680..e641458f 100644 --- a/config/xml/Bitrix24.Lib.ContactPersons.Entity.ContactPerson.dcm.xml +++ b/config/xml/Bitrix24.Lib.ContactPersons.Entity.ContactPerson.dcm.xml @@ -10,14 +10,14 @@ - + - + diff --git a/config/xml/Bitrix24.SDK.Application.Contracts.ContactPersons.Entity.UserAgentInfo.dcm.xml b/config/xml/Bitrix24.SDK.Application.Contracts.ContactPersons.Entity.UserAgentInfo.dcm.xml index 7de53bcb..779b038b 100644 --- a/config/xml/Bitrix24.SDK.Application.Contracts.ContactPersons.Entity.UserAgentInfo.dcm.xml +++ b/config/xml/Bitrix24.SDK.Application.Contracts.ContactPersons.Entity.UserAgentInfo.dcm.xml @@ -5,6 +5,6 @@ - + \ No newline at end of file diff --git a/rector.php b/rector.php index 3ce8e423..59026b68 100644 --- a/rector.php +++ b/rector.php @@ -15,6 +15,7 @@ use Rector\Naming\Rector\Class_\RenamePropertyToMatchTypeRector; use Rector\PHPUnit\Set\PHPUnitSetList; use Rector\Set\ValueObject\DowngradeLevelSetList; +use Rector\CodeQuality\Rector\Identical\FlipTypeControlToUseExclusiveTypeRector; return RectorConfig::configure() ->withPaths([ @@ -48,5 +49,6 @@ strictBooleans: true ) ->withSkip([ - RenamePropertyToMatchTypeRector::class + RenamePropertyToMatchTypeRector::class, + FlipTypeControlToUseExclusiveTypeRector::class, ]); \ No newline at end of file diff --git a/src/ApplicationInstallations/Entity/ApplicationInstallation.php b/src/ApplicationInstallations/Entity/ApplicationInstallation.php index 99568624..bda0eea2 100644 --- a/src/ApplicationInstallations/Entity/ApplicationInstallation.php +++ b/src/ApplicationInstallations/Entity/ApplicationInstallation.php @@ -347,7 +347,9 @@ public function linkContactPerson(Uuid $uuid): void #[\Override] public function unlinkContactPerson(): void { - $this->updatedAt = new CarbonImmutable(); + if (null === $this->contactPersonId) { + return; + } $this->events[] = new Events\ApplicationInstallationContactPersonUnlinkedEvent( $this->id, @@ -356,13 +358,14 @@ public function unlinkContactPerson(): void ); $this->contactPersonId = null; + $this->updatedAt = new CarbonImmutable(); } #[\Override] public function linkBitrix24PartnerContactPerson(Uuid $uuid): void { - $this->updatedAt = new CarbonImmutable(); $this->bitrix24PartnerContactPersonId = $uuid; + $this->updatedAt = new CarbonImmutable(); $this->events[] = new Events\ApplicationInstallationBitrix24PartnerContactPersonLinkedEvent( $this->id, @@ -374,7 +377,9 @@ public function linkBitrix24PartnerContactPerson(Uuid $uuid): void #[\Override] public function unlinkBitrix24PartnerContactPerson(): void { - $this->updatedAt = new CarbonImmutable(); + if (null === $this->bitrix24PartnerContactPersonId) { + return; + } $this->events[] = new Events\ApplicationInstallationBitrix24PartnerContactPersonUnlinkedEvent( $this->id, @@ -383,13 +388,14 @@ public function unlinkBitrix24PartnerContactPerson(): void ); $this->bitrix24PartnerContactPersonId = null; + $this->updatedAt = new CarbonImmutable(); } #[\Override] public function linkBitrix24Partner(Uuid $uuid): void { - $this->updatedAt = new CarbonImmutable(); $this->bitrix24PartnerId = $uuid; + $this->updatedAt = new CarbonImmutable(); $this->events[] = new Events\ApplicationInstallationBitrix24PartnerLinkedEvent( $this->id, diff --git a/src/ApplicationInstallations/Infrastructure/Doctrine/ApplicationInstallationRepository.php b/src/ApplicationInstallations/Infrastructure/Doctrine/ApplicationInstallationRepository.php index d6644c6a..28717da4 100644 --- a/src/ApplicationInstallations/Infrastructure/Doctrine/ApplicationInstallationRepository.php +++ b/src/ApplicationInstallations/Infrastructure/Doctrine/ApplicationInstallationRepository.php @@ -102,6 +102,32 @@ public function findByExternalId(string $externalId): array ; } + /** + * Get the current installation on the portal without input parameters. + * The system allows only one active installation per portal, + * therefore, the current one is interpreted as the installation with status active. + * If, for any reason, there are multiple, select the most recent by createdAt. + * + * @throws ApplicationInstallationNotFoundException + */ + public function getCurrent(): ApplicationInstallationInterface + { + $applicationInstallation = $this->getEntityManager()->getRepository(ApplicationInstallation::class) + ->createQueryBuilder('appInstallation') + ->where('appInstallation.status = :status') + ->orderBy('appInstallation.createdAt', 'DESC') + ->setParameter('status', ApplicationInstallationStatus::active) + ->getQuery() + ->getOneOrNullResult() + ; + + if (null === $applicationInstallation) { + throw new ApplicationInstallationNotFoundException('current active application installation not found'); + } + + return $applicationInstallation; + } + /** * Find application installation by application token. * diff --git a/src/ApplicationInstallations/UseCase/InstallContactPerson/Command.php b/src/ApplicationInstallations/UseCase/InstallContactPerson/Command.php new file mode 100644 index 00000000..d3b09b01 --- /dev/null +++ b/src/ApplicationInstallations/UseCase/InstallContactPerson/Command.php @@ -0,0 +1,42 @@ +validate(); + } + + private function validate(): void + { + if (null !== $this->email && !filter_var($this->email, FILTER_VALIDATE_EMAIL)) { + throw new \InvalidArgumentException('Invalid email format.'); + } + + if (null !== $this->externalId && '' === trim($this->externalId)) { + throw new \InvalidArgumentException('External ID cannot be empty if provided.'); + } + + if ($this->bitrix24UserId <= 0) { + throw new \InvalidArgumentException('Bitrix24 User ID must be a positive integer.'); + } + } +} diff --git a/src/ApplicationInstallations/UseCase/InstallContactPerson/Handler.php b/src/ApplicationInstallations/UseCase/InstallContactPerson/Handler.php new file mode 100644 index 00000000..c29bcd6a --- /dev/null +++ b/src/ApplicationInstallations/UseCase/InstallContactPerson/Handler.php @@ -0,0 +1,122 @@ +logger->info('ContactPerson.InstallContactPerson.start', [ + 'applicationInstallationId' => $command->applicationInstallationId, + 'bitrix24UserId' => $command->bitrix24UserId, + 'bitrix24PartnerId' => $command->bitrix24PartnerId?->toRfc4122() ?? '', + ]); + + $createdContactPersonId = ''; + + try { + if (null !== $command->mobilePhoneNumber) { + try { + $this->guardMobilePhoneNumber($command->mobilePhoneNumber); + } catch (InvalidArgumentException) { + // Ошибка уже залогирована внутри гарда. + // Прерываем создание контакта, но не останавливаем установку приложения. + return; + } + } + + /** @var null|AggregateRootEventsEmitterInterface|ApplicationInstallationInterface $applicationInstallation */ + $applicationInstallation = $this->applicationInstallationRepository->getById($command->applicationInstallationId); + + $uuidV7 = Uuid::v7(); + + $contactPerson = new ContactPerson( + $uuidV7, + ContactPersonStatus::active, + $command->fullName, + $command->email, + null, + $command->mobilePhoneNumber, + null, + $command->comment, + $command->externalId, + $command->bitrix24UserId, + $command->bitrix24PartnerId, + $command->userAgentInfo, + true + ); + + $this->contactPersonRepository->save($contactPerson); + + if ($contactPerson->isPartner()) { + $applicationInstallation->linkBitrix24PartnerContactPerson($uuidV7); + } else { + $applicationInstallation->linkContactPerson($uuidV7); + } + + $this->applicationInstallationRepository->save($applicationInstallation); + + $this->flusher->flush($contactPerson, $applicationInstallation); + + $createdContactPersonId = $uuidV7->toRfc4122(); + } catch (ApplicationInstallationNotFoundException $applicationInstallationNotFoundException) { + $this->logger->warning('ContactPerson.InstallContactPerson.applicationInstallationNotFound', [ + 'applicationInstallationId' => $command->applicationInstallationId, + 'message' => $applicationInstallationNotFoundException->getMessage(), + ]); + + throw $applicationInstallationNotFoundException; + } finally { + $this->logger->info('ContactPerson.InstallContactPerson.finish', [ + 'applicationInstallationId' => $command->applicationInstallationId, + 'bitrix24UserId' => $command->bitrix24UserId, + 'bitrix24PartnerId' => $command->bitrix24PartnerId?->toRfc4122() ?? '', + 'contact_person_id' => $createdContactPersonId, + ]); + } + } + + private function guardMobilePhoneNumber(PhoneNumber $mobilePhoneNumber): void + { + if (!$this->phoneNumberUtil->isValidNumber($mobilePhoneNumber)) { + $this->logger->warning('ContactPerson.InstallContactPerson.InvalidMobilePhoneNumber', [ + 'mobilePhoneNumber' => (string) $mobilePhoneNumber, + ]); + + throw new InvalidArgumentException('Invalid mobile phone number.'); + } + + if (PhoneNumberType::MOBILE !== $this->phoneNumberUtil->getNumberType($mobilePhoneNumber)) { + $this->logger->warning('ContactPerson.InstallContactPerson.MobilePhoneNumberMustBeMobile', [ + 'mobilePhoneNumber' => (string) $mobilePhoneNumber, + ]); + + throw new InvalidArgumentException('Phone number must be mobile.'); + } + } +} diff --git a/src/ApplicationInstallations/UseCase/UnlinkContactPerson/Command.php b/src/ApplicationInstallations/UseCase/UnlinkContactPerson/Command.php new file mode 100644 index 00000000..2059747c --- /dev/null +++ b/src/ApplicationInstallations/UseCase/UnlinkContactPerson/Command.php @@ -0,0 +1,23 @@ +validate(); + } + + private function validate(): void + { + // no-op for now, but keep a place for future checks + } +} diff --git a/src/ApplicationInstallations/UseCase/UnlinkContactPerson/Handler.php b/src/ApplicationInstallations/UseCase/UnlinkContactPerson/Handler.php new file mode 100644 index 00000000..f2b47c37 --- /dev/null +++ b/src/ApplicationInstallations/UseCase/UnlinkContactPerson/Handler.php @@ -0,0 +1,69 @@ +logger->info('ContactPerson.UnlinkContactPerson.start', [ + 'contactPersonId' => $command->contactPersonId, + 'applicationInstallationId' => $command->applicationInstallationId, + ]); + + try { + /** @var AggregateRootEventsEmitterInterface|ContactPersonInterface $contactPerson */ + $contactPerson = $this->contactPersonRepository->getById($command->contactPersonId); + + /** @var AggregateRootEventsEmitterInterface|ApplicationInstallationInterface $applicationInstallation */ + $applicationInstallation = $this->applicationInstallationRepository->getById($command->applicationInstallationId); + + $entitiesToFlush = []; + + if ($contactPerson->isPartner()) { + $applicationInstallation->unlinkBitrix24PartnerContactPerson(); + } else { + $applicationInstallation->unlinkContactPerson(); + } + + $this->applicationInstallationRepository->save($applicationInstallation); + $entitiesToFlush[] = $applicationInstallation; + + $contactPerson->markAsDeleted($command->comment); + $this->contactPersonRepository->save($contactPerson); + $entitiesToFlush[] = $contactPerson; + + $this->flusher->flush(...$entitiesToFlush); + } catch (ApplicationInstallationNotFoundException|ContactPersonNotFoundException $e) { + $this->logger->warning('ContactPerson.UnlinkContactPerson.notFound', [ + 'message' => $e->getMessage(), + ]); + + throw $e; + } finally { + $this->logger->info('ContactPerson.UnlinkContactPerson.finish', [ + 'contactPersonId' => $command->contactPersonId, + 'applicationInstallationId' => $command->applicationInstallationId, + ]); + } + } +} diff --git a/src/ContactPersons/Entity/ContactPerson.php b/src/ContactPersons/Entity/ContactPerson.php index 9a57203d..5764db62 100644 --- a/src/ContactPersons/Entity/ContactPerson.php +++ b/src/ContactPersons/Entity/ContactPerson.php @@ -15,13 +15,12 @@ use Bitrix24\SDK\Application\Contracts\ContactPersons\Events\ContactPersonEmailChangedEvent; use Bitrix24\SDK\Application\Contracts\ContactPersons\Events\ContactPersonEmailVerifiedEvent; use Bitrix24\SDK\Application\Contracts\ContactPersons\Events\ContactPersonFullNameChangedEvent; +use Bitrix24\SDK\Application\Contracts\ContactPersons\Events\ContactPersonMobilePhoneChangedEvent; use Bitrix24\SDK\Application\Contracts\ContactPersons\Events\ContactPersonMobilePhoneVerifiedEvent; use Bitrix24\SDK\Core\Exceptions\InvalidArgumentException; use Bitrix24\SDK\Core\Exceptions\LogicException; use Carbon\CarbonImmutable; use libphonenumber\PhoneNumber; -use libphonenumber\PhoneNumberType; -use libphonenumber\PhoneNumberUtil; use Symfony\Component\Uid\Uuid; class ContactPerson extends AggregateRoot implements ContactPersonInterface @@ -30,9 +29,9 @@ class ContactPerson extends AggregateRoot implements ContactPersonInterface private CarbonImmutable $updatedAt; - private ?bool $isEmailVerified = false; + private bool $isEmailVerified = false; - private ?bool $isMobilePhoneVerified = false; + private bool $isMobilePhoneVerified = false; public function __construct( private readonly Uuid $id, @@ -44,9 +43,9 @@ public function __construct( private ?CarbonImmutable $mobilePhoneVerifiedAt, private ?string $comment, private ?string $externalId, - private readonly ?int $bitrix24UserId, + private readonly int $bitrix24UserId, private ?Uuid $bitrix24PartnerId, - private readonly ?UserAgentInfo $userAgentInfo, + private readonly UserAgentInfo $userAgentInfo, private readonly bool $isEmitContactPersonCreatedEvent = false, ) { $this->createdAt = new CarbonImmutable(); @@ -157,12 +156,27 @@ public function getEmail(): ?string return $this->email; } + /** + * Changes the contact person's email address. + * + * If an empty string is provided (including a string containing only whitespace), + * it will be normalized to `null` so that the database stores `NULL` instead of an empty value. + */ #[\Override] public function changeEmail(?string $email): void { - $this->email = $email; + if (null !== $email) { + $email = trim($email); + if ('' === $email) { + $email = null; + } + } + $this->email = $email; + $this->isEmailVerified = false; + $this->emailVerifiedAt = null; $this->updatedAt = new CarbonImmutable(); + $this->events[] = new ContactPersonEmailChangedEvent( $this->id, $this->updatedAt, @@ -173,6 +187,7 @@ public function changeEmail(?string $email): void public function markEmailAsVerified(?CarbonImmutable $verifiedAt = null): void { $this->isEmailVerified = true; + $this->emailVerifiedAt = $verifiedAt ?? new CarbonImmutable(); $this->events[] = new ContactPersonEmailVerifiedEvent( $this->id, @@ -180,32 +195,40 @@ public function markEmailAsVerified(?CarbonImmutable $verifiedAt = null): void ); } + #[\Override] + public function isPartner(): bool + { + return $this->getBitrix24PartnerId() instanceof Uuid; + } + #[\Override] public function getEmailVerifiedAt(): ?CarbonImmutable { return $this->emailVerifiedAt; } + /** + * Changes the contact's mobile phone number. + * + * Note: This method does not validate the phone number. + * Make sure to use it through the appropriate use case, + * where validation is performed. + * + * If you use this method outside a use case, + * ensure that you pass a valid mobile phone number. + */ #[\Override] public function changeMobilePhone(?PhoneNumber $phoneNumber): void { - if ($phoneNumber instanceof PhoneNumber) { - $phoneUtil = PhoneNumberUtil::getInstance(); - $isValidNumber = $phoneUtil->isValidNumber($phoneNumber); - - if (!$isValidNumber) { - throw new InvalidArgumentException('Invalid phone number.'); - } - - $numberType = $phoneUtil->getNumberType($phoneNumber); - if (PhoneNumberType::MOBILE !== $numberType) { - throw new InvalidArgumentException('Phone number must be mobile.'); - } - - $this->mobilePhoneNumber = $phoneNumber; - } - + $this->mobilePhoneNumber = $phoneNumber; + $this->isMobilePhoneVerified = false; + $this->mobilePhoneVerifiedAt = null; $this->updatedAt = new CarbonImmutable(); + + $this->events[] = new ContactPersonMobilePhoneChangedEvent( + $this->id, + $this->updatedAt, + ); } #[\Override] @@ -277,12 +300,6 @@ public function setBitrix24PartnerId(?Uuid $uuid): void $this->updatedAt = new CarbonImmutable(); } - #[\Override] - public function isPartner(): bool - { - return $this->bitrix24PartnerId instanceof Uuid; - } - #[\Override] public function isEmailVerified(): bool { diff --git a/src/ContactPersons/Enum/ContactPersonType.php b/src/ContactPersons/Enum/ContactPersonType.php new file mode 100644 index 00000000..edcf52a3 --- /dev/null +++ b/src/ContactPersons/Enum/ContactPersonType.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the MIT-LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Bitrix24\Lib\ContactPersons\Enum; + +enum ContactPersonType: string +{ + case personal = 'personal'; + case partner = 'partner'; +} diff --git a/src/ContactPersons/Infrastructure/Doctrine/ContactPersonRepository.php b/src/ContactPersons/Infrastructure/Doctrine/ContactPersonRepository.php index 863b4d35..4fb9b76f 100644 --- a/src/ContactPersons/Infrastructure/Doctrine/ContactPersonRepository.php +++ b/src/ContactPersons/Infrastructure/Doctrine/ContactPersonRepository.php @@ -9,6 +9,7 @@ use Bitrix24\SDK\Application\Contracts\ContactPersons\Entity\ContactPersonStatus; use Bitrix24\SDK\Application\Contracts\ContactPersons\Exceptions\ContactPersonNotFoundException; use Bitrix24\SDK\Application\Contracts\ContactPersons\Repository\ContactPersonRepositoryInterface; +use Bitrix24\SDK\Application\Contracts\Events\AggregateRootEventsEmitterInterface; use Bitrix24\SDK\Core\Exceptions\InvalidArgumentException; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; @@ -17,7 +18,7 @@ class ContactPersonRepository implements ContactPersonRepositoryInterface { - private readonly EntityRepository $repository; // Внутренний репозиторий для базовых операций + private readonly EntityRepository $repository; public function __construct(private readonly EntityManagerInterface $entityManager) { @@ -54,6 +55,11 @@ public function delete(Uuid $uuid): void $this->save($contactPerson); } + /** + * @phpstan-return ContactPersonInterface&AggregateRootEventsEmitterInterface + * + * @throws ContactPersonNotFoundException + */ #[\Override] public function getById(Uuid $uuid): ContactPersonInterface { @@ -122,7 +128,7 @@ public function findByExternalId(string $externalId, ?ContactPersonStatus $conta $criteria = ['externalId' => $externalId]; if ($contactPersonStatus instanceof ContactPersonStatus) { - $criteria['contactPersonStatus'] = $contactPersonStatus->name; + $criteria['status'] = $contactPersonStatus->name; } return $this->repository->findBy($criteria); diff --git a/src/ContactPersons/UseCase/ChangeProfile/Command.php b/src/ContactPersons/UseCase/ChangeProfile/Command.php new file mode 100644 index 00000000..0dc45ccf --- /dev/null +++ b/src/ContactPersons/UseCase/ChangeProfile/Command.php @@ -0,0 +1,32 @@ +validate(); + } + + private function validate(): void + { + // Note: empty email is allowed for profile changes. + // If you pass an empty string (or whitespace), it will be normalized to `null` + // on the entity level, so the database will store `NULL` instead of an empty string. + if ('' !== trim($this->email) && !filter_var($this->email, FILTER_VALIDATE_EMAIL)) { + throw new InvalidArgumentException('Invalid email format.'); + } + } +} diff --git a/src/ContactPersons/UseCase/ChangeProfile/Handler.php b/src/ContactPersons/UseCase/ChangeProfile/Handler.php new file mode 100644 index 00000000..54b8333e --- /dev/null +++ b/src/ContactPersons/UseCase/ChangeProfile/Handler.php @@ -0,0 +1,96 @@ +logger->info('ContactPerson.ChangeProfile.start', [ + 'contactPersonId' => $command->contactPersonId, + 'fullName' => (string) $command->fullName, + 'email' => $command->email, + 'mobilePhoneNumber' => (string) $command->mobilePhoneNumber, + ]); + + try { + /** @var AggregateRootEventsEmitterInterface|ContactPersonInterface $contactPerson */ + $contactPerson = $this->contactPersonRepository->getById($command->contactPersonId); + + if (!$command->fullName->equal($contactPerson->getFullName())) { + $contactPerson->changeFullName($command->fullName); + } + + if ($command->email !== $contactPerson->getEmail()) { + $contactPerson->changeEmail($command->email); + } + + $this->guardMobilePhoneNumber($command->mobilePhoneNumber); + if (!$command->mobilePhoneNumber->equals($contactPerson->getMobilePhone())) { + $contactPerson->changeMobilePhone($command->mobilePhoneNumber); + } + + $this->contactPersonRepository->save($contactPerson); + $this->flusher->flush($contactPerson); + + $this->logger->info('ContactPerson.ChangeProfile.finish', [ + 'contactPersonId' => $contactPerson->getId()->toRfc4122(), + 'updatedFields' => [ + 'fullName' => (string) $command->fullName, + 'email' => $command->email, + 'mobilePhoneNumber' => (string) $command->mobilePhoneNumber, + ], + ]); + } catch (ContactPersonNotFoundException $contactPersonNotFoundException) { + $this->logger->warning('ContactPerson.ChangeProfile.contactPersonNotFound', [ + 'contactPersonId' => $command->contactPersonId->toRfc4122(), + 'message' => $contactPersonNotFoundException->getMessage(), + ]); + + throw $contactPersonNotFoundException; + } finally { + $this->logger->info('ContactPerson.ChangeProfile.finish', [ + 'contactPersonId' => $command->contactPersonId->toRfc4122(), + ]); + } + } + + private function guardMobilePhoneNumber(PhoneNumber $mobilePhoneNumber): void + { + if (!$this->phoneNumberUtil->isValidNumber($mobilePhoneNumber)) { + $this->logger->warning('ContactPerson.ChangeProfile.InvalidMobilePhoneNumber', [ + 'mobilePhoneNumber' => (string) $mobilePhoneNumber, + ]); + + throw new InvalidArgumentException('Invalid mobile phone number.'); + } + + if (PhoneNumberType::MOBILE !== $this->phoneNumberUtil->getNumberType($mobilePhoneNumber)) { + $this->logger->warning('ContactPerson.ChangeProfile.MobilePhoneNumberMustBeMobile', [ + 'mobilePhoneNumber' => (string) $mobilePhoneNumber, + ]); + + throw new InvalidArgumentException('Phone number must be mobile.'); + } + } +} diff --git a/src/ContactPersons/UseCase/MarkEmailAsVerified/Command.php b/src/ContactPersons/UseCase/MarkEmailAsVerified/Command.php new file mode 100644 index 00000000..82013e56 --- /dev/null +++ b/src/ContactPersons/UseCase/MarkEmailAsVerified/Command.php @@ -0,0 +1,34 @@ +validate(); + } + + private function validate(): void + { + $email = trim($this->email); + + // Email verification requires a real (non-empty) email address. + // An empty value cannot be confirmed, so we fail fast with a clear error. + if ('' === $email) { + throw new \InvalidArgumentException('Cannot confirm an empty email.'); + } + + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + throw new \InvalidArgumentException('Invalid email format.'); + } + } +} diff --git a/src/ContactPersons/UseCase/MarkEmailAsVerified/Handler.php b/src/ContactPersons/UseCase/MarkEmailAsVerified/Handler.php new file mode 100644 index 00000000..40f2fb6b --- /dev/null +++ b/src/ContactPersons/UseCase/MarkEmailAsVerified/Handler.php @@ -0,0 +1,59 @@ +logger->info('ContactPerson.MarkEmailVerification.start', [ + 'contactPersonId' => $command->contactPersonId->toRfc4122(), + 'email' => $command->email, + ]); + + try { + /** @var AggregateRootEventsEmitterInterface|ContactPersonInterface $contactPerson */ + $contactPerson = $this->contactPersonRepository->getById($command->contactPersonId); + + $actualEmail = $contactPerson->getEmail(); + + if (mb_strtolower((string) $actualEmail) === mb_strtolower($command->email)) { + $contactPerson->markEmailAsVerified($command->emailVerifiedAt); + $this->contactPersonRepository->save($contactPerson); + $this->flusher->flush($contactPerson); + } else { + $this->logger->warning('ContactPerson.MarkEmailVerification.emailMismatch', [ + 'contactPersonId' => $command->contactPersonId->toRfc4122(), + 'actualEmail' => $actualEmail, + 'expectedEmail' => $command->email, + ]); + } + } catch (ContactPersonNotFoundException $contactPersonNotFoundException) { + $this->logger->warning('ContactPerson.MarkEmailVerification.contactPersonNotFound', [ + 'contactPersonId' => $command->contactPersonId->toRfc4122(), + 'message' => $contactPersonNotFoundException->getMessage(), + ]); + + throw $contactPersonNotFoundException; + } finally { + $this->logger->info('ContactPerson.MarkEmailVerification.finish', [ + 'contactPersonId' => $command->contactPersonId->toRfc4122(), + ]); + } + } +} diff --git a/src/ContactPersons/UseCase/MarkMobilePhoneAsVerified/Command.php b/src/ContactPersons/UseCase/MarkMobilePhoneAsVerified/Command.php new file mode 100644 index 00000000..6e339c65 --- /dev/null +++ b/src/ContactPersons/UseCase/MarkMobilePhoneAsVerified/Command.php @@ -0,0 +1,22 @@ +validate(); + } + + private function validate(): void {} +} diff --git a/src/ContactPersons/UseCase/MarkMobilePhoneAsVerified/Handler.php b/src/ContactPersons/UseCase/MarkMobilePhoneAsVerified/Handler.php new file mode 100644 index 00000000..9d45766e --- /dev/null +++ b/src/ContactPersons/UseCase/MarkMobilePhoneAsVerified/Handler.php @@ -0,0 +1,71 @@ +phoneNumberUtil->format($command->phone, PhoneNumberFormat::E164); + + $this->logger->info('ContactPerson.MarkMobilePhoneVerification.start', [ + 'contactPersonId' => $command->contactPersonId->toRfc4122(), + 'phone' => $expectedMobilePhoneE164, + ]); + + try { + /** @var AggregateRootEventsEmitterInterface|ContactPersonInterface $contactPerson */ + $contactPerson = $this->contactPersonRepository->getById($command->contactPersonId); + + $actualPhone = $contactPerson->getMobilePhone(); + + if (null !== $actualPhone && $command->phone->equals($actualPhone)) { + $contactPerson->markMobilePhoneAsVerified($command->phoneVerifiedAt); + + $this->contactPersonRepository->save($contactPerson); + $this->flusher->flush($contactPerson); + } else { + // Format the current mobile phone number to the international E.164 format + $actualMobilePhoneE164 = $this->phoneNumberUtil->format($actualPhone, PhoneNumberFormat::E164); + + $this->logger->warning('ContactPerson.MarkMobilePhoneVerification.phoneMismatch', [ + 'contactPersonId' => $command->contactPersonId->toRfc4122(), + 'actualPhone' => $actualMobilePhoneE164, + 'expectedPhone' => $expectedMobilePhoneE164, + ]); + + return; + } + } catch (ContactPersonNotFoundException $contactPersonNotFoundException) { + $this->logger->warning('ContactPerson.MarkMobilePhoneVerification.contactPersonNotFound', [ + 'contactPersonId' => $command->contactPersonId->toRfc4122(), + 'message' => $contactPersonNotFoundException->getMessage(), + ]); + + throw $contactPersonNotFoundException; + } finally { + $this->logger->info('ContactPerson.MarkMobilePhoneVerification.finish', [ + 'contactPersonId' => $command->contactPersonId->toRfc4122(), + ]); + } + } +} diff --git a/tests/Functional/ApplicationInstallations/Builders/ApplicationInstallationBuilder.php b/tests/Functional/ApplicationInstallations/Builders/ApplicationInstallationBuilder.php index 264a03a5..b36f97a1 100644 --- a/tests/Functional/ApplicationInstallations/Builders/ApplicationInstallationBuilder.php +++ b/tests/Functional/ApplicationInstallations/Builders/ApplicationInstallationBuilder.php @@ -17,11 +17,11 @@ class ApplicationInstallationBuilder private Uuid $bitrix24AccountId; - private readonly ?Uuid $contactPersonId; + private ?Uuid $contactPersonId; - private readonly ?Uuid $bitrix24PartnerContactPersonId; + private ?Uuid $bitrix24PartnerContactPersonId; - private readonly ?Uuid $bitrix24PartnerId; + private ?Uuid $bitrix24PartnerId = null; private ?string $externalId = null; @@ -43,7 +43,6 @@ public function __construct() $this->bitrix24AccountId = Uuid::v7(); $this->bitrix24PartnerContactPersonId = Uuid::v7(); $this->contactPersonId = Uuid::v7(); - $this->bitrix24PartnerId = Uuid::v7(); $this->portalUsersCount = random_int(1, 1_000_000); } @@ -61,6 +60,13 @@ public function withApplicationToken(string $applicationToken): self return $this; } + public function withBitrix24PartnerId(?Uuid $uuid): self + { + $this->bitrix24PartnerId = $uuid; + + return $this; + } + public function withApplicationStatusInstallation(ApplicationInstallationStatus $applicationInstallationStatus): self { $this->status = $applicationInstallationStatus; @@ -82,6 +88,20 @@ public function withBitrix24AccountId(Uuid $uuid): self return $this; } + public function withContactPersonId(?Uuid $uuid): self + { + $this->contactPersonId = $uuid; + + return $this; + } + + public function withBitrix24PartnerContactPersonId(?Uuid $uuid): self + { + $this->bitrix24PartnerContactPersonId = $uuid; + + return $this; + } + public function withPortalLicenseFamily(PortalLicenseFamily $portalLicenseFamily): self { $this->portalLicenseFamily = $portalLicenseFamily; diff --git a/tests/Functional/ApplicationInstallations/UseCase/InstallContactPerson/HandlerTest.php b/tests/Functional/ApplicationInstallations/UseCase/InstallContactPerson/HandlerTest.php new file mode 100644 index 00000000..076c62b6 --- /dev/null +++ b/tests/Functional/ApplicationInstallations/UseCase/InstallContactPerson/HandlerTest.php @@ -0,0 +1,296 @@ + + * + * For the full copyright and license information, please view the MIT-LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Bitrix24\Lib\Tests\Functional\ApplicationInstallations\UseCase\InstallContactPerson; + +use Bitrix24\Lib\ApplicationInstallations\Infrastructure\Doctrine\ApplicationInstallationRepository; +use Bitrix24\Lib\ApplicationInstallations\UseCase\InstallContactPerson\Command; +use Bitrix24\Lib\ApplicationInstallations\UseCase\InstallContactPerson\Handler; +use Bitrix24\Lib\Bitrix24Accounts\Infrastructure\Doctrine\Bitrix24AccountRepository; +use Bitrix24\Lib\ContactPersons\Infrastructure\Doctrine\ContactPersonRepository; +use Bitrix24\Lib\Services\Flusher; +use Bitrix24\Lib\Tests\EntityManagerFactory; +use Bitrix24\Lib\Tests\Functional\ApplicationInstallations\Builders\ApplicationInstallationBuilder; +use Bitrix24\Lib\Tests\Functional\Bitrix24Accounts\Builders\Bitrix24AccountBuilder; +use Bitrix24\Lib\Tests\Functional\ContactPersons\Builders\ContactPersonBuilder; +use Bitrix24\SDK\Application\ApplicationStatus; +use Bitrix24\SDK\Application\Contracts\ApplicationInstallations\Entity\ApplicationInstallationStatus; +use Bitrix24\SDK\Application\Contracts\ApplicationInstallations\Events\ApplicationInstallationContactPersonLinkedEvent; +use Bitrix24\SDK\Application\Contracts\ApplicationInstallations\Exceptions\ApplicationInstallationNotFoundException; +use Bitrix24\SDK\Application\Contracts\Bitrix24Accounts\Entity\Bitrix24AccountStatus; +use Bitrix24\SDK\Application\Contracts\ContactPersons\Events\ContactPersonCreatedEvent; +use Bitrix24\SDK\Application\PortalLicenseFamily; +use Bitrix24\SDK\Core\Credentials\Scope; +use libphonenumber\PhoneNumber; +use libphonenumber\PhoneNumberUtil; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use Psr\Log\NullLogger; +use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\Stopwatch\Stopwatch; +use Symfony\Component\Uid\Uuid; + +/** + * @internal + */ +#[CoversClass(Handler::class)] +class HandlerTest extends TestCase +{ + /** + * @var PhoneNumberUtil + */ + public $phoneNumberUtil; + + private Handler $handler; + + private Flusher $flusher; + + private ContactPersonRepository $repository; + + private ApplicationInstallationRepository $applicationInstallationRepository; + + private Bitrix24AccountRepository $bitrix24accountRepository; + + private TraceableEventDispatcher $eventDispatcher; + + #[\Override] + protected function setUp(): void + { + $entityManager = EntityManagerFactory::get(); + $this->eventDispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch()); + $this->repository = new ContactPersonRepository($entityManager); + $this->applicationInstallationRepository = new ApplicationInstallationRepository($entityManager); + $this->bitrix24accountRepository = new Bitrix24AccountRepository($entityManager); + $this->phoneNumberUtil = PhoneNumberUtil::getInstance(); + $this->flusher = new Flusher($entityManager, $this->eventDispatcher); + $this->handler = new Handler( + $this->applicationInstallationRepository, + $this->repository, + $this->phoneNumberUtil, + $this->flusher, + new NullLogger() + ); + } + + #[Test] + public function testInstallContactPersonSuccess(): void + { + // Подготовка Bitrix24 аккаунта и установки приложения + $applicationToken = Uuid::v7()->toRfc4122(); + $memberId = Uuid::v4()->toRfc4122(); + $externalId = Uuid::v7()->toRfc4122(); + + $bitrix24Account = (new Bitrix24AccountBuilder()) + ->withApplicationScope(new Scope(['crm'])) + ->withStatus(Bitrix24AccountStatus::new) + ->withApplicationToken($applicationToken) + ->withMemberId($memberId) + ->withMaster(true) + ->withSetToken() + ->withInstalled() + ->build() + ; + + $this->bitrix24accountRepository->save($bitrix24Account); + + $applicationInstallation = (new ApplicationInstallationBuilder()) + ->withApplicationStatus(new ApplicationStatus('F')) + ->withPortalLicenseFamily(PortalLicenseFamily::free) + ->withBitrix24AccountId($bitrix24Account->getId()) + ->withApplicationStatusInstallation(ApplicationInstallationStatus::active) + ->withApplicationToken($applicationToken) + ->withContactPersonId(null) + ->withBitrix24PartnerContactPersonId(null) + ->withExternalId($externalId) + ->build() + ; + + $this->applicationInstallationRepository->save($applicationInstallation); + $this->flusher->flush(); + + // Данные контакта + $contactPersonBuilder = new ContactPersonBuilder(); + $contactPerson = $contactPersonBuilder + ->withEmail('john.doe@example.com') + ->withMobilePhoneNumber($this->createPhoneNumber('+79991234567')) + ->withComment('Test comment') + ->withExternalId($externalId) + ->withBitrix24UserId($bitrix24Account->getBitrix24UserId()) + ->withBitrix24PartnerId($applicationInstallation->getBitrix24PartnerId()) + ->build() + ; + + // Запуск use-case + $this->handler->handle( + new Command( + $applicationInstallation->getId(), + $contactPerson->getFullName(), + $bitrix24Account->getBitrix24UserId(), + $contactPerson->getUserAgentInfo(), + $contactPerson->getEmail(), + $contactPerson->getMobilePhone(), + $contactPerson->getComment(), + $contactPerson->getExternalId(), + $contactPerson->getBitrix24PartnerId(), + ) + ); + + // Проверки: событие, связь и наличие контакта + $dispatchedEvents = $this->eventDispatcher->getOrphanedEvents(); + $this->assertContains(ContactPersonCreatedEvent::class, $dispatchedEvents); + $this->assertContains(ApplicationInstallationContactPersonLinkedEvent::class, $dispatchedEvents); + + $foundInstallation = $this->applicationInstallationRepository->getById($applicationInstallation->getId()); + $contactPersonId = $foundInstallation->getContactPersonId(); + $this->assertNotNull($contactPersonId); + + $foundContactPerson = $this->repository->getById($contactPersonId); + $this->assertEquals($contactPersonId, $foundContactPerson->getId()); + $this->assertEquals($contactPerson->getEmail(), $foundContactPerson->getEmail()); + $this->assertEquals($contactPerson->getMobilePhone(), $foundContactPerson->getMobilePhone()); + $this->assertEquals($contactPerson->getFullName(), $foundContactPerson->getFullName()); + $this->assertEquals($contactPerson->getComment(), $foundContactPerson->getComment()); + $this->assertEquals($contactPerson->getExternalId(), $foundContactPerson->getExternalId()); + $this->assertEquals($contactPerson->getBitrix24UserId(), $foundContactPerson->getBitrix24UserId()); + $this->assertEquals($contactPerson->getBitrix24PartnerId(), $foundContactPerson->getBitrix24PartnerId()); + } + + #[Test] + public function testInstallContactPersonWithWrongApplicationInstallationId(): void + { + // Подготовим входные данные контакта (без реальной установки) + $contactPersonBuilder = new ContactPersonBuilder(); + $contactPerson = $contactPersonBuilder + ->withEmail('john.doe@example.com') + ->withMobilePhoneNumber($this->createPhoneNumber('+79991234567')) + ->withComment('Test comment') + ->withExternalId(Uuid::v7()->toRfc4122()) + ->build() + ; + + $uuidV7 = Uuid::v7(); + + $this->expectException(ApplicationInstallationNotFoundException::class); + + $this->handler->handle( + new Command( + $uuidV7, + $contactPerson->getFullName(), + random_int(1, 1_000_000), + $contactPerson->getUserAgentInfo(), + $contactPerson->getEmail(), + $contactPerson->getMobilePhone(), + $contactPerson->getComment(), + $contactPerson->getExternalId(), + $contactPerson->getBitrix24PartnerId(), + ) + ); + } + + #[Test] + public function testInstallContactPersonWithInvalidEmail(): void + { + // Подготовим входные данные контакта + $contactPersonBuilder = new ContactPersonBuilder(); + $contactPerson = $contactPersonBuilder + ->withEmail('invalid-email') + ->build() + ; + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid email format.'); + + new Command( + Uuid::v7(), + $contactPerson->getFullName(), + 1, + $contactPerson->getUserAgentInfo(), + $contactPerson->getEmail(), + $contactPerson->getMobilePhone(), + $contactPerson->getComment(), + $contactPerson->getExternalId(), + $contactPerson->getBitrix24PartnerId(), + ); + } + + #[Test] + #[DataProvider('invalidPhoneProvider')] + public function testInstallContactPersonWithInvalidPhone(string $phoneNumber, string $region): void + { + // Подготовка Bitrix24 аккаунта и установки приложения + $applicationToken = Uuid::v7()->toRfc4122(); + $memberId = Uuid::v7()->toRfc4122(); + + $bitrix24Account = (new Bitrix24AccountBuilder()) + ->withApplicationToken($applicationToken) + ->withMemberId($memberId) + ->build() + ; + $this->bitrix24accountRepository->save($bitrix24Account); + + $applicationInstallation = (new ApplicationInstallationBuilder()) + ->withBitrix24AccountId($bitrix24Account->getId()) + ->withApplicationToken($applicationToken) + ->withApplicationStatus(new ApplicationStatus('F')) + ->withPortalLicenseFamily(PortalLicenseFamily::free) + ->build() + ; + $this->applicationInstallationRepository->save($applicationInstallation); + $this->flusher->flush(); + + $invalidPhoneNumber = $this->phoneNumberUtil->parse($phoneNumber, $region); + + $contactPersonBuilder = new ContactPersonBuilder(); + $contactPerson = $contactPersonBuilder + ->withEmail('john.doe@example.com') + ->withMobilePhoneNumber($invalidPhoneNumber) + ->build() + ; + + $this->handler->handle( + new Command( + $applicationInstallation->getId(), + $contactPerson->getFullName(), + $bitrix24Account->getBitrix24UserId(), + $contactPerson->getUserAgentInfo(), + $contactPerson->getEmail(), + $contactPerson->getMobilePhone(), + $contactPerson->getComment(), + $contactPerson->getExternalId(), + $contactPerson->getBitrix24PartnerId(), + ) + ); + + // Проверяем, что контакт не был создан + $foundInstallation = $this->applicationInstallationRepository->getById($applicationInstallation->getId()); + $this->assertNull($foundInstallation->getBitrix24PartnerId()); + } + + public static function invalidPhoneProvider(): array + { + return [ + 'invalid format' => ['123', 'RU'], + 'not mobile' => ['+74951234567', 'RU'], // Moscow landline + ]; + } + + private function createPhoneNumber(string $number): PhoneNumber + { + $phoneNumberUtil = PhoneNumberUtil::getInstance(); + + return $phoneNumberUtil->parse($number, 'RU'); + } +} diff --git a/tests/Functional/ApplicationInstallations/UseCase/UnlinkContactPerson/HandlerTest.php b/tests/Functional/ApplicationInstallations/UseCase/UnlinkContactPerson/HandlerTest.php new file mode 100644 index 00000000..ee82d512 --- /dev/null +++ b/tests/Functional/ApplicationInstallations/UseCase/UnlinkContactPerson/HandlerTest.php @@ -0,0 +1,370 @@ + + * + * For the full copyright and license information, please view the MIT-LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Bitrix24\Lib\Tests\Functional\ApplicationInstallations\UseCase\UnlinkContactPerson; + +use Bitrix24\Lib\ApplicationInstallations\Infrastructure\Doctrine\ApplicationInstallationRepository; +use Bitrix24\Lib\ApplicationInstallations\UseCase\UnlinkContactPerson\Command; +use Bitrix24\Lib\ApplicationInstallations\UseCase\UnlinkContactPerson\Handler; +use Bitrix24\Lib\Bitrix24Accounts\Infrastructure\Doctrine\Bitrix24AccountRepository; +use Bitrix24\Lib\ContactPersons\Infrastructure\Doctrine\ContactPersonRepository; +use Bitrix24\Lib\Services\Flusher; +use Bitrix24\Lib\Tests\EntityManagerFactory; +use Bitrix24\Lib\Tests\Functional\ApplicationInstallations\Builders\ApplicationInstallationBuilder; +use Bitrix24\Lib\Tests\Functional\Bitrix24Accounts\Builders\Bitrix24AccountBuilder; +use Bitrix24\Lib\Tests\Functional\ContactPersons\Builders\ContactPersonBuilder; +use Bitrix24\SDK\Application\ApplicationStatus; +use Bitrix24\SDK\Application\Contracts\ApplicationInstallations\Entity\ApplicationInstallationStatus; +use Bitrix24\SDK\Application\Contracts\ApplicationInstallations\Events\ApplicationInstallationBitrix24PartnerContactPersonUnlinkedEvent; +use Bitrix24\SDK\Application\Contracts\ApplicationInstallations\Events\ApplicationInstallationContactPersonUnlinkedEvent; +use Bitrix24\SDK\Application\Contracts\ApplicationInstallations\Exceptions\ApplicationInstallationNotFoundException; +use Bitrix24\SDK\Application\Contracts\Bitrix24Accounts\Entity\Bitrix24AccountStatus; +use Bitrix24\SDK\Application\Contracts\ContactPersons\Events\ContactPersonDeletedEvent; +use Bitrix24\SDK\Application\Contracts\ContactPersons\Exceptions\ContactPersonNotFoundException; +use Bitrix24\SDK\Application\PortalLicenseFamily; +use Bitrix24\SDK\Core\Credentials\Scope; +use Bitrix24\SDK\Core\Exceptions\InvalidArgumentException; +use libphonenumber\PhoneNumber; +use libphonenumber\PhoneNumberUtil; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use Psr\Log\NullLogger; +use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\Stopwatch\Stopwatch; +use Symfony\Component\Uid\Uuid; + +/** + * @internal + */ +#[CoversClass(Handler::class)] +class HandlerTest extends TestCase +{ + private Handler $handler; + + private Flusher $flusher; + + private ContactPersonRepository $repository; + + private ApplicationInstallationRepository $applicationInstallationRepository; + + private Bitrix24AccountRepository $bitrix24accountRepository; + + private TraceableEventDispatcher $eventDispatcher; + + #[\Override] + protected function setUp(): void + { + $this->truncateAllTables(); + $entityManager = EntityManagerFactory::get(); + $this->eventDispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch()); + $this->repository = new ContactPersonRepository($entityManager); + $this->applicationInstallationRepository = new ApplicationInstallationRepository($entityManager); + $this->bitrix24accountRepository = new Bitrix24AccountRepository($entityManager); + $this->flusher = new Flusher($entityManager, $this->eventDispatcher); + $this->handler = new Handler( + $this->applicationInstallationRepository, + $this->repository, + $this->flusher, + new NullLogger() + ); + } + + /** + * @throws InvalidArgumentException|\Random\RandomException + */ + #[Test] + public function testUninstallContactPersonSuccess(): void + { + // Подготовка Bitrix24 аккаунта и установки приложения + $applicationToken = Uuid::v7()->toRfc4122(); + $memberId = Uuid::v4()->toRfc4122(); + $externalId = Uuid::v7()->toRfc4122(); + + $bitrix24Account = (new Bitrix24AccountBuilder()) + ->withApplicationScope(new Scope(['crm'])) + ->withStatus(Bitrix24AccountStatus::new) + ->withApplicationToken($applicationToken) + ->withMemberId($memberId) + ->withMaster(true) + ->withSetToken() + ->withInstalled() + ->build(); + + $this->bitrix24accountRepository->save($bitrix24Account); + + $applicationInstallation = (new ApplicationInstallationBuilder()) + ->withApplicationStatus(new ApplicationStatus('F')) + ->withPortalLicenseFamily(PortalLicenseFamily::free) + ->withBitrix24AccountId($bitrix24Account->getId()) + ->withApplicationStatusInstallation(ApplicationInstallationStatus::active) + ->withApplicationToken($applicationToken) + ->withContactPersonId(null) + ->withBitrix24PartnerContactPersonId(null) + ->withExternalId($externalId) + ->build(); + + $this->applicationInstallationRepository->save($applicationInstallation); + + // Создаём контакт и привязываем к установке + $contactPersonBuilder = new ContactPersonBuilder(); + $contactPerson = $contactPersonBuilder + ->withEmail('john.doe@example.com') + ->withMobilePhoneNumber($this->createPhoneNumber('+79991234567')) + ->withComment('Test comment') + ->withExternalId($externalId) + ->withBitrix24UserId($bitrix24Account->getBitrix24UserId()) + ->build(); + + $this->repository->save($contactPerson); + $applicationInstallation->linkContactPerson($contactPerson->getId()); + $this->applicationInstallationRepository->save($applicationInstallation); + $this->flusher->flush(); + + // Запуск use-case + $this->handler->handle( + new Command( + $contactPerson->getId(), + $applicationInstallation->getId(), + 'Deleted by test' + ) + ); + + // Проверки: события отвязки и удаления контакта + $dispatchedEvents = $this->eventDispatcher->getOrphanedEvents(); + $this->assertContains(ContactPersonDeletedEvent::class, $dispatchedEvents); + $this->assertContains(ApplicationInstallationContactPersonUnlinkedEvent::class, $dispatchedEvents); + + // Перечитаем установку и проверим, что контакт отвязан + $foundInstallation = $this->applicationInstallationRepository->getById($applicationInstallation->getId()); + $this->assertNull($foundInstallation->getContactPersonId()); + + // Контакт помечен как удалённый и недоступен через getById + $this->expectException(ContactPersonNotFoundException::class); + $this->repository->getById($contactPerson->getId()); + } + + #[Test] + public function testUninstallContactPersonNotFound(): void + { + // Подготовка Bitrix24 аккаунта и установки приложения (чтобы getCurrent() вернул установку) + $applicationToken = Uuid::v7()->toRfc4122(); + $memberId = Uuid::v4()->toRfc4122(); + $externalId = Uuid::v7()->toRfc4122(); + + $bitrix24Account = (new Bitrix24AccountBuilder()) + ->withApplicationScope(new Scope(['crm'])) + ->withStatus(Bitrix24AccountStatus::new) + ->withApplicationToken($applicationToken) + ->withMemberId($memberId) + ->withMaster(true) + ->withSetToken() + ->withInstalled() + ->build(); + + $this->bitrix24accountRepository->save($bitrix24Account); + + $applicationInstallation = (new ApplicationInstallationBuilder()) + ->withApplicationStatus(new ApplicationStatus('F')) + ->withPortalLicenseFamily(PortalLicenseFamily::free) + ->withBitrix24AccountId($bitrix24Account->getId()) + ->withApplicationStatusInstallation(ApplicationInstallationStatus::active) + ->withApplicationToken($applicationToken) + ->withContactPersonId(null) + ->withBitrix24PartnerContactPersonId(null) + ->withExternalId($externalId) + ->build(); + + $this->applicationInstallationRepository->save($applicationInstallation); + $this->flusher->flush(); + + // Ожидаем исключение, т.к. контактного лица с таким ID нет + $this->expectException(ContactPersonNotFoundException::class); + + $this->handler->handle( + new Command( + Uuid::v7(), + $applicationInstallation->getId(), + 'Deleted by test' + ) + ); + } + + #[Test] + public function testUninstallContactPersonWithWrongApplicationInstallationId(): void + { + // Создадим контактное лицо, но не будем создавать установку приложения, + // чтобы репозиторий вернул ApplicationInstallationNotFoundException при getCurrent() + $externalId = Uuid::v7()->toRfc4122(); + $contactPerson = (new ContactPersonBuilder()) + ->withEmail('john.doe@example.com') + ->withMobilePhoneNumber($this->createPhoneNumber('+79991234567')) + ->withComment('Test comment') + ->withExternalId($externalId) + ->build(); + + $this->repository->save($contactPerson); + $this->flusher->flush(); + + $this->expectException(ApplicationInstallationNotFoundException::class); + + $this->handler->handle( + new Command( + $contactPerson->getId(), + Uuid::v7(), + 'Deleted by test' + ) + ); + } + + /** + * @throws InvalidArgumentException|\Random\RandomException + */ + #[Test] + public function testUninstallPartnerContactPersonSuccess(): void + { + // Подготовка Bitrix24 аккаунта и установки приложения + $applicationToken = Uuid::v7()->toRfc4122(); + $memberId = Uuid::v4()->toRfc4122(); + $externalId = Uuid::v7()->toRfc4122(); + + $bitrix24Account = (new Bitrix24AccountBuilder()) + ->withApplicationScope(new Scope(['crm'])) + ->withStatus(Bitrix24AccountStatus::new) + ->withApplicationToken($applicationToken) + ->withMemberId($memberId) + ->withMaster(true) + ->withSetToken() + ->withInstalled() + ->build(); + + $this->bitrix24accountRepository->save($bitrix24Account); + + $applicationInstallation = (new ApplicationInstallationBuilder()) + ->withApplicationStatus(new ApplicationStatus('F')) + ->withPortalLicenseFamily(PortalLicenseFamily::free) + ->withBitrix24AccountId($bitrix24Account->getId()) + ->withApplicationStatusInstallation(ApplicationInstallationStatus::active) + ->withApplicationToken($applicationToken) + ->withContactPersonId(null) + ->withBitrix24PartnerContactPersonId(null) + ->withExternalId($externalId) + ->build(); + + $this->applicationInstallationRepository->save($applicationInstallation); + + // Создаём контакт и привязываем как партнёрский к установке + $contactPersonBuilder = new ContactPersonBuilder(); + $contactPerson = $contactPersonBuilder + ->withEmail('john.doe@example.com') + ->withMobilePhoneNumber($this->createPhoneNumber('+79991234567')) + ->withComment('Test comment') + ->withExternalId($externalId) + ->withBitrix24UserId($bitrix24Account->getBitrix24UserId()) + ->withBitrix24PartnerId(Uuid::v7()) + ->build(); + + $this->repository->save($contactPerson); + $applicationInstallation->linkBitrix24PartnerContactPerson($contactPerson->getId()); + $this->applicationInstallationRepository->save($applicationInstallation); + $this->flusher->flush(); + + // Запуск use-case + $this->handler->handle( + new Command( + $contactPerson->getId(), + $applicationInstallation->getId(), + 'Deleted by test' + ) + ); + + // Проверки: события отвязки и удаления контакта + $dispatchedEvents = $this->eventDispatcher->getOrphanedEvents(); + $this->assertContains(ContactPersonDeletedEvent::class, $dispatchedEvents); + $this->assertContains(ApplicationInstallationBitrix24PartnerContactPersonUnlinkedEvent::class, $dispatchedEvents); + + // Перечитаем установку и проверим, что партнёрский контакт отвязан + $foundInstallation = $this->applicationInstallationRepository->getById($applicationInstallation->getId()); + $this->assertNull($foundInstallation->getBitrix24PartnerContactPersonId()); + + $this->expectException(ContactPersonNotFoundException::class); + $this->repository->getById($contactPerson->getId()); + } + + #[Test] + public function testUninstallPartnerContactPersonWithWrongApplicationInstallationId(): void + { + // Создадим контактное лицо, но не будем создавать установку приложения, + // чтобы репозиторий вернул ApplicationInstallationNotFoundException при getCurrent() + $externalId = Uuid::v7()->toRfc4122(); + $contactPerson = (new ContactPersonBuilder()) + ->withEmail('john.doe@example.com') + ->withMobilePhoneNumber($this->createPhoneNumber('+79991234567')) + ->withComment('Test comment') + ->withExternalId($externalId) + ->build(); + + $this->repository->save($contactPerson); + $this->flusher->flush(); + + $this->expectException(ApplicationInstallationNotFoundException::class); + + $this->handler->handle( + new Command( + $contactPerson->getId(), + Uuid::v7(), + 'Deleted by test' + ) + ); + } + + private function createPhoneNumber(string $number): PhoneNumber + { + $phoneNumberUtil = PhoneNumberUtil::getInstance(); + return $phoneNumberUtil->parse($number, 'RU'); + } + + private function truncateAllTables(): void + { + $entityManager = EntityManagerFactory::get(); + $connection = $entityManager->getConnection(); + $schemaManager = $connection->createSchemaManager(); + + $names = $schemaManager->introspectTableNames(); + + if ($names === []) { + return; + } + + $quotedTables = []; + + foreach ($names as $name) { + $tableName = $name->toString(); + $quotedTables[] = $tableName; + } + + $sql = 'TRUNCATE ' . implode(', ', $quotedTables) . ' RESTART IDENTITY CASCADE'; + + $connection->beginTransaction(); + try { + $connection->executeStatement($sql); + $connection->commit(); + } catch (\Throwable $throwable) { + $connection->rollBack(); + throw $throwable; + } + + $entityManager->clear(); + } +} diff --git a/tests/Functional/ContactPersons/Builders/ContactPersonBuilder.php b/tests/Functional/ContactPersons/Builders/ContactPersonBuilder.php new file mode 100644 index 00000000..71cf7aca --- /dev/null +++ b/tests/Functional/ContactPersons/Builders/ContactPersonBuilder.php @@ -0,0 +1,131 @@ +id = Uuid::v7(); + $this->fullName = DemoDataGenerator::getFullName(); + $this->bitrix24UserId = random_int(1, 1_000_000); + } + + public function withStatus(ContactPersonStatus $contactPersonStatus): self + { + $this->status = $contactPersonStatus; + + return $this; + } + + public function withFullName(FullName $fullName): self + { + $this->fullName = $fullName; + + return $this; + } + + public function withEmail(string $email): self + { + $this->email = $email; + + return $this; + } + + public function withMobilePhoneNumber(PhoneNumber $mobilePhoneNumber): self + { + $this->mobilePhoneNumber = $mobilePhoneNumber; + + return $this; + } + + public function withComment(string $comment): self + { + $this->comment = $comment; + + return $this; + } + + public function withExternalId(string $externalId): self + { + $this->externalId = $externalId; + + return $this; + } + + public function withBitrix24UserId(int $bitrix24UserId): self + { + $this->bitrix24UserId = $bitrix24UserId; + + return $this; + } + + public function withBitrix24PartnerId(?Uuid $uuid): self + { + $this->bitrix24PartnerId = $uuid; + + return $this; + } + + public function withUserAgentInfo(UserAgentInfo $userAgentInfo): self + { + $this->userAgentInfo = $userAgentInfo; + + return $this; + } + + public function build(): ContactPerson + { + $userAgentInfo = $this->userAgentInfo ?? new UserAgentInfo( + DemoDataGenerator::getUserAgentIp(), + DemoDataGenerator::getUserAgent() + ); + + return new ContactPerson( + $this->id, + $this->status, + $this->fullName, + $this->email, + null, + $this->mobilePhoneNumber, + null, + $this->comment, + $this->externalId, + $this->bitrix24UserId, + $this->bitrix24PartnerId, + $userAgentInfo + ); + } +} \ No newline at end of file diff --git a/tests/Functional/ContactPersons/UseCase/ChangeProfile/HandlerTest.php b/tests/Functional/ContactPersons/UseCase/ChangeProfile/HandlerTest.php new file mode 100644 index 00000000..22373836 --- /dev/null +++ b/tests/Functional/ContactPersons/UseCase/ChangeProfile/HandlerTest.php @@ -0,0 +1,181 @@ + + * + * For the full copyright and license information, please view the MIT-LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Bitrix24\Lib\Tests\Functional\ContactPersons\UseCase\ChangeProfile; + +use Bitrix24\Lib\ContactPersons\Infrastructure\Doctrine\ContactPersonRepository; +use Bitrix24\Lib\ContactPersons\UseCase\ChangeProfile\Command; +use Bitrix24\Lib\ContactPersons\UseCase\ChangeProfile\Handler; +use Bitrix24\Lib\Services\Flusher; +use Bitrix24\Lib\Tests\EntityManagerFactory; +use Bitrix24\Lib\Tests\Functional\ContactPersons\Builders\ContactPersonBuilder; +use Bitrix24\SDK\Application\Contracts\ContactPersons\Entity\FullName; +use Bitrix24\SDK\Application\Contracts\ContactPersons\Events\ContactPersonEmailChangedEvent; +use Bitrix24\SDK\Application\Contracts\ContactPersons\Events\ContactPersonFullNameChangedEvent; +use Bitrix24\SDK\Application\Contracts\ContactPersons\Events\ContactPersonMobilePhoneChangedEvent; +use libphonenumber\PhoneNumber; +use libphonenumber\PhoneNumberFormat; +use libphonenumber\PhoneNumberUtil; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use Psr\Log\NullLogger; +use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\Stopwatch\Stopwatch; +use Symfony\Component\Uid\Uuid; +use Bitrix24\SDK\Application\Contracts\ContactPersons\Exceptions\ContactPersonNotFoundException; + +/** + * @internal + */ +#[CoversClass(Handler::class)] +class HandlerTest extends TestCase +{ + /** + * @var PhoneNumberUtil + */ + public $phoneNumberUtil; + + private Handler $handler; + + private Flusher $flusher; + + private ContactPersonRepository $repository; + + private TraceableEventDispatcher $eventDispatcher; + + #[\Override] + protected function setUp(): void + { + $entityManager = EntityManagerFactory::get(); + $this->eventDispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch()); + $this->repository = new ContactPersonRepository($entityManager); + $this->phoneNumberUtil = PhoneNumberUtil::getInstance(); + $this->flusher = new Flusher($entityManager, $this->eventDispatcher); + $this->handler = new Handler( + $this->repository, + $this->phoneNumberUtil, + $this->flusher, + new NullLogger() + ); + } + + #[Test] + public function testUpdateExistingContactPerson(): void + { + // Создаем контактное лицо через билдера + $contactPersonBuilder = new ContactPersonBuilder(); + $contactPerson = $contactPersonBuilder + ->withEmail('john.doe@example.com') + ->withMobilePhoneNumber($this->createPhoneNumber('+79991234567')) + ->withComment('Initial comment') + ->withExternalId(Uuid::v7()->toRfc4122()) + ->withBitrix24UserId(random_int(1, 1_000_000)) + ->withBitrix24PartnerId(Uuid::v7()) + ->build() + ; + + $this->repository->save($contactPerson); + $this->flusher->flush(); + + // Обновляем контактное лицо через команду + $this->handler->handle( + new Command( + $contactPerson->getId(), + new FullName('Jane Doe'), + 'jane.doe@example.com', + $this->createPhoneNumber('+79997654321') + ) + ); + + // Проверяем, что изменения сохранились + $updatedContactPerson = $this->repository->getById($contactPerson->getId()); + $formattedPhone = $this->phoneNumberUtil->format($updatedContactPerson->getMobilePhone(), PhoneNumberFormat::E164); + + $dispatchedEvents = $this->eventDispatcher->getOrphanedEvents(); + $this->assertContains(ContactPersonEmailChangedEvent::class, $dispatchedEvents); + $this->assertContains(ContactPersonMobilePhoneChangedEvent::class, $dispatchedEvents); + $this->assertContains(ContactPersonFullNameChangedEvent::class, $dispatchedEvents); + $this->assertEquals('Jane Doe', $updatedContactPerson->getFullName()->name); + $this->assertEquals('jane.doe@example.com', $updatedContactPerson->getEmail()); + $this->assertEquals('+79997654321', $formattedPhone); + } + + #[Test] + public function testUpdateWithNonExistentContactPerson(): void + { + $this->expectException(ContactPersonNotFoundException::class); + + $this->handler->handle( + new Command( + Uuid::v7(), + new FullName('Jane Doe'), + 'jane.doe@example.com', + $this->createPhoneNumber('+79997654321') + ) + ); + } + + #[Test] + public function testUpdateWithSameData(): void + { + // Создаем контактное лицо через билдера + $email = 'john.doe@example.com'; + $fullName = new FullName('John Doe'); + $phone = '+79991234567'; + + $contactPersonBuilder = new ContactPersonBuilder(); + $contactPerson = $contactPersonBuilder + ->withEmail($email) + ->withFullName($fullName) + ->withMobilePhoneNumber($this->createPhoneNumber($phone)) + ->withExternalId(Uuid::v7()->toRfc4122()) + ->withBitrix24UserId(random_int(1, 1_000_000)) + ->withBitrix24PartnerId(Uuid::v7()) + ->build() + ; + + $this->repository->save($contactPerson); + $this->flusher->flush(); + + // Обновляем контактное лицо теми же данными + $this->handler->handle( + new Command( + $contactPerson->getId(), + $fullName, + $email, + $this->createPhoneNumber($phone) + ) + ); + + // Проверяем, что события не были отправлены + $dispatchedEvents = $this->eventDispatcher->getOrphanedEvents(); + $this->assertNotContains(ContactPersonEmailChangedEvent::class, $dispatchedEvents); + $this->assertNotContains(ContactPersonMobilePhoneChangedEvent::class, $dispatchedEvents); + $this->assertNotContains(ContactPersonFullNameChangedEvent::class, $dispatchedEvents); + + // Проверяем, что данные не изменились + $updatedContactPerson = $this->repository->getById($contactPerson->getId()); + $this->assertEquals($fullName->name, $updatedContactPerson->getFullName()->name); + $this->assertEquals($email, $updatedContactPerson->getEmail()); + $this->assertEquals($phone, $this->phoneNumberUtil->format($updatedContactPerson->getMobilePhone(), PhoneNumberFormat::E164)); + } + + private function createPhoneNumber(string $number): PhoneNumber + { + $phoneNumberUtil = PhoneNumberUtil::getInstance(); + + return $phoneNumberUtil->parse($number, 'RU'); + } +} diff --git a/tests/Functional/ContactPersons/UseCase/MarkEmailAsVerified/HandlerTest.php b/tests/Functional/ContactPersons/UseCase/MarkEmailAsVerified/HandlerTest.php new file mode 100644 index 00000000..176627fa --- /dev/null +++ b/tests/Functional/ContactPersons/UseCase/MarkEmailAsVerified/HandlerTest.php @@ -0,0 +1,148 @@ + + * + * For the full copyright and license information, please view the MIT-LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Bitrix24\Lib\Tests\Functional\ContactPersons\UseCase\MarkEmailAsVerified; + +use Bitrix24\Lib\ContactPersons\Entity\ContactPerson; +use Bitrix24\Lib\ContactPersons\Infrastructure\Doctrine\ContactPersonRepository; +use Bitrix24\Lib\ContactPersons\UseCase\MarkEmailAsVerified\Command; +use Bitrix24\Lib\ContactPersons\UseCase\MarkEmailAsVerified\Handler; +use Bitrix24\Lib\Services\Flusher; +use Bitrix24\Lib\Tests\EntityManagerFactory; +use Bitrix24\Lib\Tests\Functional\ContactPersons\Builders\ContactPersonBuilder; +use Bitrix24\SDK\Application\Contracts\ContactPersons\Exceptions\ContactPersonNotFoundException; +use Carbon\CarbonImmutable; +use libphonenumber\PhoneNumber; +use libphonenumber\PhoneNumberUtil; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use Psr\Log\NullLogger; +use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\Stopwatch\Stopwatch; +use Symfony\Component\Uid\Uuid; + +/** + * @internal + */ +#[CoversClass(Handler::class)] +class HandlerTest extends TestCase +{ + private Handler $handler; + + private Flusher $flusher; + + private ContactPersonRepository $repository; + + private TraceableEventDispatcher $eventDispatcher; + + #[\Override] + protected function setUp(): void + { + $entityManager = EntityManagerFactory::get(); + $this->eventDispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch()); + $this->repository = new ContactPersonRepository($entityManager); + $this->flusher = new Flusher($entityManager, $this->eventDispatcher); + $this->handler = new Handler( + $this->repository, + $this->flusher, + new NullLogger() + ); + } + + #[Test] + public function testConfirmEmailVerificationSuccess(): void + { + $contactPerson = $this->createContactPerson('john.doe@example.com'); + + $verifiedAt = new CarbonImmutable('2025-01-01T10:00:00+00:00'); + $this->handler->handle( + new Command($contactPerson->getId(), 'john.doe@example.com', $verifiedAt) + ); + + $updatedContactPerson = $this->repository->getById($contactPerson->getId()); + $this->assertTrue($updatedContactPerson->isEmailVerified()); + $this->assertSame($verifiedAt->toISOString(), $updatedContactPerson->getEmailVerifiedAt()?->toISOString()); + } + + #[Test] + #[DataProvider('invalidMarkEmailVerificationProvider')] + public function testConfirmEmailVerificationFails( + bool $useRealContactId, + string $emailInCommand, + ?string $expectedExceptionClass = null + ): void { + $contactPerson = $this->createContactPerson('john.doe@example.com'); + $contactId = $useRealContactId ? $contactPerson->getId() : Uuid::v7(); + + if (null !== $expectedExceptionClass) { + $this->expectException($expectedExceptionClass); + } + + $this->handler->handle(new Command($contactId, $emailInCommand)); + + if (null === $expectedExceptionClass) { + // Если исключение не ожидалось (например, при несовпадении email), проверяем, что статус не изменился + $reloaded = $this->repository->getById($contactPerson->getId()); + $this->assertFalse($reloaded->isEmailVerified()); + } + } + + public static function invalidMarkEmailVerificationProvider(): array + { + return [ + 'contact person not found' => [ + 'useRealContactId' => false, + 'emailInCommand' => 'john.doe@example.com', + 'expectedExceptionClass' => ContactPersonNotFoundException::class, + ], + 'email mismatch' => [ + 'useRealContactId' => true, + 'emailInCommand' => 'another.email@example.com', + 'expectedExceptionClass' => null, + ], + 'invalid email format' => [ + 'useRealContactId' => true, + 'emailInCommand' => 'not-an-email', + 'expectedExceptionClass' => \InvalidArgumentException::class, + ], + ]; + } + + private function createContactPerson(string $email): ContactPerson + { + $contactPerson = (new ContactPersonBuilder()) + ->withEmail($email) + ->withMobilePhoneNumber($this->createPhoneNumber('+79991234567')) + ->withComment('Test comment') + ->withExternalId(Uuid::v7()->toRfc4122()) + ->withBitrix24UserId(random_int(1, 1_000_000)) + ->withBitrix24PartnerId(Uuid::v7()) + ->build() + ; + + $this->repository->save($contactPerson); + $this->flusher->flush(); + + return $contactPerson; + } + + private function createPhoneNumber(string $number): PhoneNumber + { + $phoneNumberUtil = PhoneNumberUtil::getInstance(); + + return $phoneNumberUtil->parse($number, 'RU'); + } +} diff --git a/tests/Functional/ContactPersons/UseCase/MarkMobilePhoneAsVerified/HandlerTest.php b/tests/Functional/ContactPersons/UseCase/MarkMobilePhoneAsVerified/HandlerTest.php new file mode 100644 index 00000000..dfe01136 --- /dev/null +++ b/tests/Functional/ContactPersons/UseCase/MarkMobilePhoneAsVerified/HandlerTest.php @@ -0,0 +1,158 @@ + + * + * For the full copyright and license information, please view the MIT-LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Bitrix24\Lib\Tests\Functional\ContactPersons\UseCase\MarkMobilePhoneAsVerified; + +use Bitrix24\Lib\ContactPersons\Entity\ContactPerson; +use Bitrix24\Lib\ContactPersons\Infrastructure\Doctrine\ContactPersonRepository; +use Bitrix24\Lib\ContactPersons\UseCase\MarkMobilePhoneAsVerified\Command; +use Bitrix24\Lib\ContactPersons\UseCase\MarkMobilePhoneAsVerified\Handler; +use Bitrix24\Lib\Services\Flusher; +use Bitrix24\Lib\Tests\EntityManagerFactory; +use Bitrix24\Lib\Tests\Functional\ContactPersons\Builders\ContactPersonBuilder; +use Bitrix24\SDK\Application\Contracts\ContactPersons\Exceptions\ContactPersonNotFoundException; +use libphonenumber\PhoneNumber; +use libphonenumber\PhoneNumberUtil; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use Psr\Log\NullLogger; +use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\Stopwatch\Stopwatch; +use Symfony\Component\Uid\Uuid; + +/** + * @internal + */ +#[CoversClass(Handler::class)] +class HandlerTest extends TestCase +{ + /** + * @var PhoneNumberUtil + */ + public $phoneNumberUtil; + + private Handler $handler; + + private Flusher $flusher; + + private ContactPersonRepository $repository; + + private TraceableEventDispatcher $eventDispatcher; + + #[\Override] + protected function setUp(): void + { + $entityManager = EntityManagerFactory::get(); + $this->eventDispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch()); + $this->repository = new ContactPersonRepository($entityManager); + $this->phoneNumberUtil = PhoneNumberUtil::getInstance(); + $this->flusher = new Flusher($entityManager, $this->eventDispatcher); + $this->handler = new Handler( + $this->repository, + $this->phoneNumberUtil, + $this->flusher, + new NullLogger() + ); + } + + #[Test] + public function testConfirmPhoneVerification(): void + { + $phoneNumber = $this->createPhoneNumber('+79991234567'); + $contactPerson = $this->createContactPerson($phoneNumber); + + $this->assertFalse($contactPerson->isMobilePhoneVerified()); + + $this->handler->handle(new Command($contactPerson->getId(), $phoneNumber)); + + $updatedContactPerson = $this->repository->getById($contactPerson->getId()); + $this->assertTrue($updatedContactPerson->isMobilePhoneVerified()); + } + + #[Test] + #[DataProvider('invalidPhoneVerificationProvider')] + public function testConfirmPhoneVerificationFails( + bool $useRealContactId, + string $phoneNumberInCommand, + ?string $expectedExceptionClass = null + ): void { + $realPhoneNumber = $this->createPhoneNumber('+79991234567'); + $contactPerson = $this->createContactPerson($realPhoneNumber); + + $contactId = $useRealContactId ? $contactPerson->getId() : Uuid::v7(); + + if (null !== $expectedExceptionClass) { + $this->expectException($expectedExceptionClass); + } + + $phoneNumber = $this->createPhoneNumber($phoneNumberInCommand); + $this->handler->handle(new Command($contactId, $phoneNumber)); + + if (null === $expectedExceptionClass) { + // Если исключение не ожидалось (например, при несовпадении телефона), проверяем, что статус не изменился + $reloaded = $this->repository->getById($contactPerson->getId()); + $this->assertFalse($reloaded->isMobilePhoneVerified()); + } + } + + public static function invalidPhoneVerificationProvider(): array + { + return [ + 'contact person not found' => [ + 'useRealContactId' => false, + 'phoneNumberInCommand' => '+79991234567', + 'expectedExceptionClass' => ContactPersonNotFoundException::class, + ], + 'phone mismatch' => [ + 'useRealContactId' => true, + 'phoneNumberInCommand' => '+79990000000', + 'expectedExceptionClass' => null, + ], + 'invalid phone format' => [ + 'useRealContactId' => true, + 'phoneNumberInCommand' => '123', + 'expectedExceptionClass' => null, + // Actually Command doesn't validate phone format in this package, it's a PhoneNumber object. + // In Handler.php there's no guard for phone in MarkMobilePhoneAsVerified, it just compares them. + ], + ]; + } + + private function createContactPerson(PhoneNumber $phoneNumber): ContactPerson + { + $contactPerson = (new ContactPersonBuilder()) + ->withEmail('john.doe@example.com') + ->withMobilePhoneNumber($phoneNumber) + ->withComment('Test comment') + ->withExternalId(Uuid::v7()->toRfc4122()) + ->withBitrix24UserId(random_int(1, 1_000_000)) + ->withBitrix24PartnerId(Uuid::v7()) + ->build() + ; + + $this->repository->save($contactPerson); + $this->flusher->flush(); + + return $contactPerson; + } + + private function createPhoneNumber(string $number): PhoneNumber + { + $phoneNumberUtil = PhoneNumberUtil::getInstance(); + + return $phoneNumberUtil->parse($number, 'RU'); + } +} diff --git a/tests/Unit/ApplicationInstallations/UseCase/InstallContactPerson/CommandTest.php b/tests/Unit/ApplicationInstallations/UseCase/InstallContactPerson/CommandTest.php new file mode 100644 index 00000000..5df7c535 --- /dev/null +++ b/tests/Unit/ApplicationInstallations/UseCase/InstallContactPerson/CommandTest.php @@ -0,0 +1,156 @@ +expectException($expectedException); + } + + $command = new Command( + $applicationInstallationId, + $fullName, + $bitrix24UserId, + $userAgentInfo, + $email, + $mobilePhoneNumber, + $comment, + $externalId, + $bitrix24PartnerId + ); + + self::assertSame($applicationInstallationId, $command->applicationInstallationId); + self::assertSame($fullName, $command->fullName); + self::assertSame($bitrix24UserId, $command->bitrix24UserId); + self::assertSame($userAgentInfo, $command->userAgentInfo); + self::assertSame($email, $command->email); + self::assertSame($mobilePhoneNumber, $command->mobilePhoneNumber); + self::assertSame($comment, $command->comment); + self::assertSame($externalId, $command->externalId); + self::assertSame($bitrix24PartnerId, $command->bitrix24PartnerId); + } + + public static function commandDataProvider(): array + { + $fullName = new FullName('John Doe'); + $userAgentInfo = new UserAgentInfo(null); + + return [ + 'valid data' => [ + Uuid::v7(), + $fullName, + 123, + $userAgentInfo, + 'john.doe@example.com', + new PhoneNumber(), + 'Test comment', + 'ext-123', + Uuid::v7(), + ], + 'invalid email: empty' => [ + Uuid::v7(), + $fullName, + 123, + $userAgentInfo, + '', + null, + null, + null, + null, + \InvalidArgumentException::class, + ], + 'invalid email: spaces' => [ + Uuid::v7(), + $fullName, + 123, + $userAgentInfo, + ' ', + null, + null, + null, + null, + \InvalidArgumentException::class, + ], + 'invalid email: format' => [ + Uuid::v7(), + $fullName, + 123, + $userAgentInfo, + 'not-an-email', + null, + null, + null, + null, + \InvalidArgumentException::class, + ], + 'invalid external id: empty string' => [ + Uuid::v7(), + $fullName, + 123, + $userAgentInfo, + null, + null, + null, + ' ', + null, + \InvalidArgumentException::class, + ], + 'invalid user id: zero' => [ + Uuid::v7(), + $fullName, + 0, + $userAgentInfo, + null, + null, + null, + null, + null, + \InvalidArgumentException::class, + ], + 'invalid user id: negative' => [ + Uuid::v7(), + $fullName, + -1, + $userAgentInfo, + null, + null, + null, + null, + null, + \InvalidArgumentException::class, + ], + ]; + } +} diff --git a/tests/Unit/ContactPersons/Entity/ContactPersonTest.php b/tests/Unit/ContactPersons/Entity/ContactPersonTest.php new file mode 100644 index 00000000..03b9c38e --- /dev/null +++ b/tests/Unit/ContactPersons/Entity/ContactPersonTest.php @@ -0,0 +1,61 @@ +expectException($expectedException); + } + + $command = new Command( + $uuid, + $fullName, + $email, + $mobilePhoneNumber + ); + + self::assertEquals($uuid, $command->contactPersonId); + self::assertEquals($fullName, $command->fullName); + self::assertSame($email, $command->email); + self::assertEquals($mobilePhoneNumber, $command->mobilePhoneNumber); + } + + public static function commandDataProvider(): array + { + $fullName = new FullName('John Doe'); + + return [ + 'valid data' => [ + Uuid::v7(), + $fullName, + 'john.doe@example.com', + new PhoneNumber(), + ], + 'empty email is valid' => [ + Uuid::v7(), + $fullName, + '', + new PhoneNumber(), + ], + 'invalid email format' => [ + Uuid::v7(), + $fullName, + 'not-an-email', + new PhoneNumber(), + InvalidArgumentException::class, + ], + ]; + } +} diff --git a/tests/Unit/ContactPersons/UseCase/MarkEmailAsVerified/CommandTest.php b/tests/Unit/ContactPersons/UseCase/MarkEmailAsVerified/CommandTest.php new file mode 100644 index 00000000..61c9c2b1 --- /dev/null +++ b/tests/Unit/ContactPersons/UseCase/MarkEmailAsVerified/CommandTest.php @@ -0,0 +1,77 @@ +expectException($expectedException); + } + + $command = new Command( + $uuid, + $email, + $emailVerifiedAt + ); + + self::assertEquals($uuid, $command->contactPersonId); + self::assertSame($email, $command->email); + self::assertEquals($emailVerifiedAt, $command->emailVerifiedAt); + } + + public static function commandDataProvider(): array + { + return [ + 'valid data' => [ + Uuid::v7(), + 'john.doe@example.com', + new CarbonImmutable(), + ], + 'valid data without date' => [ + Uuid::v7(), + 'john.doe@example.com', + null, + ], + 'invalid email: empty' => [ + Uuid::v7(), + '', + null, + \InvalidArgumentException::class, + ], + 'invalid email: spaces' => [ + Uuid::v7(), + ' ', + null, + \InvalidArgumentException::class, + ], + 'invalid email: format' => [ + Uuid::v7(), + 'not-an-email', + null, + \InvalidArgumentException::class, + ], + ]; + } +}