Compare commits

...

25 Commits

Author SHA1 Message Date
e674cfe30e Merge tag '5.12.1'
All checks were successful
Make release / release (push) Successful in 53m18s
2025-05-06 12:03:51 -05:00
ef31486d2e Release 5.12.1 2025-05-06 10:53:11 +02:00
03e10b96e9 Add changelog
[STOMAIL-7383]
2025-05-06 10:11:46 +02:00
54359dc610 Fix handling of Japanese characters in custom field blocks
[STOMAIL-7383]
2025-05-06 10:11:46 +02:00
5c7a746470 Add changelog
STOMAIL-7401
2025-05-05 21:49:38 +02:00
83033c7991 Fix listing action items overlaying with row border
STOMAIL-7401
2025-05-05 21:49:38 +02:00
b480fb510f Fix failing tests in May in years when February has 28 days
STOMAIL-7400
2025-05-05 19:17:25 +02:00
043d8d187b Add changelog
STOMAIL-7397
2025-05-05 15:07:00 +02:00
1faf5822a2 Update Migration_20230831_143755_Db to work properly with multi query
STOMAIL-7397
2025-05-05 15:07:00 +02:00
0898ed7b56 Update used Automate Woo plugin in Circle CI
- latest version: 6.1.11
 - previous version: 6.0.33
2025-05-05 13:37:22 +02:00
d5908a6fb9 Update used WooCommerce plugin in Circle CI
- latest version: 9.8.3
 - previous version: 9.7.1
2025-05-05 13:37:22 +02:00
b6ed846b8b Update used WordPress images in Circle CI
- latest version: 6.8.1-php8.3
 - previous version: 6.7.2
2025-05-05 13:37:22 +02:00
75cf32c211 Fix the link to the woo settings in the marketing optin block 2025-05-05 12:41:12 +02:00
80e0a92243 Declare Woo checkout opt-in block compatible with API v3 2025-05-05 12:41:12 +02:00
2ccaff11ae Update subscription form block to declare compatibility with api v3
This allows the Block editor to run in iframe mode.
The iframe mode is more performant and expected in QIT tests.
2025-05-05 12:41:12 +02:00
d98722a94a Split a long query into chunks
[STOMAIL-7399]
2025-05-05 10:39:42 +02:00
3b75c10670 Re-subscribe subscribers into the segments
when a bot unsubscribes subscriber
they are unsubscribed globaly and in segment
this adds them back

[STOMAIL-7399]
2025-05-05 10:39:42 +02:00
7dea7bbf00 Add a test to check Subscriber Segments
Since the bot clicks to an unsubscribe link

the subscriber is alos unsubscribed from the segments

[STOMAIL-7399]
2025-05-05 10:39:42 +02:00
f6c81b2583 Add a data factory for Subscriber Segments
[STOMAIL-7399]
2025-05-05 10:39:42 +02:00
8bc5c1b976 Refactor SQL query into two separate queries
[STOMAIL-7399]
2025-05-05 10:39:42 +02:00
2f77fc3912 Add migration to re-subscribe subscribers unsubscribed by bots
In the migration, we consider subscribers who made three and more clicks
and unsubscribed within 4 seconds before and 4 seconds after unsubscribe
as unsubscribed by a bot.

From research of affected sites, it seems that the issue started in late March or early April.
So we restrict the date to the start of March.
STOMAIL-7399
2025-05-05 10:39:42 +02:00
3d970898d7 Upgrade vulnerable dependencies http-proxy-middleware 2025-04-30 12:01:26 +02:00
ff3614821a Release 5.12.0 2025-04-30 07:43:38 +02:00
6acf7a5137 Release 5.12.0 2025-04-30 07:43:38 +02:00
e9127734b2 Release 5.12.0 2025-04-30 07:43:38 +02: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: