Files
piratepoet/mailpoet/tests/integration/AutomaticEmails/WooCommerce/Events/AbandonedCartTest.php
 Ján Mikláš 0198eae41a Update WC action when setting cart item to zero
[MAILPOET-6069]
2024-09-23 08:32:06 +02:00

431 lines
17 KiB
PHP

<?php declare(strict_types = 1);
namespace MailPoet\AutomaticEmails\WooCommerce\Events;
use MailPoet\AutomaticEmails\WooCommerce\WooCommerce as WooCommerceEmail;
use MailPoet\Cron\Workers\SendingQueue\SendingQueue;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\ScheduledTaskSubscriberEntity;
use MailPoet\Entities\SendingQueueEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Newsletter\Scheduler\AutomaticEmailScheduler;
use MailPoet\Newsletter\Sending\ScheduledTasksRepository;
use MailPoet\Newsletter\Sending\ScheduledTaskSubscribersRepository;
use MailPoet\Newsletter\Sending\SendingQueuesRepository;
use MailPoet\Settings\SettingsController;
use MailPoet\Settings\TrackingConfig;
use MailPoet\Statistics\Track\SubscriberActivityTracker;
use MailPoet\Statistics\Track\SubscriberCookie;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\Test\DataFactories\Newsletter as NewsletterFactory;
use MailPoet\Test\DataFactories\NewsletterOption as NewsletterOptionFactory;
use MailPoet\Test\DataFactories\Subscriber as SubscriberFactory;
use MailPoet\Util\Cookies;
use MailPoet\WooCommerce\Helper as WooCommerceHelper;
use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Carbon\Carbon;
use PHPUnit\Framework\MockObject\MockObject;
use WC_Cart;
use WooCommerce;
use WP_User;
/**
* @group woo
*/
class AbandonedCartTest extends \MailPoetTest {
const SCHEDULE_EMAIL_AFTER_HOURS = 5;
/** @var Carbon */
private $currentTime;
/** @var WPFunctions&MockObject */
private $wp;
/** @var \WooCommerce */
private $wooCommerce;
/** @var WC_Cart|MockObject */
private $wooCommerceCartMock;
/** @var WooCommerceHelper|MockObject */
private $wooCommerceHelperMock;
/** @var SubscriberActivityTracker&MockObject */
private $subscriberActivityTrackerMock;
/** @var ScheduledTasksRepository */
private $scheduledTasksRepository;
/** @var SendingQueuesRepository */
private $sendingQueuesRepository;
/** @var ScheduledTaskSubscribersRepository */
private $scheduledTaskSubscribersRepository;
/** @var AutomaticEmailScheduler */
private $automaticEmailScheduler;
/** @var WC_Cart */
private $cartBackup;
public function _before() {
global $woocommerce;
$this->wooCommerce = $woocommerce;
$this->scheduledTasksRepository = $this->diContainer->get(ScheduledTasksRepository::class);
$this->sendingQueuesRepository = $this->diContainer->get(SendingQueuesRepository::class);
$this->scheduledTaskSubscribersRepository = $this->diContainer->get(ScheduledTaskSubscribersRepository::class);
$this->currentTime = Carbon::now()->millisecond(0);
Carbon::setTestNow($this->currentTime);
$this->subscriberActivityTrackerMock = $this->createMock(SubscriberActivityTracker::class);
/** @var WPFunctions|MockObject $wp - for phpstan */
$wp = $this->makeEmpty(WPFunctions::class, [
'currentTime' => function ($arg) {
if ($arg === 'timestamp') {
return $this->currentTime->getTimestamp();
} elseif ($arg === 'mysql') {
return $this->currentTime->format('Y-m-d H:i:s');
}
},
]);
$this->wp = $wp;
WPFunctions::set($this->wp);
$this->automaticEmailScheduler = $this->diContainer->get(AutomaticEmailScheduler::class);
$this->wooCommerceCartMock = $this->mockWooCommerceClass(WC_Cart::class, ['is_empty', 'get_cart']);
$this->cartBackup = $this->wooCommerce->cart;
$this->wooCommerce->cart = $this->wooCommerceCartMock;
/** @var WooCommerceHelper|MockObject $wooCommerceHelperMock - for phpstan */
$wooCommerceHelperMock = $this->make(WooCommerceHelper::class, [
'isWooCommerceActive' => true,
'WC' => $this->wooCommerce,
]);
$this->wooCommerceHelperMock = $wooCommerceHelperMock;
}
public function testItGetsEventDetails() {
$settings = $this->diContainer->get(SettingsController::class);
$wp = new WPFunctions();
$wcHelper = new WooCommerceHelper($wp);
$cookies = new Cookies();
$subscriberCookie = new SubscriberCookie($cookies, new TrackingConfig($settings));
$event = new AbandonedCart(
$wp,
$wcHelper,
$subscriberCookie,
$this->diContainer->get(SubscriberActivityTracker::class),
$this->diContainer->get(AutomaticEmailScheduler::class),
$this->diContainer->get(SubscribersRepository::class)
);
$result = $event->getEventDetails();
$this->assertNotEmpty($result);
$this->assertEquals($result['slug'], AbandonedCart::SLUG);
}
public function testItRegistersWooCommerceCartEvents() {
$abandonedCartEmail = $this->createAbandonedCartEmail();
$registeredActions = [];
$this->wp->method('addAction')->willReturnCallback(function ($name) use (&$registeredActions) {
$registeredActions[] = $name;
});
$abandonedCartEmail->init();
verify($registeredActions)->arrayContains('woocommerce_add_to_cart');
verify($registeredActions)->arrayContains('woocommerce_cart_item_removed');
verify($registeredActions)->arrayContains('woocommerce_after_cart_item_quantity_update');
verify($registeredActions)->arrayContains('woocommerce_remove_cart_item');
verify($registeredActions)->arrayContains('woocommerce_cart_emptied');
verify($registeredActions)->arrayContains('woocommerce_cart_item_restored');
verify($registeredActions)->arrayContains('woocommerce_load_cart_from_session');
verify($registeredActions)->arrayContains('woocommerce_cart_loaded_from_session');
}
public function testItRegistersToSubscriberActivityEvent() {
$abandonedCartEmail = $this->createAbandonedCartEmail();
$this->subscriberActivityTrackerMock
->expects($this->once())
->method('registerCallback');
$abandonedCartEmail->init();
}
public function testItFindsUserByWordPressSession() {
$this->createNewsletter();
$this->createSubscriberAsCurrentUser();
$this->wooCommerceCartMock->method('is_empty')->willReturn(false);
$abandonedCartEmail = $this->createAbandonedCartEmail();
$abandonedCartEmail->init();
$abandonedCartEmail->handleCartChange();
$this->assertCount(1, $this->scheduledTasksRepository->findAll());
}
public function testItSchedulesAbandonedCartAlsoForNonSubscribedSubscribers() {
$this->createNewsletter();
$this->createSubscriberAsCurrentUser(SubscriberEntity::STATUS_UNCONFIRMED);
$this->wooCommerceCartMock->method('is_empty')->willReturn(false);
$abandonedCartEmail = $this->createAbandonedCartEmail();
$abandonedCartEmail->init();
$abandonedCartEmail->handleCartChange();
$this->assertCount(1, $this->scheduledTasksRepository->findAll());
}
public function testItFindsUserByCookie() {
$this->createNewsletter();
$subscriber = $this->createSubscriber();
$this->wp->method('wpGetCurrentUser')->willReturn(
$this->makeEmpty(WP_User::class, [
'exists' => false,
])
);
$_COOKIE['mailpoet_subscriber'] = json_encode([
'subscriber_id' => $subscriber->getId(),
]);
$this->wooCommerceCartMock->method('is_empty')->willReturn(false);
$abandonedCartEmail = $this->createAbandonedCartEmail();
$abandonedCartEmail->init();
$abandonedCartEmail->handleCartChange();
$this->assertCount(1, $this->scheduledTasksRepository->findAll());
}
public function testItSchedulesEmailWhenItemAddedToCart() {
$this->createNewsletter();
$this->createSubscriberAsCurrentUser();
$this->wooCommerceCartMock->method('is_empty')->willReturn(false);
$this->wooCommerceCartMock->method('get_cart')->willReturn([
['product_id' => 123], ['product_id' => 456], // dummy product IDs
]);
$abandonedCartEmail = $this->createAbandonedCartEmail();
$abandonedCartEmail->init();
$abandonedCartEmail->handleCartChange();
$expectedTime = $this->getExpectedScheduledTime();
$scheduledTasks = $this->scheduledTasksRepository->findAll();
$this->assertCount(1, $scheduledTasks);
$this->assertEquals($scheduledTasks[0]->getStatus(), ScheduledTaskEntity::STATUS_SCHEDULED);
$this->tester->assertEqualDateTimes($scheduledTasks[0]->getScheduledAt(), $expectedTime, 1);
$sendingQueue = $this->sendingQueuesRepository->findOneBy(['task' => $scheduledTasks[0]]);
$this->assertInstanceOf(SendingQueueEntity::class, $sendingQueue);
$this->assertEquals($sendingQueue->getMeta(), [AbandonedCart::TASK_META_NAME => [123, 456]]);
}
public function testItPostponesEmailWhenCartEdited() {
$newsletter = $this->createNewsletter();
$subscriber = $this->createSubscriberAsCurrentUser();
$scheduledInNearFuture = clone $this->currentTime;
$scheduledInNearFuture->addMinutes(5);
$this->createSendingTask($newsletter, $subscriber, $scheduledInNearFuture);
$this->wooCommerceCartMock->method('is_empty')->willReturn(false);
$abandonedCartEmail = $this->createAbandonedCartEmail();
$abandonedCartEmail->init();
$abandonedCartEmail->handleCartChange();
$expectedTime = $this->getExpectedScheduledTime();
$this->entityManager->clear();
$scheduledTasks = $this->scheduledTasksRepository->findAll();
$this->assertCount(1, $scheduledTasks);
$this->assertEquals($scheduledTasks[0]->getStatus(), ScheduledTaskEntity::STATUS_SCHEDULED);
$this->tester->assertEqualDateTimes($scheduledTasks[0]->getScheduledAt(), $expectedTime, 1);
}
public function testItCancelsEmailWhenCartEmpty() {
$newsletter = $this->createNewsletter();
$subscriber = $this->createSubscriberAsCurrentUser();
$scheduledInFuture = clone $this->currentTime;
$scheduledInFuture->addHours(2);
$this->createSendingTask($newsletter, $subscriber, $scheduledInFuture);
$this->wooCommerceCartMock->method('is_empty')->willReturn(true);
$abandonedCartEmail = $this->createAbandonedCartEmail();
$abandonedCartEmail->init();
$abandonedCartEmail->handleCartChange();
$this->assertCount(0, $this->scheduledTasksRepository->findAll());
$this->assertCount(0, $this->scheduledTaskSubscribersRepository->findAll());
$this->assertCount(0, $this->sendingQueuesRepository->findAll());
}
public function testItSchedulesEmailWhenCartLoadedFromSessionOnLogin() {
$this->createNewsletter();
$subscriber = $this->createSubscriberAsCurrentUser();
$this->wooCommerceCartMock->method('is_empty')->willReturn(false);
$this->wooCommerceCartMock->method('get_cart')->willReturn([
['product_id' => 789], ['product_id' => 987], // dummy product IDs
]);
$abandonedCartEmail = $this->createAbandonedCartEmail();
$abandonedCartEmail->init();
$this->wp->method('getCurrentUserId')->willReturn($subscriber->getWpUserId());
$this->wp->method('getUserMeta')->willReturn(1); // mock for _woocommerce_load_saved_cart_after_login
$abandonedCartEmail->handleUserLogin();
$abandonedCartEmail->handleCartChangeOnLogin();
$scheduledTasks = $this->scheduledTasksRepository->findAll();
$this->assertCount(1, $scheduledTasks);
$this->assertEquals($scheduledTasks[0]->getStatus(), ScheduledTaskEntity::STATUS_SCHEDULED);
$sendingQueue = $this->sendingQueuesRepository->findOneBy(['task' => $scheduledTasks[0]]);
$this->assertInstanceOf(SendingQueueEntity::class, $sendingQueue);
$this->assertEquals($sendingQueue->getMeta(), [AbandonedCart::TASK_META_NAME => [789, 987]]);
}
public function testItSchedulesNewEmailWhenEmailAlreadySent() {
$newsletter = $this->createNewsletter();
$subscriber = $this->createSubscriberAsCurrentUser();
$scheduledInPast = clone $this->currentTime;
$scheduledInPast->addHours(-10);
$this->createSendingTask($newsletter, $subscriber, $scheduledInPast);
$this->wooCommerceCartMock->method('is_empty')->willReturn(false);
$abandonedCartEmail = $this->createAbandonedCartEmail();
$abandonedCartEmail->init();
$abandonedCartEmail->handleCartChange();
$expectedTime = $this->getExpectedScheduledTime();
$this->entityManager->clear();
$this->assertCount(2, $this->scheduledTasksRepository->findAll());
$completed = $this->scheduledTasksRepository->findOneBy(['status' => ScheduledTaskEntity::STATUS_COMPLETED]);
$this->assertInstanceOf(ScheduledTaskEntity::class, $completed);
$this->tester->assertEqualDateTimes($completed->getScheduledAt(), $scheduledInPast, 1);
$scheduled = $this->scheduledTasksRepository->findOneBy(['status' => ScheduledTaskEntity::STATUS_SCHEDULED]);
$this->assertInstanceOf(ScheduledTaskEntity::class, $scheduled);
$this->tester->assertEqualDateTimes($scheduled->getScheduledAt(), $expectedTime, 1);
}
public function testItPostponesEmailWhenSubscriberIsActiveOnSite() {
$newsletter = $this->createNewsletter();
$subscriber = $this->createSubscriberAsCurrentUser();
$scheduledInNearFuture = clone $this->currentTime;
$scheduledInNearFuture->addMinutes(5);
$this->createSendingTask($newsletter, $subscriber, $scheduledInNearFuture);
$this->wooCommerceCartMock->method('is_empty')->willReturn(false);
$abandonedCartEmail = $this->createAbandonedCartEmail();
$abandonedCartEmail->init();
$subscriberEntity = $this->entityManager->find(SubscriberEntity::class, $subscriber->getId());
$this->assertInstanceOf(SubscriberEntity::class, $subscriberEntity);
$abandonedCartEmail->handleSubscriberActivity($subscriberEntity);
$expectedTime = $this->getExpectedScheduledTime();
$this->entityManager->clear();
$scheduledTasks = $this->scheduledTasksRepository->findAll();
$this->assertCount(1, $scheduledTasks);
$this->assertEquals($scheduledTasks[0]->getStatus(), ScheduledTaskEntity::STATUS_SCHEDULED);
$this->tester->assertEqualDateTimes($scheduledTasks[0]->getScheduledAt(), $expectedTime, 1);
}
private function createAbandonedCartEmail() {
$settings = $this->diContainer->get(SettingsController::class);
$subscribersRepository = $this->diContainer->get(SubscribersRepository::class);
$automaticEmailScheduler = $this->automaticEmailScheduler;
return $this->make(AbandonedCart::class, [
'wp' => $this->wp,
'wooCommerceHelper' => $this->wooCommerceHelperMock,
'subscriberCookie' => new SubscriberCookie(new Cookies(), new TrackingConfig($settings)),
'subscriberActivityTracker' => $this->subscriberActivityTrackerMock,
'scheduler' => $automaticEmailScheduler,
'subscribersRepository' => $subscribersRepository,
]);
}
private function createNewsletter(): NewsletterEntity {
$newsletter = (new NewsletterFactory())
->withType(NewsletterEntity::TYPE_AUTOMATIC)
->withActiveStatus()
->create();
(new NewsletterOptionFactory())->createMultipleOptions($newsletter, [
'group' => WooCommerceEmail::SLUG,
'event' => AbandonedCart::SLUG,
'afterTimeType' => 'hours',
'afterTimeNumber' => self::SCHEDULE_EMAIL_AFTER_HOURS,
'sendTo' => 'user',
]);
return $newsletter;
}
private function createSendingTask(NewsletterEntity $newsletter, SubscriberEntity $subscriber, Carbon $scheduleAt): ScheduledTaskEntity {
$scheduledTask = new ScheduledTaskEntity();
$scheduledTask->setType(SendingQueue::TASK_TYPE);
$this->entityManager->persist($scheduledTask);
$this->entityManager->flush();
$sendingQueue = new SendingQueueEntity();
$sendingQueue->setNewsletter($newsletter);
$sendingQueue->setTask($scheduledTask);
$this->entityManager->persist($sendingQueue);
$this->entityManager->flush();
$sendingQueueSubscriber = new ScheduledTaskSubscriberEntity($scheduledTask, $subscriber, ScheduledTaskSubscriberEntity::STATUS_PROCESSED);
$this->entityManager->persist($sendingQueueSubscriber);
$this->entityManager->flush();
$scheduledTask->setScheduledAt($scheduleAt);
$scheduledTask->setSendingQueue($sendingQueue);
$scheduledTask->setStatus(($this->currentTime < $scheduleAt) ? ScheduledTaskEntity::STATUS_SCHEDULED : ScheduledTaskEntity::STATUS_COMPLETED);
$this->entityManager->flush();
return $scheduledTask;
}
private function createSubscriber(string $status = SubscriberEntity::STATUS_SUBSCRIBED): SubscriberEntity {
return (new SubscriberFactory())
->withWpUserId(123)
->withStatus($status)
->create();
}
private function createSubscriberAsCurrentUser(string $status = SubscriberEntity::STATUS_SUBSCRIBED): SubscriberEntity {
$subscriber = $this->createSubscriber($status);
$this->wp->method('wpGetCurrentUser')->willReturn(
$this->makeEmpty(WP_User::class, [
'ID' => $subscriber->getWpUserId(),
'exists' => true,
])
);
return $subscriber;
}
private function getExpectedScheduledTime() {
$expectedTime = clone $this->currentTime;
$expectedTime->addHours(self::SCHEDULE_EMAIL_AFTER_HOURS);
return $expectedTime;
}
/**
* @param class-string<WooCommerce|WC_Cart> $className
*/
private function mockWooCommerceClass($className, array $methods) {
// WooCommerce class needs to be mocked without default 'disallowMockingUnknownTypes'
// since WooCommerce may not be active (would result in error mocking undefined class)
return $this->getMockBuilder($className)
->disableOriginalConstructor()
->disableOriginalClone()
->disableArgumentCloning()
->setMethods($methods)
->getMock();
}
public function _after() {
parent::_after();
// Restore original cart object
$this->wooCommerce->cart = $this->cartBackup;
}
}