Skip to content

Commit

Permalink
fix: change the current product criteria for sync (#3416)
Browse files Browse the repository at this point in the history
* fix: change the current product criteria for sync

* fix: undefined/unused variable and $donation_product type

* test: update tests

* test: remove failing test

* test: improve tests

* chore: use constant to hold former subscriber statuses

* test: bring back deleted tests with mock

---------

Co-authored-by: dkoo <derrick.koo@automattic.com>
  • Loading branch information
leogermani and dkoo committed Sep 18, 2024
1 parent 8224a8d commit 28a84bc
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 46 deletions.
114 changes: 113 additions & 1 deletion includes/reader-activation/sync/class-woocommerce.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,118 @@ public static function should_sync_order( $order ) {
return true;
}

/**
* Get the order containing what we consider to be the "Current Product" for a given user.
*
* All the payment fields that are synced relate to this product.
*
* The criteria for the "Current Product" are:
* 1. The most recent subscription (either regular subscriptions or recurring donations).
* 2. If no active subscriptions, the most recently cancelled or expired subscription.
* 3. If no subscriptions at all, the most recent one-time donation.
*
* @param \WC_Customer $customer Customer object.
*
* @return \WC_Order|false Order object or false.
*/
private static function get_current_product_order_for_sync( $customer ) {
if ( ! is_a( $customer, 'WC_Customer' ) ) {
return false;
}

$user_id = $customer->get_id();

// 1. The most recent subscription (either regular subscriptions or recurring donations).
$active_subscriptions = WooCommerce_Connection::get_active_subscriptions_for_user( $user_id );
if ( ! empty( $active_subscriptions ) ) {
return \wcs_get_subscription( reset( $active_subscriptions ) );
}

// 2. If no active subscriptions, the most recently cancelled or expired subscription.
$most_recent_cancelled_or_expired_subscription = self::get_most_recent_cancelled_or_expired_subscription( $user_id );
if ( $most_recent_cancelled_or_expired_subscription ) {
return \wcs_get_subscription( $most_recent_cancelled_or_expired_subscription );
}

// 3. If no subscriptions at all, the most recent one-time donation.
$one_time_donation_order = self::get_one_time_donation_order_for_user( $user_id );
if ( $one_time_donation_order ) {
return $one_time_donation_order;
}

/**
* Filter the order containing what we consider to be the "Current Product" for a given user when nothing is found.
*
* This is used for tests to mock the return value.
*
* @param false $current_product_order The returned value.
* @return int $user_id The user ID.
*/
return apply_filters( 'newspack_reader_activation_get_current_product_order_for_sync', false, $user_id );
}

/**
* Get the most recent cancelled or expired subscription for a user.
*
* @param int $user_id User ID.
*
* @return ?WCS_Subscription A Subscription object or null.
*/
private static function get_most_recent_cancelled_or_expired_subscription( $user_id ) {
$subcriptions = array_reduce(
array_keys( \wcs_get_users_subscriptions( $user_id ) ),
function( $acc, $subscription_id ) {
$subscription = \wcs_get_subscription( $subscription_id );
if ( $subscription->has_status( WooCommerce_Connection::FORMER_SUBSCRIBER_STATUSES ) ) {
$acc[] = $subscription_id;
}
return $acc;
},
[]
);

if ( ! empty( $subcriptions ) ) {
return reset( $subcriptions );
}
}

/**
* Get the most recent one-time donation order for a user.
*
* @param int $user_id User ID.
*
* @return ?WC_Order An Order object or null.
*/
private static function get_one_time_donation_order_for_user( $user_id ) {
$donation_product = Donations::get_donation_product( 'once' );
if ( ! $donation_product ) {
return;
}
$user_has_donated = \wc_customer_bought_product( null, $user_id, $donation_product );
if ( ! $user_has_donated ) {
return;
}

// If user has donated, we'll loop through their orders to find the most recent donation.
// If this method was called, that's because they don't have any active subscriptions, so there shouldn't be too many.
$args = [
'customer_id' => $user_id,
'status' => [ 'wc-completed' ],
'limit' => -1,
'order' => 'DESC',
'orderby' => 'date',
'return' => 'objects',
];

// Return the most recent completed order.
$orders = \wc_get_orders( $args );
foreach ( $orders as $order ) {
if ( Donations::is_donation_order( $order ) ) {
return $order;
}
}
}

/**
* Get data about a customer's order to sync to the connected ESP.
*
Expand Down Expand Up @@ -212,7 +324,7 @@ public static function get_contact_from_customer( $customer, $payment_page_url =
$metadata['registration_date'] = $customer->get_date_created()->date( Metadata::DATE_FORMAT );
$metadata['total_paid'] = \wc_format_localized_price( $customer->get_total_spent() );

$order = WooCommerce_Connection::get_last_successful_order( $customer );
$order = self::get_current_product_order_for_sync( $customer );

// Get the order metadata.
$order_metadata = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ class WooCommerce_Connection {
const ACTIVE_SUBSCRIPTION_STATUSES = [ 'active', 'pending', 'pending-cancel' ];
const ACTIVE_ORDER_STATUSES = [ 'processing', 'completed' ];


/**
* These are the status a subscription can have for us to consider it from a former subscriber.
*/
const FORMER_SUBSCRIBER_STATUSES = [ 'on-hold', 'cancelled', 'expired' ];

/**
* Initialize.
*
Expand Down Expand Up @@ -160,51 +166,6 @@ function( $acc, $subscription_id ) use ( $product_ids ) {
return $subcriptions;
}

/**
* Get the most recent active subscription, or the last successful order for a given customer.
*
* @param \WC_Customer $customer Customer object.
*
* @return \WC_Order|false Order object or false.
*/
public static function get_last_successful_order( $customer ) {
if ( ! is_a( $customer, 'WC_Customer' ) ) {
return false;
}

$user_id = $customer->get_id();

// Prioritize any currently active subscriptions.
$active_subscriptions = self::get_active_subscriptions_for_user( $user_id );
if ( ! empty( $active_subscriptions ) ) {
return \wcs_get_subscription( reset( $active_subscriptions ) );
}

// If no active subscriptions, get the most recent completed order.
// See https://github.com/woocommerce/woocommerce/wiki/wc_get_orders-and-WC_Order_Query for query args.
$args = [
'customer_id' => $user_id,
'status' => [ 'wc-completed' ],
'limit' => 1,
'order' => 'DESC',
'orderby' => 'date',
'return' => 'objects',
];

// Return the most recent completed order.
$orders = \wc_get_orders( $args );
if ( ! empty( $orders ) ) {
return reset( $orders );
}

// If no completed orders or active subscriptions, they might still have an inactive subscription.
if ( ! empty( $user_subscriptions ) ) {
return reset( $user_subscriptions );
}

return false;
}

/**
* Filter post request made by the Stripe Gateway for Stripe payments.
*
Expand Down
5 changes: 5 additions & 0 deletions tests/mocks/newsletters-mocks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php // phpcs:disable WordPress.Files.FileName.InvalidClassFileName, Squiz.Commenting.FunctionComment.Missing, Squiz.Commenting.ClassComment.Missing, Squiz.Commenting.VariableComment.Missing, Squiz.Commenting.FileComment.Missing, Generic.Files.OneObjectStructurePerFile.MultipleFound, Universal.Files.SeparateFunctionsFromOO.Mixed

if ( ! class_exists( 'Newspack_Newsletters_Contacts' ) ) {
class Newspack_Newsletters_Contacts {}
}
4 changes: 4 additions & 0 deletions tests/mocks/wc-mocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,7 @@ function( $a, $b ) {
);
return $orders;
}

function wc_customer_bought_product( $customer_email, $user_id, $product_id ) {
return false;
}
47 changes: 47 additions & 0 deletions tests/unit-tests/reader-activation-sync-woocommerce.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@
* Tests Reader Activation Sync WooCommerce.
*/
class Newspack_Test_RAS_Sync_WooCommerce extends WP_UnitTestCase {

/**
* The current order that will be returned in the filter
*
* @var ?WC_Order
*/
public static $current_order = false;

const USER_DATA = [
'user_login' => 'test_user',
'user_email' => 'test@example.com',
Expand All @@ -37,6 +45,32 @@ public function set_up() { // phpcs:ignore Squiz.Commenting.FunctionComment.Miss
// Reset the user.
wp_delete_user( self::$user_id );
self::$user_id = wp_insert_user( self::USER_DATA );

add_filter(
'newspack_reader_activation_get_current_product_order_for_sync',
[ __CLASS__, 'get_current_order' ]
);
}

/**
* Tear down the test.
*
* @return void
*/
public function tear_down() {
remove_filter(
'newspack_reader_activation_get_current_product_order_for_sync',
[ __CLASS__, 'get_current_order' ]
);
}

/**
* Get the current order for the test.
*
* @return ?WC_Order
*/
public static function get_current_order() {
return self::$current_order;
}

/**
Expand All @@ -57,6 +91,8 @@ public function test_payment_metadata_basic() {
],
];
$order = \wc_create_order( $order_data );
self::$current_order = $order;

$payment_page_url = 'https://example.com/donate';
$contact_data = Sync\WooCommerce::get_contact_from_order( $order, $payment_page_url );
$today = gmdate( 'Y-m-d' );
Expand Down Expand Up @@ -109,6 +145,8 @@ public function test_payment_metadata_utm() {
],
];
$order = \wc_create_order( $order_data );
self::$current_order = $order;

$contact_data = Sync\WooCommerce::get_contact_from_order( $order );
$this->assertEquals( 'test_source', $contact_data['metadata']['payment_page_utm_source'] );
$this->assertEquals( 'test_campaign', $contact_data['metadata']['payment_page_utm_campaign'] );
Expand All @@ -125,6 +163,9 @@ public function test_payment_metadata_with_failed_order() {
'total' => 60,
]
);

self::$current_order = $order;

$contact_data = Sync\WooCommerce::get_contact_from_order( $order );
$this->assertEmpty( $contact_data['metadata']['last_payment_date'] );
$this->assertEmpty( $contact_data['metadata']['last_payment_amount'] );
Expand All @@ -140,6 +181,9 @@ public function test_payment_metadata_from_customer() {
'total' => 70,
];
$order = \wc_create_order( $order_data );

self::$current_order = $order;

$contact_data = Sync\WooCommerce::get_contact_from_customer( self::$user_id );
$this->assertEquals( '$' . $order_data['total'], $contact_data['metadata']['last_payment_amount'] );
$this->assertEquals( gmdate( 'Y-m-d' ), $contact_data['metadata']['last_payment_date'] );
Expand All @@ -156,6 +200,9 @@ public function test_payment_metadata_from_customer_with_last_order_failed() {
'date_paid' => gmdate( 'Y-m-d', strtotime( '-1 week' ) ),
];
$order = \wc_create_order( $completed_order_data );

self::$current_order = $order;

// A more recent, but failed, order.
$failed_order_data = [
'customer_id' => self::$user_id,
Expand Down
2 changes: 2 additions & 0 deletions tests/unit-tests/reader-activation-sync.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
use Newspack\Reader_Activation\Sync;
use Newspack\Reader_Activation\ESP_Sync;

require_once __DIR__ . '/../mocks/newsletters-mocks.php';

/**
* Test the Esp_Metadata_Sync class.
*/
Expand Down

0 comments on commit 28a84bc

Please sign in to comment.