diff --git a/mailpoet/lib/Config/Hooks.php b/mailpoet/lib/Config/Hooks.php index 1130286d66..82c580726b 100644 --- a/mailpoet/lib/Config/Hooks.php +++ b/mailpoet/lib/Config/Hooks.php @@ -432,6 +432,12 @@ class Hooks { [$this->hooksWooCommerce, 'updateSubscriberEngagement'], 7 ); + // See class-wc-order.php, which says this about this action + // "Fires when the order progresses from a pending payment status to a paid one" + $this->wp->addAction( + 'woocommerce_order_payment_status_changed', + [$this->hooksWooCommerce, 'updateSubscriberLastPurchase'] + ); } public function setupWooCommerceTracking() { diff --git a/mailpoet/lib/Config/HooksWooCommerce.php b/mailpoet/lib/Config/HooksWooCommerce.php index 9dec42c0ac..b620d8f413 100644 --- a/mailpoet/lib/Config/HooksWooCommerce.php +++ b/mailpoet/lib/Config/HooksWooCommerce.php @@ -142,6 +142,14 @@ class HooksWooCommerce { } } + public function updateSubscriberLastPurchase($orderId) { + try { + $this->subscriberEngagement->updateSubscriberLastPurchase($orderId); + } catch (\Throwable $e) { + $this->logError($e, 'WooCommerce Update Subscriber Last Purchase'); + } + } + public function declareHposCompatibility() { try { if (class_exists('\Automattic\WooCommerce\Utilities\FeaturesUtil')) { diff --git a/mailpoet/lib/Statistics/Track/Clicks.php b/mailpoet/lib/Statistics/Track/Clicks.php index e168cca172..8da4de99af 100644 --- a/mailpoet/lib/Statistics/Track/Clicks.php +++ b/mailpoet/lib/Statistics/Track/Clicks.php @@ -117,7 +117,7 @@ class Clicks { // track open event $this->opens->track($data, $displayImage = false); // Update engagement date - $this->subscribersRepository->maybeUpdateLastEngagement($subscriber); + $this->subscribersRepository->maybeUpdateLastClickAt($subscriber); } $url = $this->processUrl($link->getUrl(), $newsletter, $subscriber, $queue, $wpUserPreview); $this->redirectToUrl($url); diff --git a/mailpoet/lib/Statistics/Track/Opens.php b/mailpoet/lib/Statistics/Track/Opens.php index c775bb3979..2074024340 100644 --- a/mailpoet/lib/Statistics/Track/Opens.php +++ b/mailpoet/lib/Statistics/Track/Opens.php @@ -63,7 +63,7 @@ class Opens { $this->statisticsOpensRepository->flush(); } } - $this->subscribersRepository->maybeUpdateLastEngagement($subscriber); + $this->subscribersRepository->maybeUpdateLastOpenAt($subscriber); return $this->returnResponse($displayImage); } $statistics = new StatisticsOpenEntity($newsletter, $queue, $subscriber); @@ -74,7 +74,7 @@ class Opens { } $this->statisticsOpensRepository->persist($statistics); $this->statisticsOpensRepository->flush(); - $this->subscribersRepository->maybeUpdateLastEngagement($subscriber); + $this->subscribersRepository->maybeUpdateLastOpenAt($subscriber); $this->statisticsOpensRepository->recalculateSubscriberScore($subscriber); } return $this->returnResponse($displayImage); diff --git a/mailpoet/lib/Statistics/Track/SubscriberActivityTracker.php b/mailpoet/lib/Statistics/Track/SubscriberActivityTracker.php index b4064fc768..b497def7e1 100644 --- a/mailpoet/lib/Statistics/Track/SubscriberActivityTracker.php +++ b/mailpoet/lib/Statistics/Track/SubscriberActivityTracker.php @@ -92,7 +92,7 @@ class SubscriberActivityTracker { } private function processTracking(SubscriberEntity $subscriber): void { - $this->subscribersRepository->maybeUpdateLastEngagement($subscriber); + $this->subscribersRepository->maybeUpdateLastPageViewAt($subscriber); $this->pageViewCookie->setPageViewTimestamp($this->wp->currentTime('timestamp')); foreach ($this->callbacks as $callback) { $callback($subscriber); diff --git a/mailpoet/lib/Subscribers/SubscribersRepository.php b/mailpoet/lib/Subscribers/SubscribersRepository.php index 3154d73a41..ea2c720069 100644 --- a/mailpoet/lib/Subscribers/SubscribersRepository.php +++ b/mailpoet/lib/Subscribers/SubscribersRepository.php @@ -340,7 +340,7 @@ class SubscribersRepository extends Repository { } public function maybeUpdateLastEngagement(SubscriberEntity $subscriberEntity): void { - $now = CarbonImmutable::createFromTimestamp((int)$this->wp->currentTime('timestamp')); + $now = $this->getCurrentDateTime(); // Do not update engagement if was recently updated to avoid unnecessary updates in DB if ($subscriberEntity->getLastEngagementAt() && $subscriberEntity->getLastEngagementAt() > $now->subMinute()) { return; @@ -350,6 +350,50 @@ class SubscribersRepository extends Repository { $this->flush(); } + public function maybeUpdateLastOpenAt(SubscriberEntity $subscriberEntity): void { + $now = $this->getCurrentDateTime(); + // Avoid unnecessary DB calls + if ($subscriberEntity->getLastOpenAt() && $subscriberEntity->getLastOpenAt() > $now->subMinute()) { + return; + } + $subscriberEntity->setLastOpenAt($now); + $subscriberEntity->setLastEngagementAt($now); + $this->flush(); + } + + public function maybeUpdateLastClickAt(SubscriberEntity $subscriberEntity): void { + $now = $this->getCurrentDateTime(); + // Avoid unnecessary DB calls + if ($subscriberEntity->getLastClickAt() && $subscriberEntity->getLastClickAt() > $now->subMinute()) { + return; + } + $subscriberEntity->setLastClickAt($now); + $subscriberEntity->setLastEngagementAt($now); + $this->flush(); + } + + public function maybeUpdateLastPurchaseAt(SubscriberEntity $subscriberEntity): void { + $now = $this->getCurrentDateTime(); + // Avoid unnecessary DB calls + if ($subscriberEntity->getLastPurchaseAt() && $subscriberEntity->getLastPurchaseAt() > $now->subMinute()) { + return; + } + $subscriberEntity->setLastPurchaseAt($now); + $subscriberEntity->setLastEngagementAt($now); + $this->flush(); + } + + public function maybeUpdateLastPageViewAt(SubscriberEntity $subscriberEntity): void { + $now = $this->getCurrentDateTime(); + // Avoid unnecessary DB calls + if ($subscriberEntity->getLastPageViewAt() && $subscriberEntity->getLastPageViewAt() > $now->subMinute()) { + return; + } + $subscriberEntity->setLastPageViewAt($now); + $subscriberEntity->setLastEngagementAt($now); + $this->flush(); + } + /** * @param array $ids * @return string[] @@ -501,4 +545,8 @@ class SubscribersRepository extends Repository { return count($subscribers); } + + private function getCurrentDateTime(): CarbonImmutable { + return CarbonImmutable::createFromTimestamp((int)$this->wp->currentTime('timestamp')); + } } diff --git a/mailpoet/lib/WooCommerce/SubscriberEngagement.php b/mailpoet/lib/WooCommerce/SubscriberEngagement.php index e514bd0f30..bcb3deeae1 100644 --- a/mailpoet/lib/WooCommerce/SubscriberEngagement.php +++ b/mailpoet/lib/WooCommerce/SubscriberEngagement.php @@ -35,4 +35,18 @@ class SubscriberEngagement { $this->subscribersRepository->maybeUpdateLastEngagement($subscriber); } + + public function updateSubscriberLastPurchase($orderId): void { + $order = $this->woocommerceHelper->wcGetOrder($orderId); + if (!$order instanceof WC_Order) { + return; + } + + $subscriber = $this->subscribersRepository->findOneBy(['email' => $order->get_billing_email()]); + if (!$subscriber instanceof SubscriberEntity) { + return; + } + + $this->subscribersRepository->maybeUpdateLastPurchaseAt($subscriber); + } } diff --git a/mailpoet/tests/integration/Statistics/Track/ClicksTest.php b/mailpoet/tests/integration/Statistics/Track/ClicksTest.php index 0cf6d2bb96..8154197db1 100644 --- a/mailpoet/tests/integration/Statistics/Track/ClicksTest.php +++ b/mailpoet/tests/integration/Statistics/Track/ClicksTest.php @@ -499,7 +499,7 @@ class ClicksTest extends \MailPoetTest { expect($link)->equals('http://example.com/?email=test@example.com&newsletter_subject=Subject'); } - public function testItUpdatesSubscriberEngagementForHumanAgent() { + public function testItUpdatesSubscriberTimestampsForHumanAgent() { $now = Carbon::now(); $wpMock = $this->createMock(WPFunctions::class); $wpMock->expects($this->any()) @@ -531,8 +531,11 @@ class ClicksTest extends \MailPoetTest { ], $this); $clicks->track($data); $savedEngagementTime = $this->subscriber->getLastEngagementAt(); + $savedClickTime = $this->subscriber->getLastClickAt(); $this->assertInstanceOf(\DateTimeInterface::class, $savedEngagementTime); + $this->assertInstanceOf(\DateTimeInterface::class, $savedClickTime); expect($savedEngagementTime->getTimestamp())->equals($now->getTimestamp()); + expect($savedClickTime->getTimestamp())->equals($now->getTimestamp()); } public function testItUpdatesSubscriberEngagementForUnknownAgent() { @@ -566,8 +569,11 @@ class ClicksTest extends \MailPoetTest { ], $this); $clicks->track($data); $savedEngagementTime = $this->subscriber->getLastEngagementAt(); + $savedClickTime = $this->subscriber->getLastClickAt(); $this->assertInstanceOf(\DateTimeInterface::class, $savedEngagementTime); + $this->assertInstanceOf(\DateTimeInterface::class, $savedClickTime); expect($savedEngagementTime->getTimestamp())->equals($now->getTimestamp()); + expect($savedClickTime->getTimestamp())->equals($now->getTimestamp()); } public function testItUpdatesSubscriberEngagementForMachineAgent() { @@ -601,14 +607,17 @@ class ClicksTest extends \MailPoetTest { ], $this); $clicks->track($data); $savedEngagementTime = $this->subscriber->getLastEngagementAt(); + $savedClickTime = $this->subscriber->getLastClickAt(); $this->assertInstanceOf(\DateTimeInterface::class, $savedEngagementTime); + $this->assertInstanceOf(\DateTimeInterface::class, $savedClickTime); expect($savedEngagementTime->getTimestamp())->equals($now->getTimestamp()); + expect($savedClickTime->getTimestamp())->equals($now->getTimestamp()); } public function testItWontUpdateSubscriberThatWasRecentlyUpdated() { - $lastEngagement = Carbon::now()->subSeconds(10); + $lastClickTime = Carbon::now()->subSeconds(10); $clicksRepository = $this->diContainer->get(StatisticsClicksRepository::class); - $this->subscriber->setLastEngagementAt($lastEngagement); + $this->subscriber->setLastClickAt($lastClickTime); $data = $this->trackData; $data->userAgent = UserAgentEntity::MACHINE_USER_AGENTS[0]; $clicks = Stub::construct($this->clicks, [ @@ -625,6 +634,6 @@ class ClicksTest extends \MailPoetTest { 'redirectToUrl' => null, ], $this); $clicks->track($data); - expect($this->subscriber->getLastEngagementAt())->equals($lastEngagement); + expect($this->subscriber->getLastClickAt())->equals($lastClickTime); } } diff --git a/mailpoet/tests/integration/Statistics/Track/OpensTest.php b/mailpoet/tests/integration/Statistics/Track/OpensTest.php index 91698b6ea4..df3ab21824 100644 --- a/mailpoet/tests/integration/Statistics/Track/OpensTest.php +++ b/mailpoet/tests/integration/Statistics/Track/OpensTest.php @@ -374,8 +374,11 @@ class OpensTest extends \MailPoetTest { $opens->track($this->trackData); $savedEngagementTime = $this->subscriber->getLastEngagementAt(); + $savedOpenTime = $this->subscriber->getLastOpenAt(); $this->assertInstanceOf(\DateTimeInterface::class, $savedEngagementTime); + $this->assertInstanceOf(\DateTimeInterface::class, $savedOpenTime); expect($savedEngagementTime->getTimestamp())->equals($now->getTimestamp()); + expect($savedOpenTime->getTimestamp())->equals($now->getTimestamp()); } public function testItUpdatesSubscriberEngagementForUnknownAgent() { @@ -395,11 +398,14 @@ class OpensTest extends \MailPoetTest { $opens->track($this->trackData); $savedEngagementTime = $this->subscriber->getLastEngagementAt(); + $savedOpenTime = $this->subscriber->getLastOpenAt(); $this->assertInstanceOf(\DateTimeInterface::class, $savedEngagementTime); + $this->assertInstanceOf(\DateTimeInterface::class, $savedOpenTime); expect($savedEngagementTime->getTimestamp())->equals($now->getTimestamp()); + expect($savedOpenTime->getTimestamp())->equals($now->getTimestamp()); } - public function testItUpdatesSubscriberEngagementForMachineAgent() { + public function testItUpdatesSubscriberTimestampsForMachineAgent() { $now = Carbon::now(); $wpMock = $this->createMock(WPFunctions::class); $wpMock->expects($this->once()) @@ -416,7 +422,10 @@ class OpensTest extends \MailPoetTest { $opens->track($this->trackData); $savedEngagementTime = $this->subscriber->getLastEngagementAt(); + $savedOpenTime = $this->subscriber->getLastOpenAt(); $this->assertInstanceOf(\DateTimeInterface::class, $savedEngagementTime); + $this->assertInstanceOf(\DateTimeInterface::class, $savedOpenTime); expect($savedEngagementTime->getTimestamp())->equals($now->getTimestamp()); + expect($savedOpenTime->getTimestamp())->equals($now->getTimestamp()); } } diff --git a/mailpoet/tests/integration/Statistics/Track/SubscriberActivityTrackerTest.php b/mailpoet/tests/integration/Statistics/Track/SubscriberActivityTrackerTest.php index 17d991565b..98c0138e20 100644 --- a/mailpoet/tests/integration/Statistics/Track/SubscriberActivityTrackerTest.php +++ b/mailpoet/tests/integration/Statistics/Track/SubscriberActivityTrackerTest.php @@ -106,6 +106,8 @@ class SubscriberActivityTrackerTest extends \MailPoetTest { $this->assertInstanceOf(SubscriberEntity::class, $subscriber); expect($subscriber->getLastEngagementAt())->greaterThan(Carbon::now()->subMinute()); expect($subscriber->getLastEngagementAt())->lessThan(Carbon::now()->addMinute()); + expect($subscriber->getLastPageViewAt())->greaterThan(Carbon::now()->subMinute()); + expect($subscriber->getLastPageViewAt())->lessThan(Carbon::now()->addMinute()); } /** diff --git a/mailpoet/tests/integration/WooCommerce/SubscriberEngagementTest.php b/mailpoet/tests/integration/WooCommerce/SubscriberEngagementTest.php index 09e8f46aae..c6ed910730 100644 --- a/mailpoet/tests/integration/WooCommerce/SubscriberEngagementTest.php +++ b/mailpoet/tests/integration/WooCommerce/SubscriberEngagementTest.php @@ -2,6 +2,7 @@ namespace MailPoet\WooCommerce; +use DateTimeInterface; use MailPoet\Config\SubscriberChangesNotifier; use MailPoet\Entities\SubscriberEntity; use MailPoet\Subscribers\SubscribersRepository; @@ -49,7 +50,7 @@ class SubscriberEngagementTest extends \MailPoetTest { expect($subscriber->getLastEngagementAt())->equals($now); } - public function testItDoesntUpdateAnythingForNonExistingOder() { + public function testItDoesntUpdateAnythingForNonExistingOrder() { $subscriber = $this->createSubscriber(); $this->wooCommerceHelperMock ->expects($this->once()) @@ -60,6 +61,21 @@ class SubscriberEngagementTest extends \MailPoetTest { expect($subscriber->getLastEngagementAt())->null(); } + public function testItUpdatesTimestampsWhenOrderChangesToPaidStatus(): void { + $subscriber = $this->createSubscriber(); + expect($subscriber->getLastEngagementAt())->null(); + expect($subscriber->getLastPurchaseAt())->null(); + $order = $this->tester->createWooCommerceOrder(['status' => 'pending', 'billing_email' => $subscriber->getEmail()]); + $order->set_status('processing'); + $order->save(); + $engagementTime = $subscriber->getLastEngagementAt(); + $purchaseTime = $subscriber->getLastPurchaseAt(); + $this->assertInstanceOf(DateTimeInterface::class, $engagementTime); + $this->assertInstanceOf(DateTimeInterface::class, $purchaseTime); + expect($engagementTime)->equals($purchaseTime); + expect($engagementTime)->greaterThan(Carbon::now()->subMinute()); + } + public function testItDoesntThrowAnErrorForNonExistingSubscriber() { $order = $this->createOrderMock('some@email.com'); $this->wooCommerceHelperMock @@ -83,7 +99,7 @@ class SubscriberEngagementTest extends \MailPoetTest { private function createSubscriber(): SubscriberEntity { $subscriber = new SubscriberEntity(); - $subscriber->setEmail('subscriber@egagement.com'); + $subscriber->setEmail('subscriber@engagement.com'); $this->entityManager->persist($subscriber); $this->entityManager->flush(); return $subscriber;