$subscription_id, 'action' => $action, 'wpnonce' => $nonce, ), wc_get_endpoint_url( $view_subscription_endpoint, $subscription_id, wc_get_page_permalink( 'myaccount' ) ) ); } /** * Get typos. * * @param int $number Number. * @param string $typo Typo. * * @return string */ function subscrpt_get_typos( $number, $typo ) { if ( $number == 1 && $typo == 'days' ) { return ucfirst( __( 'day', 'subscription' ) ); } elseif ( $number == 1 && $typo == 'weeks' ) { return ucfirst( __( 'week', 'subscription' ) ); } elseif ( $number == 1 && $typo == 'months' ) { return ucfirst( __( 'month', 'subscription' ) ); } elseif ( $number == 1 && $typo == 'years' ) { return ucfirst( __( 'year', 'subscription' ) ); } else { return ucfirst( $typo ); } } /** * Format time with trial. * * @param mixed $time Time. * @param null|string $trial Trial. * * @return string */ function subscrpt_next_date( $time, $trial = null ) { if ( null === $trial ) { $start_date = time(); } else { $start_date = strtotime( $trial ); } return gmdate( 'F d, Y', strtotime( $time, $start_date ) ); } /** * Check if subscription-pro activated. * * @return bool */ function subscrpt_pro_activated(): bool { return class_exists( 'Sdevs_Wc_Subscription_Pro' ); } /** * Get renewal process settings. * * @return bool */ function subscrpt_is_auto_renew_enabled() { return 'auto' === get_option( 'subscrpt_renewal_process', 'auto' ); } /** * Get maximum payments for a subscription, checking variation, product, and subscription meta. * * @param int $subscription_id Subscription ID. * @return string|int Maximum payments or empty string if not set. */ function subscrpt_get_max_payments( $subscription_id ) { $product_id = get_post_meta( $subscription_id, '_subscrpt_product_id', true ); if ( ! $product_id ) { return ''; } $max_payments = null; // Check for variation first $variation_id = get_post_meta( $subscription_id, '_subscrpt_variation_id', true ); if ( $variation_id ) { $max_payments = get_post_meta( $variation_id, '_subscrpt_max_no_payment', true ); } // Fallback to product if variation doesn't have max payments or no variation if ( ! $max_payments ) { $max_payments = get_post_meta( $product_id, '_subscrpt_max_no_payment', true ); } // Also check subscription's own meta data as final fallback if ( ! $max_payments ) { $max_payments = get_post_meta( $subscription_id, '_subscrpt_max_no_payment', true ); } return $max_payments ?: ''; } /** * Count total payments made. * * @param int $subscription_id Subscription ID. * @return int Number of payments made. */ function subscrpt_count_payments_made( $subscription_id ) { global $wpdb; $table_name = $wpdb->prefix . 'subscrpt_order_relation'; // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $relations = $wpdb->get_results( $wpdb->prepare( "SELECT sr.*, p.post_status, p.post_date FROM $table_name sr INNER JOIN {$wpdb->posts} p ON sr.order_id = p.ID WHERE sr.subscription_id = %d ORDER BY p.post_date ASC", $subscription_id ) ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared // Define all payment-related order types (allow filtering for extensibility) $payment_types = apply_filters( 'subscrpt_payment_order_types', array( 'new', 'renew', 'early-renew' ) ); // Count successful payments $successful_count = 0; foreach ( $relations as $relation ) { // Count all payment-related types if ( in_array( $relation->type, $payment_types ) ) { // Get the actual WooCommerce order $order = wc_get_order( $relation->order_id ); if ( $order ) { // Check if order was paid/successful if ( $order->is_paid() || in_array( $order->get_status(), array( 'completed', 'processing', 'on-hold' ) ) ) { ++$successful_count; } } } } return $successful_count; } /** * Check if subscription has reached its maximum payment limit. * * @param int $subscription_id Subscription ID. * @return bool True if limit reached, false otherwise. */ function subscrpt_is_max_payments_reached( $subscription_id ) { // Get the product ID from subscription $product_id = get_post_meta( $subscription_id, '_subscrpt_product_id', true ); if ( ! $product_id ) { return false; } // Get maximum payments using helper function $max_payments = subscrpt_get_max_payments( $subscription_id ); // Allow override of total installments $max_payments = apply_filters( 'subscrpt_split_payment_total_override', $max_payments, $subscription_id, $product_id ); // If no limit set or unlimited, more payments are allowed if ( ! $max_payments || intval( $max_payments ) <= 0 ) { return false; } // Count payments made $payments_made = subscrpt_count_payments_made( $subscription_id ); // Enhanced completion logic considering failed payments $is_reached = subscrpt_check_enhanced_completion( $subscription_id, $payments_made, $max_payments ); // Fire action when split payment plan is completed (first time only) if ( $is_reached && ! get_post_meta( $subscription_id, '_subscrpt_split_payment_completed_fired', true ) ) { // Add completion milestone note subscrpt_add_payment_completion_note( $subscription_id, $payments_made, $max_payments ); // Allow customization of subscription status after completion $expire_status = apply_filters( 'subscrpt_split_payment_expire_status', 'expired', $subscription_id, $payments_made, $max_payments ); // Update subscription status if different from current $current_status = get_post_status( $subscription_id ); if ( $current_status !== $expire_status ) { wp_update_post( array( 'ID' => $subscription_id, 'post_status' => $expire_status, ) ); } do_action( 'subscrpt_split_payment_completed', $subscription_id, $payments_made, $max_payments ); update_post_meta( $subscription_id, '_subscrpt_split_payment_completed_fired', true ); // Handle split payment access timing if Pro version is active if ( function_exists( 'subscrpt_pro_activated' ) && subscrpt_pro_activated() ) { if ( class_exists( '\SpringDevs\SubscriptionPro\Illuminate\SplitPaymentHandler' ) ) { \SpringDevs\SubscriptionPro\Illuminate\SplitPaymentHandler::handle_split_payment_completion( $subscription_id, $payments_made, $max_payments ); } } } return $is_reached; } /** * Get remaining payments for a subscription. * * @param int $subscription_id Subscription ID. * @return int|string Number of remaining payments or 'unlimited'. */ function subscrpt_get_remaining_payments( $subscription_id ) { // Get maximum payments using helper function $max_payments = subscrpt_get_max_payments( $subscription_id ); // If no limit set or unlimited if ( ! $max_payments || intval( $max_payments ) <= 0 ) { return 'unlimited'; } // Count payments made $payments_made = subscrpt_count_payments_made( $subscription_id ); // Calculate remaining $remaining = intval( $max_payments ) - intval( $payments_made ); return max( 0, $remaining ); } /** * Get payment type for a subscription (handles variations properly). * * @param int $subscription_id Subscription ID. * @return string Payment type ('split_payment' or 'recurring'). */ function subscrpt_get_payment_type( $subscription_id ) { $product_id = get_post_meta( $subscription_id, '_subscrpt_product_id', true ); $variation_id = get_post_meta( $subscription_id, '_subscrpt_variation_id', true ); $payment_type = 'recurring'; // Default // Check variation first if it exists if ( $variation_id ) { $variation_payment_type = get_post_meta( $variation_id, '_subscrpt_payment_type', true ); if ( $variation_payment_type ) { $payment_type = $variation_payment_type; } } // Fallback to product if no variation payment type if ( $payment_type === 'recurring' && $product_id ) { $product_payment_type = get_post_meta( $product_id, '_subscrpt_payment_type', true ); if ( $product_payment_type ) { $payment_type = $product_payment_type; } } // Final fallback: check subscription's own meta data if ( $payment_type === 'recurring' ) { $subscription_payment_type = get_post_meta( $subscription_id, '_subscrpt_payment_type', true ); if ( $subscription_payment_type ) { $payment_type = $subscription_payment_type; } } return $payment_type; } /** * Enhanced completion check considering failed payments and access suspension. * * @param int $subscription_id Subscription ID. * @param int $payments_made Number of successful payments made. * @param int $max_payments Maximum payments required. * @return bool True if subscription should be considered complete. */ function subscrpt_check_enhanced_completion( $subscription_id, $payments_made, $max_payments ) { // Standard completion check if ( $payments_made >= $max_payments ) { return true; } // Check for access suspension due to payment failures if ( function_exists( '\SpringDevs\SubscriptionPro\Illuminate\PaymentFailureHandler::is_access_suspended' ) ) { $is_suspended = \SpringDevs\SubscriptionPro\Illuminate\PaymentFailureHandler::is_access_suspended( $subscription_id ); if ( $is_suspended ) { // If access is suspended, check if we should force completion $force_completion_on_suspension = apply_filters( 'subscrpt_force_completion_on_suspension', false, $subscription_id ); if ( $force_completion_on_suspension ) { return true; } } } // Check for maximum failure threshold $failure_count = get_post_meta( $subscription_id, '_subscrpt_payment_failure_count', true ) ?: 0; $max_failures_before_completion = apply_filters( 'subscrpt_max_failures_before_completion', 0, $subscription_id ); if ( $max_failures_before_completion > 0 && $failure_count >= $max_failures_before_completion ) { // Force completion after too many failures return true; } // Check for time-based completion (e.g., if too much time has passed) $completion_timeout_days = apply_filters( 'subscrpt_completion_timeout_days', 0, $subscription_id ); if ( $completion_timeout_days > 0 ) { $start_date = get_post_meta( $subscription_id, '_subscrpt_start_date', true ); if ( $start_date ) { $timeout_timestamp = $start_date + ( $completion_timeout_days * DAY_IN_SECONDS ); if ( current_time( 'timestamp' ) >= $timeout_timestamp ) { return true; } } } return false; } /** * Count total payment attempts (including failed ones) for a subscription. * * @param int $subscription_id Subscription ID. * @return array Array with 'successful', 'failed', and 'total' counts. */ function subscrpt_count_all_payment_attempts( $subscription_id ) { global $wpdb; $table_name = $wpdb->prefix . 'subscrpt_order_relation'; // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $relations = $wpdb->get_results( $wpdb->prepare( "SELECT sr.*, p.post_status, p.post_date FROM $table_name sr INNER JOIN {$wpdb->posts} p ON sr.order_id = p.ID WHERE sr.subscription_id = %d ORDER BY p.post_date ASC", $subscription_id ) ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared // Define all payment-related order types $payment_types = apply_filters( 'subscrpt_payment_order_types', array( 'new', 'renew', 'early-renew' ) ); $successful_count = 0; $failed_count = 0; foreach ( $relations as $relation ) { if ( in_array( $relation->type, $payment_types ) ) { $order = wc_get_order( $relation->order_id ); if ( $order ) { if ( $order->is_paid() || in_array( $order->get_status(), array( 'completed', 'processing', 'on-hold' ) ) ) { ++$successful_count; } elseif ( in_array( $order->get_status(), array( 'failed', 'cancelled' ) ) ) { ++$failed_count; } } } } return array( 'successful' => $successful_count, 'failed' => $failed_count, 'total' => $successful_count + $failed_count, ); } if ( ! function_exists( 'wps_subscription_order_relation_type_cast' ) ) { /** * Return Label against key. * * @param string $key Key to return cast Value. * * @return string */ function order_relation_type_cast( string $key ) { // add Deprecated notice _deprecated_function( 'order_relation_type_cast', '1.5.3', 'wps_subscription_order_relation_type_cast' ); return wps_subscription_order_relation_type_cast( $key ); } /** * Order relation type cast. * * @param string $key Key. * * @return string */ function wps_subscription_order_relation_type_cast( string $key ) { $relational_type_keys = apply_filters( 'subscrpt_order_relational_types', array( 'new' => __( 'New Subscription Order', 'subscription' ), 'renew' => __( 'Renewal Order', 'subscription' ), ) ); return isset( $relational_type_keys[ $key ] ) ? $relational_type_keys[ $key ] : '-'; } } if ( ! function_exists( 'wps_subscription_is_wc_order_hpos_enabled' ) ) { /** * Check if HPOS enabled. */ function is_wc_order_hpos_enabled() { // add Deprecated notice _deprecated_function( 'is_wc_order_hpos_enabled', '1.5.3', 'wps_subscription_is_wc_order_hpos_enabled' ); return wps_subscription_is_wc_order_hpos_enabled(); } /** * Check if HPOS enabled. * * @return bool */ function wps_subscription_is_wc_order_hpos_enabled() { return function_exists( 'wc_get_container' ) ? wc_get_container() ->get( CustomOrdersTableController::class ) ->custom_orders_table_usage_is_enabled() : false; } } if ( ! function_exists( 'sdevs_wp_strtotime' ) ) { /** * Resolve a relative date string against a base timestamp, in site timezone. * * The relative interval is applied to the site-local wall clock (so "+1 month" * keeps the same local time across DST changes), and a real UTC timestamp is * returned. * * Do not reimplement this as strtotime( wp_date( ... ) ): wp_date() renders the * site-local wall clock while strtotime() parses it as UTC (WP sets PHP's default * timezone to UTC), so the site's UTC offset gets added on every call. For * recurring dates that compounds — a daily subscription on a UTC+7 site renews * every 31 hours and skips a calendar day every few renewals. * * @param string $str string. * @param int|null $base_timestamp base timestamp. * * @return int */ function sdevs_wp_strtotime( $str, $base_timestamp = null ) { $base = null === $base_timestamp ? time() : (int) $base_timestamp; try { $date = new DateTime( '@' . $base ); $modified = $date->setTimezone( wp_timezone() )->modify( $str ); if ( $modified instanceof DateTime ) { return $modified->getTimestamp(); } } catch ( Exception $e ) { // Unparsable string — fall through to strtotime(). return strtotime( $str, $base ); } return strtotime( $str, $base ); } } if ( ! function_exists( 'sdevs_order_status_label' ) ) { /** * Get order status label from slug. * * @param string $status Status. * * @return string */ function sdevs_order_status_label( $status ) { $order_statuses = wc_get_order_statuses(); return ( isset( $order_statuses[ "wc-{$status}" ] ) ? $order_statuses[ "wc-{$status}" ] : $status ); } } if ( ! function_exists( 'wps_subscription_get_timing_types' ) ) { /** * Get labels. * * @param bool $key_value key_value. * * @return array */ function get_timing_types( $key_value = false ): array { // add Deprecated notice _deprecated_function( 'get_timing_types', '1.5.3', 'wps_subscription_get_timing_types' ); return wps_subscription_get_timing_types( $key_value ); } /** * Get timing types. * * @param bool $key_value Key value. * * @return array */ function wps_subscription_get_timing_types( $key_value = false ): array { return $key_value ? array( 'days' => 'Daily', 'weeks' => 'Weekly', 'months' => 'Monthly', 'years' => 'Yearly', ) : array( array( 'label' => __( 'Day', 'subscription' ), 'value' => 'days', ), array( 'label' => __( 'Week', 'subscription' ), 'value' => 'weeks', ), array( 'label' => __( 'Month', 'subscription' ), 'value' => 'months', ), array( 'label' => __( 'Year', 'subscription' ), 'value' => 'years', ), ); } } /** * Get WC product in subscription wrapper. * * @deprecated 1.8.17 Use SpringDevs\Subscription\Illuminate\Subscription\Subscription::get_subs_product(). */ function sdevs_get_subscription_product( $product ) { // Deprecated notice. _deprecated_function( 'sdevs_get_subscription_product', '1.8.17', 'SpringDevs\Subscription\Illuminate\Subscription\Subscription::get_subs_product' ); return Subscription::get_subs_product( $product ); } /** * Logger * * @param mixed $message Message. * @param bool $should_print Print the output. */ function subscrpt_write_log( $message, bool $should_print = false ): void { $logger = wc_get_logger(); $message = is_array( $message ) || is_object( $message ) ? wp_json_encode( $message ) : $message; $logger->add( 'wp_subscription', $message ); echo esc_html( $should_print ? $message : '' ); } /** * Debug Logger * * @param mixed $log logs. */ function subscrpt_write_debug_log( $log ): void { if ( defined( 'WP_DEBUG' ) && WP_DEBUG === true ) { if ( is_array( $log ) || is_object( $log ) ) { error_log( print_r( $log, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions } else { error_log( 'wp_subscription: ' . $log ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions } } } /** * Add payment completion note for split payment subscriptions. * * @param int $subscription_id Subscription ID. * @param int $payments_made Number of payments made. * @param int $max_payments Maximum number of payments. */ function subscrpt_add_payment_completion_note( $subscription_id, $payments_made, $max_payments ) { // 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; } // Create completion note $completion_note = sprintf( /* translators: %1$d: payments made, %2$d: total payments */ __( 'Split payment plan completed successfully! %1$d of %2$d payments received.', 'subscription' ), $payments_made, $max_payments ); // Add the completion note $comment_id = wp_insert_comment( array( 'comment_author' => 'Subscription for WooCommerce', 'comment_content' => $completion_note, 'comment_post_ID' => $subscription_id, 'comment_type' => 'order_note', ) ); update_comment_meta( $comment_id, '_subscrpt_activity', __( 'Split Payment - Plan Complete', 'subscription' ) ); update_comment_meta( $comment_id, '_subscrpt_activity_type', 'split_payment' ); // Add payment summary note $payment_summary = sprintf( /* translators: %1$d: payments made, %2$d: total payments, %3$s: completion date */ __( 'Payment Summary: %1$d of %2$d installments completed on %3$s. All payments received successfully.', 'subscription' ), $payments_made, $max_payments, date_i18n( wc_date_format(), current_time( 'timestamp' ) ) ); $summary_comment_id = wp_insert_comment( array( 'comment_author' => 'Subscription for WooCommerce', 'comment_content' => $payment_summary, 'comment_post_ID' => $subscription_id, 'comment_type' => 'order_note', ) ); update_comment_meta( $summary_comment_id, '_subscrpt_activity', __( 'Payment Summary - Complete', 'subscription' ) ); update_comment_meta( $summary_comment_id, '_subscrpt_activity_type', 'split_payment_summary' ); }