Add customer order fields
[MAILPOET-5168]
This commit is contained in:
@@ -6,117 +6,129 @@ use MailPoet\Automation\Engine\Data\Field;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Payloads\CustomerPayload;
|
||||
|
||||
class CustomerFieldsFactory {
|
||||
/** @var CustomerOrderFieldsFactory */
|
||||
private $customerOrderFieldsFactory;
|
||||
|
||||
public function __construct(
|
||||
CustomerOrderFieldsFactory $customerOrderFieldsFactory
|
||||
) {
|
||||
$this->customerOrderFieldsFactory = $customerOrderFieldsFactory;
|
||||
}
|
||||
|
||||
/** @return Field[] */
|
||||
public function getFields(): array {
|
||||
return [
|
||||
new Field(
|
||||
'woocommerce:customer:billing-company',
|
||||
Field::TYPE_STRING,
|
||||
__('Billing company', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
return $customer ? $customer->get_billing_company() : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:billing-phone',
|
||||
Field::TYPE_STRING,
|
||||
__('Billing phone', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
return $customer ? $customer->get_billing_phone() : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:billing-city',
|
||||
Field::TYPE_STRING,
|
||||
__('Billing city', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
return $customer ? $customer->get_billing_city() : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:billing-postcode',
|
||||
Field::TYPE_STRING,
|
||||
__('Billing postcode', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
return $customer ? $customer->get_billing_postcode() : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:billing-state',
|
||||
Field::TYPE_STRING,
|
||||
__('Billing state/county', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
return $customer ? $customer->get_billing_state() : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:billing-country',
|
||||
Field::TYPE_STRING,
|
||||
__('Billing country', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
return $customer ? $customer->get_billing_country() : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:shipping-company',
|
||||
Field::TYPE_STRING,
|
||||
__('Shipping company', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
return $customer ? $customer->get_shipping_company() : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:shipping-phone',
|
||||
Field::TYPE_STRING,
|
||||
__('Shipping phone', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
return $customer ? $customer->get_shipping_phone() : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:shipping-city',
|
||||
Field::TYPE_STRING,
|
||||
__('Shipping city', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
return $customer ? $customer->get_shipping_city() : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:shipping-postcode',
|
||||
Field::TYPE_STRING,
|
||||
__('Shipping postcode', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
return $customer ? $customer->get_shipping_postcode() : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:shipping-state',
|
||||
Field::TYPE_STRING,
|
||||
__('Shipping state/county', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
return $customer ? $customer->get_shipping_state() : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:shipping-country',
|
||||
Field::TYPE_STRING,
|
||||
__('Shipping country', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
return $customer ? $customer->get_shipping_country() : null;
|
||||
}
|
||||
),
|
||||
];
|
||||
return array_merge(
|
||||
[
|
||||
new Field(
|
||||
'woocommerce:customer:billing-company',
|
||||
Field::TYPE_STRING,
|
||||
__('Billing company', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
return $customer ? $customer->get_billing_company() : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:billing-phone',
|
||||
Field::TYPE_STRING,
|
||||
__('Billing phone', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
return $customer ? $customer->get_billing_phone() : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:billing-city',
|
||||
Field::TYPE_STRING,
|
||||
__('Billing city', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
return $customer ? $customer->get_billing_city() : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:billing-postcode',
|
||||
Field::TYPE_STRING,
|
||||
__('Billing postcode', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
return $customer ? $customer->get_billing_postcode() : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:billing-state',
|
||||
Field::TYPE_STRING,
|
||||
__('Billing state/county', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
return $customer ? $customer->get_billing_state() : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:billing-country',
|
||||
Field::TYPE_STRING,
|
||||
__('Billing country', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
return $customer ? $customer->get_billing_country() : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:shipping-company',
|
||||
Field::TYPE_STRING,
|
||||
__('Shipping company', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
return $customer ? $customer->get_shipping_company() : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:shipping-phone',
|
||||
Field::TYPE_STRING,
|
||||
__('Shipping phone', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
return $customer ? $customer->get_shipping_phone() : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:shipping-city',
|
||||
Field::TYPE_STRING,
|
||||
__('Shipping city', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
return $customer ? $customer->get_shipping_city() : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:shipping-postcode',
|
||||
Field::TYPE_STRING,
|
||||
__('Shipping postcode', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
return $customer ? $customer->get_shipping_postcode() : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:shipping-state',
|
||||
Field::TYPE_STRING,
|
||||
__('Shipping state/county', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
return $customer ? $customer->get_shipping_state() : null;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:shipping-country',
|
||||
Field::TYPE_STRING,
|
||||
__('Shipping country', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
return $customer ? $customer->get_shipping_country() : null;
|
||||
}
|
||||
),
|
||||
],
|
||||
$this->customerOrderFieldsFactory->getFields()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,117 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce\Fields;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
use MailPoet\Automation\Engine\Data\Field;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Payloads\CustomerPayload;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\WooCommerce;
|
||||
use WC_Customer;
|
||||
|
||||
class CustomerOrderFieldsFactory {
|
||||
/** @var WooCommerce */
|
||||
private $wooCommerce;
|
||||
|
||||
public function __construct(
|
||||
WooCommerce $wooCommerce
|
||||
) {
|
||||
$this->wooCommerce = $wooCommerce;
|
||||
}
|
||||
|
||||
/** @return Field[] */
|
||||
public function getFields(): array {
|
||||
return [
|
||||
new Field(
|
||||
'woocommerce:customer:spent-total',
|
||||
Field::TYPE_NUMBER,
|
||||
__('Total spent', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
return $customer ? (float)$customer->get_total_spent() : 0.0;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:spent-average',
|
||||
Field::TYPE_NUMBER,
|
||||
__('Average spent', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
$totalSpent = $customer ? (float)$customer->get_total_spent() : 0.0;
|
||||
$orderCount = $customer ? (int)$customer->get_order_count() : 0;
|
||||
return $orderCount > 0 ? ($totalSpent / $orderCount) : 0.0;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:order-count',
|
||||
Field::TYPE_INTEGER,
|
||||
__('Order count', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
return $customer ? $customer->get_order_count() : 0;
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:first-paid-order-date',
|
||||
Field::TYPE_DATETIME,
|
||||
__('First paid order date', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
if (!$customer) {
|
||||
return null;
|
||||
}
|
||||
return $this->getPaidOrderDate($customer, true);
|
||||
}
|
||||
),
|
||||
new Field(
|
||||
'woocommerce:customer:last-paid-order-date',
|
||||
Field::TYPE_DATETIME,
|
||||
__('Last paid order date', 'mailpoet'),
|
||||
function (CustomerPayload $payload) {
|
||||
$customer = $payload->getCustomer();
|
||||
if (!$customer) {
|
||||
return null;
|
||||
}
|
||||
return $this->getPaidOrderDate($customer, false);
|
||||
}
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private function getPaidOrderDate(WC_Customer $customer, bool $fetchFirst): ?DateTimeImmutable {
|
||||
$wpdb = $this->wordPress->getWpdb();
|
||||
$sorting = $fetchFirst ? 'ASC' : 'DESC';
|
||||
$statuses = array_map(function (string $status) {
|
||||
return "wc-$status";
|
||||
}, $this->wooCommerce->wcGetIsPaidStatuses());
|
||||
$statusesPlaceholder = implode(',', array_fill(0, count($statuses), '%s'));
|
||||
|
||||
if ($this->wooCommerce->isWooCommerceCustomOrdersTableEnabled()) {
|
||||
$statement = (string)$wpdb->prepare("
|
||||
SELECT o.date_created_gmt
|
||||
FROM {$wpdb->prefix}wc_orders o
|
||||
WHERE o.customer_id = %d
|
||||
AND o.status IN ($statusesPlaceholder)
|
||||
AND o.total_amount > 0
|
||||
ORDER BY o.date_created_gmt {$sorting}
|
||||
LIMIT 1
|
||||
", array_merge([$customer->get_id()], $statuses));
|
||||
} else {
|
||||
$statement = (string)$wpdb->prepare("
|
||||
SELECT p.post_date_gmt
|
||||
FROM {$wpdb->prefix}posts p
|
||||
LEFT JOIN {$wpdb->prefix}postmeta pm_total ON p.ID = pm_total.post_id AND pm_total.meta_key = '_order_total'
|
||||
LEFT JOIN {$wpdb->prefix}postmeta pm_user ON p.ID = pm_user.post_id AND pm_user.meta_key = '_customer_user'
|
||||
WHERE p.post_type = 'shop_order'
|
||||
AND p.post_status IN ($statusesPlaceholder)
|
||||
AND pm_user.meta_value = %d
|
||||
AND pm_total.meta_value > 0
|
||||
ORDER BY p.post_date_gmt {$sorting}
|
||||
LIMIT 1
|
||||
", array_merge($statuses, [$customer->get_id()]));
|
||||
}
|
||||
|
||||
$date = $wpdb->get_var($statement);
|
||||
return $date ? new DateTimeImmutable($date, new DateTimeZone('GMT')) : null;
|
||||
}
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Automation\Integrations\WooCommerce;
|
||||
|
||||
use Automattic\WooCommerce\Utilities\OrderUtil;
|
||||
|
||||
class WooCommerce {
|
||||
public function isWooCommerceActive(): bool {
|
||||
return class_exists('WooCommerce');
|
||||
}
|
||||
|
||||
public function wcGetIsPaidStatuses(): array {
|
||||
return wc_get_is_paid_statuses();
|
||||
}
|
||||
|
||||
public function isWooCommerceCustomOrdersTableEnabled(): bool {
|
||||
return $this->isWooCommerceActive()
|
||||
&& method_exists(OrderUtil::class, 'custom_orders_table_usage_is_enabled')
|
||||
&& OrderUtil::custom_orders_table_usage_is_enabled();
|
||||
}
|
||||
}
|
@@ -179,9 +179,11 @@ class ContainerConfigurator implements IContainerConfigurator {
|
||||
$container->autowire(\MailPoet\Automation\Integrations\MailPoet\SubjectTransformers\SubscriberSubjectToWordPressUserSubjectTransformer::class)->setPublic(true)->setShared(false);
|
||||
|
||||
// Automation - WooCommerce integration
|
||||
$container->autowire(\MailPoet\Automation\Integrations\WooCommerce\WooCommerce::class)->setPublic(true);
|
||||
$container->autowire(\MailPoet\Automation\Integrations\WooCommerce\ContextFactory::class)->setPublic(true);
|
||||
$container->autowire(\MailPoet\Automation\Integrations\WooCommerce\WooCommerceIntegration::class)->setPublic(true);
|
||||
$container->autowire(\MailPoet\Automation\Integrations\WooCommerce\Fields\CustomerFieldsFactory::class)->setPublic(true);
|
||||
$container->autowire(\MailPoet\Automation\Integrations\WooCommerce\Fields\CustomerOrderFieldsFactory::class)->setPublic(true);
|
||||
$container->autowire(\MailPoet\Automation\Integrations\WooCommerce\Triggers\OrderStatusChangedTrigger::class)->setPublic(true)->setShared(false);
|
||||
$container->autowire(\MailPoet\Automation\Integrations\WooCommerce\Subjects\OrderSubject::class)->setPublic(true)->setShared(false);
|
||||
$container->autowire(\MailPoet\Automation\Integrations\WooCommerce\Subjects\OrderStatusChangeSubject::class)->setPublic(true)->setShared(false);
|
||||
|
@@ -88,6 +88,10 @@ parameters:
|
||||
message: '/^Parameter #2 \$args of static method Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\TaskLists::add_task\(\) expects array, MailPoet\\WooCommerce\\MailPoetTask given.$/'
|
||||
count: 1
|
||||
path: ../../lib/Config/HooksWooCommerce.php
|
||||
-
|
||||
message: "#^Cannot cast string|void to string\\.$#"
|
||||
count: 2
|
||||
path: ../../lib/Automation/Integrations/WooCommerce/Fields/CustomerOrderFieldsFactory.php
|
||||
|
||||
reportUnmatchedIgnoredErrors: true
|
||||
dynamicConstantNames:
|
||||
|
@@ -0,0 +1,111 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace integration\Automation\Integrations\WooCommerce\Fields;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use MailPoet\Automation\Engine\Data\Field;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Payloads\CustomerPayload;
|
||||
use MailPoet\Automation\Integrations\WooCommerce\Subjects\CustomerSubject;
|
||||
use WC_Customer;
|
||||
use WC_Order;
|
||||
|
||||
/**
|
||||
* @group woo
|
||||
*/
|
||||
class CustomerOrderFieldsFactoryTest extends \MailPoetTest {
|
||||
public function testOrderStatsFields(): void {
|
||||
$fields = $this->getFieldsMap();
|
||||
|
||||
// check definitions
|
||||
$spentTotalField = $fields['woocommerce:customer:spent-total'];
|
||||
$this->assertSame('Total spent', $spentTotalField->getName());
|
||||
$this->assertSame('number', $spentTotalField->getType());
|
||||
$this->assertSame([], $spentTotalField->getArgs());
|
||||
|
||||
$spentAverageField = $fields['woocommerce:customer:spent-average'];
|
||||
$this->assertSame('Average spent', $spentAverageField->getName());
|
||||
$this->assertSame('number', $spentAverageField->getType());
|
||||
$this->assertSame([], $spentAverageField->getArgs());
|
||||
|
||||
$orderCountField = $fields['woocommerce:customer:order-count'];
|
||||
$this->assertSame('Order count', $orderCountField->getName());
|
||||
$this->assertSame('integer', $orderCountField->getType());
|
||||
$this->assertSame([], $orderCountField->getArgs());
|
||||
|
||||
// check values (guest)
|
||||
$this->createOrder(0, 12.3);
|
||||
$this->createOrder(0, 0);
|
||||
$this->createOrder(0, 150.0);
|
||||
|
||||
$this->assertSame(0.0, $spentTotalField->getValue(new CustomerPayload()));
|
||||
$this->assertSame(0.0, $spentAverageField->getValue(new CustomerPayload()));
|
||||
|
||||
// check values (registered)
|
||||
$id = $this->tester->createCustomer('customer@example.com');
|
||||
$this->createOrder($id, 12.3);
|
||||
$this->createOrder($id, 0);
|
||||
$this->createOrder($id, 150.0);
|
||||
$this->createOrder($id + 1, 12345.0); // other user
|
||||
|
||||
$customerPayload = new CustomerPayload(new WC_Customer($id));
|
||||
$this->assertSame(162.3, $spentTotalField->getValue($customerPayload));
|
||||
$this->assertSame(54.1, $spentAverageField->getValue($customerPayload));
|
||||
$this->assertSame(3, $orderCountField->getValue($customerPayload));
|
||||
}
|
||||
|
||||
public function testOrderDateFields(): void {
|
||||
$fields = $this->getFieldsMap();
|
||||
|
||||
// check definitions
|
||||
$firstPaidOrderDateField = $fields['woocommerce:customer:first-paid-order-date'];
|
||||
$this->assertSame('First paid order date', $firstPaidOrderDateField->getName());
|
||||
$this->assertSame('datetime', $firstPaidOrderDateField->getType());
|
||||
$this->assertSame([], $firstPaidOrderDateField->getArgs());
|
||||
|
||||
$lastPaidOrderDateField = $fields['woocommerce:customer:last-paid-order-date'];
|
||||
$this->assertSame('Last paid order date', $lastPaidOrderDateField->getName());
|
||||
$this->assertSame('datetime', $lastPaidOrderDateField->getType());
|
||||
$this->assertSame([], $lastPaidOrderDateField->getArgs());
|
||||
|
||||
// check values (guest)
|
||||
$this->createOrder(0, 0, '2023-05-03 08:22:38');
|
||||
$this->createOrder(0, 12.3, '2023-05-12 17:42:11');
|
||||
$this->assertNull($firstPaidOrderDateField->getValue(new CustomerPayload()));
|
||||
$this->assertNull($lastPaidOrderDateField->getValue(new CustomerPayload()));
|
||||
|
||||
// check values (registered)
|
||||
$id = $this->tester->createCustomer('customer@example.com');
|
||||
$this->createOrder($id, 0, '2023-05-03 08:22:38');
|
||||
$this->createOrder($id, 12.3, '2023-05-12 17:42:11');
|
||||
$this->createOrder($id, 0, '2023-05-19 21:35:03');
|
||||
$this->createOrder($id, 150.0, '2023-05-26 11:13:53');
|
||||
$this->createOrder($id, 0, '2023-06-01 14:05:01');
|
||||
$this->createOrder($id + 1, 0, '2023-06-05 15:42:56'); // other user
|
||||
|
||||
$customerPayload = new CustomerPayload(new WC_Customer($id));
|
||||
$this->assertEquals(new DateTimeImmutable('2023-05-12 17:42:11'), $firstPaidOrderDateField->getValue($customerPayload));
|
||||
$this->assertEquals(new DateTimeImmutable('2023-05-26 11:13:53'), $lastPaidOrderDateField->getValue($customerPayload));
|
||||
}
|
||||
|
||||
private function createOrder(int $customerId, float $total, string $date = '2023-06-01 14:03:27'): WC_Order {
|
||||
$order = $this->tester->createWooCommerceOrder([
|
||||
'customer_id' => $customerId,
|
||||
'total' => (string)$total,
|
||||
'date_created' => $date,
|
||||
]);
|
||||
$order->set_status('wc-completed');
|
||||
$order->save();
|
||||
$this->tester->updateWooOrderStats($order->get_id());
|
||||
return $order;
|
||||
}
|
||||
|
||||
/** @return array<string, Field> */
|
||||
private function getFieldsMap(): array {
|
||||
$factory = $this->diContainer->get(CustomerSubject::class);
|
||||
$fields = [];
|
||||
foreach ($factory->getFields() as $field) {
|
||||
$fields[$field->getKey()] = $field;
|
||||
}
|
||||
return $fields;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user