Merge tag '5.12.1'
All checks were successful
Make release / release (push) Successful in 53m18s

This commit is contained in:
2025-05-06 12:03:51 -05:00
21 changed files with 458 additions and 86 deletions

View File

@ -197,10 +197,10 @@ jobs:
- run:
name: Download additional WP Plugins for tests
command: |
./do download:woo-commerce-zip 9.8.2
./do download:woo-commerce-zip 9.8.3
./do download:woo-commerce-subscriptions-zip 7.4.0
./do download:woo-commerce-memberships-zip
./do download:automate-woo-zip 6.1.10
./do download:automate-woo-zip 6.1.11
- run:
name: Dump tests ENV variables for acceptance tests
command: |

View File

@ -195,8 +195,8 @@ div.mailpoet-listing-bulk-actions-container {
border-bottom: 1px solid $color-tertiary-light;
box-shadow: none;
max-width: 30vw;
padding: $grid-gap-medium $grid-gap-half;
vertical-align: middle;
padding: 12px $grid-gap-half;
vertical-align: top;
@include respond-to(small-screen) {
max-width: none;
@ -310,13 +310,10 @@ a.mailpoet-listing-title {
.mailpoet-listing-actions {
align-items: center;
display: none;
display: flex;
flex-wrap: wrap;
left: 0;
line-height: 15px;
position: absolute;
top: 0;
width: 100%;
visibility: hidden;
a {
color: $color-text-light;
@ -340,7 +337,7 @@ a.mailpoet-listing-title {
}
tr:hover & {
display: flex;
visibility: visible;
}
@include respond-to(small-screen) {

View File

@ -1,8 +1,15 @@
import slugify from 'slugify';
export function formatCustomFieldBlockName(blockName, customField) {
const name = slugify(customField.name, { lower: true })
let name = slugify(customField.name, { lower: true })
.replace(/[^a-z0-9]+/g, '')
.replace(/-$/, '');
// Ensure unique block names by appending ID if the slug is empty or too short
// (which can happen with certain character sets)
if (!name || name.length < 2) {
name = `field${customField.id}`;
}
return `${blockName}-${name}`;
}

View File

@ -1,5 +1,5 @@
{
"apiVersion": 2,
"apiVersion": 3,
"name": "mailpoet/marketing-optin-block",
"version": "0.1.0",
"title": "MailPoet Marketing Opt-in",

View File

@ -18,6 +18,7 @@ const adminUrl = getSetting('adminUrl');
const { optinEnabled, defaultText } = getSetting('mailpoet_data');
function EmptyState(): JSX.Element {
const adminUrlToEnableOptIn = `${adminUrl}admin.php?page=mailpoet-settings#/woocommerce`;
return (
<Placeholder
icon={<Icon icon={megaphone} />}
@ -32,10 +33,10 @@ function EmptyState(): JSX.Element {
</span>
<Button
variant="primary"
href={`${adminUrl}admin.php?page=mailpoet-settings#/woocommerce`}
target="_blank"
rel="noopener noreferrer"
className="wp-block-mailpoet-newsletter-block-placeholder__button"
onClick={() => {
window.open(adminUrlToEnableOptIn, '_blank', 'noopener noreferrer');
}}
>
{__('Enable opt-in for Checkout', 'mailpoet')}
</Button>

View File

@ -4,12 +4,13 @@ import { Icon } from './icon.jsx';
const wp = window.wp;
const { Placeholder, PanelBody } = wp.components;
const { BlockIcon, InspectorControls } = wp.blockEditor;
const { BlockIcon, InspectorControls, useBlockProps } = wp.blockEditor;
const ServerSideRender = wp.serverSideRender;
const allForms = window.mailpoet_forms;
function Edit({ attributes, setAttributes }) {
const blockProps = useBlockProps();
function displayFormsSelect() {
if (!Array.isArray(allForms)) return null;
if (allForms.length === 0) return null;
@ -27,7 +28,7 @@ function Edit({ attributes, setAttributes }) {
{window.locale.selectForm}
</option>
{allForms.map((form) => (
<option value={form.id}>
<option value={form.id} key={`form-${form.id}`}>
{form.name +
(form.status === 'disabled'
? ` (${window.locale.inactive})`
@ -63,7 +64,7 @@ function Edit({ attributes, setAttributes }) {
}
return (
<>
<div {...blockProps}>
<InspectorControls>
<PanelBody title="MailPoet Subscription Form" initialOpen>
{selectFormSettings()}
@ -81,7 +82,7 @@ function Edit({ attributes, setAttributes }) {
)}
{attributes.formId !== null && renderForm()}
</div>
</>
</div>
);
}

View File

@ -6,6 +6,7 @@ const { registerBlockType } = wp.blocks;
registerBlockType('mailpoet/subscription-form-block-render', {
title: window.locale.subscriptionForm,
apiVersion: 3,
attributes: {
formId: {
type: 'number',
@ -19,6 +20,7 @@ registerBlockType('mailpoet/subscription-form-block-render', {
registerBlockType('mailpoet/subscription-form-block', {
title: window.locale.subscriptionForm,
apiVersion: 3,
icon: Icon,
category: 'widgets',
example: {},

View File

@ -1,5 +1,9 @@
== Changelog ==
= 5.12.1 - 2025-05-06 =
* Fixed: listing action items overlaying with row border when on two lines on smaller screens;
* Fixed: Handling of Japanese characters in custom field blocks in the form editor.
= 5.12.0 - 2025-04-29 =
* Improved: more consistent look and feel of MailPoet pages with WordPress;
* Improved: optimized email template images to decrease their file size;

View File

@ -0,0 +1,90 @@
<?php declare(strict_types = 1);
namespace MailPoet\Migrations\App;
use MailPoet\Entities\StatisticsClickEntity;
use MailPoet\Entities\StatisticsUnsubscribeEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Entities\SubscriberSegmentEntity;
use MailPoet\Migrator\AppMigration;
use MailPoetVendor\Doctrine\DBAL\Connection;
class Migration_20250501_114655_App extends AppMigration {
private const DB_QUERY_CHUNK_SIZE = 1000;
public function run(): void {
$clicksStatsTable = $this->entityManager->getClassMetadata(StatisticsClickEntity::class)->getTableName();
$unsubscribeStatsTable = $this->entityManager->getClassMetadata(StatisticsUnsubscribeEntity::class)->getTableName();
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
$subscribersSegmentsTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
// First get all subscriber IDs that were unsubscribed by a bot
$subscriberIds = $this->entityManager->getConnection()->executeQuery(
"SELECT DISTINCT mp_unsub.subscriber_id
FROM {$unsubscribeStatsTable} AS mp_unsub
LEFT JOIN {$clicksStatsTable} AS mp_click
ON mp_unsub.newsletter_id = mp_click.newsletter_id
AND mp_unsub.subscriber_id = mp_click.subscriber_id
AND ABS(TIMESTAMPDIFF(SECOND, mp_click.created_at, mp_unsub.created_at)) <= 4
WHERE mp_unsub.created_at > '2025-03-01'
GROUP BY mp_unsub.subscriber_id
HAVING COUNT(mp_click.id) >= 3"
)->fetchFirstColumn();
if (empty($subscriberIds)) {
return;
}
// Process subscriber IDs in chunks
foreach (array_chunk($subscriberIds, self::DB_QUERY_CHUNK_SIZE) as $chunk) {
$this->processSubscriberChunk(
$chunk,
$subscribersTable,
$subscribersSegmentsTable,
$unsubscribeStatsTable
);
}
}
private function processSubscriberChunk(
array $subscriberIds,
string $subscribersTable,
string $subscribersSegmentsTable,
string $unsubscribeStatsTable
): void {
// Switch the global subscriber status to subscribed
$this->entityManager->getConnection()->executeQuery(
"UPDATE {$subscribersTable}
SET status = :subscribedStatus
WHERE id IN (:subscriberIds)
AND status = :unsubscribedStatus",
[
'subscribedStatus' => SubscriberEntity::STATUS_SUBSCRIBED,
'unsubscribedStatus' => SubscriberEntity::STATUS_UNSUBSCRIBED,
'subscriberIds' => $subscriberIds,
],
[
'subscriberIds' => Connection::PARAM_INT_ARRAY,
]
);
// Update the subscriber_segment table, find rows that were unsubscribed at the same time
$this->entityManager->getConnection()->executeQuery(
"UPDATE {$subscribersSegmentsTable} AS mp_subseg
JOIN {$unsubscribeStatsTable} AS mp_unsub
ON mp_subseg.subscriber_id = mp_unsub.subscriber_id
AND ABS(TIMESTAMPDIFF(SECOND, mp_subseg.updated_at, mp_unsub.created_at)) <= 2
SET mp_subseg.status = :subscribedStatus
WHERE mp_subseg.status = :unsubscribedStatus
AND mp_subseg.subscriber_id IN (:subscriberIds)",
[
'subscribedStatus' => SubscriberEntity::STATUS_SUBSCRIBED,
'unsubscribedStatus' => SubscriberEntity::STATUS_UNSUBSCRIBED,
'subscriberIds' => $subscriberIds,
],
[
'subscriberIds' => Connection::PARAM_INT_ARRAY,
]
);
}
}

View File

@ -115,7 +115,7 @@ class Migration_20230831_143755_Db extends DbMigration {
$stepId = strval($item['step_id']);
$stepKey = strval($steps[$stepId]['key'] ?? 'unknown');
$triggerId = $steps['root']['next_steps'][0]['id'];
$triggerKey = $steps['root']['next_steps'][0]['key'];
$triggerKey = $steps['root']['next_steps'][0]['key'] ?? 'unknown';
$queries[] = "UPDATE {$logsTable} SET step_key = '{$stepKey}' WHERE id = {$id}";
@ -132,7 +132,16 @@ class Migration_20230831_143755_Db extends DbMigration {
}
}
$this->connection->executeStatement(implode(';', $queries));
$nativeConnection = $this->connection->getNativeConnection();
// If the connection is a mysqli object, we can use the multi_query method
if (is_object($nativeConnection) && get_class($nativeConnection) === 'mysqli') {
$query = implode(';', $queries);
$nativeConnection->multi_query($query);
} else { // Otherwise, we need to execute each query individually (e.g. PDO or SQLite)
foreach ($queries as $query) {
$this->connection->executeStatement($query);
}
}
}
}
}

View File

@ -2,7 +2,7 @@
/*
* Plugin Name: MailPoet
* Version: 5.12.0
* Version: 5.12.1
* Plugin URI: https://www.mailpoet.com
* Description: Create and send newsletters, post notifications and welcome emails from your WordPress.
* Author: MailPoet
@ -20,7 +20,7 @@
*/
$mailpoetPlugin = [
'version' => '5.12.0',
'version' => '5.12.1',
'filename' => __FILE__,
'path' => dirname(__FILE__),
'autoloader' => dirname(__FILE__) . '/vendor/autoload.php',

View File

@ -3,7 +3,7 @@ Contributors: mailpoet, woocommerce, automattic
Tags: email marketing, post notification, woocommerce emails, email automation, newsletter
Requires at least: 6.7
Tested up to: 6.8
Stable tag: 5.12.0
Stable tag: 5.12.1
Requires PHP: 7.4
License: GPLv3
License URI: https://www.gnu.org/licenses/gpl-3.0.html
@ -222,11 +222,8 @@ Check our [Knowledge Base](https://kb.mailpoet.com) or contact us through our [s
== Changelog ==
= 5.12.0 - 2025-04-29 =
* Improved: more consistent look and feel of MailPoet pages with WordPress;
* Improved: optimized email template images to decrease their file size;
* Improved: better handling of unsubscribes via the link provided in the List-Unsubscribe header;
* Fixed: unreadable customer name in automation analytics when Gravatar fails to load;
* Fixed: "Custom HTML" block in form editor doesn't preserve "Automatically add paragraphs" setting.
= 5.12.1 - 2025-05-06 =
* Fixed: listing action items overlaying with row border when on two lines on smaller screens;
* Fixed: Handling of Japanese characters in custom field blocks in the form editor.
[See the changelog for all versions.](https://github.com/mailpoet/mailpoet/blob/trunk/mailpoet/changelog.txt)

View File

@ -0,0 +1,51 @@
<?php declare(strict_types = 1);
namespace MailPoet\Test\DataFactories;
use MailPoet\DI\ContainerWrapper;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\SendingQueueEntity;
use MailPoet\Entities\StatisticsUnsubscribeEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoetVendor\Doctrine\ORM\EntityManager;
use PHPUnit\Framework\Assert;
class StatisticsUnsubscribes {
protected $data;
/** @var NewsletterEntity */
private $newsletter;
/** @var SubscriberEntity */
private $subscriber;
public function __construct(
NewsletterEntity $newsletter,
SubscriberEntity $subscriber
) {
$this->newsletter = $newsletter;
$this->subscriber = $subscriber;
}
public function withCreatedAt(\DateTimeInterface $createdAt): self {
$this->data['createdAt'] = $createdAt;
return $this;
}
public function create(): StatisticsUnsubscribeEntity {
$entityManager = ContainerWrapper::getInstance()->get(EntityManager::class);
$queue = $this->newsletter->getLatestQueue();
Assert::assertInstanceOf(SendingQueueEntity::class, $queue);
$entity = new StatisticsUnsubscribeEntity(
$this->newsletter,
$queue,
$this->subscriber
);
if (($this->data['createdAt'] ?? null) instanceof \DateTimeInterface) {
$entity->setCreatedAt($this->data['createdAt']);
}
$entityManager->persist($entity);
$entityManager->flush();
return $entity;
}
}

View File

@ -0,0 +1,61 @@
<?php declare(strict_types = 1);
namespace MailPoet\Test\DataFactories;
use MailPoet\DI\ContainerWrapper;
use MailPoet\Entities\SegmentEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Entities\SubscriberSegmentEntity;
use MailPoetVendor\Doctrine\ORM\EntityManager;
class SubscriberSegment {
protected $data;
/** @var SubscriberEntity */
private $subscriber;
/** @var SegmentEntity */
private $segment;
public function __construct(
SubscriberEntity $subscriber,
SegmentEntity $segment,
string $status = SubscriberEntity::STATUS_SUBSCRIBED
) {
$this->subscriber = $subscriber;
$this->segment = $segment;
$this->data['status'] = $status;
}
public function withStatus(string $status): self {
$this->data['status'] = $status;
return $this;
}
public function withUpdatedAt(\DateTimeInterface $updatedAt): self {
$this->data['updatedAt'] = $updatedAt;
return $this;
}
public function create(): SubscriberSegmentEntity {
$entityManager = ContainerWrapper::getInstance()->get(EntityManager::class);
$entity = new SubscriberSegmentEntity($this->segment, $this->subscriber, $this->data['status']);
if (isset($this->data['status'])) {
$entity->setStatus($this->data['status']);
}
$entityManager->persist($entity);
$entityManager->flush();
$entityManager->refresh($entity);
if (($this->data['updatedAt'] ?? null) instanceof \DateTimeInterface) {
$subscribersSegmentsTable = $entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
$entityManager->getConnection()->executeQuery("UPDATE {$subscribersSegmentsTable} SET updated_at = :updatedAt WHERE id = :id", [
'updatedAt' => $this->data['updatedAt']->format('Y-m-d H:i:s'),
'id' => $entity->getId(),
]);
};
$entityManager->refresh($entity);
return $entity;
}
}

View File

@ -122,22 +122,22 @@ class GutenbergFormBlockCest {
$i->amEditingPostWithId($postId);
$this->closeDialog($i);
$i->waitForText('My Gutenberg form');
$i->switchToIframe('iframe[name="editor-canvas"]');
$i->click('[aria-label="Add title"]');
$i->click('[aria-label="Add block"]');
$i->switchToIFrame();
$i->fillField('[placeholder="Search"]', 'MailPoet Subscription Form');
$i->waitForElement(Locator::contains('button', 'MailPoet Subscription Form'));
$i->click(Locator::contains('button', 'MailPoet Subscription Form'));
$i->switchToIframe('iframe[name="editor-canvas"]');
$i->waitForElement('[aria-label="Block: MailPoet Subscription Form"]');
$i->selectOption('.mailpoet-block-create-forms-list', 'Acceptance Test Block Form');
$i->waitForElementVisible('[data-automation-id="form_email"]');
$i->waitForElementVisible('[data-automation-id="form_first_name"]');
$i->waitForElementVisible('[data-automation-id="form_last_name"]');
// From WP 6.6 the button label is Save
if (version_compare($i->getWordPressVersion(), '6.6', '<')) {
$i->click('Update');
} else {
$i->click('Save');
}
$i->switchToIFrame();
$i->click('Save');
$i->waitForText('Post updated.');
$i->wantTo('Verify the added form on the front-end');

View File

@ -63,7 +63,9 @@ class WooCheckoutBlocksCest {
$i->wantTo('Check a message when opt-in is disabled');
$i->login();
$i->amOnAdminPage("post.php?post={$this->checkoutPostId}&action=edit");
$i->switchToIframe('iframe[name="editor-canvas"]');
$i->canSee('MailPoet marketing opt-in would be shown here if enabled. You can enable from the settings page.');
$i->switchToIframe();
$i->logOut();
$this->orderProduct($i, $customerEmail, true, false);
$i->login();
@ -223,25 +225,27 @@ class WooCheckoutBlocksCest {
$i->wantTo('Choose a pattern was not present, skipping action.');
}
$this->closeDialog($i);
$i->switchToIframe('iframe[name="editor-canvas"]');
$i->click('[aria-label="Add title"]'); // For block inserter to show up
$i->click('[aria-label="Add block"]');
$i->switchToIframe();
$i->fillField('[placeholder="Search"]', 'Checkout');
$i->waitForElement(Locator::contains('button > span > span', 'Checkout'));
$i->click(Locator::contains('button > span > span', 'Checkout')); // Select Checkout block
$i->switchToIframe('iframe[name="editor-canvas"]');
$i->waitForElement('[aria-label="Block: Checkout"]');
// Close dialog with Compatibility notice
$i->switchToIframe();
$this->closeDialog($i);
// Enable registration during the checkout
$i->switchToIframe('iframe[name="editor-canvas"]');
$i->click('[aria-label="Block: Contact Information"]');
// From WP 6.6 the button label is Save
$version = (string)preg_replace('/-(RC|beta)\d*/', '', $i->getWordPressVersion());
if (version_compare($version, '6.6', '<')) {
$i->click('Update');
} else {
$i->click('Save');
}
$i->switchToIframe();
$i->click('Save');
$i->waitForText('Page updated.');
$i->logOut();
return $postId;

View File

@ -20,7 +20,7 @@ class ReporterTest extends \MailPoetTest {
$defaultSegment = (new Segment())->withType(SegmentEntity::TYPE_DEFAULT)->create();
$this->createSentNewsletter(NewsletterEntity::TYPE_STANDARD, Carbon::now()->subDays(2), [$defaultSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '1']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_STANDARD, Carbon::now()->subDays(8), [$defaultSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '2']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_STANDARD, Carbon::now()->subDays(89), [$defaultSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '3']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_STANDARD, Carbon::now()->subMonths(2), [$defaultSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '3']]]);
$processed = $this->reporter->getData();
$this->assertEquals(1, $processed['Number of standard newsletters sent in last 7 days']);
@ -32,7 +32,7 @@ class ReporterTest extends \MailPoetTest {
$dynamicSegment = (new Segment())->withType(SegmentEntity::TYPE_DYNAMIC)->create();
$this->createSentNewsletter(NewsletterEntity::TYPE_STANDARD, Carbon::now()->subDays(2), [$dynamicSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '1']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_STANDARD, Carbon::now()->subDays(8), [$dynamicSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '2']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_STANDARD, Carbon::now()->subDays(89), [$dynamicSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '3']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_STANDARD, Carbon::now()->subMonths(2), [$dynamicSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '3']]]);
$processed = $this->reporter->getData();
$this->assertEquals(1, $processed['Number of standard newsletters sent in last 7 days']);
@ -47,7 +47,7 @@ class ReporterTest extends \MailPoetTest {
$defaultSegment = (new Segment())->withType(SegmentEntity::TYPE_DEFAULT)->create();
$this->createSentNewsletter(NewsletterEntity::TYPE_STANDARD, Carbon::now()->subDays(2), [$defaultSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '1', 'filterSegment' => ['theDataDoesNot' => 'matter']]]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_STANDARD, Carbon::now()->subDays(8), [$defaultSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '2', 'filterSegment' => ['theDataDoesNot' => 'matter']]]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_STANDARD, Carbon::now()->subDays(89), [$defaultSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '3', 'filterSegment' => ['theDataDoesNot' => 'matter']]]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_STANDARD, Carbon::now()->subMonths(2), [$defaultSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '3', 'filterSegment' => ['theDataDoesNot' => 'matter']]]]);
$processed = $this->reporter->getData();
$this->assertEquals(1, $processed['Number of standard newsletters sent in last 7 days']);
@ -65,7 +65,7 @@ class ReporterTest extends \MailPoetTest {
$defaultSegment = (new Segment())->withType(SegmentEntity::TYPE_DEFAULT)->create();
$this->createSentNewsletter(NewsletterEntity::TYPE_NOTIFICATION_HISTORY, Carbon::now()->subDays(2), [$defaultSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '1']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_NOTIFICATION_HISTORY, Carbon::now()->subDays(8), [$defaultSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '2']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_NOTIFICATION_HISTORY, Carbon::now()->subDays(89), [$defaultSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '3']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_NOTIFICATION_HISTORY, Carbon::now()->subMonths(2), [$defaultSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '3']]]);
$processed = $this->reporter->getData();
$this->assertEquals(1, $processed['Number of post notification campaigns sent in the last 7 days']);
@ -77,7 +77,7 @@ class ReporterTest extends \MailPoetTest {
$dynamicSegment = (new Segment())->withType(SegmentEntity::TYPE_DYNAMIC)->create();
$this->createSentNewsletter(NewsletterEntity::TYPE_NOTIFICATION_HISTORY, Carbon::now()->subDays(2), [$dynamicSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '1']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_NOTIFICATION_HISTORY, Carbon::now()->subDays(8), [$dynamicSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '2']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_NOTIFICATION_HISTORY, Carbon::now()->subDays(89), [$dynamicSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '3']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_NOTIFICATION_HISTORY, Carbon::now()->subMonths(2), [$dynamicSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '3']]]);
$processed = $this->reporter->getData();
$this->assertEquals(1, $processed['Number of post notification campaigns sent in the last 7 days']);
@ -92,7 +92,7 @@ class ReporterTest extends \MailPoetTest {
$defaultSegment = (new Segment())->withType(SegmentEntity::TYPE_DEFAULT)->create();
$this->createSentNewsletter(NewsletterEntity::TYPE_NOTIFICATION_HISTORY, Carbon::now()->subDays(2), [$defaultSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '1', 'filterSegment' => ['not' => 'relevant']]]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_NOTIFICATION_HISTORY, Carbon::now()->subDays(8), [$defaultSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '2', 'filterSegment' => ['not' => 'relevant']]]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_NOTIFICATION_HISTORY, Carbon::now()->subDays(89), [$defaultSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '3', 'filterSegment' => ['not' => 'relevant']]]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_NOTIFICATION_HISTORY, Carbon::now()->subMonths(2), [$defaultSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '3', 'filterSegment' => ['not' => 'relevant']]]]);
$processed = $this->reporter->getData();
$this->assertEquals(1, $processed['Number of post notification campaigns sent in the last 7 days']);
@ -110,7 +110,7 @@ class ReporterTest extends \MailPoetTest {
$defaultSegment = (new Segment())->withType(SegmentEntity::TYPE_DEFAULT)->create();
$this->createSentNewsletter(NewsletterEntity::TYPE_RE_ENGAGEMENT, Carbon::now()->subDays(2), [$defaultSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '1']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_RE_ENGAGEMENT, Carbon::now()->subDays(8), [$defaultSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '2']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_RE_ENGAGEMENT, Carbon::now()->subDays(89), [$defaultSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '3']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_RE_ENGAGEMENT, Carbon::now()->subMonths(2), [$defaultSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '3']]]);
$processed = $this->reporter->getData();
$this->assertEquals(1, $processed['Number of re-engagement campaigns sent in the last 7 days']);
@ -122,7 +122,7 @@ class ReporterTest extends \MailPoetTest {
$dynamicSegment = (new Segment())->withType(SegmentEntity::TYPE_DYNAMIC)->create();
$this->createSentNewsletter(NewsletterEntity::TYPE_RE_ENGAGEMENT, Carbon::now()->subDays(2), [$dynamicSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '1']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_RE_ENGAGEMENT, Carbon::now()->subDays(8), [$dynamicSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '2']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_RE_ENGAGEMENT, Carbon::now()->subDays(89), [$dynamicSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '3']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_RE_ENGAGEMENT, Carbon::now()->subMonths(2), [$dynamicSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '3']]]);
$processed = $this->reporter->getData();
$this->assertEquals(1, $processed['Number of re-engagement campaigns sent in the last 7 days']);
@ -137,7 +137,7 @@ class ReporterTest extends \MailPoetTest {
$dynamicSegment = (new Segment())->withType(SegmentEntity::TYPE_DEFAULT)->create();
$this->createSentNewsletter(NewsletterEntity::TYPE_RE_ENGAGEMENT, Carbon::now()->subDays(2), [$dynamicSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '1', 'filterSegment' => ['theDataDoesNot' => 'matter']]]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_RE_ENGAGEMENT, Carbon::now()->subDays(8), [$dynamicSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '2', 'filterSegment' => ['theDataDoesNot' => 'matter']]]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_RE_ENGAGEMENT, Carbon::now()->subDays(89), [$dynamicSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '3', 'filterSegment' => ['theDataDoesNot' => 'matter']]]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_RE_ENGAGEMENT, Carbon::now()->subMonths(2), [$dynamicSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '3', 'filterSegment' => ['theDataDoesNot' => 'matter']]]]);
$processed = $this->reporter->getData();
$this->assertEquals(1, $processed['Number of re-engagement campaigns sent in the last 7 days']);
@ -154,7 +154,7 @@ class ReporterTest extends \MailPoetTest {
public function testItWorksWithLegacyWelcomeEmails(): void {
$this->createSentNewsletter(NewsletterEntity::TYPE_WELCOME, Carbon::now()->subDays(2), [], ['sendingQueueOptions' => ['meta' => ['campaignId' => '1']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_WELCOME, Carbon::now()->subDays(8), [], ['sendingQueueOptions' => ['meta' => ['campaignId' => '2']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_WELCOME, Carbon::now()->subDays(89), [], ['sendingQueueOptions' => ['meta' => ['campaignId' => '3']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_WELCOME, Carbon::now()->subMonths(2), [], ['sendingQueueOptions' => ['meta' => ['campaignId' => '3']]]);
$processed = $this->reporter->getData();
$this->assertSame(1, $processed['Number of legacy welcome email campaigns sent in the last 7 days']);
$this->assertSame(2, $processed['Number of legacy welcome email campaigns sent in the last 30 days']);
@ -164,7 +164,7 @@ class ReporterTest extends \MailPoetTest {
public function testItWorksWithLegacyAbandonedCartEmails(): void {
$this->createSentNewsletter(NewsletterEntity::TYPE_AUTOMATIC, Carbon::now()->subDays(2), [], ['sendingQueueOptions' => ['meta' => ['campaignId' => '1', 'cart_product_ids' => ['123']]]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_AUTOMATIC, Carbon::now()->subDays(8), [], ['sendingQueueOptions' => ['meta' => ['campaignId' => '2', 'cart_product_ids' => ['1234']]]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_AUTOMATIC, Carbon::now()->subDays(89), [], ['sendingQueueOptions' => ['meta' => ['campaignId' => '3', 'cart_product_ids' => ['1235']]]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_AUTOMATIC, Carbon::now()->subMonths(2), [], ['sendingQueueOptions' => ['meta' => ['campaignId' => '3', 'cart_product_ids' => ['1235']]]]);
$processed = $this->reporter->getData();
$this->assertSame(1, $processed['Number of legacy abandoned cart campaigns sent in the last 7 days']);
$this->assertSame(2, $processed['Number of legacy abandoned cart campaigns sent in the last 30 days']);
@ -174,7 +174,7 @@ class ReporterTest extends \MailPoetTest {
public function testItWorksWithLegacyPurchasedProductEmails(): void {
$this->createSentNewsletter(NewsletterEntity::TYPE_AUTOMATIC, Carbon::now()->subDays(2), [], ['sendingQueueOptions' => ['meta' => ['campaignId' => '1', 'orderedProducts' => ['123']]]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_AUTOMATIC, Carbon::now()->subDays(8), [], ['sendingQueueOptions' => ['meta' => ['campaignId' => '2', 'orderedProducts' => ['1234']]]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_AUTOMATIC, Carbon::now()->subDays(89), [], ['sendingQueueOptions' => ['meta' => ['campaignId' => '3', 'orderedProducts' => ['1235']]]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_AUTOMATIC, Carbon::now()->subMonths(2), [], ['sendingQueueOptions' => ['meta' => ['campaignId' => '3', 'orderedProducts' => ['1235']]]]);
$processed = $this->reporter->getData();
$this->assertSame(1, $processed['Number of legacy purchased product campaigns sent in the last 7 days']);
$this->assertSame(2, $processed['Number of legacy purchased product campaigns sent in the last 30 days']);
@ -184,7 +184,7 @@ class ReporterTest extends \MailPoetTest {
public function testItWorksWithLegacyPurchasedInCategoryEmails(): void {
$this->createSentNewsletter(NewsletterEntity::TYPE_AUTOMATIC, Carbon::now()->subDays(2), [], ['sendingQueueOptions' => ['meta' => ['campaignId' => '1', 'orderedProductCategories' => ['123']]]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_AUTOMATIC, Carbon::now()->subDays(8), [], ['sendingQueueOptions' => ['meta' => ['campaignId' => '2', 'orderedProductCategories' => ['1234']]]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_AUTOMATIC, Carbon::now()->subDays(89), [], ['sendingQueueOptions' => ['meta' => ['campaignId' => '3', 'orderedProductCategories' => ['1235']]]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_AUTOMATIC, Carbon::now()->subMonths(2), [], ['sendingQueueOptions' => ['meta' => ['campaignId' => '3', 'orderedProductCategories' => ['1235']]]]);
$processed = $this->reporter->getData();
$this->assertSame(1, $processed['Number of legacy purchased in category campaigns sent in the last 7 days']);
$this->assertSame(2, $processed['Number of legacy purchased in category campaigns sent in the last 30 days']);
@ -194,7 +194,7 @@ class ReporterTest extends \MailPoetTest {
public function testItWorksWithLegacyFirstPurchaseEmails(): void {
$this->createSentNewsletter(NewsletterEntity::TYPE_AUTOMATIC, Carbon::now()->subDays(2), [], ['sendingQueueOptions' => ['meta' => ['campaignId' => '1', 'order_amount' => 123, 'order_date' => '2024-03-01', 'order_id' => '1']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_AUTOMATIC, Carbon::now()->subDays(8), [], ['sendingQueueOptions' => ['meta' => ['campaignId' => '2', 'order_amount' => 123, 'order_date' => '2024-03-01', 'order_id' => '2']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_AUTOMATIC, Carbon::now()->subDays(89), [], ['sendingQueueOptions' => ['meta' => ['campaignId' => '3', 'order_amount' => 123, 'order_date' => '2024-03-01', 'order_id' => '3']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_AUTOMATIC, Carbon::now()->subMonths(2), [], ['sendingQueueOptions' => ['meta' => ['campaignId' => '3', 'order_amount' => 123, 'order_date' => '2024-03-01', 'order_id' => '3']]]);
$processed = $this->reporter->getData();
$this->assertSame(1, $processed['Number of legacy first purchase campaigns sent in the last 7 days']);
$this->assertSame(2, $processed['Number of legacy first purchase campaigns sent in the last 30 days']);
@ -204,7 +204,7 @@ class ReporterTest extends \MailPoetTest {
public function testItWorksForAutomationEmails(): void {
$this->createSentNewsletter(NewsletterEntity::TYPE_AUTOMATION, Carbon::now()->subDays(2), [], ['sendingQueueOptions' => ['meta' => ['campaignId' => '1', 'orderedProductCategories' => ['123']]]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_AUTOMATION, Carbon::now()->subDays(8), [], ['sendingQueueOptions' => ['meta' => ['campaignId' => '2', 'orderedProductCategories' => ['1234']]]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_AUTOMATION, Carbon::now()->subDays(89), [], ['sendingQueueOptions' => ['meta' => ['campaignId' => '3', 'orderedProductCategories' => ['1235']]]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_AUTOMATION, Carbon::now()->subMonths(2), [], ['sendingQueueOptions' => ['meta' => ['campaignId' => '3', 'orderedProductCategories' => ['1235']]]]);
$processed = $this->reporter->getData();
@ -218,15 +218,15 @@ class ReporterTest extends \MailPoetTest {
$dynamicSegment = (new Segment())->withType(SegmentEntity::TYPE_DYNAMIC)->create();
$this->createSentNewsletter(NewsletterEntity::TYPE_STANDARD, Carbon::now()->subDays(2), [$defaultSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '1']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_STANDARD, Carbon::now()->subDays(8), [$defaultSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '2']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_STANDARD, Carbon::now()->subDays(89), [$defaultSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '3']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_STANDARD, Carbon::now()->subMonths(2), [$defaultSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '3']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_RE_ENGAGEMENT, Carbon::now()->subDays(2), [$defaultSegment, $dynamicSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '4']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_RE_ENGAGEMENT, Carbon::now()->subDays(8), [$defaultSegment, $dynamicSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '5']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_RE_ENGAGEMENT, Carbon::now()->subDays(89), [$defaultSegment, $dynamicSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '6']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_RE_ENGAGEMENT, Carbon::now()->subMonths(2), [$defaultSegment, $dynamicSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '6']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_STANDARD, Carbon::now()->subDays(2), [$defaultSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '7', 'filterSegment' => ['theDataDoesNot' => 'matter']]]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_STANDARD, Carbon::now()->subDays(8), [$defaultSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '8', 'filterSegment' => ['theDataDoesNot' => 'matter']]]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_STANDARD, Carbon::now()->subDays(89), [$defaultSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '9', 'filterSegment' => ['theDataDoesNot' => 'matter']]]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_STANDARD, Carbon::now()->subMonths(2), [$defaultSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '9', 'filterSegment' => ['theDataDoesNot' => 'matter']]]]);
$processed = $this->reporter->getData();
$this->assertEquals(3, $processed['Number of campaigns sent in the last 7 days']);
@ -244,7 +244,7 @@ class ReporterTest extends \MailPoetTest {
public function testItDoesNotDoubleCountDuplicateCampaignIds(): void {
$defaultSegment = (new Segment())->withType(SegmentEntity::TYPE_DEFAULT)->create();
$this->createSentNewsletter(NewsletterEntity::TYPE_NOTIFICATION_HISTORY, Carbon::now()->subDays(89), [$defaultSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '1']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_NOTIFICATION_HISTORY, Carbon::now()->subMonths(2), [$defaultSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '1']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_NOTIFICATION_HISTORY, Carbon::now()->subDays(8), [$defaultSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '1']]]);
$this->createSentNewsletter(NewsletterEntity::TYPE_NOTIFICATION_HISTORY, Carbon::now()->subDays(2), [$defaultSegment], ['sendingQueueOptions' => ['meta' => ['campaignId' => '1']]]);
$processed = $this->reporter->getData();

View File

@ -0,0 +1,150 @@
<?php declare(strict_types = 1);
namespace integration\Migrations\App;
use DateTimeImmutable;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Migrations\App\Migration_20250501_114655_App;
use MailPoet\Test\DataFactories\Newsletter as NewsletterFactory;
use MailPoet\Test\DataFactories\NewsletterLink as NewsletterLinkFactory;
use MailPoet\Test\DataFactories\Segment as SegmentFactory;
use MailPoet\Test\DataFactories\StatisticsClicks;
use MailPoet\Test\DataFactories\StatisticsUnsubscribes;
use MailPoet\Test\DataFactories\Subscriber;
use MailPoet\Test\DataFactories\SubscriberSegment as SubscriberSegmentFactory;
// phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps
class Migration_20250501_114655_App_Test extends \MailPoetTest {
/** @var Migration_20250501_114655_App */
private $migration;
public function _before() {
parent::_before();
$this->migration = new Migration_20250501_114655_App($this->diContainer);
}
public function testItPausesInvalidTasksWithUnprocessedSubscribers(): void {
$subscriberFactory = new Subscriber();
$subscriberUnsubscribedBefore = $subscriberFactory->withEmail('subscriber1@example.com')
->withCreatedAt(new DateTimeImmutable('2024-12-01 10:00:00'))
->withStatus(SubscriberEntity::STATUS_UNSUBSCRIBED)
->create();
$subscriberUnsubscribedByBot = $subscriberFactory->withEmail('subscriber2@example.com')
->withCreatedAt(new DateTimeImmutable('2024-12-01 10:00:00'))
->withStatus(SubscriberEntity::STATUS_UNSUBSCRIBED)
->create();
$subscriberUnsubscribedByUserManyClicks = $subscriberFactory->withEmail('subscriber3@example.com')
->withCreatedAt(new DateTimeImmutable('2024-12-01 10:00:00'))
->withStatus(SubscriberEntity::STATUS_UNSUBSCRIBED)
->create();
$subscriberUnsubscribedByUserSingleClick = $subscriberFactory->withEmail('subscriber4@example.com')
->withCreatedAt(new DateTimeImmutable('2024-12-01 10:00:00'))
->withStatus(SubscriberEntity::STATUS_UNSUBSCRIBED)
->create();
$newsletterFactory = new NewsletterFactory();
$newsletter = $newsletterFactory->withType(NewsletterEntity::TYPE_STANDARD)
->withSubject('Test Newsletter')
->withSendingQueue()
->withStatus(NewsletterEntity::STATUS_SENT)
->create();
$newsletterLinkFactory = new NewsletterLinkFactory($newsletter);
$newsletterLink = $newsletterLinkFactory
->withUrl('https://example.com/test')
->create();
$newsletterLink2 = $newsletterLinkFactory
->withUrl('https://example.com/test2')
->create();
$newsletterLink3 = $newsletterLinkFactory
->withUrl('https://example.com/test3')
->create();
// Create a test segment
$segment = (new SegmentFactory())->create();
// Create subscriber segments for each subscriber
$subscriberSegmentFactory = new SubscriberSegmentFactory($subscriberUnsubscribedBefore, $segment);
$subscriberSegmentBefore = $subscriberSegmentFactory
->withStatus(SubscriberEntity::STATUS_UNSUBSCRIBED)
->withUpdatedAt(new DateTimeImmutable('2024-12-01 10:00:00'))
->create();
$subscriberSegmentFactory = new SubscriberSegmentFactory($subscriberUnsubscribedByBot, $segment);
$subscriberSegmentByBot = $subscriberSegmentFactory
->withStatus(SubscriberEntity::STATUS_UNSUBSCRIBED)
->withUpdatedAt(new DateTimeImmutable('2025-04-01 10:00:03'))
->create();
$subscriberSegmentFactory = new SubscriberSegmentFactory($subscriberUnsubscribedByUserManyClicks, $segment);
$subscriberSegmentByUserManyClicks = $subscriberSegmentFactory
->withStatus(SubscriberEntity::STATUS_UNSUBSCRIBED)
->withUpdatedAt(new DateTimeImmutable('2025-04-01 10:00:55'))
->create();
$subscriberSegmentFactory = new SubscriberSegmentFactory($subscriberUnsubscribedByUserSingleClick, $segment);
$subscriberSegmentByUserSingleClick = $subscriberSegmentFactory
->withStatus(SubscriberEntity::STATUS_UNSUBSCRIBED)
->withUpdatedAt(new DateTimeImmutable('2025-04-01 10:00:00'))
->create();
// $subscriberUnsubscribedBefore Has many suspicious clicks but unsubscribed before the issue
$subscriber1ClickFactory = new StatisticsClicks($newsletterLink, $subscriberUnsubscribedBefore);
$subscriber1ClickFactory->withCreatedAt(new DateTimeImmutable('2024-12-01 10:00:00'))->create();
$subscriber1ClickFactory = new StatisticsClicks($newsletterLink2, $subscriberUnsubscribedBefore);
$subscriber1ClickFactory->withCreatedAt(new DateTimeImmutable('2024-12-01 10:00:00'))->create();
$subscriber1ClickFactory = new StatisticsClicks($newsletterLink3, $subscriberUnsubscribedBefore);
$subscriber1ClickFactory->withCreatedAt(new DateTimeImmutable('2024-12-01 10:00:00'))->create();
$subscriber1UnsubscribeFactory = new StatisticsUnsubscribes($newsletter, $subscriberUnsubscribedBefore);
$subscriber1UnsubscribeFactory->withCreatedAt(new DateTimeImmutable('2024-12-01 10:00:00'))->create();
// $subscriberUnsubscribedByBot Has many suspicious clicks but and unsubscribed after the issue
$subscriber2ClickFactory = new StatisticsClicks($newsletterLink, $subscriberUnsubscribedByBot);
$subscriber2ClickFactory->withCreatedAt(new DateTimeImmutable('2025-04-01 10:00:00'))->create();
$subscriber2ClickFactory = new StatisticsClicks($newsletterLink2, $subscriberUnsubscribedByBot);
$subscriber2ClickFactory->withCreatedAt(new DateTimeImmutable('2025-04-01 10:00:02'))->create();
$subscriber2ClickFactory = new StatisticsClicks($newsletterLink3, $subscriberUnsubscribedByBot);
$subscriber2ClickFactory->withCreatedAt(new DateTimeImmutable('2025-04-01 10:00:05'))->create();
$subscriber2UnsubscribeFactory = new StatisticsUnsubscribes($newsletter, $subscriberUnsubscribedByBot);
$subscriber2UnsubscribeFactory->withCreatedAt(new DateTimeImmutable('2025-04-01 10:00:03'))->create();
// $subscriberUnsubscribedByUserManyClicks Has many clicks but they are spread in time so it's not suspicious
$subscriber3ClickFactory = new StatisticsClicks($newsletterLink, $subscriberUnsubscribedByUserManyClicks);
$subscriber3ClickFactory->withCreatedAt(new DateTimeImmutable('2025-04-01 10:00:00'))->create();
$subscriber3ClickFactory = new StatisticsClicks($newsletterLink2, $subscriberUnsubscribedByUserManyClicks);
$subscriber3ClickFactory->withCreatedAt(new DateTimeImmutable('2025-04-01 10:00:30'))->create();
$subscriber3ClickFactory = new StatisticsClicks($newsletterLink3, $subscriberUnsubscribedByUserManyClicks);
$subscriber3ClickFactory->withCreatedAt(new DateTimeImmutable('2025-04-01 10:00:50'))->create();
$subscriber3UnsubscribeFactory = new StatisticsUnsubscribes($newsletter, $subscriberUnsubscribedByUserManyClicks);
$subscriber3UnsubscribeFactory->withCreatedAt(new DateTimeImmutable('2025-04-01 10:00:55'))->create();
// $subscriberUnsubscribedByUserSingleClick Has one click and unsubscribed after the issue
$subscriber4ClickFactory = new StatisticsClicks($newsletterLink, $subscriberUnsubscribedByUserSingleClick);
$subscriber4ClickFactory->withCreatedAt(new DateTimeImmutable('2025-04-01 10:00:00'))->create();
$subscriber4UnsubscribeFactory = new StatisticsUnsubscribes($newsletter, $subscriberUnsubscribedByUserSingleClick);
$subscriber4UnsubscribeFactory->withCreatedAt(new DateTimeImmutable('2025-04-01 10:00:00'))->create();
$this->migration->run();
$this->entityManager->refresh($subscriberUnsubscribedBefore);
$this->entityManager->refresh($subscriberUnsubscribedByBot);
$this->entityManager->refresh($subscriberUnsubscribedByUserManyClicks);
$this->entityManager->refresh($subscriberUnsubscribedByUserSingleClick);
$this->entityManager->refresh($subscriberSegmentBefore);
$this->entityManager->refresh($subscriberSegmentByBot);
$this->entityManager->refresh($subscriberSegmentByUserManyClicks);
$this->entityManager->refresh($subscriberSegmentByUserSingleClick);
$this->assertEquals(SubscriberEntity::STATUS_UNSUBSCRIBED, $subscriberUnsubscribedBefore->getStatus());
$this->assertEquals(SubscriberEntity::STATUS_SUBSCRIBED, $subscriberUnsubscribedByBot->getStatus());
$this->assertEquals(SubscriberEntity::STATUS_UNSUBSCRIBED, $subscriberUnsubscribedByUserManyClicks->getStatus());
$this->assertEquals(SubscriberEntity::STATUS_UNSUBSCRIBED, $subscriberUnsubscribedByUserSingleClick->getStatus());
$this->assertEquals(SubscriberEntity::STATUS_UNSUBSCRIBED, $subscriberSegmentBefore->getStatus());
$this->assertEquals(SubscriberEntity::STATUS_SUBSCRIBED, $subscriberSegmentByBot->getStatus());
$this->assertEquals(SubscriberEntity::STATUS_UNSUBSCRIBED, $subscriberSegmentByUserManyClicks->getStatus());
$this->assertEquals(SubscriberEntity::STATUS_UNSUBSCRIBED, $subscriberSegmentByUserSingleClick->getStatus());
}
}

View File

@ -34,6 +34,7 @@
"d3-color@<3.1.0": ">=3.1.0",
"debug@>=4.0.0 <4.3.1": ">=4.3.1",
"decode-uri-component@<0.2.1": ">=0.2.1",
"http-proxy-middleware": ">=2.0.9",
"json5@<1.0.2": ">=1.0.2",
"path-to-regexp@<0.1.12": "0.1.12",
"react": "18.3.1",

43
pnpm-lock.yaml generated
View File

@ -13,6 +13,7 @@ overrides:
d3-color@<3.1.0: '>=3.1.0'
debug@>=4.0.0 <4.3.1: '>=4.3.1'
decode-uri-component@<0.2.1: '>=0.2.1'
http-proxy-middleware: '>=2.0.9'
json5@<1.0.2: '>=1.0.2'
path-to-regexp@<0.1.12: 0.1.12
react: 18.3.1
@ -8287,7 +8288,7 @@ packages:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'}
dependencies:
debug: 4.3.4
debug: 4.4.0(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
dev: true
@ -8684,7 +8685,7 @@ packages:
/axios@1.7.4:
resolution: {integrity: sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==}
dependencies:
follow-redirects: 1.15.6
follow-redirects: 1.15.6(debug@4.4.0)
form-data: 4.0.0
proxy-from-env: 1.1.0
transitivePeerDependencies:
@ -12095,7 +12096,7 @@ packages:
resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==}
dev: true
/follow-redirects@1.15.6:
/follow-redirects@1.15.6(debug@4.4.0):
resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==}
engines: {node: '>=4.0'}
peerDependencies:
@ -12103,6 +12104,8 @@ packages:
peerDependenciesMeta:
debug:
optional: true
dependencies:
debug: 4.4.0(supports-color@8.1.1)
dev: true
/for-each@0.3.3:
@ -12380,7 +12383,7 @@ packages:
dependencies:
basic-ftp: 5.0.5
data-uri-to-buffer: 6.0.2
debug: 4.3.4
debug: 4.4.0(supports-color@8.1.1)
fs-extra: 11.2.0
transitivePeerDependencies:
- supports-color
@ -12910,31 +12913,26 @@ packages:
- supports-color
dev: true
/http-proxy-middleware@2.0.6(@types/express@4.17.21):
resolution: {integrity: sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==}
engines: {node: '>=12.0.0'}
peerDependencies:
'@types/express': ^4.17.13
peerDependenciesMeta:
'@types/express':
optional: true
/http-proxy-middleware@3.0.5:
resolution: {integrity: sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
'@types/express': 4.17.21
'@types/http-proxy': 1.17.15
http-proxy: 1.18.1
debug: 4.4.0(supports-color@8.1.1)
http-proxy: 1.18.1(debug@4.4.0)
is-glob: 4.0.3
is-plain-obj: 3.0.0
is-plain-object: 5.0.0
micromatch: 4.0.8
transitivePeerDependencies:
- debug
- supports-color
dev: true
/http-proxy@1.18.1:
/http-proxy@1.18.1(debug@4.4.0):
resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==}
engines: {node: '>=8.0.0'}
dependencies:
eventemitter3: 4.0.7
follow-redirects: 1.15.6
follow-redirects: 1.15.6(debug@4.4.0)
requires-port: 1.0.0
transitivePeerDependencies:
- debug
@ -15567,7 +15565,7 @@ packages:
dependencies:
'@tootallnate/quickjs-emscripten': 0.23.0
agent-base: 7.1.1
debug: 4.3.4
debug: 4.4.0(supports-color@8.1.1)
get-uri: 6.0.3
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.5
@ -16443,7 +16441,7 @@ packages:
engines: {node: '>= 14'}
dependencies:
agent-base: 7.1.1
debug: 4.3.4
debug: 4.4.0(supports-color@8.1.1)
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.5
lru-cache: 7.18.3
@ -17920,7 +17918,7 @@ packages:
engines: {node: '>= 14'}
dependencies:
agent-base: 7.1.1
debug: 4.3.4
debug: 4.4.0(supports-color@8.1.1)
socks: 2.8.3
transitivePeerDependencies:
- supports-color
@ -19503,7 +19501,7 @@ packages:
express: 4.21.0
graceful-fs: 4.2.11
html-entities: 2.5.2
http-proxy-middleware: 2.0.6(@types/express@4.17.21)
http-proxy-middleware: 3.0.5
ipaddr.js: 2.2.0
launch-editor: 2.8.1
open: 8.4.2
@ -19520,7 +19518,6 @@ packages:
ws: 8.18.0
transitivePeerDependencies:
- bufferutil
- debug
- supports-color
- utf-8-validate
dev: true

View File

@ -75,7 +75,7 @@ services:
- mailhog-data:/mailhog-data
wordpress:
image: wordpress:${WORDPRESS_IMAGE_VERSION:-6.8.0-php8.3}
image: wordpress:${WORDPRESS_IMAGE_VERSION:-6.8.1-php8.3}
container_name: wordpress_${CIRCLE_NODE_INDEX:-default}
depends_on:
smtp: