Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: change the current product criteria for sync #3416

Merged
merged 7 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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