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 $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; } }