diff --git a/cli/commands/Bulk_Add_Users_Command.php b/cli/commands/Bulk_Add_Users_Command.php new file mode 100644 index 0000000..f4ad1ac --- /dev/null +++ b/cli/commands/Bulk_Add_Users_Command.php @@ -0,0 +1,356 @@ +] + * : The number of users to add. Default is 10. + * + * [--password=] + * : The password for the new users. Default is 'password123'. + * + * [--months-back=] + * : The number of months in the past to randomly assign start dates (only used with --with-membership). Default is 1. + * + * [--with-membership] + * : Create users with memberships assigned. Without this flag, only user accounts are created. + * + * [--membership_level_id=] + * : The membership level ID to assign to each user (requires --with-membership). If not specified, levels are randomly assigned. + * + * [--for-siege=] + * : Output a txt file for Siege with test login POST commands, using the specified domain (e.g., http://example.com). + * + * ## EXAMPLES + * + * wp pmpro-toolkit bulk-add-users --count=50 + * wp pmpro-toolkit bulk-add-users --count=100 --with-membership --membership_level_id=2 + * wp pmpro-toolkit bulk-add-users --count=100 --with-membership --months-back=6 + * wp pmpro-toolkit bulk-add-users --count=10 --for-siege=[domain including http:// or https://] + */ + public function __invoke( $args, $assoc_args ) { + global $wpdb; + + $count = isset( $assoc_args['count'] ) ? intval( $assoc_args['count'] ) : 10; + $custom_password = isset( $assoc_args['password'] ) ? $assoc_args['password'] : 'password123'; + $with_membership = isset( $assoc_args['with-membership'] ); + $specified_membership_level_id = isset( $assoc_args['membership_level_id'] ) ? intval( $assoc_args['membership_level_id'] ) : 0; + $months = isset( $assoc_args['months-back'] ) ? intval( $assoc_args['months-back'] ) : 1; + $siege_lines = array(); + $siege_domain = isset( $assoc_args['for-siege'] ) ? rtrim( $assoc_args['for-siege'], '/' ) : ''; + + // Validate months-back parameter + if ( $months > 6 ) { + WP_CLI::error( __( '--months-back cannot be greater than 6. Please specify a value between 1 and 6.', 'pmpro-toolkit' ) ); + return; + } + + // If membership_level_id is provided without --with-membership, show warning + if ( $specified_membership_level_id && ! $with_membership ) { + WP_CLI::warning( __( '--membership_level_id specified but --with-membership not set. Memberships will not be created.', 'pmpro-toolkit' ) ); + } + + $level_ids = array(); + + // Only fetch levels if we're creating memberships + if ( $with_membership ) { + // Fetch all active membership levels + $levels = $wpdb->get_results( + "SELECT id FROM {$wpdb->pmpro_membership_levels} WHERE allow_signups = 1" + ); + + if ( empty( $levels ) ) { + WP_CLI::error( __( 'No available membership levels found. Cannot create memberships.', 'pmpro-toolkit' ) ); + return; + } + + // Build a list of level IDs + $level_ids = wp_list_pluck( $levels, 'id' ); + + // Validate provided membership_level_id if set + if ( $specified_membership_level_id && ! in_array( $specified_membership_level_id, $level_ids, true ) ) { + WP_CLI::error( + sprintf( + __( 'Specified membership level ID %1$d is not valid. Available levels: %2$s', 'pmpro-toolkit' ), + $specified_membership_level_id, + implode( ', ', $level_ids ) + ) + ); + return; + } + } + + $batch_size = 50; + $processed_count = 0; + $memberships_batch = array(); + $subscriptions_batch = array(); + + $operation_text = $with_membership ? + __( 'Adding users with memberships', 'pmpro-toolkit' ) : + __( 'Adding users', 'pmpro-toolkit' ); + + $progress = \WP_CLI\Utils\make_progress_bar( $operation_text, $count ); + + for ( $i = 1; $i <= $count; $i++ ) { + // Get a unique first/last name combination + $name_parts = $this->generate_unique_name(); + $first_name = $name_parts['first_name']; + $last_name = $name_parts['last_name']; + $display_name = $name_parts['display_name']; + + $username = 'test_' . sanitize_title($display_name) . '_' . uniqid(); + // Use @pmpro.test domain for testing/demo purposes + $email = $username . '@pmpro.test'; + $password = $custom_password ?: wp_generate_password( 16, true, true ); + + // Prepare user data with names + $user_data = array( + 'user_login' => $username, + 'user_pass' => $password, + 'user_email' => $email, + 'first_name' => $first_name, + 'last_name' => $last_name, + 'display_name' => $display_name, + ); + $user_id = wp_insert_user( $user_data ); + + if ( is_wp_error( $user_id ) ) { + WP_CLI::warning( + sprintf( + __( 'Failed to create user: %s', 'pmpro-toolkit' ), + $username + ) + ); + $progress->tick(); + continue; + } + + // If --for-siege is set, build the Siege test line for this user. + if ( $siege_domain ) { + $siege_lines[] = sprintf( + '%s/wp-json/toolkit/v1/test-login POST {"username":"%s","password":"%s"}', + $siege_domain, + $username, + $password + ); + } + + // Only create membership data if --with-membership flag is set + if ( $with_membership ) { + $membership_level_id = $specified_membership_level_id ? + $specified_membership_level_id : + $level_ids[ array_rand( $level_ids ) ]; + + // Generate random start date if months > 0, else use current time + $startdate = $this->get_random_past_date( $months ); + $modified = $startdate; + + $memberships_batch[] = $wpdb->prepare( + "(%d, %d, %s, NULL, %s, 'active')", + $user_id, + $membership_level_id, + $startdate, + $modified + ); + + // Calculate next payment date as 1 month after startdate + $next_payment_date = date( 'Y-m-d H:i:s', strtotime( '+1 month', strtotime( $startdate ) ) ); + + $subscriptions_batch[] = $wpdb->prepare( + "(%d, %d, 'stripe', 'test', %s, 'active', %s, %s, 9.99, 1, 'Month', %s)", + $user_id, + $membership_level_id, + uniqid( 'txn_test_' ), + $startdate, + $next_payment_date, + $modified + ); + } + + ++$processed_count; + $progress->tick(); + + // Batch insert every $batch_size users or at the end (only if creating memberships) + if ( $with_membership && ( $processed_count % $batch_size === 0 || $i === $count ) ) { + if ( ! empty( $memberships_batch ) ) { + $result = $wpdb->query( + "INSERT INTO {$wpdb->prefix}pmpro_memberships_users + (user_id, membership_id, startdate, enddate, modified, status) VALUES " . + implode( ',', $memberships_batch ) + ); + + if ( false === $result || ! empty( $wpdb->last_error ) ) { + WP_CLI::warning( + sprintf( + __( 'Database error during memberships batch insert: %s', 'pmpro-toolkit' ), + $wpdb->last_error + ) + ); + } + $memberships_batch = array(); + } + + if ( ! empty( $subscriptions_batch ) ) { + $result = $wpdb->query( + "INSERT INTO {$wpdb->prefix}pmpro_subscriptions + (user_id, membership_level_id, gateway, gateway_environment, subscription_transaction_id, status, startdate, next_payment_date, billing_amount, cycle_number, cycle_period, modified) VALUES " . + implode( ',', $subscriptions_batch ) + ); + + if ( false === $result || ! empty( $wpdb->last_error ) ) { + WP_CLI::warning( + sprintf( + __( 'Database error during subscriptions batch insert: %s', 'pmpro-toolkit' ), + $wpdb->last_error + ) + ); + } + $subscriptions_batch = array(); + } + } + } + + $progress->finish(); + + // If --for-siege was set, output the file. + if ( $siege_domain && ! empty( $siege_lines ) ) { + $upload_dir = wp_upload_dir(); + $toolkit_dir = $upload_dir['basedir'] . '/pmpro-toolkit'; + if ( ! is_dir( $toolkit_dir ) ) { + wp_mkdir_p( $toolkit_dir ); + } + $siege_file = sprintf( + '%s/siege-users-%s.txt', + $toolkit_dir, + uniqid() + ); + file_put_contents( $siege_file, implode( "\n", $siege_lines ) ); + $siege_url = $upload_dir['baseurl'] . '/pmpro-toolkit/' . basename( $siege_file ); + WP_CLI::log( sprintf( + __( 'Siege test file generated: %s', 'pmpro-toolkit' ), + esc_url( $siege_url ) + ) ); + } + + // Build success message based on what was created + if ( $with_membership ) { + $message = sprintf( + __( 'Successfully added %1$d users with memberships. %2$s', 'pmpro-toolkit' ), + $processed_count, + $specified_membership_level_id + ? sprintf( __( 'All assigned membership level ID %d.', 'pmpro-toolkit' ), $specified_membership_level_id ) + : __( 'Membership levels were randomly assigned.', 'pmpro-toolkit' ) + ); + } else { + $message = sprintf( + __( 'Successfully added %d users without memberships.', 'pmpro-toolkit' ), + $processed_count + ); + } + + WP_CLI::success( $message ); + } + + /** + * Helper function to get a random date in the past X months. + * + * @param int $months Number of months back. + * @return string MySQL datetime string. + */ + private function get_random_past_date( $months ) { + if ( $months <= 0 ) { + return current_time( 'mysql' ); + } + + $now = current_time( 'timestamp' ); + $earliest = strtotime( '-' . $months . ' months', $now ); + $random_ts = mt_rand( $earliest, $now ); + + return date( 'Y-m-d H:i:s', $random_ts ); + } + + /** + * Initialize the name pool with first/last combinations and shuffle. + */ + private function init_name_pool() { + // Expanded name pools + $first_names = array( + 'Alice','Bob','Carol','David','Eve','Frank','Grace','Hank','Ivy','Jack', + 'Karen','Liam','Mona','Nate','Olivia','Paul','Quincy','Rachel','Steve','Tina', + 'Uma','Victor','Wendy','Xander','Yvonne','Zack','Aaron','Bianca','Cody','Diana', + 'Ethan','Fiona','Gavin','Hailey','Ian','Jenna','Kyle','Laura','Miles','Nia', + 'Omar','Piper','Quinn','Riley','Sara','Trent','Ursula','Vince','Willow','Xena', + 'Yara','Zane','Amber','Blake','Chloe','Derek','Elena','Felix','Gemma','Hudson', + 'Isla','Jonah','Kendra','Leon','Maya','Noah','Opal','Preston','Quilla','Roman', + 'Selena','Tyler','Ulric','Valeria','Wyatt','Ximena','Yosef','Zephyr','Adrian','Bailey', + 'Carmen','Dylan','Ella','Finn','Giselle','Holden','India','Jared','Kylie','Lucas' + ); + $last_names = array( + 'Smith','Johnson','Williams','Brown','Jones','Miller','Davis','Garcia','Rodriguez','Wilson', + 'Anderson','Thomas','Taylor','Moore','Jackson','Martin','Lee','Perez','Thompson','White', + 'Harris','Sanchez','Clark','Ramirez','Lewis','Robinson','Walker','Young','Allen','King', + 'Wright','Scott','Torres','Nguyen','Hill','Flores','Green','Adams','Nelson','Baker', + 'Hall','Rivera','Campbell','Mitchell','Carter','Roberts','Gomez','Phillips','Evans','Turner', + 'Diaz','Parker','Cruz','Edwards','Collins','Reyes','Stewart','Morris','Morales','Murphy', + 'Cook','Rogers','Gutierrez','Ortiz','Morgan','Cooper','Peterson','Bailey','Reed','Kelly', + 'Howard','Ramos','Kim','Cox','Ward','Richardson','Watson','Brooks','Chavez','Wood', + 'James','Bennett','Gray','Mendoza','Ruiz','Hughes','Price','Alvarez','Castillo','Sanders' + ); + + // Build combinations + foreach ( $first_names as $first ) { + foreach ( $last_names as $last ) { + $this->name_combinations[] = array( + 'first_name' => $first, + 'last_name' => $last, + 'display_name' => sprintf( '%s %s', $first, $last ), + ); + } + } + + // Randomize order + shuffle( $this->name_combinations ); + $this->name_pool_initialized = true; + } + + /** + * Return a unique name combination, initializing pool on first call. + * + * @return array { + * @type string $first_name + * @type string $last_name + * @type string $display_name + * } + */ + private function generate_unique_name() { + if ( ! $this->name_pool_initialized || empty( $this->name_combinations ) ) { + $this->name_combinations = array(); + $this->init_name_pool(); + } + // Pop one combination off the pool + return array_pop( $this->name_combinations ); + } +} diff --git a/cli/commands/Bulk_Checkout_Users_Command.php b/cli/commands/Bulk_Checkout_Users_Command.php new file mode 100644 index 0000000..6258223 --- /dev/null +++ b/cli/commands/Bulk_Checkout_Users_Command.php @@ -0,0 +1,170 @@ +] + * : Number of users to create and checkout. Default: 100 + * + * [--months-back=] + * : Randomize checkout date within the past X months. Default: 1 + * + * [--endpoint=] + * : The REST endpoint URL for test checkout. Default: current site URL + /wp-json/toolkit/v1/test-checkout + * + * [--membership_level_id=] + * : The membership level ID to assign to each user. + * + * [--password=] + * : The password for the new users. Default is 'password123'. + * + * ## EXAMPLES + * + * wp pmpro-toolkit bulk-checkout-users --count=100 --months-back=3 + */ + public function __invoke( $args, $assoc_args ) { + $count = isset( $assoc_args['count'] ) ? intval( $assoc_args['count'] ) : 100; + $months = isset( $assoc_args['months-back'] ) ? intval( $assoc_args['months-back'] ) : 1; + $endpoint = isset( $assoc_args['endpoint'] ) ? esc_url_raw( $assoc_args['endpoint'] ) : home_url( '/wp-json/toolkit/v1/test-checkout' ); + + // Use Bulk_Add_Users_Command to create users WITHOUT memberships + $bulk_add = new \PMPro_Toolkit\Bulk_Add_Users_Command(); + + // Store created users by hooking into user_register + $created_users = array(); + add_action( + 'user_register', + function ( $user_id ) use ( &$created_users ) { + $user = get_userdata( $user_id ); + if ( $user ) { + $created_users[] = array( + 'ID' => $user->ID, + 'user_login' => $user->user_login, + 'user_email' => $user->user_email, + ); + } + }, + 10, + 1 + ); + + // Prepare args for Bulk_Add_Users_Command (exclude checkout-specific args and don't create memberships) + $user_assoc_args = $assoc_args; + unset( $user_assoc_args['endpoint'] ); + unset( $user_assoc_args['months-back'] ); // We'll handle date randomization in checkout + // Explicitly do NOT include --with-membership flag + + WP_CLI::log( __( 'Creating users for checkout simulation...', 'pmpro-toolkit' ) ); + + // Call Bulk_Add_Users_Command to create users without memberships + $bulk_add->__invoke( $args, $user_assoc_args ); + + // Clean up the hook + remove_all_actions( 'user_register' ); + + if ( empty( $created_users ) ) { + WP_CLI::error( __( 'No users were created. Aborting checkout requests.', 'pmpro-toolkit' ) ); + return; + } + + WP_CLI::log( + sprintf( + __( 'Created %d users. Starting checkout simulation...', 'pmpro-toolkit' ), + count( $created_users ) + ) + ); + + $progress = \WP_CLI\Utils\make_progress_bar( 'Running checkouts for created users', count( $created_users ) ); + $failed = 0; + $success = 0; + + foreach ( $created_users as $user ) { + // Simulate checkout with date randomization if specified + $checkout_date = $this->get_random_checkout_date( $months ); + + $payload = array( + 'user_login' => $user['user_login'], + 'user_email' => $user['user_email'], + 'skip_gateway' => true, + 'cleanup' => false, // Don't delete users we just created + 'checkout_date' => $checkout_date, // Add this to endpoint if needed + ); + + // Add membership level if specified + if ( isset( $assoc_args['membership_level_id'] ) ) { + $payload['membership_level'] = intval( $assoc_args['membership_level_id'] ); + } + + $response = wp_remote_post( + $endpoint, + array( + 'headers' => array( 'Content-Type' => 'application/json' ), + 'body' => wp_json_encode( $payload ), + 'timeout' => 30, + ) + ); + + if ( is_wp_error( $response ) ) { + WP_CLI::warning( + sprintf( + __( 'Checkout failed for %1$s: %2$s', 'pmpro-toolkit' ), + $user['user_login'], + $response->get_error_message() + ) + ); + ++$failed; + } elseif ( wp_remote_retrieve_response_code( $response ) >= 300 ) { + WP_CLI::warning( + sprintf( + __( 'Checkout failed for %1$s: HTTP %2$d', 'pmpro-toolkit' ), + $user['user_login'], + wp_remote_retrieve_response_code( $response ) + ) + ); + ++$failed; + } else { + ++$success; + } + + $progress->tick(); + } + + $progress->finish(); + + WP_CLI::log( __( 'Bulk user checkout complete.', 'pmpro-toolkit' ) ); + WP_CLI::log( sprintf( __( 'Success: %1$d, Failed: %2$d', 'pmpro-toolkit' ), $success, $failed ) ); + + if ( $failed === 0 ) { + WP_CLI::success( __( 'All checkouts processed successfully.', 'pmpro-toolkit' ) ); + } else { + WP_CLI::warning( sprintf( __( '%d checkouts failed to process.', 'pmpro-toolkit' ), $failed ) ); + } + } + + /** + * Get a random date within the specified months back from now. + * + * @param int $months Number of months back. + * @return string MySQL datetime string. + */ + private function get_random_checkout_date( $months ) { + if ( $months <= 0 ) { + return current_time( 'mysql' ); + } + + $now = current_time( 'timestamp' ); + $earliest = strtotime( '-' . $months . ' months', $now ); + $random_timestamp = mt_rand( $earliest, $now ); + + return date( 'Y-m-d H:i:s', $random_timestamp ); + } +} + diff --git a/cli/commands/Cleanup_Actions_Command.php b/cli/commands/Cleanup_Actions_Command.php new file mode 100644 index 0000000..9ce5042 --- /dev/null +++ b/cli/commands/Cleanup_Actions_Command.php @@ -0,0 +1,97 @@ +] + * : Only delete actions with this hook name. + * + * [--group=] + * : Only delete actions in this group. + * + * [--status=] + * : Only delete actions with this status (default: completed). + * + * [--args=] + * : Only delete actions with these exact args. Example: --args='{"user_id":123,"something":"value"}' + * + * ## EXAMPLES + * + * wp pmpro-toolkit cleanup-actions + * wp pmpro-toolkit cleanup-actions --hook=pmpro_schedule_daily + * wp pmpro-toolkit cleanup-actions --hook=pmpro_schedule_hourly --status=pending + * wp pmpro-toolkit cleanup-actions --group=pmpro_email --status=failed + * wp pmpro-toolkit cleanup-actions --args='{"user_id":123}' + */ + public function __invoke( $args, $assoc_args ) { + if ( ! class_exists( 'PMPro_Action_Scheduler' ) ) { + WP_CLI::error( __( 'PMPro_Action_Scheduler class not found.', 'pmpro-toolkit' ) ); + return; + } + + $hook = isset( $assoc_args['hook'] ) ? $assoc_args['hook'] : null; + $group = isset( $assoc_args['group'] ) ? $assoc_args['group'] : null; + $status = isset( $assoc_args['status'] ) ? $assoc_args['status'] : 'completed'; + + // Args can be passed as a JSON string (for advanced/automated use) + $args_array = array(); + if ( isset( $assoc_args['args'] ) && ! empty( $assoc_args['args'] ) ) { + $args_json = $assoc_args['args']; + $args_array = json_decode( $args_json, true ); + if ( ! is_array( $args_array ) ) { + WP_CLI::error( __( 'Could not parse --args. Please provide a valid JSON array.', 'pmpro-toolkit' ) ); + return; + } + } + + WP_CLI::line( sprintf( __( 'Looking for actions...', 'pmpro-toolkit' ) ) ); + + $deleted_count = PMPro_Action_Scheduler::instance()->remove_actions( + $hook, + $args_array, + $group, + $status + ); + + if ( $deleted_count === 0 ) { + WP_CLI::success( __( 'No actions found to delete.', 'pmpro-toolkit' ) ); + return; + } + + WP_CLI::success( + sprintf( + /* translators: %d: count, %s: details */ + __( 'Deleted %1$d action(s) [%2$s].', 'pmpro-toolkit' ), + $deleted_count, + $this->build_criteria_description( $hook, $group, $status, $args_array ) + ) + ); + } + + /** + * Helper to describe criteria used in the CLI message. + */ + private function build_criteria_description( $hook, $group, $status, $args_array ) { + $bits = array(); + if ( $hook ) { + $bits[] = "hook: $hook"; } + if ( $group ) { + $bits[] = "group: $group"; } + if ( $status ) { + $bits[] = "status: $status"; } + if ( ! empty( $args_array ) ) { + $bits[] = 'args: ' . json_encode( $args_array ); + } + return $bits ? implode( ', ', $bits ) : __( 'no filter', 'pmpro-toolkit' ); + } +} diff --git a/cli/commands/Expire_Memberships_Command.php b/cli/commands/Expire_Memberships_Command.php new file mode 100644 index 0000000..67e0c97 --- /dev/null +++ b/cli/commands/Expire_Memberships_Command.php @@ -0,0 +1,201 @@ +] + * : Limit the number of users to process. Default is 0 (no limit). + * + * [--user_ids=] + * : Comma-separated list of user IDs to process. + * + * [--days=] + * : Number of days to set the subscription expiration date. Can be positive (future expiry) or negative (already expired). Default is 1. + * + * [--expired] + * : If set, override --days and set expiration date to yesterday (-1). + * + * [--with_as] + * : If set, trigger the membership expiration reminder emails via Action Scheduler. + * + * ## EXAMPLES + * + * wp pmpro-toolkit expire-memberships + * wp pmpro-toolkit expire-memberships --limit=10 + * wp pmpro-toolkit expire-memberships --user_ids=1,2,3 + * wp pmpro-toolkit expire-memberships --user_ids=1,2,3 --limit=2 + * wp pmpro-toolkit expire-memberships --days=5 + * wp pmpro-toolkit expire-memberships --days=-3 + * wp pmpro-toolkit expire-memberships --expired + * wp pmpro-toolkit expire-memberships --with_as + */ + public function __invoke( $args, $assoc_args ) { + + global $wpdb; + + $limit = isset( $assoc_args['limit'] ) ? intval( $assoc_args['limit'] ) : 0; + $user_ids_arg = isset( $assoc_args['user_ids'] ) ? $assoc_args['user_ids'] : ''; + $days = isset( $assoc_args['days'] ) ? intval( $assoc_args['days'] ) : 1; + $with_as = isset( $assoc_args['with_as'] ); + $expired = isset( $assoc_args['expired'] ); + + if ( $expired ) { + $days = -1; + } + + if ( ! empty( $user_ids_arg ) ) { + $user_ids = array_map( 'intval', array_filter( array_map( 'trim', explode( ',', $user_ids_arg ) ) ) ); + if ( $limit > 0 ) { + $user_ids = array_slice( $user_ids, 0, $limit ); + } + } else { + $users = $wpdb->get_col( "SELECT ID FROM {$wpdb->users}" ); + if ( $limit > 0 ) { + $user_ids = array_slice( $users, 0, $limit ); + } else { + $user_ids = $users; + } + } + + if ( empty( $user_ids ) ) { + return; // No users to assign subscriptions to. + } + + $processed_count = 0; + + // Initialize a WP-CLI progress bar. + $progress = \WP_CLI\Utils\make_progress_bar( + sprintf( + /* translators: %d: number of users */ + __( 'Processing %d users', 'pmpro-toolkit' ), + count( $user_ids ) + ), + count( $user_ids ) + ); + + foreach ( $user_ids as $user_id ) { + // Check for an existing subscription. + $subscription = $wpdb->get_row( + $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}pmpro_subscriptions WHERE user_id = %d AND status = 'active' ORDER BY id DESC LIMIT 1", + $user_id + ) + ); + + $next_payment_date = date( 'Y-m-d H:i:s', strtotime( "{$days} days" ) ); + + // Determine membership_level_id to use + if ( $subscription ) { + $membership_level_id = $subscription->membership_level_id; + } else { + // Try to get user's current level from pmpro_memberships_users table + $membership_level_id = $wpdb->get_var( + $wpdb->prepare( + "SELECT membership_id FROM {$wpdb->prefix}pmpro_memberships_users WHERE user_id = %d AND status = 'active' ORDER BY enddate DESC LIMIT 1", + $user_id + ) + ); + + if ( ! $membership_level_id ) { + // Fallback to first available membership level id from pmpro_membership_levels table + $membership_level_id = $wpdb->get_var( + "SELECT id FROM {$wpdb->prefix}pmpro_membership_levels ORDER BY id ASC LIMIT 1" + ); + } + } + + if ( $subscription ) { + // Update existing subscription's next payment date. + $wpdb->update( + "{$wpdb->prefix}pmpro_subscriptions", + array( + 'next_payment_date' => $next_payment_date, + 'modified' => current_time( 'mysql' ), + ), + array( 'id' => $subscription->id ) + ); + } else { + // Insert new subscription. + $wpdb->insert( + "{$wpdb->prefix}pmpro_subscriptions", + array( + 'user_id' => $user_id, + 'membership_level_id' => $membership_level_id, + 'gateway' => 'stripe', + 'gateway_environment' => 'test', + 'subscription_transaction_id' => uniqid( 'txn_test_' ), + 'status' => 'active', + 'startdate' => current_time( 'mysql' ), + 'next_payment_date' => $next_payment_date, + 'billing_amount' => 9.99, + 'cycle_number' => 1, + 'cycle_period' => 'Month', + 'modified' => current_time( 'mysql' ), + ) + ); + } + + // Sync pmpro_memberships_users table. + $enddate = date( 'Y-m-d H:i:s', strtotime( "{$days} days" ) ); + + // Check if a record exists for this user and membership level + $existing = $wpdb->get_var( + $wpdb->prepare( + "SELECT id FROM {$wpdb->prefix}pmpro_memberships_users WHERE user_id = %d AND membership_id = %d", + $user_id, + $membership_level_id + ) + ); + + $data = array( + 'user_id' => $user_id, + 'membership_id' => $membership_level_id, + 'startdate' => current_time( 'mysql' ), + 'enddate' => $enddate, + 'modified' => current_time( 'mysql' ), + 'status' => 'active', + ); + + if ( $existing ) { + $wpdb->update( "{$wpdb->prefix}pmpro_memberships_users", $data, array( 'id' => $existing ) ); + } else { + $wpdb->insert( "{$wpdb->prefix}pmpro_memberships_users", $data ); + } + + // Clear the expiration notice meta so reminders get sent again. + delete_user_meta( $user_id, 'pmpro_expiration_notice_' . $membership_level_id ); + + ++$processed_count; + $progress->tick(); + } + + // Finish the progress bar. + $progress->finish(); + + $expiry_direction = $days < 0 ? 'already expired' : 'set to expire in the future'; + + WP_CLI::success( sprintf( _n( 'Expiring subscription added/updated for %1$d user (%2$s).', 'Expiring subscriptions added/updated for %1$d users (%2$s).', $processed_count, 'pmpro-toolkit' ), $processed_count, $expiry_direction ) ); + + // Trigger membership expiration emails via Action Scheduler if applicable. + if ( class_exists( 'PMPro_Scheduled_Actions' ) && $with_as ) { + WP_CLI::log( 'Triggering membership expiration emails via scheduled actions...' ); + if ( $expired || $days < 0 ) { + PMPro_Scheduled_Actions::instance()->pmpro_expire_memberships(); + WP_CLI::success( 'Membership expiration emails scheduled.' ); + } else { + PMPro_Scheduled_Actions::instance()->membership_expiration_reminders(); + WP_CLI::success( 'Membership expiring soon emails scheduled.' ); + } + } + } +} diff --git a/cli/commands/Payment_Reminders_Command.php b/cli/commands/Payment_Reminders_Command.php new file mode 100644 index 0000000..2573c80 --- /dev/null +++ b/cli/commands/Payment_Reminders_Command.php @@ -0,0 +1,145 @@ +] + * : Limit the number of subscriptions to process. Default is 0 (no limit). + * + * [--user_ids=] + * : Comma-separated list of user IDs to process. + * + * [--days=] + * : Set next_payment_date to n days from now (positive = future, negative = past). Default is 7. + * + * [--with_as] + * : If set, trigger the recurring payment reminder scheduler. + * + * ## EXAMPLES + * + * wp pmpro-toolkit payment-reminders + * wp pmpro-toolkit payment-reminders --limit=10 + * wp pmpro-toolkit payment-reminders --user_ids=1,2,3 + * wp pmpro-toolkit payment-reminders --days=3 + * wp pmpro-toolkit payment-reminders --with_as + */ + public function __invoke( $args, $assoc_args ) { + + global $wpdb; + + $limit = isset( $assoc_args['limit'] ) ? intval( $assoc_args['limit'] ) : 0; + $user_ids_arg = isset( $assoc_args['user_ids'] ) ? $assoc_args['user_ids'] : ''; + $days = isset( $assoc_args['days'] ) ? intval( $assoc_args['days'] ) : 7; + $with_as = isset( $assoc_args['with_as'] ); + + // Get users to process. + if ( ! empty( $user_ids_arg ) ) { + $user_ids = array_map( 'intval', array_filter( array_map( 'trim', explode( ',', $user_ids_arg ) ) ) ); + } else { + $user_ids = $wpdb->get_col( "SELECT ID FROM {$wpdb->users}" ); + } + + if ( $limit > 0 ) { + $user_ids = array_slice( $user_ids, 0, $limit ); + } + + if ( empty( $user_ids ) ) { + WP_CLI::warning( __( 'No users found to process.', 'pmpro-toolkit' ) ); + return; + } + + $total_users = count( $user_ids ); + $progress = \WP_CLI\Utils\make_progress_bar( __( 'Processing users', 'pmpro-toolkit' ), $total_users ); + + $processed_count = 0; + foreach ( $user_ids as $user_id ) { + $progress->tick(); + // Get all active subscriptions for user. + $subscriptions = $wpdb->get_results( + $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}pmpro_subscriptions WHERE user_id = %d AND status = 'active'", + $user_id + ) + ); + + if ( empty( $subscriptions ) ) { + continue; + } + + foreach ( $subscriptions as $subscription ) { + $reminder_window = $days > 0 ? $days - 1 : 0; + $next_payment_date = date( 'Y-m-d 00:00:00', strtotime( "+{$reminder_window} days", current_time( 'timestamp' ) ) ); + $wpdb->update( + "{$wpdb->prefix}pmpro_subscriptions", + array( + 'next_payment_date' => $next_payment_date, + 'modified' => current_time( 'mysql' ), + ), + array( 'id' => $subscription->id ) + ); + + // Remove meta fields related to recurring payment reminders, so reminders get sent again. + $wpdb->delete( + "{$wpdb->prefix}pmpro_subscriptionmeta", + array( + 'pmpro_subscription_id' => $subscription->id, + 'meta_key' => 'pmprorm_last_next_payment_date', + ) + ); + $wpdb->delete( + "{$wpdb->prefix}pmpro_subscriptionmeta", + array( + 'pmpro_subscription_id' => $subscription->id, + 'meta_key' => 'pmprorm_last_days', + ) + ); + + ++$processed_count; + } + } + $progress->finish(); + + WP_CLI::success( + sprintf( + _n( + 'Adjusted next_payment_date for %d subscription. Cleared reminder meta.', + 'Adjusted next_payment_date for %d subscriptions. Cleared reminder meta.', + $processed_count, + 'pmpro-toolkit' + ), + $processed_count + ) + ); + + // Optionally trigger the scheduler. + if ( $with_as && class_exists( 'PMPro_Scheduled_Actions' ) ) { + PMPro_Scheduled_Actions::instance()->schedule_recurring_payment_reminder_tasks(); + // Log the count of scheduled tasks. + WP_CLI::success( + sprintf( + _n( + 'Scheduled %d recurring payment reminder.', + 'Scheduled %d recurring payment reminders.', + $processed_count, + 'pmpro-toolkit' + ), + $processed_count + ) + ); + + } + } +} diff --git a/pmpro-toolkit.php b/pmpro-toolkit.php index df0b4a8..7305a17 100755 --- a/pmpro-toolkit.php +++ b/pmpro-toolkit.php @@ -92,6 +92,16 @@ function pmprodev_init_options() { // Load CLI command class only if enabled via Toolkit setting. require_once plugin_dir_path( __FILE__ ) . 'cli/Toolkit_Commands.php'; \WP_CLI::add_command( 'pmpro-toolkit', 'PMPro_Toolkit\\Toolkit_Commands' ); + + // Test data commands (one class per command). + foreach ( glob( plugin_dir_path( __FILE__ ) . 'cli/commands/*_Command.php' ) as $pmprodev_cli_command_file ) { + require_once $pmprodev_cli_command_file; + } + \WP_CLI::add_command( 'pmpro-toolkit bulk-add-users', 'PMPro_Toolkit\\Bulk_Add_Users_Command' ); + \WP_CLI::add_command( 'pmpro-toolkit bulk-checkout-users', 'PMPro_Toolkit\\Bulk_Checkout_Users_Command' ); + \WP_CLI::add_command( 'pmpro-toolkit expire-memberships', 'PMPro_Toolkit\\Expire_Memberships_Command' ); + \WP_CLI::add_command( 'pmpro-toolkit payment-reminders', 'PMPro_Toolkit\\Payment_Reminders_Command' ); + \WP_CLI::add_command( 'pmpro-toolkit cleanup-actions', 'PMPro_Toolkit\\Cleanup_Actions_Command' ); } /**