diff --git a/mailpoet/assets/js/src/automation/integrations/woocommerce/index.tsx b/mailpoet/assets/js/src/automation/integrations/woocommerce/index.tsx
index 4520d1a4c8..0cb5f83a05 100644
--- a/mailpoet/assets/js/src/automation/integrations/woocommerce/index.tsx
+++ b/mailpoet/assets/js/src/automation/integrations/woocommerce/index.tsx
@@ -1,5 +1,8 @@
import { registerStepType } from '../../editor/store';
import { step as OrderStatusChanged } from './steps/order-status-changed';
+import { step as OrderCompletedTrigger } from './steps/order-completed';
+import { step as OrderCancelledTrigger } from './steps/order-cancelled';
+import { step as OrderCreatedTrigger } from './steps/order-created';
import { step as AbandonedCartTrigger } from './steps/abandoned-cart';
import { MailPoet } from '../../../mailpoet';
import { step as BuysAProductTrigger } from './steps/buys-a-product';
@@ -12,6 +15,9 @@ export const initialize = (): void => {
return;
}
registerStepType(OrderStatusChanged);
+ registerStepType(OrderCompletedTrigger);
+ registerStepType(OrderCancelledTrigger);
+ registerStepType(OrderCreatedTrigger);
registerStepType(AbandonedCartTrigger);
registerStepType(BuysAProductTrigger);
registerStepType(BuysFromACategory);
diff --git a/mailpoet/assets/js/src/automation/integrations/woocommerce/steps/order-cancelled/icon.tsx b/mailpoet/assets/js/src/automation/integrations/woocommerce/steps/order-cancelled/icon.tsx
new file mode 100644
index 0000000000..02b52dc60b
--- /dev/null
+++ b/mailpoet/assets/js/src/automation/integrations/woocommerce/steps/order-cancelled/icon.tsx
@@ -0,0 +1,36 @@
+export function Icon(): JSX.Element {
+ return (
+
+ );
+}
diff --git a/mailpoet/assets/js/src/automation/integrations/woocommerce/steps/order-cancelled/index.tsx b/mailpoet/assets/js/src/automation/integrations/woocommerce/steps/order-cancelled/index.tsx
new file mode 100644
index 0000000000..ea52a7a854
--- /dev/null
+++ b/mailpoet/assets/js/src/automation/integrations/woocommerce/steps/order-cancelled/index.tsx
@@ -0,0 +1,18 @@
+import { __, _x } from '@wordpress/i18n';
+import { StepType } from '../../../../editor/store';
+import { Icon } from './icon';
+
+const keywords = [__('woocommerce', 'mailpoet'), __('order', 'mailpoet')];
+export const step: StepType = {
+ key: 'woocommerce:order-cancelled',
+ group: 'triggers',
+ title: () => __('Order cancelled', 'mailpoet'),
+ description: () =>
+ __('Start the automation when an order is cancelled.', 'mailpoet'),
+ subtitle: () => _x('Trigger', 'noun', 'mailpoet'),
+ keywords,
+ foreground: '#2271b1',
+ background: '#f0f6fc',
+ icon: () => ,
+ edit: () => null,
+} as const;
diff --git a/mailpoet/assets/js/src/automation/integrations/woocommerce/steps/order-completed/icon.tsx b/mailpoet/assets/js/src/automation/integrations/woocommerce/steps/order-completed/icon.tsx
new file mode 100644
index 0000000000..34b4de906c
--- /dev/null
+++ b/mailpoet/assets/js/src/automation/integrations/woocommerce/steps/order-completed/icon.tsx
@@ -0,0 +1,36 @@
+export function Icon(): JSX.Element {
+ return (
+
+ );
+}
diff --git a/mailpoet/assets/js/src/automation/integrations/woocommerce/steps/order-completed/index.tsx b/mailpoet/assets/js/src/automation/integrations/woocommerce/steps/order-completed/index.tsx
new file mode 100644
index 0000000000..3c28ae90bb
--- /dev/null
+++ b/mailpoet/assets/js/src/automation/integrations/woocommerce/steps/order-completed/index.tsx
@@ -0,0 +1,18 @@
+import { __, _x } from '@wordpress/i18n';
+import { StepType } from '../../../../editor/store';
+import { Icon } from './icon';
+
+const keywords = [__('woocommerce', 'mailpoet'), __('order', 'mailpoet')];
+export const step: StepType = {
+ key: 'woocommerce:order-completed',
+ group: 'triggers',
+ title: () => __('Order completed', 'mailpoet'),
+ description: () =>
+ __('Start the automation when an order is completed.', 'mailpoet'),
+ subtitle: () => _x('Trigger', 'noun', 'mailpoet'),
+ keywords,
+ foreground: '#2271b1',
+ background: '#f0f6fc',
+ icon: () => ,
+ edit: () => null,
+} as const;
diff --git a/mailpoet/assets/js/src/automation/integrations/woocommerce/steps/order-created/icon.tsx b/mailpoet/assets/js/src/automation/integrations/woocommerce/steps/order-created/icon.tsx
new file mode 100644
index 0000000000..6e452fc6ab
--- /dev/null
+++ b/mailpoet/assets/js/src/automation/integrations/woocommerce/steps/order-created/icon.tsx
@@ -0,0 +1,16 @@
+export function Icon(): JSX.Element {
+ return (
+
+ );
+}
diff --git a/mailpoet/assets/js/src/automation/integrations/woocommerce/steps/order-created/index.tsx b/mailpoet/assets/js/src/automation/integrations/woocommerce/steps/order-created/index.tsx
new file mode 100644
index 0000000000..6966d88c0e
--- /dev/null
+++ b/mailpoet/assets/js/src/automation/integrations/woocommerce/steps/order-created/index.tsx
@@ -0,0 +1,22 @@
+import { __, _x } from '@wordpress/i18n';
+import { StepType } from '../../../../editor/store';
+import { Icon } from './icon';
+
+const keywords = [
+ __('woocommerce', 'mailpoet'),
+ __('order', 'mailpoet'),
+ __('new', 'mailpoet'),
+];
+export const step: StepType = {
+ key: 'woocommerce:order-created',
+ group: 'triggers',
+ title: () => __('Order created', 'mailpoet'),
+ description: () =>
+ __('Start the automation when an order is created.', 'mailpoet'),
+ subtitle: () => _x('Trigger', 'noun', 'mailpoet'),
+ keywords,
+ foreground: '#2271b1',
+ background: '#f0f6fc',
+ icon: () => ,
+ edit: () => null,
+} as const;
diff --git a/mailpoet/lib/Automation/Engine/WordPress.php b/mailpoet/lib/Automation/Engine/WordPress.php
index 909b82af4d..6cb815b6db 100644
--- a/mailpoet/lib/Automation/Engine/WordPress.php
+++ b/mailpoet/lib/Automation/Engine/WordPress.php
@@ -21,6 +21,10 @@ class WordPress {
return add_action($hookName, $callback, $priority, $acceptedArgs);
}
+ public function removeAction(string $hookName, callable $callback, int $priority = 10): bool {
+ return remove_action($hookName, $callback, $priority);
+ }
+
/** @param mixed ...$arg */
public function doAction(string $hookName, ...$arg): void {
do_action($hookName, ...$arg);
diff --git a/mailpoet/lib/Automation/Integrations/WooCommerce/Triggers/Orders/OrderCancelledTrigger.php b/mailpoet/lib/Automation/Integrations/WooCommerce/Triggers/Orders/OrderCancelledTrigger.php
new file mode 100644
index 0000000000..3676eacbd3
--- /dev/null
+++ b/mailpoet/lib/Automation/Integrations/WooCommerce/Triggers/Orders/OrderCancelledTrigger.php
@@ -0,0 +1,28 @@
+getSinglePayloadByClass(OrderStatusChangePayload::class);
+ return $orderPayload->getTo() === 'cancelled';
+ }
+
+ public function getArgsSchema(): ObjectSchema {
+ return Builder::object();
+ }
+}
diff --git a/mailpoet/lib/Automation/Integrations/WooCommerce/Triggers/Orders/OrderCompletedTrigger.php b/mailpoet/lib/Automation/Integrations/WooCommerce/Triggers/Orders/OrderCompletedTrigger.php
new file mode 100644
index 0000000000..99b6db9a92
--- /dev/null
+++ b/mailpoet/lib/Automation/Integrations/WooCommerce/Triggers/Orders/OrderCompletedTrigger.php
@@ -0,0 +1,28 @@
+getSinglePayloadByClass(OrderStatusChangePayload::class);
+ return $orderPayload->getTo() === 'completed';
+ }
+
+ public function getArgsSchema(): ObjectSchema {
+ return Builder::object();
+ }
+}
diff --git a/mailpoet/lib/Automation/Integrations/WooCommerce/Triggers/Orders/OrderCreatedTrigger.php b/mailpoet/lib/Automation/Integrations/WooCommerce/Triggers/Orders/OrderCreatedTrigger.php
new file mode 100644
index 0000000000..057f83bce0
--- /dev/null
+++ b/mailpoet/lib/Automation/Integrations/WooCommerce/Triggers/Orders/OrderCreatedTrigger.php
@@ -0,0 +1,100 @@
+wp = $wp;
+ }
+
+ public function getKey(): string {
+ return 'woocommerce:order-created';
+ }
+
+ public function getName(): string {
+ return __('Order created', 'mailpoet');
+ }
+
+ public function registerHooks(): void {
+ $this->wp->addAction(
+ 'woocommerce_new_order',
+ [
+ $this,
+ 'handleCreate',
+ ],
+ 10,
+ 2
+ );
+ }
+
+ /**
+ * @param int $orderId
+ * @param \WC_Order $order
+ * @return void
+ */
+ public function handleCreate($orderId, $order) {
+
+ if (in_array($orderId, $this->processedOrders)) {
+ return;
+ }
+ /**
+ * Creating an order via wc_create_order() does not yet set crucial information like the customer's email address.
+ * It just creates the order object and saves it to the database. We need therefore to wait for the order to have at least the billing address stored.
+ **/
+ if (!$order->get_billing_email()) {
+ add_action(
+ 'woocommerce_after_order_object_save',
+ function($order) use ($orderId) {
+ if ((int)$orderId !== (int)$order->get_id()) {
+ return;
+ }
+ $this->handleCreate($order->get_id(), $order);
+ }
+ );
+ return;
+ }
+ $this->processedOrders[] = $orderId;
+ $this->wp->doAction(Hooks::TRIGGER, $this, [
+ new Subject(OrderSubject::KEY, ['order_id' => $order->get_id()]),
+ new Subject(CustomerSubject::KEY, ['customer_id' => $order->get_customer_id()]),
+ ]);
+ }
+
+ public function isTriggeredBy(StepRunArgs $args): bool {
+ return true;
+ }
+
+ public function getArgsSchema(): ObjectSchema {
+ return Builder::object();
+ }
+
+ public function getSubjectKeys(): array {
+ return [
+ OrderSubject::KEY,
+ CustomerSubject::KEY,
+ ];
+ }
+
+ public function validate(StepValidationArgs $args): void {
+ }
+}
diff --git a/mailpoet/lib/Automation/Integrations/WooCommerce/Triggers/Orders/OrderStatusChangedTrigger.php b/mailpoet/lib/Automation/Integrations/WooCommerce/Triggers/Orders/OrderStatusChangedTrigger.php
index 9376ef9dfc..cb57361ce4 100644
--- a/mailpoet/lib/Automation/Integrations/WooCommerce/Triggers/Orders/OrderStatusChangedTrigger.php
+++ b/mailpoet/lib/Automation/Integrations/WooCommerce/Triggers/Orders/OrderStatusChangedTrigger.php
@@ -19,10 +19,10 @@ use MailPoet\WP\Functions;
class OrderStatusChangedTrigger implements Trigger {
/** @var Functions */
- private $wp;
+ protected $wp;
/** @var WooCommerce */
- private $woocommerce;
+ protected $woocommerce;
public function __construct(
Functions $wp,
diff --git a/mailpoet/lib/Automation/Integrations/WooCommerce/WooCommerceIntegration.php b/mailpoet/lib/Automation/Integrations/WooCommerce/WooCommerceIntegration.php
index 95b9ccb6c1..4fdb935ed6 100644
--- a/mailpoet/lib/Automation/Integrations/WooCommerce/WooCommerceIntegration.php
+++ b/mailpoet/lib/Automation/Integrations/WooCommerce/WooCommerceIntegration.php
@@ -11,6 +11,9 @@ use MailPoet\Automation\Integrations\WooCommerce\SubjectTransformers\WordPressUs
use MailPoet\Automation\Integrations\WooCommerce\Triggers\AbandonedCart\AbandonedCartTrigger;
use MailPoet\Automation\Integrations\WooCommerce\Triggers\BuysAProductTrigger;
use MailPoet\Automation\Integrations\WooCommerce\Triggers\BuysFromACategoryTrigger;
+use MailPoet\Automation\Integrations\WooCommerce\Triggers\Orders\OrderCancelledTrigger;
+use MailPoet\Automation\Integrations\WooCommerce\Triggers\Orders\OrderCompletedTrigger;
+use MailPoet\Automation\Integrations\WooCommerce\Triggers\Orders\OrderCreatedTrigger;
use MailPoet\Automation\Integrations\WooCommerce\Triggers\Orders\OrderStatusChangedTrigger;
class WooCommerceIntegration {
@@ -18,6 +21,14 @@ class WooCommerceIntegration {
/** @var OrderStatusChangedTrigger */
private $orderStatusChangedTrigger;
+ /** @var OrderCreatedTrigger */
+ private $orderCreatedTrigger;
+
+ /** @var OrderCompletedTrigger */
+ private $orderCompletedTrigger;
+
+ private $orderCancelledTrigger;
+
/** @var AbandonedCartTrigger */
private $abandonedCartTrigger;
@@ -50,6 +61,9 @@ class WooCommerceIntegration {
public function __construct(
OrderStatusChangedTrigger $orderStatusChangedTrigger,
+ OrderCreatedTrigger $orderCreatedTrigger,
+ OrderCompletedTrigger $orderCompletedTrigger,
+ OrderCancelledTrigger $orderCancelledTrigger,
AbandonedCartTrigger $abandonedCartTrigger,
BuysAProductTrigger $buysAProductTrigger,
BuysFromACategoryTrigger $buysFromACategoryTrigger,
@@ -62,6 +76,9 @@ class WooCommerceIntegration {
WooCommerce $wooCommerce
) {
$this->orderStatusChangedTrigger = $orderStatusChangedTrigger;
+ $this->orderCreatedTrigger = $orderCreatedTrigger;
+ $this->orderCompletedTrigger = $orderCompletedTrigger;
+ $this->orderCancelledTrigger = $orderCancelledTrigger;
$this->abandonedCartTrigger = $abandonedCartTrigger;
$this->buysAProductTrigger = $buysAProductTrigger;
$this->buysFromACategoryTrigger = $buysFromACategoryTrigger;
@@ -88,6 +105,9 @@ class WooCommerceIntegration {
$registry->addSubject($this->orderStatusChangeSubject);
$registry->addSubject($this->customerSubject);
$registry->addTrigger($this->orderStatusChangedTrigger);
+ $registry->addTrigger($this->orderCreatedTrigger);
+ $registry->addTrigger($this->orderCompletedTrigger);
+ $registry->addTrigger($this->orderCancelledTrigger);
$registry->addTrigger($this->abandonedCartTrigger);
$registry->addTrigger($this->buysAProductTrigger);
$registry->addTrigger($this->buysFromACategoryTrigger);
diff --git a/mailpoet/lib/DI/ContainerConfigurator.php b/mailpoet/lib/DI/ContainerConfigurator.php
index 67afef26eb..6f7a973cd3 100644
--- a/mailpoet/lib/DI/ContainerConfigurator.php
+++ b/mailpoet/lib/DI/ContainerConfigurator.php
@@ -208,6 +208,9 @@ class ContainerConfigurator implements IContainerConfigurator {
$container->autowire(\MailPoet\Automation\Integrations\WooCommerce\Fields\TermOptionsBuilder::class)->setPublic(true);
$container->autowire(\MailPoet\Automation\Integrations\WooCommerce\Fields\TermParentsLoader::class)->setPublic(true);
$container->autowire(\MailPoet\Automation\Integrations\WooCommerce\Triggers\Orders\OrderStatusChangedTrigger::class)->setPublic(true)->setShared(false);
+ $container->autowire(\MailPoet\Automation\Integrations\WooCommerce\Triggers\Orders\OrderCreatedTrigger::class)->setPublic(true)->setShared(false);
+ $container->autowire(\MailPoet\Automation\Integrations\WooCommerce\Triggers\Orders\OrderCompletedTrigger::class)->setPublic(true)->setShared(false);
+ $container->autowire(\MailPoet\Automation\Integrations\WooCommerce\Triggers\Orders\OrderCancelledTrigger::class)->setPublic(true)->setShared(false);
$container->autowire(\MailPoet\Automation\Integrations\WooCommerce\Triggers\BuysAProductTrigger::class)->setPublic(true)->setShared(false);
$container->autowire(\MailPoet\Automation\Integrations\WooCommerce\Triggers\BuysFromACategoryTrigger::class)->setPublic(true)->setShared(false);
$container->autowire(\MailPoet\Automation\Integrations\WooCommerce\Subjects\OrderSubject::class)->setPublic(true)->setShared(false);
diff --git a/mailpoet/tests/integration/Automation/Integrations/WooCommerce/Triggers/Orders/AbstractOrderSingleStatusTest.php b/mailpoet/tests/integration/Automation/Integrations/WooCommerce/Triggers/Orders/AbstractOrderSingleStatusTest.php
new file mode 100644
index 0000000000..7ee7f50c6f
--- /dev/null
+++ b/mailpoet/tests/integration/Automation/Integrations/WooCommerce/Triggers/Orders/AbstractOrderSingleStatusTest.php
@@ -0,0 +1,57 @@
+createMock(OrderStatusChangePayload::class);
+ $statusChangePayload->method('getFrom')->willReturn($from);
+ $statusChangePayload->method('getTo')->willReturn($to);
+ $stepRunArgs = $this->createMock(StepRunArgs::class);
+ $stepRunArgs->method('getSinglePayloadByClass')->willReturn($statusChangePayload);
+ $this->assertEquals($expected, $this->getTestee()->isTriggeredBy($stepRunArgs));
+ }
+
+ public function dataTestIsTriggeredBy() {
+ $statuses = [
+ 'pending',
+ 'processing',
+ 'on-hold',
+ 'completed',
+ 'cancelled',
+ 'refunded',
+ 'failed',
+ ];
+
+ $data = [];
+ foreach ($statuses as $from) {
+ foreach ($statuses as $to) {
+ if ($from === $to) {
+ continue;
+ }
+ $data[sprintf('from_%s_to_%s', $from, $to)] = [
+ 'from' => $from,
+ 'to' => $to,
+ 'expected' => $to === $this->getTriggerStatus(),
+ ];
+ }
+ }
+ return $data;
+ }
+}
diff --git a/mailpoet/tests/integration/Automation/Integrations/WooCommerce/Triggers/Orders/OrderCancelledTriggerTest.php b/mailpoet/tests/integration/Automation/Integrations/WooCommerce/Triggers/Orders/OrderCancelledTriggerTest.php
new file mode 100644
index 0000000000..472032089b
--- /dev/null
+++ b/mailpoet/tests/integration/Automation/Integrations/WooCommerce/Triggers/Orders/OrderCancelledTriggerTest.php
@@ -0,0 +1,19 @@
+diContainer->get(OrderCancelledTrigger::class);
+ }
+
+ protected function getTriggerStatus(): string {
+ return 'cancelled';
+ }
+}
diff --git a/mailpoet/tests/integration/Automation/Integrations/WooCommerce/Triggers/Orders/OrderCompletedTriggerTest.php b/mailpoet/tests/integration/Automation/Integrations/WooCommerce/Triggers/Orders/OrderCompletedTriggerTest.php
new file mode 100644
index 0000000000..a5b5ffc57e
--- /dev/null
+++ b/mailpoet/tests/integration/Automation/Integrations/WooCommerce/Triggers/Orders/OrderCompletedTriggerTest.php
@@ -0,0 +1,19 @@
+diContainer->get(OrderCompletedTrigger::class);
+ }
+
+ protected function getTriggerStatus(): string {
+ return 'completed';
+ }
+}
diff --git a/mailpoet/tests/integration/Automation/Integrations/WooCommerce/Triggers/Orders/OrderCreatedTriggerTest.php b/mailpoet/tests/integration/Automation/Integrations/WooCommerce/Triggers/Orders/OrderCreatedTriggerTest.php
new file mode 100644
index 0000000000..4df1acd25f
--- /dev/null
+++ b/mailpoet/tests/integration/Automation/Integrations/WooCommerce/Triggers/Orders/OrderCreatedTriggerTest.php
@@ -0,0 +1,71 @@
+testee = $this->diContainer->get(OrderCreatedTrigger::class);
+ $this->runStorage = $this->diContainer->get(AutomationRunStorage::class);
+ }
+
+ public function testCreatesRunWhenANewOrderIsCreated() {
+ $orderCreatedTriggerStep = new Step(
+ uniqid(),
+ Step::TYPE_TRIGGER,
+ $this->testee->getKey(),
+ [],
+ []
+ );
+ $automation = (new AutomationFactory())
+ ->withStep($orderCreatedTriggerStep)
+ ->withDelayAction()
+ ->withStatusActive()
+ ->create();
+ $this->testee->registerHooks();
+
+ $this->assertEmpty($this->runStorage->getAutomationRunsForAutomation($automation));
+
+ $order = wc_create_order(['customer_id' => 1]);
+ $this->assertInstanceOf(\WC_Order::class, $order);
+
+ $runs = $this->runStorage->getAutomationRunsForAutomation($automation);
+ $this->assertCount(1, $runs);
+ /** @var AutomationRun $run */
+ $run = current($runs);
+
+ $subject = $run->getSubjects(OrderSubject::KEY)[0];
+ $this->assertEquals($order->get_id(), $subject->getArgs()['order_id']);
+
+ // Test with no email address available yet.
+ $order = wc_create_order();
+ $this->assertInstanceOf(\WC_Order::class, $order);
+
+ $runs = $this->runStorage->getAutomationRunsForAutomation($automation);
+ $this->assertCount(1, $runs);
+ $order->set_billing_email('someone@example.com');
+ $order->save();
+ $runs = $this->runStorage->getAutomationRunsForAutomation($automation);
+ $this->assertCount(2, $runs);
+ $order->set_status('failed');
+ $order->save();
+ $runs = $this->runStorage->getAutomationRunsForAutomation($automation);
+ $this->assertCount(2, $runs);
+ }
+}
diff --git a/mailpoet/tests/integration/Automation/Integrations/WooCommerce/Triggers/Orders/OrderStatusChangeTriggerTest.php b/mailpoet/tests/integration/Automation/Integrations/WooCommerce/Triggers/Orders/OrderStatusChangeTriggerTest.php
index 0f4b6dbc10..d67c0e568d 100644
--- a/mailpoet/tests/integration/Automation/Integrations/WooCommerce/Triggers/Orders/OrderStatusChangeTriggerTest.php
+++ b/mailpoet/tests/integration/Automation/Integrations/WooCommerce/Triggers/Orders/OrderStatusChangeTriggerTest.php
@@ -6,6 +6,9 @@ use MailPoet\Automation\Engine\Data\StepRunArgs;
use MailPoet\Automation\Integrations\WooCommerce\Payloads\OrderStatusChangePayload;
use MailPoet\Automation\Integrations\WooCommerce\Triggers\Orders\OrderStatusChangedTrigger;
+/**
+ * @group woo
+ */
class OrderStatusChangeTriggerTest extends \MailPoetTest {
/**
* @dataProvider dataTestIsTriggeredBy