diff --git a/includes/reader-activation/sync/class-contact-metadata.php b/includes/reader-activation/sync/class-contact-metadata.php index fdee8623a8..14e220141c 100644 --- a/includes/reader-activation/sync/class-contact-metadata.php +++ b/includes/reader-activation/sync/class-contact-metadata.php @@ -13,6 +13,10 @@ * Reader Activation Class. */ abstract class Contact_Metadata { + /** + * The date format to use for all date fields, which is YYYY-MM-DD HH:MM:SS. + */ + const DATE_FORMAT = 'Y-m-d H:i:s'; /** * The WP_User object. @@ -122,4 +126,21 @@ public function get_full_name() { } return ''; } + + /** + * Format a date string to MM/DD/YYYY. + * + * @param string $date_string Date string from WooCommerce. + * @return string Formatted date or empty string. + */ + protected function format_date( $date_string ) { + if ( empty( $date_string ) || '0' === $date_string ) { + return ''; + } + $timestamp = strtotime( $date_string ); + if ( ! $timestamp ) { + return ''; + } + return gmdate( self::DATE_FORMAT, $timestamp ); + } } diff --git a/includes/reader-activation/sync/class-contact-sync.php b/includes/reader-activation/sync/class-contact-sync.php index 10d2365766..ea02039559 100644 --- a/includes/reader-activation/sync/class-contact-sync.php +++ b/includes/reader-activation/sync/class-contact-sync.php @@ -11,6 +11,7 @@ use Newspack\Reader_Activation\Integrations; use Newspack\Data_Events; use Newspack\Logger; +use Newspack\Reader_Activation\Sync\Metadata; defined( 'ABSPATH' ) || exit; @@ -132,6 +133,12 @@ public static function sync( $contact, $context = '', $existing_contact = null ) } } + // Added logging here to more easily monitor integration sync data. Can be removed once integrations are released. + if ( 'legacy' !== Metadata::get_version() ) { + Logger::log( sprintf( 'Syncing contact %s for context "%s".', $contact['email'] ?? 'unknown', $context ) ); + Logger::log( $contact ); + } + return self::push_to_integrations( $contact, $context, $existing_contact ); } diff --git a/includes/reader-activation/sync/contact-metadata/class-donation.php b/includes/reader-activation/sync/contact-metadata/class-donation.php index c392dbda74..6e68e2ca11 100644 --- a/includes/reader-activation/sync/contact-metadata/class-donation.php +++ b/includes/reader-activation/sync/contact-metadata/class-donation.php @@ -7,23 +7,32 @@ namespace Newspack\Reader_Activation\Sync\Contact_Metadata; -use Newspack\Reader_Activation\Sync\Contact_Metadata; +use Newspack\Donations; +use Newspack\WooCommerce_Connection; defined( 'ABSPATH' ) || exit; /** * Donation metadata class. + * + * Extends Subscription to reuse subscription data access helpers, + * with the filter inverted to only consider donation subscriptions. */ -class Donation extends Contact_Metadata { +class Donation extends Subscription { /** - * Whether or not the metadata fields of this class are available to be synced. + * Cache for the one-time donation order. * - * @return boolean + * @var \WC_Order|null */ - public static function is_available() { - return true; - } + private $one_time_donation_order_cache; + + /** + * Whether the one-time donation order has been resolved. + * + * @var bool + */ + private $one_time_donation_order_resolved = false; /** * The name of the metadata class, used as a section name for the fields handled by this class when syncing and in the UI for selecting which fields to sync. @@ -62,6 +71,224 @@ public static function get_fields() { * @return array */ public function get_metadata() { - return []; + if ( ! $this->user || ! function_exists( 'wcs_get_users_subscriptions' ) ) { + return []; + } + + return [ + 'Donor_Status' => $this->get_donor_status(), + 'Active_Donation_Count' => $this->get_active_subscription_count(), + 'Current_Donation_Start_Date' => $this->get_current_subscription_start_date(), + 'Current_Donation_End_Date' => $this->get_current_subscription_end_date(), + 'Current_Donation_Cycle' => $this->get_current_subscription_billing_cycle(), + 'Current_Recurring_Donation' => $this->get_current_subscription_recurring_payment(), + 'Next_Donation_Date' => $this->get_current_subscription_next_payment_date(), + 'Current_Donation_Product_Name' => $this->get_current_donation_product_name(), + 'Previous_Donation_Product' => $this->get_previous_subscription_product(), + 'Previous_Donation_Amount' => $this->get_previous_donation_amount(), + 'Last_Donation_Amount' => $this->get_last_donation_amount(), + 'Last_Donation_Date' => $this->get_last_donation_date(), + ]; + } + + /** + * Whether the given subscription is relevant to this metadata class. + * + * For Donation, only donation subscriptions are relevant. + * + * @param \WC_Subscription $subscription Subscription object. + * @return bool + */ + protected function is_relevant_subscription( $subscription ) { + return Donations::is_donation_order( $subscription ); + } + + /** + * Get the most recent one-time donation order for the current user. + * + * @return \WC_Order|null + */ + protected function get_one_time_donation_order() { + if ( $this->one_time_donation_order_resolved ) { + return $this->one_time_donation_order_cache; + } + + $this->one_time_donation_order_resolved = true; + + if ( ! $this->user ) { + return null; + } + + $donation_product = Donations::get_donation_product( 'once' ); + if ( ! $donation_product ) { + return null; + } + + $user_has_donated = \wc_customer_bought_product( null, $this->user->ID, $donation_product ); + if ( ! $user_has_donated ) { + return null; + } + + $page = 1; + do { + $orders = \wc_get_orders( + [ + 'customer_id' => $this->user->ID, + 'status' => [ 'wc-completed' ], + 'limit' => 20, + 'order' => 'DESC', + 'orderby' => 'date', + 'return' => 'objects', + 'page' => $page++, + ] + ); + + foreach ( $orders as $order ) { + if ( Donations::is_donation_order( $order ) ) { + $this->one_time_donation_order_cache = $order; + return $this->one_time_donation_order_cache; + } + } + } while ( ! empty( $orders ) ); + + return null; + } + + /** + * Get the donor status label. + * + * Returns a summarized label: Monthly Donor, Yearly Donor, Ex-Monthly Donor, Ex-Yearly Donor, or Donor. + * + * @return string + */ + protected function get_donor_status() { + $subscription = $this->get_current_subscription(); + + if ( $subscription ) { + $donor_status = 'Donor'; + $billing_period = $subscription->get_billing_period(); + + if ( 'month' === $billing_period ) { + $donor_status = 'Monthly ' . $donor_status; + } elseif ( 'year' === $billing_period ) { + $donor_status = 'Yearly ' . $donor_status; + } + + if ( $subscription->has_status( WooCommerce_Connection::FORMER_SUBSCRIBER_STATUSES ) ) { + $donor_status = 'Ex-' . $donor_status; + } + + return $donor_status; + } + + // Fallback: check for one-time donation. + $one_time_order = $this->get_one_time_donation_order(); + if ( $one_time_order ) { + return 'Donor'; + } + + return ''; + } + + /** + * Get the donation product name, with fallback to one-time donation order. + * + * @return string + */ + protected function get_current_donation_product_name() { + $name = $this->get_current_subscription_product_name(); + if ( ! empty( $name ) ) { + return $name; + } + + $one_time_order = $this->get_one_time_donation_order(); + if ( $one_time_order ) { + $items = $one_time_order->get_items(); + if ( ! empty( $items ) ) { + return reset( $items )->get_name(); + } + } + + return ''; + } + + /** + * Get the previous donation amount before a plan switch. + * + * @return string + */ + protected function get_previous_donation_amount() { + $subscription = $this->get_current_subscription(); + if ( ! $subscription ) { + return ''; + } + + $switch_orders = $subscription->get_related_orders( 'all', 'switch' ); + if ( empty( $switch_orders ) ) { + return ''; + } + + // Get the most recent switch order. + $switch_order = reset( $switch_orders ); + if ( is_numeric( $switch_order ) ) { + $switch_order = \wc_get_order( $switch_order ); + } + if ( ! $switch_order ) { + return ''; + } + + $switch_data = $switch_order->get_meta( '_subscription_switch_data' ); + if ( ! empty( $switch_data ) && is_array( $switch_data ) ) { + $sub_switch_data = isset( $switch_data[ $subscription->get_id() ] ) ? $switch_data[ $subscription->get_id() ] : reset( $switch_data ); + if ( ! empty( $sub_switch_data['old_subscription_id'] ) ) { + $old_subscription = \wcs_get_subscription( $sub_switch_data['old_subscription_id'] ); + if ( $old_subscription ) { + return $old_subscription->get_total(); + } + } + } + + return ''; + } + + /** + * Get the last donation amount, with fallback to one-time donation order. + * + * @return string + */ + protected function get_last_donation_amount() { + $amount = $this->get_last_payment_amount(); + if ( ! empty( $amount ) ) { + return $amount; + } + + $one_time_order = $this->get_one_time_donation_order(); + if ( $one_time_order ) { + return $one_time_order->get_total(); + } + + return ''; + } + + /** + * Get the last donation date, with fallback to one-time donation order. + * + * @return string + */ + protected function get_last_donation_date() { + $date = $this->get_last_payment_date(); + if ( ! empty( $date ) ) { + return $date; + } + + $one_time_order = $this->get_one_time_donation_order(); + if ( $one_time_order ) { + $date_paid = $one_time_order->get_date_paid(); + if ( ! empty( $date_paid ) ) { + return $date_paid->date( self::DATE_FORMAT ); + } + } + + return ''; } } diff --git a/includes/reader-activation/sync/contact-metadata/class-subscription.php b/includes/reader-activation/sync/contact-metadata/class-subscription.php index 42f148ba13..f0aed9048b 100644 --- a/includes/reader-activation/sync/contact-metadata/class-subscription.php +++ b/includes/reader-activation/sync/contact-metadata/class-subscription.php @@ -7,6 +7,9 @@ namespace Newspack\Reader_Activation\Sync\Contact_Metadata; +use Newspack\Donations; +use Newspack\Subscriptions_Meta; +use Newspack\WooCommerce_Connection; use Newspack\Reader_Activation\Sync\Contact_Metadata; defined( 'ABSPATH' ) || exit; @@ -15,6 +18,26 @@ * Subscription metadata class. */ class Subscription extends Contact_Metadata { + /** + * Cache for user subscriptions. + * + * @var \WC_Subscription[] + */ + private $user_subscriptions_cache; + + /** + * Cache for the current subscription. + * + * @var \WC_Subscription|null + */ + private $current_subscription_cache; + + /** + * Whether the current subscription has been resolved. + * + * @var bool + */ + private $current_subscription_resolved = false; /** * Whether or not the metadata fields of this class are available to be synced. @@ -22,7 +45,7 @@ class Subscription extends Contact_Metadata { * @return boolean */ public static function is_available() { - return true; + return function_exists( 'wcs_get_users_subscriptions' ); } /** @@ -63,6 +86,393 @@ public static function get_fields() { * @return array */ public function get_metadata() { - return []; + if ( ! $this->user || ! self::is_available() ) { + return []; + } + + return [ + 'Subscriber_Status' => $this->get_subscriber_status(), + 'Active_Subscription_Count' => $this->get_active_subscription_count(), + 'Current_Subscription_Start_Date' => $this->get_current_subscription_start_date(), + 'Current_Subscription_End_Date' => $this->get_current_subscription_end_date(), + 'Subscription_Cancellation_Reason' => $this->get_subscription_cancellation_reason(), + 'Current_Subscription_Billing_Cycle' => $this->get_current_subscription_billing_cycle(), + 'Current_Subscription_Recurring_Payment' => $this->get_current_subscription_recurring_payment(), + 'Current_Subscription_Next_Payment_Date' => $this->get_current_subscription_next_payment_date(), + 'Current_Subscription_Product_Name' => $this->get_current_subscription_product_name(), + 'Previous_Subscription_Product' => $this->get_previous_subscription_product(), + 'Current_Subscription_Coupon_Code' => $this->get_current_subscription_coupon_code(), + 'Last_Payment_Amount' => $this->get_last_payment_amount(), + 'Last_Payment_Date' => $this->get_last_payment_date(), + ]; + } + + /** + * Whether the given subscription is relevant to this metadata class. + * + * For Subscription, only non-donation subscriptions are relevant. + * Override in Donation class to invert the filter. + * + * @param \WC_Subscription $subscription Subscription object. + * + * @return bool + */ + protected function is_relevant_subscription( $subscription ) { + return ! Donations::is_donation_order( $subscription ); + } + + /** + * Get all relevant subscriptions for the current user. + * + * @return \WC_Subscription[] + */ + protected function get_user_subscriptions() { + if ( isset( $this->user_subscriptions_cache ) ) { + return $this->user_subscriptions_cache; + } + + $this->user_subscriptions_cache = []; + + if ( ! $this->user || ! self::is_available() ) { + return $this->user_subscriptions_cache; + } + + $all_subscriptions = \wcs_get_users_subscriptions( $this->user->ID ); + foreach ( $all_subscriptions as $subscription ) { + if ( $this->is_relevant_subscription( $subscription ) ) { + $this->user_subscriptions_cache[] = $subscription; + } + } + + return $this->user_subscriptions_cache; + } + + /** + * Get active relevant subscriptions for the current user. + * + * @return \WC_Subscription[] + */ + protected function get_active_subscriptions() { + return array_filter( + $this->get_user_subscriptions(), + function ( $subscription ) { + return $subscription->has_status( WooCommerce_Connection::ACTIVE_SUBSCRIPTION_STATUSES ); + } + ); + } + + /** + * Get the current subscription for metadata purposes. + * + * Priority: most recent active subscription, then most recent cancelled/expired/on-hold. + * + * @return \WC_Subscription|null + */ + protected function get_current_subscription() { + if ( $this->current_subscription_resolved ) { + return $this->current_subscription_cache; + } + + $this->current_subscription_resolved = true; + + $active = $this->get_active_subscriptions(); + if ( ! empty( $active ) ) { + $this->current_subscription_cache = $this->prefer_non_gift( $active ); + return $this->current_subscription_cache; + } + + $former = array_filter( + $this->get_user_subscriptions(), + function ( $subscription ) { + return $subscription->has_status( WooCommerce_Connection::FORMER_SUBSCRIBER_STATUSES ); + } + ); + if ( ! empty( $former ) ) { + $this->current_subscription_cache = $this->prefer_non_gift( $former ); + return $this->current_subscription_cache; + } + + return null; + } + + /** + * From a list of subscriptions, prefer non-gift subscriptions over gifts. + * + * Falls back to the first subscription if all are gifts or the gifting plugin is not active. + * + * @param \WC_Subscription[] $subscriptions Subscriptions to choose from. + * @return \WC_Subscription + */ + private function prefer_non_gift( $subscriptions ) { + if ( class_exists( 'WCS_Gifting' ) ) { + foreach ( $subscriptions as $subscription ) { + if ( ! \WCS_Gifting::is_gifted_subscription( $subscription ) ) { + return $subscription; + } + } + } + return reset( $subscriptions ); + } + + /** + * Get the last successful order for a subscription. + * + * @param \WC_Subscription $subscription Subscription object. + * @return \WC_Order|null + */ + protected function get_last_successful_order( $subscription ) { + $last_order = $subscription->get_last_order( + 'all', + [ + 'parent', + 'renewal', + ], + [ + 'pending', + 'failed', + 'on-hold', + 'cancelled', + 'trash', + 'draft', + 'auto-draft', + 'new', + ] + ); + + return $last_order ? $last_order : null; + } + + /** + * Get the subscriber status. + * + * @return string + */ + protected function get_subscriber_status() { + $subscription = $this->get_current_subscription(); + return $subscription ? $subscription->get_status() : ''; + } + + /** + * Get the number of active subscriptions. + * + * @return int + */ + protected function get_active_subscription_count() { + return count( $this->get_active_subscriptions() ); + } + + /** + * Get the start date of the current subscription. + * + * @return string + */ + protected function get_current_subscription_start_date() { + $subscription = $this->get_current_subscription(); + if ( ! $subscription ) { + return ''; + } + return $this->format_date( $subscription->get_date( 'start', 'site' ) ); + } + + /** + * Get the end date of the current subscription. + * + * @return string + */ + protected function get_current_subscription_end_date() { + $subscription = $this->get_current_subscription(); + if ( ! $subscription ) { + return ''; + } + return $this->format_date( $subscription->get_date( 'end', 'site' ) ); + } + + /** + * Get the cancellation reason for the current subscription. + * + * @return string + */ + protected function get_subscription_cancellation_reason() { + $subscription = $this->get_current_subscription(); + if ( ! $subscription ) { + return ''; + } + + $reason = $subscription->get_meta( Subscriptions_Meta::CANCELLATION_REASON_META_KEY ); + if ( empty( $reason ) ) { + return ''; + } + + // Exclude pending-cancel reasons — these are intermediate states, not final. + $pending_reasons = [ + Subscriptions_Meta::CANCELLATION_REASON_USER_PENDING_CANCEL, + Subscriptions_Meta::CANCELLATION_REASON_ADMIN_PENDING_CANCEL, + ]; + if ( in_array( $reason, $pending_reasons, true ) ) { + return ''; + } + + return $reason; + } + + /** + * Get the billing cycle of the current subscription. + * + * @return string + */ + protected function get_current_subscription_billing_cycle() { + $subscription = $this->get_current_subscription(); + return $subscription ? $subscription->get_billing_period() : ''; + } + + /** + * Get the recurring payment amount of the current subscription. + * + * @return string + */ + protected function get_current_subscription_recurring_payment() { + $subscription = $this->get_current_subscription(); + return $subscription ? $subscription->get_total() : ''; + } + + /** + * Get the next payment date of the current subscription. + * + * @return string + */ + protected function get_current_subscription_next_payment_date() { + $subscription = $this->get_current_subscription(); + if ( ! $subscription ) { + return ''; + } + $next_payment = $subscription->get_date( 'next_payment' ); + // When a subscription is terminated, next_payment is set to 0. + if ( ! $next_payment || '0' === $next_payment ) { + return ''; + } + return $this->format_date( $next_payment ); + } + + /** + * Get the product name of the current subscription. + * + * @return string + */ + protected function get_current_subscription_product_name() { + $subscription = $this->get_current_subscription(); + if ( ! $subscription ) { + return ''; + } + $items = $subscription->get_items(); + if ( empty( $items ) ) { + return ''; + } + return reset( $items )->get_name(); + } + + /** + * Get the previous subscription product name before a plan switch. + * + * @return string + */ + protected function get_previous_subscription_product() { + $subscription = $this->get_current_subscription(); + if ( ! $subscription ) { + return ''; + } + + $switch_orders = $subscription->get_related_orders( 'all', 'switch' ); + if ( empty( $switch_orders ) ) { + return ''; + } + + // Get the most recent switch order. + $switch_order = reset( $switch_orders ); + if ( is_numeric( $switch_order ) ) { + $switch_order = \wc_get_order( $switch_order ); + } + if ( ! $switch_order ) { + return ''; + } + + $switch_data = $switch_order->get_meta( '_subscription_switch_data' ); + if ( ! empty( $switch_data ) && is_array( $switch_data ) ) { + // Switch data is keyed by subscription ID. + $sub_switch_data = isset( $switch_data[ $subscription->get_id() ] ) ? $switch_data[ $subscription->get_id() ] : reset( $switch_data ); + if ( ! empty( $sub_switch_data['old_product_id'] ) ) { + $old_product = \wc_get_product( $sub_switch_data['old_product_id'] ); + if ( $old_product ) { + return $old_product->get_name(); + } + } + } + + return ''; + } + + /** + * Get the coupon code applied to the current subscription. + * + * @return string + */ + protected function get_current_subscription_coupon_code() { + $subscription = $this->get_current_subscription(); + if ( ! $subscription ) { + return ''; + } + + $coupons = $subscription->get_coupon_codes(); + if ( ! empty( $coupons ) ) { + return reset( $coupons ); + } + + $parent_order = $subscription->get_parent(); + if ( $parent_order ) { + $parent_coupons = $parent_order->get_coupon_codes(); + if ( ! empty( $parent_coupons ) ) { + return reset( $parent_coupons ); + } + } + + return ''; + } + + /** + * Get the last payment amount. + * + * @return string + */ + protected function get_last_payment_amount() { + $subscription = $this->get_current_subscription(); + if ( ! $subscription ) { + return ''; + } + + $last_order = $this->get_last_successful_order( $subscription ); + return $last_order ? $last_order->get_total() : ''; + } + + /** + * Get the last payment date. + * + * @return string + */ + protected function get_last_payment_date() { + $subscription = $this->get_current_subscription(); + if ( ! $subscription ) { + return ''; + } + + $last_order = $this->get_last_successful_order( $subscription ); + if ( ! $last_order ) { + return ''; + } + + $date_paid = $last_order->get_date_paid(); + if ( empty( $date_paid ) ) { + return ''; + } + + return $date_paid->date( self::DATE_FORMAT ); } } diff --git a/tests/mocks/wc-mocks.php b/tests/mocks/wc-mocks.php index 618c27f79e..b45a1921dc 100644 --- a/tests/mocks/wc-mocks.php +++ b/tests/mocks/wc-mocks.php @@ -120,8 +120,50 @@ public function save() {} $orders_database = []; $subscriptions_database = []; +class WC_Order_Item_Product { + private $data = []; + public function __construct( $data = [] ) { + $this->data = $data; + } + public function get_name() { + return $this->data['name'] ?? ''; + } + public function get_product_id() { + return $this->data['product_id'] ?? 0; + } +} + +class WC_Product { + private $data = []; + private $meta = []; + public function __construct( $data = [] ) { + $this->data = $data; + if ( isset( $data['meta'] ) ) { + $this->meta = $data['meta']; + } + } + public function get_id() { + return $this->data['id'] ?? 0; + } + public function get_name() { + return $this->data['name'] ?? ''; + } + public function get_type() { + return $this->data['type'] ?? 'simple'; + } + public function get_children() { + return $this->data['children'] ?? []; + } + public function get_meta( $key, $single = true ) { + return $this->meta[ $key ] ?? ''; + } +} + + +$products_database = []; + class WC_Order { - public $data = [ 'items' => [] ]; + public $data = []; public $meta = []; public function __construct( $data ) { global $orders_database; @@ -129,11 +171,14 @@ public function __construct( $data ) { if ( ! isset( $data['date_paid'] ) ) { $data['date_paid'] = gmdate( 'Y-m-d H:i:s' ); } - $this->data = array_merge( $data, $this->data ); + if ( ! isset( $data['items'] ) ) { + $data['items'] = []; + } + $this->data = $data; if ( $data['status'] === 'completed' ) { // Update customer's total spent. $customer = new WC_Customer( $this->get_customer_id() ); - $total_spent = $customer->get_total_spent() + $this->get_total(); + $total_spent = (float) $customer->get_total_spent() + (float) $this->get_total(); update_user_meta( $customer->get_id(), 'wc_total_spent', $total_spent ); // Add the order to the mock DB. } @@ -161,6 +206,9 @@ public function get_items() { return $this->data['items']; } public function get_date_paid() { + if ( empty( $this->data['date_paid'] ) ) { + return null; + } return new WC_DateTime( $this->data['date_paid'] ); } public function get_date_completed() { @@ -172,6 +220,9 @@ public function get_total() { public function get_status() { return $this->data['status']; } + public function get_coupon_codes() { + return $this->data['coupon_codes'] ?? []; + } } class WC_Subscription { @@ -239,11 +290,28 @@ public function get_billing_period() { public function get_billing_interval() { return $this->data['billing_interval']; } - public function get_last_order() { - if ( ! empty( $this->orders ) ) { - return end( $this->orders ); + public function get_last_order( $output = 'all', $types = [], $exclude_statuses = [] ) { + if ( empty( $this->orders ) ) { + return false; } - return false; + if ( ! empty( $exclude_statuses ) ) { + foreach ( $this->orders as $order ) { + if ( ! $order->has_status( $exclude_statuses ) ) { + return $order; + } + } + return false; + } + return reset( $this->orders ); + } + public function get_related_orders( $output = 'all', $type = '' ) { + return $this->data['related_orders'][ $type ] ?? []; + } + public function get_coupon_codes() { + return $this->data['coupon_codes'] ?? []; + } + public function get_parent() { + return $this->data['parent_order'] ?? null; } public function get_date( $type ) { return $this->data['dates'][ $type ] ?? 0; @@ -275,6 +343,11 @@ public function save() { class WC_Subscriptions { } +if ( ! class_exists( 'WC_Subscriptions_Product' ) ) { + class WC_Subscriptions_Product { + } +} + function wc_create_order( $data ) { return new WC_Order( $data ); } @@ -322,6 +395,10 @@ function wc_bool_to_string( $bool ) { } function wc_get_orders( $args ) { global $orders_database; + // For simplicity, this mock will only return a single page of results. + if ( isset( $args['page'] ) && $args['page'] > 1 ) { + return []; + } $orders = $orders_database; if ( isset( $args['customer_id'] ) ) { // Filter by customer. @@ -351,5 +428,29 @@ function( $a, $b ) { } function wc_customer_bought_product( $customer_email, $user_id, $product_id ) { + global $orders_database; + foreach ( $orders_database as $order ) { + if ( $order->get_customer_id() !== $user_id ) { + continue; + } + foreach ( $order->get_items() as $item ) { + if ( $item->get_product_id() === $product_id ) { + return true; + } + } + } return false; } +function wc_get_order( $order_id ) { + global $orders_database; + foreach ( $orders_database as $order ) { + if ( $order->get_id() === $order_id ) { + return $order; + } + } + return false; +} +function wc_get_product( $product_id ) { + global $products_database; + return $products_database[ $product_id ] ?? false; +} diff --git a/tests/unit-tests/reader-activation-sync/class-test-donation-metadata.php b/tests/unit-tests/reader-activation-sync/class-test-donation-metadata.php new file mode 100644 index 0000000000..f83946fcbc --- /dev/null +++ b/tests/unit-tests/reader-activation-sync/class-test-donation-metadata.php @@ -0,0 +1,435 @@ + 'donor_test_user', + 'user_email' => 'donor@example.com', + 'user_pass' => 'password', + ]; + + public static function set_up_before_class() { + self::$user_id = wp_insert_user( self::USER_DATA ); + self::set_up_donation_products(); + } + + private static function set_up_donation_products() { + global $products_database; + + // Register the parent grouped product. + $products_database[ self::$donation_product_id ] = new WC_Product( + [ + 'id' => self::$donation_product_id, + 'name' => 'Newspack Donation', + 'type' => 'grouped', + 'children' => [ + self::$donation_once_product_id, + self::$donation_month_product_id, + self::$donation_year_product_id, + ], + ] + ); + + // Register child products with the types/meta the Donations class expects. + $products_database[ self::$donation_once_product_id ] = new WC_Product( + [ + 'id' => self::$donation_once_product_id, + 'name' => 'Donation once', + 'type' => 'simple', + ] + ); + $products_database[ self::$donation_month_product_id ] = new WC_Product( + [ + 'id' => self::$donation_month_product_id, + 'name' => 'Donation month', + 'type' => 'subscription', + 'meta' => [ '_subscription_period' => 'month' ], + ] + ); + $products_database[ self::$donation_year_product_id ] = new WC_Product( + [ + 'id' => self::$donation_year_product_id, + 'name' => 'Donation year', + 'type' => 'subscription', + 'meta' => [ '_subscription_period' => 'year' ], + ] + ); + + update_option( Donations::DONATION_PRODUCT_ID_OPTION, self::$donation_product_id ); + } + + public function set_up() { + global $orders_database, $subscriptions_database; + $orders_database = []; + $subscriptions_database = []; + wp_delete_user( self::$user_id ); + self::$user_id = wp_insert_user( self::USER_DATA ); + } + + /** + * Helper to create a donation subscription (items contain a donation product). + * + * @param array $overrides Arguments to override the defaults when creating the subscription. + */ + private function create_donation_subscription( $overrides = [] ) { + $defaults = [ + 'customer_id' => self::$user_id, + 'status' => 'active', + 'total' => '10.00', + 'billing_period' => 'month', + 'billing_interval' => 1, + 'date_paid' => '2025-01-15 12:00:00', + 'dates' => [ + 'start' => '2025-01-15 12:00:00', + 'end' => 0, + 'next_payment' => '2025-02-15 12:00:00', + ], + 'items' => [ + new WC_Order_Item_Product( + [ + 'name' => 'Donation month', + 'product_id' => self::$donation_month_product_id, + ] + ), + ], + ]; + + return wcs_create_subscription( array_merge( $defaults, $overrides ) ); + } + + /** + * Helper to create a non-donation subscription. + * + * @param array $overrides Arguments to override the defaults when creating the subscription. + */ + private function create_non_donation_subscription( $overrides = [] ) { + $defaults = [ + 'customer_id' => self::$user_id, + 'status' => 'active', + 'total' => '30.00', + 'billing_period' => 'month', + 'billing_interval' => 1, + 'date_paid' => '2025-01-15 12:00:00', + 'dates' => [ + 'start' => '2025-01-15 12:00:00', + 'end' => 0, + 'next_payment' => '2025-02-15 12:00:00', + ], + 'items' => [ + new WC_Order_Item_Product( + [ + 'name' => 'Premium Plan', + 'product_id' => 999, + ] + ), + ], + ]; + + return wcs_create_subscription( array_merge( $defaults, $overrides ) ); + } + + public function test_get_fields_returns_expected_keys() { + $fields = Donation::get_fields(); + $this->assertArrayHasKey( 'Donor_Status', $fields ); + $this->assertArrayHasKey( 'Active_Donation_Count', $fields ); + $this->assertArrayHasKey( 'Current_Donation_Start_Date', $fields ); + $this->assertArrayHasKey( 'Current_Donation_End_Date', $fields ); + $this->assertArrayHasKey( 'Current_Donation_Cycle', $fields ); + $this->assertArrayHasKey( 'Current_Recurring_Donation', $fields ); + $this->assertArrayHasKey( 'Next_Donation_Date', $fields ); + $this->assertArrayHasKey( 'Current_Donation_Product_Name', $fields ); + $this->assertArrayHasKey( 'Previous_Donation_Product', $fields ); + $this->assertArrayHasKey( 'Previous_Donation_Amount', $fields ); + $this->assertArrayHasKey( 'Last_Donation_Amount', $fields ); + $this->assertArrayHasKey( 'Last_Donation_Date', $fields ); + $this->assertCount( 12, $fields ); + } + + public function test_section_name() { + $this->assertSame( 'Donation', Donation::get_section_name() ); + } + + public function test_metadata_empty_for_nonexistent_user() { + $metadata = ( new Donation( 0 ) )->get_metadata(); + $this->assertEmpty( $metadata ); + } + + public function test_ignores_non_donation_subscriptions() { + $this->create_non_donation_subscription(); + + $metadata = ( new Donation( self::$user_id ) )->get_metadata(); + $this->assertSame( '', $metadata['Donor_Status'] ); + $this->assertSame( 0, $metadata['Active_Donation_Count'] ); + } + + public function test_monthly_donor_status() { + $this->create_donation_subscription( [ 'billing_period' => 'month' ] ); + + $metadata = ( new Donation( self::$user_id ) )->get_metadata(); + $this->assertSame( 'Monthly Donor', $metadata['Donor_Status'] ); + } + + public function test_yearly_donor_status() { + $this->create_donation_subscription( + [ + 'billing_period' => 'year', + 'items' => [ + new WC_Order_Item_Product( + [ + 'name' => 'Donation year', + 'product_id' => self::$donation_year_product_id, + ] + ), + ], + ] + ); + + $metadata = ( new Donation( self::$user_id ) )->get_metadata(); + $this->assertSame( 'Yearly Donor', $metadata['Donor_Status'] ); + } + + public function test_ex_monthly_donor_status() { + $this->create_donation_subscription( + [ + 'status' => 'cancelled', + 'billing_period' => 'month', + 'dates' => [ + 'start' => '2025-01-01 00:00:00', + 'end' => '2025-06-01 00:00:00', + 'next_payment' => 0, + ], + ] + ); + + $metadata = ( new Donation( self::$user_id ) )->get_metadata(); + $this->assertSame( 'Ex-Monthly Donor', $metadata['Donor_Status'] ); + } + + public function test_ex_yearly_donor_status() { + $this->create_donation_subscription( + [ + 'status' => 'expired', + 'billing_period' => 'year', + 'dates' => [ + 'start' => '2024-01-01 00:00:00', + 'end' => '2025-01-01 00:00:00', + 'next_payment' => 0, + ], + 'items' => [ + new WC_Order_Item_Product( + [ + 'name' => 'Donation year', + 'product_id' => self::$donation_year_product_id, + ] + ), + ], + ] + ); + + $metadata = ( new Donation( self::$user_id ) )->get_metadata(); + $this->assertSame( 'Ex-Yearly Donor', $metadata['Donor_Status'] ); + } + + public function test_donor_status_with_non_standard_billing_period() { + $this->create_donation_subscription( [ 'billing_period' => 'week' ] ); + $metadata = ( new Donation( self::$user_id ) )->get_metadata(); + $this->assertSame( 'Donor', $metadata['Donor_Status'] ); + } + + public function test_active_donation_count() { + $this->create_donation_subscription(); + $this->create_donation_subscription( [ 'total' => '20.00' ] ); + $this->create_non_donation_subscription(); // Should not count. + + $metadata = ( new Donation( self::$user_id ) )->get_metadata(); + $this->assertSame( 2, $metadata['Active_Donation_Count'] ); + } + + public function test_donation_dates() { + $this->create_donation_subscription( + [ + 'dates' => [ + 'start' => '2025-02-14 12:00:00', + 'end' => 0, + 'next_payment' => '2025-03-14 12:00:00', + ], + ] + ); + + $metadata = ( new Donation( self::$user_id ) )->get_metadata(); + $this->assertSame( '2025-02-14 12:00:00', $metadata['Current_Donation_Start_Date'] ); + $this->assertSame( '', $metadata['Current_Donation_End_Date'] ); + $this->assertSame( '2025-03-14 12:00:00', $metadata['Next_Donation_Date'] ); + } + + public function test_donation_billing_cycle_and_amount() { + $this->create_donation_subscription( + [ + 'billing_period' => 'year', + 'total' => '120.00', + 'items' => [ + new WC_Order_Item_Product( + [ + 'name' => 'Donation year', + 'product_id' => self::$donation_year_product_id, + ] + ), + ], + ] + ); + + $metadata = ( new Donation( self::$user_id ) )->get_metadata(); + $this->assertSame( 'year', $metadata['Current_Donation_Cycle'] ); + $this->assertSame( '120.00', $metadata['Current_Recurring_Donation'] ); + } + + public function test_donation_product_name_from_subscription() { + $this->create_donation_subscription( + [ + 'items' => [ + new WC_Order_Item_Product( + [ + 'name' => 'Monthly Donation', + 'product_id' => self::$donation_month_product_id, + ] + ), + ], + ] + ); + + $metadata = ( new Donation( self::$user_id ) )->get_metadata(); + $this->assertSame( 'Monthly Donation', $metadata['Current_Donation_Product_Name'] ); + } + + public function test_last_donation_amount_from_subscription() { + $order = wc_create_order( + [ + 'customer_id' => self::$user_id, + 'status' => 'completed', + 'total' => '10.00', + 'date_paid' => '2025-04-01 10:00:00', + ] + ); + $this->create_donation_subscription( [ 'orders' => [ $order ] ] ); + + $metadata = ( new Donation( self::$user_id ) )->get_metadata(); + $this->assertSame( '10.00', $metadata['Last_Donation_Amount'] ); + $this->assertSame( '2025-04-01 10:00:00', $metadata['Last_Donation_Date'] ); + } + + public function test_previous_donation_amount_from_switch() { + global $subscriptions_database; + + // Create an "old" subscription to represent the pre-switch state. + $old_sub = wcs_create_subscription( + [ + 'customer_id' => self::$user_id, + 'status' => 'cancelled', + 'total' => '5.00', + 'billing_period' => 'month', + 'billing_interval' => 1, + 'date_paid' => '2025-01-01 12:00:00', + 'dates' => [ + 'start' => '2025-01-01 12:00:00', + 'end' => '2025-03-01 12:00:00', + 'next_payment' => 0, + ], + 'items' => [ + new WC_Order_Item_Product( + [ + 'name' => 'Donation month', + 'product_id' => self::$donation_month_product_id, + ] + ), + ], + ] + ); + + $current_sub = $this->create_donation_subscription( [ 'total' => '15.00' ] ); + + $switch_order = wc_create_order( + [ + 'customer_id' => self::$user_id, + 'status' => 'completed', + 'total' => '15.00', + 'date_paid' => '2025-03-01 12:00:00', + 'meta' => [ + '_subscription_switch_data' => [ + $current_sub->get_id() => [ 'old_subscription_id' => $old_sub->get_id() ], + ], + ], + ] + ); + + $current_sub->data['related_orders'] = [ 'switch' => [ $switch_order ] ]; + + $metadata = ( new Donation( self::$user_id ) )->get_metadata(); + $this->assertSame( '5.00', $metadata['Previous_Donation_Amount'] ); + } + + public function test_previous_donation_amount_empty_without_switch() { + $this->create_donation_subscription(); + $metadata = ( new Donation( self::$user_id ) )->get_metadata(); + $this->assertSame( '', $metadata['Previous_Donation_Amount'] ); + } + + public function test_prefers_active_donation_over_cancelled() { + $this->create_donation_subscription( + [ + 'status' => 'cancelled', + 'total' => '5.00', + 'dates' => [ + 'start' => '2024-01-01 00:00:00', + 'end' => '2024-06-01 00:00:00', + 'next_payment' => 0, + ], + ] + ); + $this->create_donation_subscription( + [ + 'status' => 'active', + 'total' => '20.00', + 'billing_period' => 'year', + 'items' => [ + new WC_Order_Item_Product( + [ + 'name' => 'Donation year', + 'product_id' => self::$donation_year_product_id, + ] + ), + ], + ] + ); + + $metadata = ( new Donation( self::$user_id ) )->get_metadata(); + $this->assertSame( 'Yearly Donor', $metadata['Donor_Status'] ); + $this->assertSame( '20.00', $metadata['Current_Recurring_Donation'] ); + } +} diff --git a/tests/unit-tests/reader-activation-sync/class-test-subscription-metadata.php b/tests/unit-tests/reader-activation-sync/class-test-subscription-metadata.php new file mode 100644 index 0000000000..a902cfd014 --- /dev/null +++ b/tests/unit-tests/reader-activation-sync/class-test-subscription-metadata.php @@ -0,0 +1,474 @@ + 'sub_test_user', + 'user_email' => 'subtest@example.com', + 'user_pass' => 'password', + ]; + + public static function set_up_before_class() { + self::$user_id = wp_insert_user( self::USER_DATA ); + } + + public function set_up() { + global $orders_database, $subscriptions_database; + $orders_database = []; + $subscriptions_database = []; + wp_delete_user( self::$user_id ); + self::$user_id = wp_insert_user( self::USER_DATA ); + } + + /** + * Helper to create a subscription with sensible defaults. + * + * @param array $overrides Optional overrides for subscription properties. + */ + private function create_subscription( $overrides = [] ) { + $defaults = [ + 'customer_id' => self::$user_id, + 'status' => 'active', + 'total' => '10.00', + 'billing_period' => 'month', + 'billing_interval' => 1, + 'date_paid' => '2025-01-15 12:00:00', + 'dates' => [ + 'start' => '2025-01-15 12:00:00', + 'end' => 0, + 'next_payment' => '2025-02-15 12:00:00', + ], + 'items' => [ + new WC_Order_Item_Product( + [ + 'name' => 'Premium Plan', + 'product_id' => 100, + ] + ), + ], + ]; + + return wcs_create_subscription( array_merge( $defaults, $overrides ) ); + } + + /** + * Helper to create a completed order. + * + * @param array $overrides Optional overrides for order properties. + */ + private function create_order( $overrides = [] ) { + $defaults = [ + 'customer_id' => self::$user_id, + 'status' => 'completed', + 'total' => '10.00', + 'date_paid' => '2025-01-15 12:00:00', + ]; + return wc_create_order( array_merge( $defaults, $overrides ) ); + } + + public function test_get_fields_returns_expected_keys() { + $fields = Subscription::get_fields(); + $this->assertArrayHasKey( 'Subscriber_Status', $fields ); + $this->assertArrayHasKey( 'Active_Subscription_Count', $fields ); + $this->assertArrayHasKey( 'Current_Subscription_Start_Date', $fields ); + $this->assertArrayHasKey( 'Current_Subscription_End_Date', $fields ); + $this->assertArrayHasKey( 'Subscription_Cancellation_Reason', $fields ); + $this->assertArrayHasKey( 'Current_Subscription_Billing_Cycle', $fields ); + $this->assertArrayHasKey( 'Current_Subscription_Recurring_Payment', $fields ); + $this->assertArrayHasKey( 'Current_Subscription_Next_Payment_Date', $fields ); + $this->assertArrayHasKey( 'Current_Subscription_Product_Name', $fields ); + $this->assertArrayHasKey( 'Previous_Subscription_Product', $fields ); + $this->assertArrayHasKey( 'Current_Subscription_Coupon_Code', $fields ); + $this->assertArrayHasKey( 'Last_Payment_Amount', $fields ); + $this->assertArrayHasKey( 'Last_Payment_Date', $fields ); + $this->assertCount( 13, $fields ); + } + + public function test_metadata_empty_for_nonexistent_user() { + $metadata_obj = new Subscription( 0 ); + $metadata = $metadata_obj->get_metadata(); + $this->assertEmpty( $metadata ); + } + + public function test_metadata_empty_when_no_subscriptions() { + $metadata_obj = new Subscription( self::$user_id ); + $metadata = $metadata_obj->get_metadata(); + + $this->assertNotEmpty( $metadata ); + $this->assertSame( '', $metadata['Subscriber_Status'] ); + $this->assertSame( 0, $metadata['Active_Subscription_Count'] ); + $this->assertSame( '', $metadata['Current_Subscription_Start_Date'] ); + } + + public function test_active_subscription_status() { + $this->create_subscription( [ 'status' => 'active' ] ); + $metadata = ( new Subscription( self::$user_id ) )->get_metadata(); + $this->assertSame( 'active', $metadata['Subscriber_Status'] ); + } + + public function test_active_subscription_count() { + $this->create_subscription( [ 'status' => 'active' ] ); + $this->create_subscription( + [ + 'status' => 'active', + 'total' => '20.00', + ] + ); + $this->create_subscription( + [ + 'status' => 'cancelled', + 'total' => '5.00', + ] + ); + + $metadata = ( new Subscription( self::$user_id ) )->get_metadata(); + $this->assertSame( 2, $metadata['Active_Subscription_Count'] ); + } + + public function test_pending_cancel_counted_as_active() { + $this->create_subscription( [ 'status' => 'pending-cancel' ] ); + $metadata = ( new Subscription( self::$user_id ) )->get_metadata(); + $this->assertSame( 1, $metadata['Active_Subscription_Count'] ); + } + + public function test_start_date_formatted() { + $this->create_subscription( + [ + 'dates' => [ + 'start' => '2025-03-10 08:00:00', + 'end' => 0, + 'next_payment' => '2025-04-10 08:00:00', + ], + ] + ); + $metadata = ( new Subscription( self::$user_id ) )->get_metadata(); + $this->assertSame( '2025-03-10 08:00:00', $metadata['Current_Subscription_Start_Date'] ); + } + + public function test_end_date_empty_when_zero() { + $this->create_subscription( + [ + 'dates' => [ + 'start' => '2025-01-01 00:00:00', + 'end' => 0, + 'next_payment' => '2025-02-01 00:00:00', + ], + ] + ); + $metadata = ( new Subscription( self::$user_id ) )->get_metadata(); + $this->assertSame( '', $metadata['Current_Subscription_End_Date'] ); + } + + public function test_end_date_formatted_when_set() { + $this->create_subscription( + [ + 'status' => 'cancelled', + 'dates' => [ + 'start' => '2025-01-01 00:00:00', + 'end' => '2025-06-01 00:00:00', + 'next_payment' => 0, + ], + ] + ); + $metadata = ( new Subscription( self::$user_id ) )->get_metadata(); + $this->assertSame( '2025-06-01 00:00:00', $metadata['Current_Subscription_End_Date'] ); + } + + public function test_billing_cycle() { + $this->create_subscription( [ 'billing_period' => 'year' ] ); + $metadata = ( new Subscription( self::$user_id ) )->get_metadata(); + $this->assertSame( 'year', $metadata['Current_Subscription_Billing_Cycle'] ); + } + + public function test_recurring_payment() { + $this->create_subscription( [ 'total' => '25.50' ] ); + $metadata = ( new Subscription( self::$user_id ) )->get_metadata(); + $this->assertSame( '25.50', $metadata['Current_Subscription_Recurring_Payment'] ); + } + + public function test_next_payment_date() { + $this->create_subscription( + [ + 'dates' => [ + 'start' => '2025-01-01 00:00:00', + 'end' => 0, + 'next_payment' => '2025-07-01 00:00:00', + ], + ] + ); + $metadata = ( new Subscription( self::$user_id ) )->get_metadata(); + $this->assertSame( '2025-07-01 00:00:00', $metadata['Current_Subscription_Next_Payment_Date'] ); + } + + public function test_next_payment_date_empty_when_zero() { + $this->create_subscription( + [ + 'status' => 'cancelled', + 'dates' => [ + 'start' => '2025-01-01 00:00:00', + 'end' => '2025-03-01 00:00:00', + 'next_payment' => 0, + ], + ] + ); + $metadata = ( new Subscription( self::$user_id ) )->get_metadata(); + $this->assertSame( '', $metadata['Current_Subscription_Next_Payment_Date'] ); + } + + public function test_product_name() { + $this->create_subscription( + [ + 'items' => [ + new WC_Order_Item_Product( + [ + 'name' => 'Gold Membership', + 'product_id' => 200, + ] + ), + ], + ] + ); + $metadata = ( new Subscription( self::$user_id ) )->get_metadata(); + $this->assertSame( 'Gold Membership', $metadata['Current_Subscription_Product_Name'] ); + } + + public function test_product_name_empty_without_items() { + $this->create_subscription( [ 'items' => [] ] ); + $metadata = ( new Subscription( self::$user_id ) )->get_metadata(); + $this->assertSame( '', $metadata['Current_Subscription_Product_Name'] ); + } + + public function test_cancellation_reason_user_cancelled() { + $this->create_subscription( + [ + 'status' => 'cancelled', + 'meta' => [ + Subscriptions_Meta::CANCELLATION_REASON_META_KEY => Subscriptions_Meta::CANCELLATION_REASON_USER_CANCELLED, + ], + 'dates' => [ + 'start' => '2025-01-01 00:00:00', + 'end' => '2025-03-01 00:00:00', + 'next_payment' => 0, + ], + ] + ); + $metadata = ( new Subscription( self::$user_id ) )->get_metadata(); + $this->assertSame( Subscriptions_Meta::CANCELLATION_REASON_USER_CANCELLED, $metadata['Subscription_Cancellation_Reason'] ); + } + + public function test_cancellation_reason_excludes_pending_cancel() { + $this->create_subscription( + [ + 'status' => 'pending-cancel', + 'meta' => [ + Subscriptions_Meta::CANCELLATION_REASON_META_KEY => Subscriptions_Meta::CANCELLATION_REASON_USER_PENDING_CANCEL, + ], + ] + ); + $metadata = ( new Subscription( self::$user_id ) )->get_metadata(); + $this->assertSame( '', $metadata['Subscription_Cancellation_Reason'] ); + } + + public function test_cancellation_reason_excludes_admin_pending_cancel() { + $this->create_subscription( + [ + 'status' => 'pending-cancel', + 'meta' => [ + Subscriptions_Meta::CANCELLATION_REASON_META_KEY => Subscriptions_Meta::CANCELLATION_REASON_ADMIN_PENDING_CANCEL, + ], + ] + ); + $metadata = ( new Subscription( self::$user_id ) )->get_metadata(); + $this->assertSame( '', $metadata['Subscription_Cancellation_Reason'] ); + } + + public function test_cancellation_reason_empty_when_not_set() { + $this->create_subscription( [ 'status' => 'cancelled' ] ); + $metadata = ( new Subscription( self::$user_id ) )->get_metadata(); + $this->assertSame( '', $metadata['Subscription_Cancellation_Reason'] ); + } + + public function test_coupon_code_from_subscription() { + $this->create_subscription( [ 'coupon_codes' => [ 'SAVE10' ] ] ); + $metadata = ( new Subscription( self::$user_id ) )->get_metadata(); + $this->assertSame( 'SAVE10', $metadata['Current_Subscription_Coupon_Code'] ); + } + + public function test_coupon_code_from_parent_order() { + $parent_order = $this->create_order( [ 'coupon_codes' => [ 'WELCOME' ] ] ); + $this->create_subscription( [ 'parent_order' => $parent_order ] ); + $metadata = ( new Subscription( self::$user_id ) )->get_metadata(); + $this->assertSame( 'WELCOME', $metadata['Current_Subscription_Coupon_Code'] ); + } + + public function test_coupon_code_subscription_takes_precedence() { + $parent_order = $this->create_order( [ 'coupon_codes' => [ 'PARENT_COUPON' ] ] ); + $this->create_subscription( + [ + 'coupon_codes' => [ 'SUB_COUPON' ], + 'parent_order' => $parent_order, + ] + ); + $metadata = ( new Subscription( self::$user_id ) )->get_metadata(); + $this->assertSame( 'SUB_COUPON', $metadata['Current_Subscription_Coupon_Code'] ); + } + + public function test_last_payment_amount_and_date() { + $order = $this->create_order( + [ + 'total' => '15.00', + 'date_paid' => '2025-05-20 10:00:00', + ] + ); + $this->create_subscription( [ 'orders' => [ $order ] ] ); + $metadata = ( new Subscription( self::$user_id ) )->get_metadata(); + $this->assertSame( '15.00', $metadata['Last_Payment_Amount'] ); + $this->assertSame( '2025-05-20 10:00:00', $metadata['Last_Payment_Date'] ); + } + + public function test_last_payment_excludes_failed_orders() { + $good_order = $this->create_order( + [ + 'total' => '10.00', + 'date_paid' => '2025-04-01 10:00:00', + ] + ); + $failed_order = $this->create_order( + [ + 'total' => '99.00', + 'status' => 'failed', + 'date_paid' => '2025-05-01 10:00:00', + ] + ); + $this->create_subscription( [ 'orders' => [ $good_order, $failed_order ] ] ); + $metadata = ( new Subscription( self::$user_id ) )->get_metadata(); + $this->assertSame( '10.00', $metadata['Last_Payment_Amount'] ); + } + + public function test_previous_product_from_switch_order() { + global $products_database; + $products_database[50] = new WC_Product( + [ + 'id' => 50, + 'name' => 'Basic Plan', + ] + ); + + $sub = $this->create_subscription(); + + $switch_order = $this->create_order( + [ + 'meta' => [ + '_subscription_switch_data' => [ + $sub->get_id() => [ 'old_product_id' => 50 ], + ], + ], + ] + ); + + // Update the subscription's related orders to include the switch order. + $sub->data['related_orders'] = [ 'switch' => [ $switch_order ] ]; + + $metadata = ( new Subscription( self::$user_id ) )->get_metadata(); + $this->assertSame( 'Basic Plan', $metadata['Previous_Subscription_Product'] ); + + unset( $products_database[50] ); + } + + public function test_previous_product_empty_without_switch() { + $this->create_subscription(); + $metadata = ( new Subscription( self::$user_id ) )->get_metadata(); + $this->assertSame( '', $metadata['Previous_Subscription_Product'] ); + } + + public function test_current_subscription_prefers_active_over_cancelled() { + // Create a cancelled subscription first. + $this->create_subscription( + [ + 'status' => 'cancelled', + 'total' => '5.00', + 'billing_period' => 'month', + 'dates' => [ + 'start' => '2024-01-01 00:00:00', + 'end' => '2024-06-01 00:00:00', + 'next_payment' => 0, + ], + ] + ); + // Then an active subscription. + $this->create_subscription( + [ + 'status' => 'active', + 'total' => '20.00', + 'billing_period' => 'year', + 'dates' => [ + 'start' => '2025-01-01 00:00:00', + 'end' => 0, + 'next_payment' => '2026-01-01 00:00:00', + ], + ] + ); + + $metadata = ( new Subscription( self::$user_id ) )->get_metadata(); + $this->assertSame( 'active', $metadata['Subscriber_Status'] ); + $this->assertSame( '20.00', $metadata['Current_Subscription_Recurring_Payment'] ); + $this->assertSame( 'year', $metadata['Current_Subscription_Billing_Cycle'] ); + } + + public function test_falls_back_to_cancelled_subscription_when_no_active() { + $this->create_subscription( + [ + 'status' => 'cancelled', + 'total' => '15.00', + 'billing_period' => 'month', + 'dates' => [ + 'start' => '2025-01-01 00:00:00', + 'end' => '2025-06-01 00:00:00', + 'next_payment' => 0, + ], + ] + ); + + $metadata = ( new Subscription( self::$user_id ) )->get_metadata(); + $this->assertSame( 'cancelled', $metadata['Subscriber_Status'] ); + $this->assertSame( '15.00', $metadata['Current_Subscription_Recurring_Payment'] ); + } + + public function test_is_relevant_subscription_excludes_donations() { + // Create a subscription with a donation product item. + // Since Donations::is_donation_order checks product IDs against donation product IDs, + // and there are no donation products set up in the test env, all subscriptions + // should be considered non-donation (relevant for Subscription class). + $this->create_subscription(); + + $metadata = ( new Subscription( self::$user_id ) )->get_metadata(); + $this->assertSame( 'active', $metadata['Subscriber_Status'] ); + $this->assertSame( 1, $metadata['Active_Subscription_Count'] ); + } + + public function test_section_name() { + $this->assertSame( 'Subscription', Subscription::get_section_name() ); + } +}