Add unsubscribe API method

It can be used by calling:

```
$mailpoet_api = \MailPoet\API\API::MP('v1');
$mailpoet_api->unsubscribe($subscriberId);
```

[MAILPOET-5152]
This commit is contained in:
Rodrigo Primo
2023-03-27 11:41:09 -03:00
committed by Aschepikov
parent d37b961c2b
commit c507229dd2
5 changed files with 114 additions and 1 deletions

View File

@ -68,6 +68,10 @@ class API {
return $this->subscribers->unsubscribeFromLists($subscriberId, $listIds); return $this->subscribers->unsubscribeFromLists($subscriberId, $listIds);
} }
public function unsubscribe($subscriberIdOrEmail) {
return $this->subscribers->unsubscribe($subscriberIdOrEmail);
}
public function getLists(): array { public function getLists(): array {
return $this->segments->getAll(); return $this->segments->getAll();
} }

View File

@ -24,4 +24,5 @@ class APIException extends \Exception {
const LIST_USED_IN_FORM = 21; const LIST_USED_IN_FORM = 21;
const FAILED_TO_DELETE_LIST = 22; const FAILED_TO_DELETE_LIST = 22;
const LIST_TYPE_IS_NOT_SUPPORTED = 23; const LIST_TYPE_IS_NOT_SUPPORTED = 23;
const SUBSCRIBER_ALREADY_UNSUBSCRIBED = 24;
} }

View File

@ -4,11 +4,13 @@ namespace MailPoet\API\MP\v1;
use MailPoet\API\JSON\ResponseBuilders\SubscribersResponseBuilder; use MailPoet\API\JSON\ResponseBuilders\SubscribersResponseBuilder;
use MailPoet\Entities\SegmentEntity; use MailPoet\Entities\SegmentEntity;
use MailPoet\Entities\StatisticsUnsubscribeEntity;
use MailPoet\Entities\SubscriberEntity; use MailPoet\Entities\SubscriberEntity;
use MailPoet\Listing\ListingDefinition; use MailPoet\Listing\ListingDefinition;
use MailPoet\Newsletter\Scheduler\WelcomeScheduler; use MailPoet\Newsletter\Scheduler\WelcomeScheduler;
use MailPoet\Segments\SegmentsRepository; use MailPoet\Segments\SegmentsRepository;
use MailPoet\Settings\SettingsController; use MailPoet\Settings\SettingsController;
use MailPoet\Statistics\Track\Unsubscribes;
use MailPoet\Subscribers\ConfirmationEmailMailer; use MailPoet\Subscribers\ConfirmationEmailMailer;
use MailPoet\Subscribers\NewSubscriberNotificationMailer; use MailPoet\Subscribers\NewSubscriberNotificationMailer;
use MailPoet\Subscribers\RequiredCustomFieldValidator; use MailPoet\Subscribers\RequiredCustomFieldValidator;
@ -62,6 +64,9 @@ class Subscribers {
/** @var SubscriberListingRepository */ /** @var SubscriberListingRepository */
private $subscriberListingRepository; private $subscriberListingRepository;
/** @var Unsubscribes */
private $unsubscribesTracker;
public function __construct ( public function __construct (
ConfirmationEmailMailer $confirmationEmailMailer, ConfirmationEmailMailer $confirmationEmailMailer,
NewSubscriberNotificationMailer $newSubscriberNotificationMailer, NewSubscriberNotificationMailer $newSubscriberNotificationMailer,
@ -74,7 +79,8 @@ class Subscribers {
WelcomeScheduler $welcomeScheduler, WelcomeScheduler $welcomeScheduler,
RequiredCustomFieldValidator $requiredCustomFieldsValidator, RequiredCustomFieldValidator $requiredCustomFieldsValidator,
SubscriberListingRepository $subscriberListingRepository, SubscriberListingRepository $subscriberListingRepository,
WPFunctions $wp WPFunctions $wp,
Unsubscribes $unsubscribesTracker
) { ) {
$this->confirmationEmailMailer = $confirmationEmailMailer; $this->confirmationEmailMailer = $confirmationEmailMailer;
$this->newSubscriberNotificationMailer = $newSubscriberNotificationMailer; $this->newSubscriberNotificationMailer = $newSubscriberNotificationMailer;
@ -88,6 +94,7 @@ class Subscribers {
$this->requiredCustomFieldsValidator = $requiredCustomFieldsValidator; $this->requiredCustomFieldsValidator = $requiredCustomFieldsValidator;
$this->wp = $wp; $this->wp = $wp;
$this->subscriberListingRepository = $subscriberListingRepository; $this->subscriberListingRepository = $subscriberListingRepository;
$this->unsubscribesTracker = $unsubscribesTracker;
} }
public function getSubscriber($subscriberIdOrEmail): array { public function getSubscriber($subscriberIdOrEmail): array {
@ -229,6 +236,28 @@ class Subscribers {
return $this->subscribersResponseBuilder->build($subscriber); return $this->subscribersResponseBuilder->build($subscriber);
} }
public function unsubscribe($subscriberIdOrEmail): array {
$this->checkSubscriberParam($subscriberIdOrEmail);
$subscriber = $this->findSubscriber($subscriberIdOrEmail);
if ($subscriber->getStatus() === SubscriberEntity::STATUS_UNSUBSCRIBED) {
throw new APIException(__('This subscriber is already unsubscribed.', 'mailpoet'), APIException::SUBSCRIBER_ALREADY_UNSUBSCRIBED);
}
$this->unsubscribesTracker->track(
(int)$subscriber->getId(),
StatisticsUnsubscribeEntity::SOURCE_MP_API
);
$subscriber->setStatus(SubscriberEntity::STATUS_UNSUBSCRIBED);
$this->subscribersRepository->persist($subscriber);
$this->subscribersRepository->flush();
$this->subscribersSegmentRepository->unsubscribeFromSegments($subscriber);
return $this->subscribersResponseBuilder->build($subscriber);
}
public function unsubscribeFromLists($subscriberIdOrEmail, array $listIds): array { public function unsubscribeFromLists($subscriberIdOrEmail, array $listIds): array {
$this->checkSubscriberAndListParams($subscriberIdOrEmail, $listIds); $this->checkSubscriberAndListParams($subscriberIdOrEmail, $listIds);
$subscriber = $this->findSubscriber($subscriberIdOrEmail); $subscriber = $this->findSubscriber($subscriberIdOrEmail);
@ -321,6 +350,13 @@ class Subscribers {
if (empty($listIds)) { if (empty($listIds)) {
throw new APIException(__('At least one segment ID is required.', 'mailpoet'), APIException::SEGMENT_REQUIRED); throw new APIException(__('At least one segment ID is required.', 'mailpoet'), APIException::SEGMENT_REQUIRED);
} }
$this->checkSubscriberParam($subscriberIdOrEmail);
}
/**
* @throws APIException
*/
private function checkSubscriberParam($subscriberIdOrEmail): void {
if (empty($subscriberIdOrEmail)) { if (empty($subscriberIdOrEmail)) {
throw new APIException(__('A subscriber is required.', 'mailpoet'), APIException::SUBSCRIBER_NOT_EXISTS); throw new APIException(__('A subscriber is required.', 'mailpoet'), APIException::SUBSCRIBER_NOT_EXISTS);
} }

View File

@ -21,6 +21,7 @@ class StatisticsUnsubscribeEntity {
const SOURCE_ADMINISTRATOR = 'admin'; const SOURCE_ADMINISTRATOR = 'admin';
const SOURCE_ORDER_CHECKOUT = 'order_checkout'; const SOURCE_ORDER_CHECKOUT = 'order_checkout';
const SOURCE_AUTOMATION = 'automation'; const SOURCE_AUTOMATION = 'automation';
const SOURCE_MP_API = 'mp_api';
const METHOD_LINK = 'link'; const METHOD_LINK = 'link';
const METHOD_ONE_CLICK = 'one_click'; const METHOD_ONE_CLICK = 'one_click';

View File

@ -14,6 +14,7 @@ use MailPoet\Config\Changelog;
use MailPoet\CustomFields\CustomFieldsRepository; use MailPoet\CustomFields\CustomFieldsRepository;
use MailPoet\Entities\CustomFieldEntity; use MailPoet\Entities\CustomFieldEntity;
use MailPoet\Entities\SegmentEntity; use MailPoet\Entities\SegmentEntity;
use MailPoet\Entities\StatisticsUnsubscribeEntity;
use MailPoet\Entities\SubscriberEntity; use MailPoet\Entities\SubscriberEntity;
use MailPoet\Entities\SubscriberSegmentEntity; use MailPoet\Entities\SubscriberSegmentEntity;
use MailPoet\Models\ScheduledTask; use MailPoet\Models\ScheduledTask;
@ -284,6 +285,75 @@ class SubscribersTest extends \MailPoetTest {
$API->subscribeToLists($subscriber->getId(), [$segment->getId()], $options); $API->subscribeToLists($subscriber->getId(), [$segment->getId()], $options);
} }
public function testUnsubscribeRaisesExceptionWhenSubscriberIdIsNotPassed() {
try {
$this->getApi()->unsubscribe(false);
$this->fail('Subscriber does not exist exception should have been thrown.');
} catch (\Exception $e) {
expect($e->getMessage())->equals('A subscriber is required.');
}
}
public function testUnsubscribeRaisesExceptionWhenSubscriberDoesNotExist() {
try {
$this->getApi()->unsubscribe('asdf');
$this->fail('Subscriber does not exist exception should have been thrown.');
} catch (\Exception $e) {
expect($e->getMessage())->equals('This subscriber does not exist.');
}
}
public function testUnsubscribeRaisesExceptionIfSubscriberAlreadyUnsubscribed() {
$subscriber = $this->subscriberFactory
->withStatus(SubscriberEntity::STATUS_UNSUBSCRIBED)
->create();
try {
$this->getApi()->unsubscribe($subscriber->getId());
$this->fail('Subscriber already unsubscribed exception should have been thrown.');
} catch (\Exception $e) {
expect($e->getMessage())->equals('This subscriber is already unsubscribed.');
}
}
public function testUnsubscribesSubscriberFromAllListsAndChangesItsStatus() {
$subscriber = $this->subscriberFactory->create();
$segment1 = $this->getSegment('Segment 1');
$segment2 = $this->getSegment('Segment 2');
$this->getApi()->subscribeToLists($subscriber->getId(), [$segment1->getId(), $segment2->getId()]);
$this->assertSame(SubscriberEntity::STATUS_SUBSCRIBED, $subscriber->getStatus());
$result = $this->getApi()->unsubscribe($subscriber->getId());
$this->assertSame(SubscriberEntity::STATUS_UNSUBSCRIBED, $subscriber->getStatus());
foreach ($subscriber->getSubscriberSegments() as $subscriberSegment) {
$this->assertSame(SubscriberEntity::STATUS_UNSUBSCRIBED, $subscriberSegment->getStatus());
}
$this->assertSame(SubscriberEntity::STATUS_UNSUBSCRIBED, $result['status']);
$this->assertSame(SubscriberEntity::STATUS_UNSUBSCRIBED, $result['subscriptions'][0]['status']);
$this->assertSame(SubscriberEntity::STATUS_UNSUBSCRIBED, $result['subscriptions'][1]['status']);
}
public function testUnsubscribesSubscriberFromAllListsAndChangesItsStatusUsingEmailInsteadOfId() {
$subscriber = $this->subscriberFactory->create();
$segment1 = $this->getSegment('Segment 1');
$segment2 = $this->getSegment('Segment 2');
$this->getApi()->subscribeToLists($subscriber->getId(), [$segment1->getId(), $segment2->getId()]);
$this->assertSame(SubscriberEntity::STATUS_SUBSCRIBED, $subscriber->getStatus());
$result = $this->getApi()->unsubscribe($subscriber->getEmail());
$this->assertSame(SubscriberEntity::STATUS_UNSUBSCRIBED, $subscriber->getStatus());
foreach ($subscriber->getSubscriberSegments() as $subscriberSegment) {
$this->assertSame(SubscriberEntity::STATUS_UNSUBSCRIBED, $subscriberSegment->getStatus());
}
$this->assertSame(SubscriberEntity::STATUS_UNSUBSCRIBED, $result['status']);
$this->assertSame(SubscriberEntity::STATUS_UNSUBSCRIBED, $result['subscriptions'][0]['status']);
$this->assertSame(SubscriberEntity::STATUS_UNSUBSCRIBED, $result['subscriptions'][1]['status']);
}
public function testItDoesNotUnsubscribeWhenSubscriberIdNotPassedFromLists() { public function testItDoesNotUnsubscribeWhenSubscriberIdNotPassedFromLists() {
try { try {
$this->getApi()->unsubscribeFromLists(false, [1,2,3]); $this->getApi()->unsubscribeFromLists(false, [1,2,3]);
@ -961,6 +1031,7 @@ class SubscribersTest extends \MailPoetTest {
} }
public function _after() { public function _after() {
$this->truncateEntity(StatisticsUnsubscribeEntity::class);
$this->truncateEntity(SubscriberSegmentEntity::class); $this->truncateEntity(SubscriberSegmentEntity::class);
$this->truncateEntity(SubscriberEntity::class); $this->truncateEntity(SubscriberEntity::class);
$this->truncateEntity(SegmentEntity::class); $this->truncateEntity(SegmentEntity::class);