type ) { $start_date = time(); $next_date = sdevs_wp_strtotime( $recurr_timing, $start_date ); if ( $trial && ! empty( $trial ) ) { $trial_started = get_post_meta( $subscription_id, '_subscrpt_trial_started', true ); $trial_ended = get_post_meta( $subscription_id, '_subscrpt_trial_ended', true ); if ( empty( $trial_started ) && empty( $trial_ended ) ) { $start_date = sdevs_wp_strtotime( $trial ); $next_date = $start_date; update_post_meta( $subscription_id, '_subscrpt_trial_started', time() ); update_post_meta( $subscription_id, '_subscrpt_trial_ended', $start_date ); update_post_meta( $subscription_id, '_subscrpt_trial_mode', 'on' ); } if ( ! empty( $trial_ended ) ) { $start_date = $trial_ended; $next_date = $start_date; } } update_post_meta( $subscription_id, '_subscrpt_start_date', $start_date ); } elseif ( 'renew' === $subscription_history->type ) { if ( $trial ) { delete_post_meta( $subscription_id, '_subscrpt_trial' ); delete_post_meta( $subscription_id, '_subscrpt_trial_mode' ); delete_post_meta( $subscription_id, '_subscrpt_trial_started' ); delete_post_meta( $subscription_id, '_subscrpt_trial_ended' ); } $next_date = $this->get_anchored_next_date( $subscription_id, $recurr_timing, (int) $subscription_history->order_id ); } elseif ( 'early-renew' === $subscription_history->type ) { if ( $trial ) { delete_post_meta( $subscription_id, '_subscrpt_trial' ); delete_post_meta( $subscription_id, '_subscrpt_trial_mode' ); delete_post_meta( $subscription_id, '_subscrpt_trial_started' ); delete_post_meta( $subscription_id, '_subscrpt_trial_ended' ); } $next_date = sdevs_wp_strtotime( $recurr_timing, time() ); } // Split payment: no next date after the final installment. if ( in_array( $subscription_history->type, array( 'renew', 'early-renew' ), true ) && function_exists( 'subscrpt_is_max_payments_reached' ) && subscrpt_is_max_payments_reached( $subscription_id ) ) { return; } /** * Filter the subscription next payment date before it is saved. * * General-purpose hook (not scoped to split payment) for adjusting the * computed next renewal date. Note `$next_date` may be null for history * types other than 'new'/'renew'/'early-renew' (e.g. switch orders). * * The deprecated `subscrpt_split_payment_next_due_date` filter is bridged * onto this hook in LegacyCompat.php for backward compatibility. * * @param int|null $next_date Computed next payment timestamp, or null. * @param int $subscription_id Subscription ID. * @param string $recurr_timing Recurring timing string (e.g. "1 month"). * @param string $type Subscription history type. */ $next_date = apply_filters( 'subscrpt_subscription_next_date', $next_date, $subscription_id, $recurr_timing, $subscription_history->type ); update_post_meta( $subscription_id, '_subscrpt_next_date', $next_date ); } /** * Calculate a renewal's next payment date, anchored to the previous due date. * * Computing from time() instead makes every cycle inherit however late the * renewal was actually processed — with an hourly cron a due date of 02:00:05 * is missed by the 02:00:03 run and lands at 03:00, and that hour is carried * into every following cycle until a whole billing period is skipped. Anchoring * to the stored due date keeps the billing time-of-day stable instead. * * When payment arrives more than one period late (manual renewal, cron outage), * the anchor is stepped forward period by period so the returned date is always * in the future — otherwise the subscription would be due again immediately. * * Because this runs on every activating status transition of the same renewal * order (pending → processing → completed), the order that last moved the date * is recorded so repeat transitions do not advance the cycle again. * * @param int $subscription_id Subscription ID. * @param string $recurr_timing Recurring timing string (e.g. "1 month"). * @param int $renewal_order_id Renewal order driving this activation. * * @return int Next payment timestamp. */ private function get_anchored_next_date( $subscription_id, $recurr_timing, $renewal_order_id = 0 ) { $now = time(); $anchor = (int) get_post_meta( $subscription_id, '_subscrpt_next_date', true ); if ( $anchor <= 0 ) { return sdevs_wp_strtotime( $recurr_timing, $now ); } // This renewal order already advanced the date; keep it where it is. $dated_by = (int) get_post_meta( $subscription_id, '_subscrpt_next_date_set_by_order', true ); if ( $renewal_order_id && $renewal_order_id === $dated_by ) { return $anchor; } if ( $renewal_order_id ) { update_post_meta( $subscription_id, '_subscrpt_next_date_set_by_order', $renewal_order_id ); } $next_date = sdevs_wp_strtotime( $recurr_timing, $anchor ); // Step forward until the date is in the future. Bail out if the timing string does not advance. $guard = 0; while ( $next_date <= $now && $guard < 1000 ) { $stepped = sdevs_wp_strtotime( $recurr_timing, $next_date ); if ( $stepped <= $next_date ) { break; } $next_date = $stepped; ++$guard; } if ( $next_date <= $now ) { return sdevs_wp_strtotime( $recurr_timing, $now ); } return $next_date; } /** * Add custom column on order item. * * @return void */ public function register_custom_column() { ?> get_id() ); if ( ! $subscription_id ) { echo "-"; return; } $subscription_id = $subscription_id->subscription_id; // Strikes the original amount when a discount carries into renewals. $subtotal = Helper::get_subscription_recurring_price_html( $subscription_id, $item ); ?>
2; if ( $has_trial ) { echo '
+ Got ' . esc_html( $trial ) . ' free trial!'; } } /** * Take some actions based on order status changed. * * @param int $order_id Order Id. */ public function order_status_changed( $order_id ) { $order = wc_get_order( $order_id ); $post_status = 'active'; switch ( $order->get_status() ) { case 'on-hold': case 'pending': $post_status = 'pending'; break; case 'refunded': case 'failed': case 'cancelled': $post_status = 'cancelled'; break; default: $post_status = 'active'; break; } $post_status = apply_filters( 'subscript_order_status_to_post_status', $post_status, $order ); global $wpdb; $table_name = $wpdb->prefix . 'subscrpt_order_relation'; // @phpcs:ignore $histories = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE order_id=%d', array( $table_name, $order_id ) ) ); foreach ( $histories as $history ) { if ( 'new' === $history->type || 'renew' === $history->type ) { $subscription_id = $history->subscription_id; // Renewals ignore the intermediate `processing` state for renewal orders. if ( 'renew' === $history->type && 'processing' === $order->get_status() ) { continue; } // Capture the status before the max-payments check below may flip it. $current_status = get_post_status( $subscription_id ); // Split payment: complete instead of re-activate after the final installment. $target_status = $post_status; if ( 'active' === $target_status && function_exists( 'subscrpt_is_max_payments_reached' ) && subscrpt_is_max_payments_reached( $subscription_id ) ) { $target_status = 'completed'; } // Skip no-op transitions so the note and side effects don't duplicate. if ( $current_status === $target_status ) { continue; } wp_update_post( array( 'ID' => $subscription_id, 'post_status' => $target_status, ) ); // If possible change order status to completed if it has a trial subscription. $this->maybe_trigger_auto_complete_trial_order( $order_id, $subscription_id ); // Increment renewal count for completed renewal orders (wps-pro) if ( 'renew' === $history->type && 'active' === $post_status && function_exists( 'subscrpt_pro_activated' ) && subscrpt_pro_activated() ) { if ( class_exists( '\\SpringDevs\\SubscriptionPro\\Illuminate\\LimitChecker' ) ) { \SpringDevs\SubscriptionPro\Illuminate\LimitChecker::increment_renewal_count( $history->subscription_id ); } } // Add enhanced split payment activity logging $this->add_split_payment_activity_note( $history->subscription_id, $history->type, $post_status, $order ); Action::write_comment( $target_status, $history->subscription_id ); } else { do_action( 'subscrpt_order_status_changed', $order, $history ); } } } /** * Delete the subscription. * * @param int $order_id Order ID. * * @return void */ public function delete_the_subscription( $order_id ) { global $wpdb; $table_name = $wpdb->prefix . 'subscrpt_order_relation'; $histories = Helper::get_subscriptions_from_order( $order_id ); foreach ( (array) $histories as $history ) { $subscription_order_id = get_post_meta( $history->subscription_id, '_subscrpt_order_id', true ); if ( (int) $subscription_order_id === $order_id ) { wp_delete_post( $history->subscription_id, true ); } } // phpcs:ignore $wpdb->delete( $table_name, array( 'order_id' => $order_id ), array( '%d' ) ); } /** * Add enhanced split payment activity note with payment progress and access information. * * @param int $subscription_id Subscription ID. * @param string $history_type History type (new, renew, etc.). * @param string $post_status Post status. * @param \WC_Order $order WooCommerce order object. */ private function add_split_payment_activity_note( $subscription_id, $history_type, $post_status, $order ) { // Only add enhanced notes for active subscriptions if ( 'active' !== $post_status ) { return; } // Check if this is a split payment subscription if ( ! function_exists( 'subscrpt_get_payment_type' ) ) { return; } $payment_type = subscrpt_get_payment_type( $subscription_id ); if ( 'split_payment' !== $payment_type ) { return; } // Get payment progress information $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; $remaining_payments = function_exists( 'subscrpt_get_remaining_payments' ) ? subscrpt_get_remaining_payments( $subscription_id ) : 0; // Determine payment number for this order $payment_number = $payments_made; $order_total = $order->get_total(); $order_currency = $order->get_currency(); // Create enhanced activity note $comment_content = ''; $activity_type = ''; if ( 'new' === $history_type ) { $comment_content = sprintf( /* translators: %1$d: payment number, %2$d: total payments, %3$s: amount, %4$s: currency */ __( 'Split payment %1$d of %2$d received (%3$s %4$s). Initial access granted.', 'subscription' ), $payment_number, $max_payments, $order_total, $order_currency ); $activity_type = __( 'Split Payment - Initial', 'subscription' ); } elseif ( 'renew' === $history_type ) { $comment_content = sprintf( /* translators: %1$d: payment number, %2$d: total payments, %3$s: amount, %4$s: currency, %5$d: remaining */ __( 'Split payment %1$d of %2$d received (%3$s %4$s). %5$d payments remaining.', 'subscription' ), $payment_number, $max_payments, $order_total, $order_currency, $remaining_payments ); $activity_type = __( 'Split Payment - Installment', 'subscription' ); } // Add the enhanced activity note if ( $comment_content ) { $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', 'split_payment' ); // Add order note with split payment context $order_note = sprintf( /* translators: %1$d: payment number, %2$d: total payments, %3$d: subscription id */ __( 'Split payment %1$d of %2$d received for subscription #%3$d', 'subscription' ), $payment_number, $max_payments, $subscription_id ); $order->add_order_note( $order_note ); } } /** * Maybe complete the order if it has a trial subscription and is still in processing status. * * @param int $order_id Order ID. * @param int $subscription_id Subscription ID. */ public function maybe_trigger_auto_complete_trial_order( $order_id, $subscription_id ) { $order = wc_get_order( $order_id ); $order_items = $order->get_items(); // Only attempt to complete if order is still in processing status. if ( 'processing' !== $order->get_status() ) { return; } $is_subs_trial_order = false; foreach ( $order_items as $order_item ) { $subscrpt_meta = $order_item->get_meta( '_subscrpt_meta', true ); if ( ! empty( $subscrpt_meta ) && isset( $subscrpt_meta['trial'] ) ) { $is_subs_trial_order = true; } } if ( $is_subs_trial_order ) { as_enqueue_async_action( 'subscrpt_queue_trial_order_autocomplete', [ 'order_id' => $order_id ] ); $log_message = "Queued auto complete task for free trial order [ID: {$order_id}]"; subscrpt_write_log( $log_message ); subscrpt_write_debug_log( $log_message ); } } /** * Autocomplete subscription trial order. * * @param int $order_id Order ID. */ public function auto_complete_subscription_trial_order( $order_id ) { $order = wc_get_order( $order_id ); if ( ! $order ) { return; } sleep( 3 ); // Adding a small delay to ensure order status is updated before we check it. $order->update_status( 'completed', __( 'Subscription order with trial.', 'subscription' ) ); } }