From 319b9af96e5d22618fb2a205194c6f37cee43a07 Mon Sep 17 00:00:00 2001 From: Leo Germani Date: Fri, 12 Jan 2024 14:41:45 -0300 Subject: [PATCH 1/4] feat: add cli command to fix stripe covered fees --- .../woocommerce/class-woocommerce-cli.php | 218 ++++++++++++++++++ .../class-woocommerce-connection.php | 15 +- .../class-woocommerce-cover-fees.php | 50 +++- 3 files changed, 271 insertions(+), 12 deletions(-) create mode 100644 includes/reader-revenue/woocommerce/class-woocommerce-cli.php diff --git a/includes/reader-revenue/woocommerce/class-woocommerce-cli.php b/includes/reader-revenue/woocommerce/class-woocommerce-cli.php new file mode 100644 index 0000000000..77ea17cd9c --- /dev/null +++ b/includes/reader-revenue/woocommerce/class-woocommerce-cli.php @@ -0,0 +1,218 @@ +] + * : Accepted values: table, csv, json, count, yaml. Default: table + * + * @param array $args Args. + * @param array $assoc_args Assoc args. + */ + public function list_subscriptions_missing_fee( $args, $assoc_args ) { + + $format = isset( $assoc_args['format'] ) ? $assoc_args['format'] : 'table'; + + $subscriptions = $this->get_all_old_subscriptions(); + + $subscriptions = array_filter( + $subscriptions, + function( $subscription ) { + return empty( $subscription->get_fees() ); + } + ); + + if ( empty( $subscriptions ) ) { + WP_CLI::success( 'No subscriptions missing fees found.' ); + return; + } + + WP_CLI::success( 'Subscriptions missing fees:' ); + $this->output_subscriptions( $subscriptions, $format ); + + } + + /** + * Lists the subscriptions that were already fixed by the fix_subscriptions_missing_fee command. + * + * ## OPTIONS + * + * [--format=] + * : Accepted values: table, csv, json, count, yaml. Default: table + * + * @param array $args Args. + * @param array $assoc_args Assoc args. + */ + public function list_subscriptions_missing_fee_fixed( $args, $assoc_args ) { + + $format = isset( $assoc_args['format'] ) ? $assoc_args['format'] : 'table'; + + $subscriptions = $this->get_all_old_subscriptions(); + + $subscriptions = array_filter( + $subscriptions, + function( $subscription ) { + return ! empty( $subscription->get_fees() ); + } + ); + + if ( empty( $subscriptions ) ) { + WP_CLI::success( 'No subscriptions missing fees found.' ); + return; + } + + WP_CLI::success( 'Fixed subscriptions that had missing fees:' ); + $this->output_subscriptions( $subscriptions, $format ); + + } + + /** + * Updates the subscriptions that had the Stripe cover fee option added in the old way. + * + * Will look for all subcriptions that had fees added in the old way and fix them. + * + * ## OPTIONS + * + * [--dry-run] + * : If set, no changes will be made. + * + * @param [type] $args Args. + * @param [type] $assoc_args Assoc args. + */ + public function fix_subscriptions_missing_fee( $args, $assoc_args ) { + + $dry_run = isset( $assoc_args['dry-run'] ) && $assoc_args['dry-run']; + + if ( ! $dry_run ) { + WP_CLI::line( 'This command will modify the database.' ); + WP_CLI::line( 'Consider running it with --dry-run first to see what it will do.' ); + WP_CLI::confirm( 'Are you sure you want to continue?', $assoc_args ); + } + + $subscriptions = $this->get_all_old_subscriptions(); + + $subscriptions = array_filter( + $subscriptions, + function( $subscription ) { + return empty( $subscription->get_fees() ); + } + ); + + if ( empty( $subscriptions ) ) { + WP_CLI::success( 'No subscriptions missing fees found.' ); + return; + } + + foreach ( $subscriptions as $subscription ) { + + WP_CLI::success( 'Fixing subscription #' . $subscription->get_id() ); + WP_CLI::log( 'Subscription total is ' . $subscription->get_total() ); + + $fee_value = WooCommerce_Cover_Fees::get_fee_value( $subscription->get_total() ); + WP_CLI::log( 'Fee value will be: ' . $fee_value ); + + $fee_display_value = WooCommerce_Cover_Fees::get_fee_display_value( $subscription->get_total() ); + WP_CLI::log( 'Fee display value will be: ' . $fee_display_value ); + + $new_total = WooCommerce_Cover_Fees::get_total_with_fee( $subscription->get_total() ); + WP_CLI::log( 'Subscription new total will be: ' . $new_total ); + + if ( $dry_run ) { + WP_CLI::warning( 'Dry run, not saving.' ); + continue; + } + + $fee_name = sprintf( + // Translators: %s is the fee percentage. + __( 'Transaction fee (%s)', 'newspack-plugin' ), + $fee_display_value + ); + + $fee = (object) [ + 'name' => $fee_name, + 'amount' => $fee_value, + 'tax' => '', + 'taxable' => false, + 'tax_data' => '', + ]; + + $subscription->add_fee( $fee ); + $subscription->legacy_set_total( $new_total ); + $subscription->add_order_note( 'Subscription fee fixed and added via script' ); + // No need to save, we don't want to trigger any hooks. + + WP_CLI::success( 'Subscription #' . $subscription->get_id() . ' fixed.' ); + WP_CLI::log( '' ); + } + + } + + /** + * Outputs a list of subscription in CLI + * + * @param WC_Subscription[] $subscriptions The subscriptions. + * @param string $format The output format. + * @return void + */ + private function output_subscriptions( $subscriptions, $format = 'table' ) { + $subscriptions = array_map( + function( $subscription ) { + return [ + 'id' => $subscription->get_id(), + 'amount' => $subscription->get_total(), + ]; + }, + $subscriptions + ); + + WP_CLI\Utils\format_items( $format, $subscriptions, [ 'id', 'amount' ] ); + } + + /** + * Get all subscriptions that had the Stripe cover fee option added in the old way. + * + * We look at the order notes, and not the subscription meta, because there was a bug where the meta was not stored sometimes. + * + * @return ?WP_Subscription[] The subscriptions. + */ + private function get_all_old_subscriptions() { + global $wpdb; + + // phpcs:ignore + $parent_order_ids = $wpdb->get_col( + "SELECT comment_post_ID FROM {$wpdb->comments} WHERE comment_content LIKE '%transaction fee. The total amount will be updated.'" + ); + + $subscriptions = []; + + foreach ( $parent_order_ids as $parent_order_id ) { + $subs = wcs_get_subscriptions_for_order( $parent_order_id ); + if ( is_array( $subs ) && ! empty( $subs ) ) { + $subscriptions[] = array_shift( $subs ); + } + } + + return $subscriptions; + + } + + +} diff --git a/includes/reader-revenue/woocommerce/class-woocommerce-connection.php b/includes/reader-revenue/woocommerce/class-woocommerce-connection.php index c0c41b1f29..114bcfdfd9 100644 --- a/includes/reader-revenue/woocommerce/class-woocommerce-connection.php +++ b/includes/reader-revenue/woocommerce/class-woocommerce-connection.php @@ -41,6 +41,7 @@ class WooCommerce_Connection { public static function init() { include_once __DIR__ . '/class-woocommerce-order-utm.php'; include_once __DIR__ . '/class-woocommerce-cover-fees.php'; + include_once __DIR__ . '/class-woocommerce-cli.php'; \add_action( 'admin_init', [ __CLASS__, 'disable_woocommerce_setup' ] ); \add_filter( 'option_woocommerce_subscriptions_allow_switching', [ __CLASS__, 'force_allow_subscription_switching' ], 10, 2 ); @@ -50,6 +51,7 @@ public static function init() { \add_filter( 'default_option_woocommerce_subscriptions_allow_switching_nyp_price', [ __CLASS__, 'force_allow_subscription_switching' ], 10, 2 ); \add_filter( 'default_option_woocommerce_subscriptions_enable_retry', [ __CLASS__, 'force_allow_failed_payment_retry' ] ); \add_filter( 'woocommerce_email_enabled_customer_completed_order', [ __CLASS__, 'send_customizable_receipt_email' ], 10, 3 ); + \add_action( 'cli_init', [ __CLASS__, 'register_cli_commands' ] ); // WooCommerce Subscriptions. \add_action( 'add_meta_boxes', [ __CLASS__, 'remove_subscriptions_schedule_meta_box' ], 45 ); @@ -72,6 +74,15 @@ public static function init() { \add_action( 'wp_login', [ __CLASS__, 'sync_reader_on_customer_login' ], 10, 2 ); } + /** + * Register CLI command + * + * @return void + */ + public static function register_cli_commands() { + \WP_CLI::add_command( 'newspack-woocommerce', 'Newspack\\WooCommerce_Cli' ); + } + /** * Check whether everything is set up to enable customer syncing to ESP. * @@ -1172,9 +1183,9 @@ public static function force_allow_subscription_switching( $can_switch, $option_ /** * Force option for allowing retries for failed payments to ON unless the * NEWSPACK_PREVENT_WC_ALLOW_FAILED_PAYMENT_RETRIES_OVERRIDE constant is set. - * + * * See: https://woo.com/document/subscriptions/failed-payment-retry/ - * + * * @param bool $should_retry Whether WooCommerce should automatically retry failed payments. * * @return string Option value. diff --git a/includes/reader-revenue/woocommerce/class-woocommerce-cover-fees.php b/includes/reader-revenue/woocommerce/class-woocommerce-cover-fees.php index 4679465d12..fdb9d2adeb 100644 --- a/includes/reader-revenue/woocommerce/class-woocommerce-cover-fees.php +++ b/includes/reader-revenue/woocommerce/class-woocommerce-cover-fees.php @@ -79,9 +79,9 @@ public static function add_transaction_fee( $cart ) { sprintf( // Translators: %s is the fee percentage. __( 'Transaction fee (%s)', 'newspack-plugin' ), - self::get_fee_display_value() + self::get_cart_fee_display_value() ), - self::get_fee_value() + self::get_cart_fee_value() ); } @@ -175,7 +175,7 @@ public static function render_stripe_input() { 'I’d like to cover the %1$s transaction fee to ensure my full donation goes towards %2$s mission.', 'newspack-plugin' ), - esc_html( self::get_fee_display_value() ), + esc_html( self::get_cart_fee_display_value() ), esc_html( self::get_possessive( get_option( 'blogname' ) ) ) ); } @@ -236,10 +236,11 @@ private static function get_stripe_fee_static_value() { /** * Get the fee display value. + * + * @param float $subtotal The subtotal to calculate the fee for. */ - public static function get_fee_display_value() { - $subtotal = WC()->cart->get_subtotal(); - $total = self::get_total_with_fee(); + public static function get_fee_display_value( $subtotal ) { + $total = self::get_total_with_fee( $subtotal ); // Just one decimal place, please. $flat_percentage = (float) number_format( ( ( $total - $subtotal ) * 100 ) / $subtotal, 1 ); return $flat_percentage . '%'; @@ -247,11 +248,12 @@ public static function get_fee_display_value() { /** * Get the fee value. + * + * @param float $subtotal The subtotal to calculate the fee for. */ - public static function get_fee_value() { + public static function get_fee_value( $subtotal ) { $fee_multiplier = self::get_stripe_fee_multiplier_value(); $fee_static = self::get_stripe_fee_static_value(); - $subtotal = WC()->cart->get_subtotal(); $fee = ( ( ( $subtotal + $fee_static ) / ( 100 - $fee_multiplier ) ) * 100 - $subtotal ); return $fee; } @@ -259,10 +261,38 @@ public static function get_fee_value() { /** * Calculate the adjusted total, taking the fee into account. * + * @param float $subtotal The subtotal to calculate the total for. + * @return float + */ + public static function get_total_with_fee( $subtotal ) { + return $subtotal + self::get_fee_value( $subtotal ); + } + + /** + * Get the fee value for the current cart. + * + * @return float + */ + public static function get_cart_fee_value() { + return self::get_fee_value( WC()->cart->get_subtotal() ); + } + + /** + * Get the fee display value for the current cart. + * + * @return string + */ + public static function get_cart_fee_display_value() { + return self::get_fee_display_value( WC()->cart->get_subtotal() ); + } + + /** + * Get the total with fee for the current cart. + * * @return float */ - private static function get_total_with_fee() { - return WC()->cart->get_subtotal() + self::get_fee_value(); + public static function get_cart_total_with_fee() { + return self::get_total_with_fee( WC()->cart->get_subtotal() ); } } WooCommerce_Cover_Fees::init(); From 08718f5df2626dedb62591c1065b4fd9ffa4c4af Mon Sep 17 00:00:00 2001 From: Leo Germani Date: Fri, 12 Jan 2024 15:12:42 -0300 Subject: [PATCH 2/4] feat: remove duplicates and add details --- .../woocommerce/class-woocommerce-cli.php | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/includes/reader-revenue/woocommerce/class-woocommerce-cli.php b/includes/reader-revenue/woocommerce/class-woocommerce-cli.php index 77ea17cd9c..a781520641 100644 --- a/includes/reader-revenue/woocommerce/class-woocommerce-cli.php +++ b/includes/reader-revenue/woocommerce/class-woocommerce-cli.php @@ -176,14 +176,16 @@ private function output_subscriptions( $subscriptions, $format = 'table' ) { $subscriptions = array_map( function( $subscription ) { return [ - 'id' => $subscription->get_id(), - 'amount' => $subscription->get_total(), + 'id' => $subscription->get_id(), + 'date_created' => $subscription->get_date_created()->__toString(), + 'amount' => $subscription->get_total(), ]; }, $subscriptions ); - WP_CLI\Utils\format_items( $format, $subscriptions, [ 'id', 'amount' ] ); + WP_CLI\Utils\format_items( $format, $subscriptions, [ 'id', 'amount', 'date_created' ] ); + WP_CLI::log( count( $subscriptions ) . ' subscriptions found.' ); } /** @@ -202,11 +204,16 @@ private function get_all_old_subscriptions() { ); $subscriptions = []; + $ids = []; foreach ( $parent_order_ids as $parent_order_id ) { $subs = wcs_get_subscriptions_for_order( $parent_order_id ); if ( is_array( $subs ) && ! empty( $subs ) ) { - $subscriptions[] = array_shift( $subs ); + $sub = array_shift( $subs ); + if ( ! in_array( $sub->get_id(), $ids, true ) ) { + $subscriptions[] = $sub; + $ids[] = $sub->get_id(); + } } } From 6de5edca86267dfb4026903b4a4221c0b65c46cc Mon Sep 17 00:00:00 2001 From: Leo Germani Date: Mon, 15 Jan 2024 09:23:41 -0300 Subject: [PATCH 3/4] feat: add user email to subscriptions output --- .../reader-revenue/woocommerce/class-woocommerce-cli.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/includes/reader-revenue/woocommerce/class-woocommerce-cli.php b/includes/reader-revenue/woocommerce/class-woocommerce-cli.php index a781520641..abfa2cd654 100644 --- a/includes/reader-revenue/woocommerce/class-woocommerce-cli.php +++ b/includes/reader-revenue/woocommerce/class-woocommerce-cli.php @@ -175,16 +175,19 @@ function( $subscription ) { private function output_subscriptions( $subscriptions, $format = 'table' ) { $subscriptions = array_map( function( $subscription ) { + $user = $subscription->get_user(); + $email = $user instanceof \WP_User ? $user->user_email : 'guest'; return [ 'id' => $subscription->get_id(), 'date_created' => $subscription->get_date_created()->__toString(), 'amount' => $subscription->get_total(), + 'user_email' => $email, ]; }, $subscriptions ); - WP_CLI\Utils\format_items( $format, $subscriptions, [ 'id', 'amount', 'date_created' ] ); + WP_CLI\Utils\format_items( $format, $subscriptions, [ 'id', 'amount', 'user_email', 'date_created' ] ); WP_CLI::log( count( $subscriptions ) . ' subscriptions found.' ); } From 8a4bd001cedc6feceed3d3246ee28b0896246c01 Mon Sep 17 00:00:00 2001 From: Leo Germani Date: Mon, 15 Jan 2024 09:26:18 -0300 Subject: [PATCH 4/4] feat: add post meta to fixed subscription --- includes/reader-revenue/woocommerce/class-woocommerce-cli.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/reader-revenue/woocommerce/class-woocommerce-cli.php b/includes/reader-revenue/woocommerce/class-woocommerce-cli.php index abfa2cd654..eb560104e2 100644 --- a/includes/reader-revenue/woocommerce/class-woocommerce-cli.php +++ b/includes/reader-revenue/woocommerce/class-woocommerce-cli.php @@ -157,6 +157,7 @@ function( $subscription ) { $subscription->add_fee( $fee ); $subscription->legacy_set_total( $new_total ); $subscription->add_order_note( 'Subscription fee fixed and added via script' ); + update_post_meta( $subscription->get_id(), '_newspack_fixed_subscription_fees', 1 ); // No need to save, we don't want to trigger any hooks. WP_CLI::success( 'Subscription #' . $subscription->get_id() . ' fixed.' );