__( 'Pending', 'subscription' ), 'active' => __( 'Active', 'subscription' ), 'on-hold' => __( 'On Hold', 'subscription' ), 'expired' => __( 'Expired', 'subscription' ), 'pe_cancelled' => __( 'Pending Cancellation', 'subscription' ), 'cancelled' => __( 'Cancelled', 'subscription' ), 'draft' => __( 'Draft', 'subscription' ), 'trash' => __( 'Trash', 'subscription' ), ); if ( $return_all ) { return $statuses; } $status = strtolower( $status ); return isset( $statuses[ $status ] ) ? $statuses[ $status ] : ''; } /** * Generate start date * * @param null|string $trial Trial. * * @return string */ public static function start_date( $trial = null ) { if ( null === $trial ) { $start_date = time(); } else { $start_date = strtotime( $trial ); } return wp_date( get_option( 'date_format' ), $start_date ); } /** * Generate next date * * @param string $time Time. * @param null|string $trial Trial. * * @return string */ public static function next_date( $time, $trial = null ) { if ( null === $trial ) { $start_date = time(); } else { $start_date = strtotime( $trial ); } return wp_date( get_option( 'date_format' ), strtotime( $time, $start_date ) ); } /** * Get Subscriptions * * Args: * - status => [ any, active, pending, expired, pe_cancelled, cancelled, trash ] * - user_id => user_id, -1 for all users. * - posts_per_page => limit number of subscriptions. * - return => return data: ids, post, subscription_data * * @param array $args Args. */ public static function get_subscriptions( array $args = array() ) { $default_args = array( 'post_type' => 'subscrpt_order', 'post_status' => 'active', 'author' => get_current_user_id(), 'posts_per_page' => -1, 'fields' => 'all', 'return' => 'post', ); // Normalize some args. if ( isset( $args['status'] ) ) { $args['post_status'] = $args['status']; unset( $args['status'] ); } if ( isset( $args['user_id'] ) ) { $args['author'] = $args['user_id']; unset( $args['user_id'] ); } // Merge default args with provided args. $final_args = wp_parse_args( $args, $default_args ); if ( isset( $args['author'] ) ) { if ( $args['author'] === -1 ) { unset( $final_args['author'] ); } else { $final_args['author'] = (int) $args['author']; } } if ( isset( $args['product_id'] ) ) { $final_args['meta_query'] = array( array( 'key' => '_subscrpt_product_id', 'value' => (int) $args['product_id'], ), ); unset( $final_args['product_id'] ); } // Fields check $only_ids = false; if ( $final_args['fields'] === 'ids' || $final_args['return'] === 'ids' ) { $final_args['fields'] = 'all'; $only_ids = true; } // Status check $statuses = $final_args['post_status']; $final_args['post_status'] = 'any'; // Get all subscriptions. $subscriptions = get_posts( $final_args ); // Fallback filtering. // ? Sometime status filtering not works properly. So, we need to filter manually. $filtered_subscriptions = []; // Filter by status. foreach ( $subscriptions as $subscription ) { if ( ( is_array( $statuses ) && in_array( 'any', $statuses, true ) ) || $statuses === 'any' ) { $filtered_subscriptions[] = $subscription; continue; } if ( ( is_array( $statuses ) && in_array( $subscription->post_status, $statuses, true ) ) || $subscription->post_status === $statuses ) { $filtered_subscriptions[] = $subscription; } } // Final filtering (only ids, post, or full data) $subscriptions = []; foreach ( $filtered_subscriptions as $subscription ) { if ( $only_ids ) { $subscriptions[] = $subscription->ID; } elseif ( $final_args['return'] === 'subscription_data' ) { $subs_id = $subscription->ID; $subscription_data = self::get_subscription_data( $subs_id ); $subscriptions[] = $subscription_data; } else { $subscriptions[] = $subscription; } } return $subscriptions; } /** * Check subscription exists by product ID. * * @param int $product_id Product ID. * @param string|array $status Status. * * @return \WP_Post | false */ public static function subscription_exists( int $product_id, $status ) { if ( 0 === get_current_user_id() ) { return false; } $args = array( 'post_status' => $status, 'fields' => 'ids', 'product_id' => $product_id, ); $posts = self::get_subscriptions( $args ); return count( $posts ) > 0 ? $posts[0] : false; } /** * Check if product trial exixts for an user. * * @param int $product_id Product ID. * * @return boolean */ public static function check_trial( int $product_id ): bool { return ! self::subscription_exists( $product_id, array( 'expired', 'pending', 'active', 'on-hold', 'pe_cancelled', 'cancelled' ) ); } /** * Rewew when expired. * * @param int $subscription_id Subscription ID. */ public static function renew( int $subscription_id ) { $trial = get_post_meta( $subscription_id, '_subscrpt_trial', true ); if ( null !== $trial ) { update_post_meta( $subscription_id, '_subscrpt_trial', null ); } do_action( 'subscrpt_when_product_expired', $subscription_id, true ); } /** * Get Subscriptions Histories * * @param int $order_id Order ID. */ public static function get_subscriptions_from_order( $order_id ) { global $wpdb; $table_name = $wpdb->prefix . 'subscrpt_order_relation'; $histories = $wpdb->get_results( $wpdb->prepare( // @phpcs:ignore 'SELECT * FROM %i WHERE order_id=%d', array( $table_name, $order_id ) ) ); return $histories; } /** * Get Subscriptions Histories * * @param int $order_item_id Order item ID. */ public static function get_subscription_from_order_item_id( $order_item_id ) { global $wpdb; $table_name = $wpdb->prefix . 'subscrpt_order_relation'; return $wpdb->get_row( $wpdb->prepare( // @phpcs:ignore 'SELECT * FROM %i WHERE order_item_id=%d', array( $table_name, $order_item_id ) ) ); } /** * Format price with Subscription * * @param string $price Price. * @param int $subscription_id Subscription ID. * @param bool $display_trial True/False. * * @return string */ public static function format_price_with_subscription( $price, $subscription_id, $display_trial = false ) { $order_id = get_post_meta( $subscription_id, '_subscrpt_order_id', true ); $order_item_id = get_post_meta( $subscription_id, '_subscrpt_order_item_id', true ); $item_meta = wc_get_order_item_meta( $order_item_id, '_subscrpt_meta', true ); $order = wc_get_order( $order_id ); $time = '1' === $item_meta['time'] ? null : $item_meta['time'] . ' '; $type = self::get_typos( $item_meta['time'], $item_meta['type'] ); $formatted_price = wc_price( $price, array( 'currency' => $order->get_currency(), ) ) . ' / ' . $time . $type; if ( $display_trial ) { $trial = $item_meta['trial']; $has_trial = isset( $item_meta['trial'] ) && strlen( $item_meta['trial'] ) > 2; if ( $has_trial ) { $trial_html = '
+ Got ' . $trial . ' free trial!'; $formatted_price .= $trial_html; } } return apply_filters( 'subscrpt_format_price_with_subscription', $formatted_price, $price, $subscription_id ); } /** * Format price with order item * * @param string $price Price. * @param int $item_id Item Id. * @param bool $display_trial display trial?. * * @return string */ public static function format_price_with_order_item( $price, $item_id, $display_trial = false ) { $order_id = wc_get_order_id_by_order_item_id( $item_id ); $order = wc_get_order( $order_id ); $item_meta = wc_get_order_item_meta( $item_id, '_subscrpt_meta', true ); if ( ! $item_meta || ! is_array( $item_meta ) ) { return false; } $time = 1 === (int) $item_meta['time'] ? null : $item_meta['time'] . '-'; $type = self::get_typos( $item_meta['time'], $item_meta['type'], true ); $formatted_price = wc_price( $price, array( 'currency' => $order->get_currency(), ) ) . ' / ' . $time . ucfirst( $type ); if ( $display_trial ) { $has_trial = isset( $item_meta['trial'] ) && strlen( $item_meta['trial'] ) > 2; $trial = $item_meta['trial'] ?? ''; if ( $has_trial ) { // translators: %s: trial period. $trial_html = '
' . sprintf( __( '+ %s free trial!', 'subscription' ), $trial ) . ''; $formatted_price .= $trial_html; } } return apply_filters( 'subscrpt_format_price_with_subscription', $formatted_price, $price, $item_id ); } /** * Get total subscriptions by product ID. * * @param int $product_id Product ID. * @param string | array $status Status. * * @return \WP_Post | false */ public static function get_total_subscriptions_from_product( int $product_id, $status = array( 'active', 'pending', 'expired', 'pe_cancelled', 'cancelled' ) ) { $args = array( 'post_type' => 'subscrpt_order', 'post_status' => $status, 'fields' => 'ids', 'meta_query' => array( array( 'key' => '_subscrpt_product_id', 'value' => $product_id, ), ), ); $posts = get_posts( $args ); return count( $posts ); } /** * Process renewal on order. * * @param int $subscription_id Subscription Id. * @param int $order_id Order Id. * @param int $order_item_id Order Item Id. * * @return void */ public static function process_order_renewal( $subscription_id, $order_id, $order_item_id ) { global $wpdb; $history_table = $wpdb->prefix . 'subscrpt_order_relation'; // Check if this is a split payment subscription $payment_type = function_exists( 'subscrpt_get_payment_type' ) ? subscrpt_get_payment_type( $subscription_id ) : 'recurring'; $max_payments = function_exists( 'subscrpt_get_max_payments' ) ? subscrpt_get_max_payments( $subscription_id ) : 0; $payments_made = function_exists( 'subscrpt_count_payments_made' ) ? subscrpt_count_payments_made( $subscription_id ) : 0; $comment_content = ''; $activity_type = ''; if ( 'split_payment' === $payment_type && $max_payments ) { $comment_content = sprintf( /* translators: %1$s: order id, %2$d: payment number, %3$d: total payments */ __( 'Split payment installment %2$d of %3$d. Order %1$s created for subscription.', 'subscription' ), $order_id, $payments_made + 1, // +1 because this is a new renewal $max_payments ); $activity_type = __( 'Split Payment - Renewal', 'subscription' ); } else { $comment_content = sprintf( /* translators: order id. */ __( 'The order %s has been created for the subscription', 'subscription' ), $order_id ); $activity_type = __( 'Renewal Order', 'subscription' ); } $comment_id = wp_insert_comment( array( 'comment_author' => 'Subscription for WooCommerce', 'comment_content' => $comment_content, 'comment_post_ID' => $subscription_id, 'comment_type' => 'order_note', ) ); update_comment_meta( $comment_id, '_subscrpt_activity', $activity_type ); update_comment_meta( $comment_id, '_subscrpt_activity_type', 'renewal_order' ); $wpdb->insert( $history_table, array( 'subscription_id' => $subscription_id, 'order_id' => $order_id, 'order_item_id' => $order_item_id, 'type' => 'renew', ) ); // Fire action when split payment is renewed do_action( 'subscrpt_split_payment_renewed', $subscription_id, $order_id, $order_item_id ); } /** * Process new subscription on order. * * @param \WC_Order_Item $order_item Order Item. * @param string $post_status status. * @param \WC_Product $product Product. * * @return int */ public static function process_new_subscription_order( $order_item, $post_status, $product ) { global $wpdb; $history_table = $wpdb->prefix . 'subscrpt_order_relation'; // Prepare split payment arguments $split_payment_args = array( 'product_id' => $product->get_id(), 'order_id' => $order_item->get_order_id(), 'order_item_id' => $order_item->get_id(), 'post_status' => $post_status, 'max_payments' => $product->get_meta( '_subscrpt_max_no_payment' ), 'timing_per' => $product->get_meta( '_subscrpt_timing_per' ), 'timing_option' => $product->get_meta( '_subscrpt_timing_option' ), 'price' => $product->get_price(), ); // Allow modification of split payment arguments $split_payment_args = apply_filters( 'subscrpt_split_payment_args', $split_payment_args, $order_item, $product ); $args = array( 'post_title' => 'Subscription', 'post_type' => 'subscrpt_order', 'post_status' => $split_payment_args['post_status'], ); $subscription_id = wp_insert_post( $args ); wp_update_post( array( 'ID' => $subscription_id, 'post_title' => "Subscription #{$subscription_id}", ) ); // Check if this is a split payment subscription $payment_type = $product->get_meta( '_subscrpt_payment_type' ) ?: 'recurring'; $max_payments = $product->get_meta( '_subscrpt_max_no_payment' ); $comment_content = ''; $activity_type = ''; if ( 'split_payment' === $payment_type && $max_payments ) { $comment_content = sprintf( /* translators: %1$s: order id, %2$d: max payments */ __( 'Split payment subscription created successfully. Order: %1$s. Total installments: %2$d.', 'subscription' ), $order_item->get_order_id(), $max_payments ); $activity_type = __( 'Split Payment - New Subscription', 'subscription' ); } else { $comment_content = sprintf( /* translators: Order Id. */ __( 'Subscription successfully created. Order is %s', 'subscription' ), $order_item->get_order_id() ); $activity_type = __( 'New Subscription', 'subscription' ); } $comment_id = wp_insert_comment( array( 'comment_author' => 'Subscription for WooCommerce', 'comment_content' => $comment_content, 'comment_post_ID' => $subscription_id, 'comment_type' => 'order_note', ) ); update_comment_meta( $comment_id, '_subscrpt_activity', $activity_type ); update_comment_meta( $comment_id, '_subscrpt_activity_type', 'subs_created' ); update_post_meta( $subscription_id, '_subscrpt_product_id', $product->get_id() ); $wpdb->insert( $history_table, array( 'subscription_id' => $subscription_id, 'order_id' => $order_item->get_order_id(), 'order_item_id' => $order_item->get_id(), 'type' => 'new', ) ); // Fire action when split payment plan is created do_action( 'subscrpt_split_payment_created', $subscription_id, $split_payment_args, $order_item ); return $subscription_id; } /** * Get recurrings items from cart items. * * @param array $cart_items Cart items. * * @return array */ public static function get_recurrs_from_cart( $cart_items ) { $recurrs = array(); foreach ( $cart_items as $key => $cart_item ) { $product = $cart_item['data']; if ( $product->is_type( 'simple' ) && isset( $cart_item['subscription'] ) ) { $cart_subscription = $cart_item['subscription']; $type = ucfirst( $cart_subscription['type'] ); // Total amount with tax $quantity = (int) $cart_item['quantity']; $total_amount = wc_get_price_including_tax( $product, [ 'qty' => $quantity ] ); $timing_html = " / {$type}"; $price_html = wc_price( (float) $total_amount ) . $timing_html; $recurrs[ $key ] = array( 'trial_status' => ! is_null( $cart_subscription['trial'] ), 'price_html' => $price_html, 'start_date' => self::start_date( $cart_subscription['trial'] ), 'next_date' => self::next_date( ( $cart_subscription['time'] ?? 1 ) . ' ' . $cart_subscription['type'], $cart_subscription['trial'] ), 'can_user_cancel' => $cart_item['data']->get_meta( '_subscrpt_user_cancel' ), 'max_no_payment' => $cart_item['data']->get_meta( '_subscrpt_max_no_payment' ), 'price' => (float) $cart_subscription['per_cost'], 'quantity' => (int) $cart_item['quantity'], ); } } return apply_filters( 'wpsubs_cart_recurring_items', $recurrs, $cart_items ); } /** * Check if the order has subscription item. * * @param \WC_Order|int $order Order object. */ public static function order_has_subscription_item( $order ) { if ( is_int( $order ) ) { $order = wc_get_order( $order ); } $is_subscription_order = false; foreach ( $order->get_items() as $item ) { $item_data = $item->get_data() ?? array(); $item_product_id = $item_data['product_id'] ?? 0; $item_variation_id = $item_data['variation_id'] ?? 0; $product_id = $item_variation_id ? $item_variation_id : $item_product_id; $product = Subscription::get_subs_product( $product_id ); if ( $product && $product->is_enabled() ) { $is_subscription_order = true; break; } } return $is_subscription_order; } /** * Create renewal order when subscription expired. [wip] * * @param int $subscription_id Subscription ID. * @return false|\WC_Order Renewal order object or false on failure. * @throws \WC_Data_Exception Exception. * @throws \Exception Exception. */ public static function create_renewal_order( $subscription_id ) { // Check if maximum payment limit has been reached if ( subscrpt_is_max_payments_reached( $subscription_id ) ) { // Mark subscription as expired due to limit reached Action::status( 'expired', $subscription_id ); error_log( "WPS: Maximum payment limit reached for subscription #{$subscription_id}. No renewal order created." ); return false; } $order_item_id = get_post_meta( $subscription_id, '_subscrpt_order_item_id', true ); $order_id = wc_get_order_id_by_order_item_id( $order_item_id ); $old_order = self::check_order_for_renewal( $order_id ); if ( ! $old_order ) { // The stored item may belong to a trashed or non-completed order (e.g. a renewal that was deleted). Walk the relation table newest-first to find the last completed order we can use as the renewal source. foreach ( self::get_related_orders( $subscription_id ) as $row ) { $candidate = wc_get_order( (int) ( $row->order_id ?? 0 ) ); if ( $candidate && 'completed' === $candidate->get_status() ) { $old_order = $candidate; $order_item_id = (int) ( $row->order_item_id ?? 0 ); break; } } } if ( ! $old_order ) { subscrpt_write_log( "Old order not found for renewal. Skipping creating renewal order. [ Subscription ID: {$subscription_id} ]" ); return false; } $order_item = $old_order->get_item( $order_item_id ); $subscription_price = (float) get_post_meta( $subscription_id, '_subscrpt_price', true ); $qty = $order_item->get_quantity(); // Subtract tax from per-unit subscription price if prices include tax. WC_Order will calculate tax on line total. if ( wc_prices_include_tax() ) { $product = ( $order_item instanceof \WC_Order_Item_Product ) ? $order_item->get_product() : null; $tax_class = $product ? $product->get_tax_class() : ''; $tax_rates = \WC_Tax::get_rates( $tax_class ); $taxes = \WC_Tax::calc_inclusive_tax( $subscription_price, $tax_rates ); $subscription_price = $subscription_price - array_sum( $taxes ); } $line_total = $subscription_price * $qty; $product_args = array( 'name' => $order_item->get_name(), 'subtotal' => $line_total, 'total' => $line_total, ); // creating new order. $new_order_data = self::create_new_order_for_renewal( $old_order, $order_item, $product_args ); if ( ! $new_order_data ) { subscrpt_write_log( "Failed to create renewal order. [ Subscription ID: {$subscription_id} ]" ); return false; } $new_order = $new_order_data['order']; $new_order_item_id = $new_order_data['order_item_id']; self::create_renewal_history( $subscription_id, $new_order->get_id(), $new_order_item_id ); update_post_meta( $subscription_id, '_subscrpt_order_id', $new_order->get_id() ); update_post_meta( $subscription_id, '_subscrpt_order_item_id', $new_order_item_id ); self::clone_order_metadata( $new_order, $old_order ); // Allow modification of the renewal order before saving. $new_order = apply_filters( 'subscrpt_before_saving_renewal_order', $new_order, $old_order, $subscription_id ); // Save the new order. $new_order->calculate_totals(); $new_order->save(); if ( ! is_admin() && function_exists( 'wc_add_notice' ) && WC()->session ) { $message = 'Renewal Order(#' . $new_order->get_id() . ') Created.'; if ( $new_order->has_status( 'pending' ) ) { $message .= 'Please Pay now'; } wc_add_notice( $message, 'success' ); } do_action( 'subscrpt_after_create_renew_order', $new_order, $old_order, $subscription_id, false ); return $new_order; } /** * Get subscription total price. * * @param int $subscription_id Subscription ID. * @return float */ public static function get_subscription_total( $subscription_id ) { return (float) get_post_meta( $subscription_id, '_subscrpt_price', true ); } /** * Get subscription status. * * @param int $subscription_id Subscription ID. * @return string */ public static function get_subscription_status( $subscription_id ) { return get_post_status( $subscription_id ); } /** * Check if subscription has status. * * @param int $subscription_id Subscription ID. * @param string $status Status to check. * @return bool */ public static function subscription_has_status( $subscription_id, $status ) { return self::get_subscription_status( $subscription_id ) === $status; } /** * Check if subscription needs payment. * * @param int $subscription_id Subscription ID. * @return bool */ public static function subscription_needs_payment( $subscription_id ) { return true; // Always true for now } /** * Get product period (timing option). * * @param int $product_id Product ID. * @return string */ public static function get_product_period( $product_id ) { $product = wc_get_product( $product_id ); return $product ? $product->get_meta( '_subscrpt_timing_option' ) : ''; } /** * Get product interval (timing per). * * @param int $product_id Product ID. * @return int */ public static function get_product_interval( $product_id ) { $product = wc_get_product( $product_id ); return $product ? (int) $product->get_meta( '_subscrpt_timing_per' ) : 1; } /** * Get product length (max payments). * * @param int $product_id Product ID. * @return int */ public static function get_product_length( $product_id ) { $product = wc_get_product( $product_id ); return $product ? (int) $product->get_meta( '_subscrpt_max_no_payment' ) : 0; } /** * Get product trial length. * * @param int $product_id Product ID. * @return int */ public static function get_product_trial_length( $product_id ) { $product = wc_get_product( $product_id ); return $product ? (int) $product->get_meta( '_subscrpt_trial_timing_per' ) : 0; } /** * Get product signup fee. * * @param int $product_id Product ID. * @return float */ public static function get_product_signup_fee( $product_id ) { $product = wc_get_product( $product_id ); return $product ? (float) $product->get_meta( '_subscrpt_signup_fee' ) : 0.0; } /** * Get first renewal payment time. * * @param int $product_id Product ID. * @return int Timestamp */ public static function get_first_renewal_payment_time( $product_id ) { $product = wc_get_product( $product_id ); if ( ! $product ) { return 0; } $trial_period = $product->get_meta( '_subscrpt_trial_timing_per' ); $trial_option = $product->get_meta( '_subscrpt_trial_timing_option' ); if ( ! empty( $trial_period ) && ! empty( $trial_option ) ) { return strtotime( "+{$trial_period} {$trial_option}" ); } return 0; } /** * Update subscription next payment date. * * @param int $subscription_id Subscription ID. * @param string $new_date New Date string. * @return void */ public static function update_subscription_next_payment_date( $subscription_id, $new_date ) { update_post_meta( $subscription_id, '_subscrpt_next_date', strtotime( $new_date ) ); } /** * Cancel subscription. * * @param int $subscription_id Subscription ID. * @return void */ public static function cancel_subscription( $subscription_id ) { Action::status( 'cancelled', $subscription_id ); } /** * Pause subscription. * * @param int $subscription_id Subscription ID. * @return void */ public static function pause_subscription( $subscription_id ) { Action::status( 'on-hold', $subscription_id ); } /** * Resume subscription. * * @param int $subscription_id Subscription ID. * @return void */ public static function resume_subscription( $subscription_id ) { Action::status( 'active', $subscription_id ); } /** * Mark subscription payment as complete. * * @param int $subscription_id Subscription ID. * @param string $payment_id Payment/Transaction ID. * @return void */ public static function subscription_payment_complete( $subscription_id, $payment_id ) { if ( 'active' !== get_post_status( $subscription_id ) ) { Action::status( 'active', $subscription_id ); } // Allow payment gateways to add their own comments/notes do_action( 'subscrpt_subscription_payment_completed', $subscription_id, $payment_id ); } /** * Clone stripe metadata from old order. * * @param int $subscription_id Subscription Id. * @param \WC_Order $old_order Old Order Object. * @param \WC_Order $new_order New Order Object. * * @return void */ public static function clone_stripe_metadata_for_renewal( $subscription_id, $old_order, $new_order ) { $is_auto_renew = get_post_meta( $subscription_id, '_subscrpt_auto_renew', true ); if ( empty( $is_auto_renew ) && subscrpt_is_auto_renew_enabled() ) { $is_auto_renew = true; update_post_meta( $subscription_id, '_subscrpt_auto_renew', true ); } $is_auto_renew = get_post_meta( $subscription_id, '_subscrpt_auto_renew', true ); $is_auto_renew = in_array( $is_auto_renew, array( 1, '1' ), true ); $is_global_auto_renew = get_option( 'wp_subscription_stripe_auto_renew', '1' ); $is_global_auto_renew = in_array( $is_global_auto_renew, array( 1, '1' ), true ); $stripe_supported_methods = Stripe::WPSUBS_SUPPORTED_METHODS; $old_method = $old_order->get_payment_method(); $is_stripe_pm = ! empty( $old_method ) && in_array( $old_method, $stripe_supported_methods, true ); $has_stripe_meta = ! empty( $old_order->get_meta( '_stripe_customer_id' ) ) || ! empty( $old_order->get_meta( '_stripe_source_id' ) ); $stripe_enabled = ( ( $is_stripe_pm || $has_stripe_meta ) && $is_auto_renew && $is_global_auto_renew && subscrpt_is_auto_renew_enabled() ); if ( $stripe_enabled ) { $new_order->update_meta_data( '_stripe_customer_id', $old_order->get_meta( '_stripe_customer_id' ) ); $new_order->update_meta_data( '_stripe_source_id', $old_order->get_meta( '_stripe_source_id' ) ); $new_order->set_payment_method( $old_order->get_payment_method() ); $new_order->set_payment_method_title( $old_order->get_payment_method_title() ); // Add debug log. subscrpt_write_debug_log( "Stripe metadata cloned for renewal order #{$new_order->get_id()} from old order #{$old_order->get_id()}" ); } else { subscrpt_write_log( "Stripe metadata not processed. Auto renewal may fail. [ Renewal order #{$new_order->get_id()}, Old order #{$old_order->get_id()} ]" ); subscrpt_write_debug_log( "Stripe metadata did not clone for renewal order #{$new_order->get_id()} from old order #{$old_order->get_id()}" ); } } /** * Create history for renewal. * * @param int $subscription_id Subscription Id. * @param int $new_order_id New Order Id. * @param int $new_order_item_id New Order Item Id. * * @return void */ public static function create_renewal_history( $subscription_id, $new_order_id, $new_order_item_id ) { global $wpdb; $history_table = $wpdb->prefix . 'subscrpt_order_relation'; $wpdb->insert( $history_table, array( 'subscription_id' => $subscription_id, 'order_id' => $new_order_id, 'order_item_id' => $new_order_item_id, 'type' => 'renew', ) ); $comment_id = wp_insert_comment( array( 'comment_author' => 'Subscription for WooCommerce', 'comment_content' => sprintf( 'Subscription Renewal order successfully created. order is %s', $new_order_id ), 'comment_post_ID' => $subscription_id, 'comment_type' => 'order_note', ) ); update_comment_meta( $comment_id, '_subscrpt_activity', 'Renewal Order' ); update_comment_meta( $comment_id, '_subscrpt_activity_type', 'renewal_order' ); } /** * Get a subscription data. * * @param int $subscription_id Subscription ID. * @return array|null */ public static function get_subscription_data( int $subscription_id ): ?array { if ( empty( get_post_meta( $subscription_id ) ) ) { return null; } $subs_post = get_post( $subscription_id ); $user_id = ! empty( $subs_post ) ? (int) $subs_post->post_author : 0; $product_id = get_post_meta( $subscription_id, '_subscrpt_product_id', true ); $product_id = ! empty( $product_id ) ? (int) $product_id : 0; $variation_id = get_post_meta( $subscription_id, '_subscrpt_variation_id', true ); $variation_id = ! empty( $variation_id ) ? (int) $variation_id : 0; $chk_product_id = $variation_id ? $variation_id : $product_id; $status = get_post_status( $subscription_id ); // pending, active, cancelled, pe_cancelled, expired $price = get_post_meta( $subscription_id, '_subscrpt_price', true ); $signup_fee = get_post_meta( $subscription_id, '_subscrpt_signup_fee', true ); $signup_fee = ! empty( $signup_fee ) ? $signup_fee : 0; $order_id = get_post_meta( $subscription_id, '_subscrpt_order_id', true ); $order_item_id = get_post_meta( $subscription_id, '_subscrpt_order_item_id', true ); $can_user_cancel = in_array( get_post_meta( $subscription_id, '_subscrpt_user_cancel', true ), array( 1, '1', 'true', 'yes' ), true ); $start_datetime = (int) get_post_meta( $subscription_id, '_subscrpt_start_date', true ); $start_date = ! empty( $start_datetime ) ? gmdate( DATE_RFC2822, $start_datetime ) : null; $next_datetime = (int) get_post_meta( $subscription_id, '_subscrpt_next_date', true ); $next_date = ! empty( $next_datetime ) ? gmdate( DATE_RFC2822, $next_datetime ) : null; $timing_per = get_post_meta( $subscription_id, '_subscrpt_timing_per', true ); $timing_per = empty( $timing_per ) ? get_post_meta( $chk_product_id, '_subscrpt_timing_per', true ) : $timing_per; $timing_option = get_post_meta( $subscription_id, '_subscrpt_timing_option', true ); $timing_option = empty( $timing_option ) ? get_post_meta( $chk_product_id, '_subscrpt_timing_option', true ) : $timing_option; $trial_timing_per = get_post_meta( $subscription_id, '_subscrpt_trial_timing_per', true ); $trial_timing_per = empty( $trial_timing_per ) ? get_post_meta( $chk_product_id, '_subscrpt_trial_timing_per', true ) : $trial_timing_per; $trial_timing_option = get_post_meta( $subscription_id, '_subscrpt_trial_timing_option', true ); $trial_timing_option = empty( $trial_timing_option ) ? get_post_meta( $chk_product_id, '_subscrpt_trial_timing_option', true ) : $trial_timing_option; $is_auto_renew = in_array( get_post_meta( $subscription_id, '_subscrpt_auto_renew', true ), array( 1, '1', 'true', 'yes' ), true ); $is_auto_renew = ! empty( $is_auto_renew ) ? $is_auto_renew : subscrpt_is_auto_renew_enabled(); $default_grace_period = (int) get_option( 'subscrpt_default_payment_grace_period', '7' ); $default_grace_period = subscrpt_pro_activated() ? $default_grace_period : 0; $grace_end_datetime = $next_datetime + ( $default_grace_period * DAY_IN_SECONDS ); $grace_end_date = gmdate( DATE_RFC2822, $grace_end_datetime ); $grace_remaining_days = ceil( max( 0, $grace_end_datetime - time() ) / DAY_IN_SECONDS ); $subscription_data = array( 'id' => $subscription_id, 'status' => $status, 'schedule' => array( 'timing_per' => $timing_per, 'timing_option' => $timing_option, ), 'price' => $price, 'signup_fee' => $signup_fee, 'start_date' => $start_date, 'next_date' => $next_date, 'product' => array( 'product_id' => $product_id, 'variation_id' => $variation_id, ), 'order' => array( 'order_id' => $order_id, 'order_item_id' => $order_item_id, ), 'can_user_cancel' => $can_user_cancel, 'is_auto_renew' => (bool) $is_auto_renew, 'user_id' => $user_id, ); if ( ! empty( $trial_timing_per ) ) { $subscription_data['trial'] = array( 'timing_per' => $trial_timing_per, 'timing_option' => $trial_timing_option, ); } if ( ! in_array( strtolower( $status ), array( 'cancelled', 'pending' ), true ) && $next_datetime - time() <= 0 && (int) $default_grace_period > 0 ) { $subscription_data['grace_period'] = array( 'remaining_days' => $grace_remaining_days, 'end_date' => $grace_end_date, ); } return $subscription_data; } /** * Get related orders of a subscription. * * @param int $subscription_id Subscription ID. * @return array */ public static function get_related_orders( int $subscription_id ): array { global $wpdb; $table_name = $wpdb->prefix . 'subscrpt_order_relation'; // @phpcs:ignore $order_histories = $wpdb->get_results( $wpdb->prepare( 'SELECT order_id, order_item_id, type FROM %i WHERE subscription_id=%d ORDER BY id DESC', array( $table_name, $subscription_id, ) ) ); return $order_histories; } /** * Get parent order from subscription. * * @param int $subscription_id Subscription ID. */ public static function get_parent_order( int $subscription_id ) { $related_orders = self::get_related_orders( $subscription_id ); $last_order = end( $related_orders ); if ( ! $last_order || strtolower( $last_order->type ?? '' ) !== 'new' ) { foreach ( $related_orders as $order ) { if ( strtolower( $order->type ?? '' ) === 'new' ) { $last_order = $order; break; } } } $parent_order_id = $last_order->order_id ?? 0; $parent_order = wc_get_order( $parent_order_id ); return $parent_order; } /** * Create new order for renewal. * * @param \WC_Order $old_order Old Order Object. * @param \WC_Order_Item_Product $order_item Old Order Item Object. * @param array $product_args Product args for add product. * * @return array|false */ public static function create_new_order_for_renewal( \WC_Order $old_order, \WC_Order_Item_Product $order_item, array $product_args ) { $product = $order_item->get_product(); $user_id = $old_order->get_user_id(); $new_order = wc_create_order( array( 'customer_id' => $user_id, 'status' => 'pending', ) ); $product_meta = apply_filters( 'subscrpt_renewal_item_meta', wc_get_order_item_meta( $order_item->get_id(), '_subscrpt_meta', true ), $product, $order_item ); $product_args = apply_filters( 'subscrpt_renewal_product_args', $product_args, $product, $order_item ); if ( ! $product_args ) { return false; } $new_order_item_id = $new_order->add_product( $product, $order_item->get_quantity(), $product_args ); wc_update_order_item_meta( $new_order_item_id, '_subscrpt_meta', array( 'time' => $product_meta['time'], 'type' => $product_meta['type'], 'trial' => null, ) ); // Add debug log. subscrpt_write_debug_log( "Renewal order #{$new_order->get_id()} created for old order #{$old_order->get_id()}" ); return array( 'order' => $new_order, 'order_item_id' => $new_order_item_id, ); } /** * Check if old order is completed or deleted! * * @param mixed $old_order_id Old Order Id. * * @return \WC_Order|false */ public static function check_order_for_renewal( $old_order_id ) { $old_order = wc_get_order( $old_order_id ); if ( ! $old_order || 'completed' !== $old_order->get_status() ) { if ( ! is_admin() && function_exists( 'wc_add_notice' ) && WC()->session ) { return wc_add_notice( __( 'Subscription renewal isn\'t possible due to previous order not completed or deletion.', 'subscription' ), 'error' ); } return false; } return $old_order; } /** * Get delivery info from order. * * @param \WC_Order $order Order object. * @return array */ public static function get_delivery_info_from_order( \WC_Order $order ) { $customer_id = $order->get_customer_id(); $customer = new \WC_Customer( $customer_id ); $email = $customer->get_email(); // Billing info (get from order first, if empty get from customer). $billing_first_name = ! empty( $order->get_billing_first_name() ) ? $order->get_billing_first_name() : $customer->get_billing_first_name(); $billing_last_name = ! empty( $order->get_billing_last_name() ) ? $order->get_billing_last_name() : $customer->get_billing_last_name(); $billing_email = ! empty( $order->get_billing_email() ) ? $order->get_billing_email() : $customer->get_billing_email(); $billing_phone = ! empty( $order->get_billing_phone() ) ? $order->get_billing_phone() : $customer->get_billing_phone(); $billing_company = ! empty( $order->get_billing_company() ) ? $order->get_billing_company() : $customer->get_billing_company(); $billing_city = ! empty( $order->get_billing_city() ) ? $order->get_billing_city() : $customer->get_billing_city(); $billing_state = ! empty( $order->get_billing_state() ) ? $order->get_billing_state() : $customer->get_billing_state(); $billing_country = ! empty( $order->get_billing_country() ) ? $order->get_billing_country() : $customer->get_billing_country(); $billing_postcode = ! empty( $order->get_billing_postcode() ) ? $order->get_billing_postcode() : $customer->get_billing_postcode(); $billing_address_1 = ! empty( $order->get_billing_address_1() ) ? $order->get_billing_address_1() : $customer->get_billing_address_1(); $billing_address_2 = ! empty( $order->get_billing_address_2() ) ? $order->get_billing_address_2() : $customer->get_billing_address_2(); // Shipping info (get from order first, if empty get from customer). $shipping_first_name = ! empty( $order->get_shipping_first_name() ) ? $order->get_shipping_first_name() : $customer->get_shipping_first_name(); $shipping_last_name = ! empty( $order->get_shipping_last_name() ) ? $order->get_shipping_last_name() : $customer->get_shipping_last_name(); $shipping_phone = ! empty( $order->get_shipping_phone() ) ? $order->get_shipping_phone() : $customer->get_shipping_phone(); $shipping_company = ! empty( $order->get_shipping_company() ) ? $order->get_shipping_company() : $customer->get_shipping_company(); $shipping_city = ! empty( $order->get_shipping_city() ) ? $order->get_shipping_city() : $customer->get_shipping_city(); $shipping_state = ! empty( $order->get_shipping_state() ) ? $order->get_shipping_state() : $customer->get_shipping_state(); $shipping_country = ! empty( $order->get_shipping_country() ) ? $order->get_shipping_country() : $customer->get_shipping_country(); $shipping_postcode = ! empty( $order->get_shipping_postcode() ) ? $order->get_shipping_postcode() : $customer->get_shipping_postcode(); $shipping_address_1 = ! empty( $order->get_shipping_address_1() ) ? $order->get_shipping_address_1() : $customer->get_shipping_address_1(); $shipping_address_2 = ! empty( $order->get_shipping_address_2() ) ? $order->get_shipping_address_2() : $customer->get_shipping_address_2(); $order_meta = [ 'customer_id' => $order->get_customer_id(), 'email' => $email, 'billing' => [ 'first_name' => $billing_first_name, 'last_name' => $billing_last_name, 'email' => $billing_email, 'phone' => $billing_phone, 'company' => $billing_company, 'city' => $billing_city, 'state' => $billing_state, 'country' => $billing_country, 'postcode' => $billing_postcode, 'address_1' => $billing_address_1, 'address_2' => $billing_address_2, ], 'shipping' => [ 'first_name' => $shipping_first_name, 'last_name' => $shipping_last_name, 'phone' => $shipping_phone, 'company' => $shipping_company, 'city' => $shipping_city, 'state' => $shipping_state, 'country' => $shipping_country, 'postcode' => $shipping_postcode, 'address_1' => $shipping_address_1, 'address_2' => $shipping_address_2, ], ]; return $order_meta; } /** * Set delivery info to order. * * @param \WC_Order $order Order object. * @param array $order_meta Order meta data. */ public static function set_delivery_info_to_order( \WC_Order $order, array $order_meta ) { // Set Billing Info. $order->set_billing_first_name( $order_meta['billing']['first_name'] ?? '' ); $order->set_billing_last_name( $order_meta['billing']['last_name'] ?? '' ); $order->set_billing_email( $order_meta['billing']['email'] ?? '' ); $order->set_billing_phone( $order_meta['billing']['phone'] ?? '' ); $order->set_billing_company( $order_meta['billing']['company'] ?? '' ); $order->set_billing_city( $order_meta['billing']['city'] ?? '' ); $order->set_billing_state( $order_meta['billing']['state'] ?? '' ); $order->set_billing_country( $order_meta['billing']['country'] ?? '' ); $order->set_billing_postcode( $order_meta['billing']['postcode'] ?? '' ); $order->set_billing_address_1( $order_meta['billing']['address_1'] ?? '' ); $order->set_billing_address_2( $order_meta['billing']['address_2'] ?? '' ); // Set Shipping Info. $order->set_shipping_first_name( $order_meta['shipping']['first_name'] ?? '' ); $order->set_shipping_last_name( $order_meta['shipping']['last_name'] ?? '' ); $order->set_shipping_phone( $order_meta['shipping']['phone'] ?? '' ); $order->set_shipping_company( $order_meta['shipping']['company'] ?? '' ); $order->set_shipping_city( $order_meta['shipping']['city'] ?? '' ); $order->set_shipping_state( $order_meta['shipping']['state'] ?? '' ); $order->set_shipping_country( $order_meta['shipping']['country'] ?? '' ); $order->set_shipping_postcode( $order_meta['shipping']['postcode'] ?? '' ); $order->set_shipping_address_1( $order_meta['shipping']['address_1'] ?? '' ); $order->set_shipping_address_2( $order_meta['shipping']['address_2'] ?? '' ); } /** * Save meta-data from old order * * @param \WC_Order $new_order new order object. * @param \WC_Order $old_order old order object. * * @return void */ public static function clone_order_metadata( $new_order, $old_order ) { // Set customer and currency info. $new_order->set_customer_id( $old_order->get_customer_id() ); $new_order->set_currency( $old_order->get_currency() ); // Get delivery info from old order. $order_meta = self::get_delivery_info_from_order( $old_order ); // Check for any missing information. $missing_billing_info = true; foreach ( $order_meta['billing'] as $key => $value ) { if ( ! empty( $value ) ) { $missing_billing_info = false; break; } } $missing_shipping_info = true; foreach ( $order_meta['shipping'] as $key => $value ) { if ( ! empty( $value ) ) { $missing_shipping_info = false; break; } } $missing_info = $missing_billing_info || $missing_shipping_info; // Get info from the parent order if missing. if ( $missing_info ) { $subscription = self::get_subscriptions_from_order( $old_order->get_id() ); $subscription = reset( $subscription ); $subscription_id = ! empty( $subscription ) ? $subscription->subscription_id : 0; subscrpt_write_log( "Missing delivery info in old order #{$old_order->get_id()} for subscription #{$subscription_id}. Trying to get from parent order." ); $parent_order = self::get_parent_order( $subscription_id ); if ( ! empty( $parent_order ) ) { $order_meta = self::get_delivery_info_from_order( $parent_order ); } } // Set delivery info to new order. self::set_delivery_info_to_order( $new_order, $order_meta ); } } // HPOS: All order data access below uses WooCommerce CRUD and is HPOS compatible.