PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / 1.10.8
Subscriptions for WooCommerce with Stripe Recurring Payments v1.10.8
1.11.2 1.11.1 1.11.0 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 1.10.2 1.10.1 1.10.0 1.9.6 1.9.5 trunk 1.3.0 1.3.1 1.3.2 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 1.5.2 All 60 releases
subscription / includes / functions.php

functions.php in Subscriptions for WooCommerce with Stripe Recurring Payments 1.10.8, at includes/functions.php

624 lines 18.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
4 use SpringDevs\Subscription\Illuminate\Subscription\Subscription;
5 use SpringDevs\Subscription\Utils\Product;
6
7 /**
8 * Generate URL for Subscription Action.
9 *
10 * @param string $action Action.
11 * @param string $nonce nonce.
12 * @param int $subscription_id Subscription ID.
13 *
14 * @return string
15 */
16 function subscrpt_get_action_url( $action, $nonce, $subscription_id ) {
17 $view_subscription_endpoint = Subscription::get_user_endpoint( 'view_subs' );
18 return add_query_arg(
19 array(
20 'subscrpt_id' => $subscription_id,
21 'action' => $action,
22 'wpnonce' => $nonce,
23 ),
24 wc_get_endpoint_url( $view_subscription_endpoint, $subscription_id, wc_get_page_permalink( 'myaccount' ) )
25 );
26 }
27
28
29 /**
30 * Get typos.
31 *
32 * @param int $number Number.
33 * @param string $typo Typo.
34 *
35 * @return string
36 */
37 function subscrpt_get_typos( $number, $typo ) {
38 if ( $number == 1 && $typo == 'days' ) {
39 return ucfirst( __( 'day', 'subscription' ) );
40 } elseif ( $number == 1 && $typo == 'weeks' ) {
41 return ucfirst( __( 'week', 'subscription' ) );
42 } elseif ( $number == 1 && $typo == 'months' ) {
43 return ucfirst( __( 'month', 'subscription' ) );
44 } elseif ( $number == 1 && $typo == 'years' ) {
45 return ucfirst( __( 'year', 'subscription' ) );
46 } else {
47 return ucfirst( $typo );
48 }
49 }
50
51 /**
52 * Format time with trial.
53 *
54 * @param mixed $time Time.
55 * @param null|string $trial Trial.
56 *
57 * @return string
58 */
59 function subscrpt_next_date( $time, $trial = null ) {
60 if ( null === $trial ) {
61 $start_date = time();
62 } else {
63 $start_date = strtotime( $trial );
64 }
65
66 return gmdate( 'F d, Y', strtotime( $time, $start_date ) );
67 }
68
69 /**
70 * Check if subscription-pro activated.
71 *
72 * @return bool
73 */
74 function subscrpt_pro_activated(): bool {
75 return class_exists( 'Sdevs_Wc_Subscription_Pro' );
76 }
77
78 /**
79 * Get renewal process settings.
80 *
81 * @return bool
82 */
83 function subscrpt_is_auto_renew_enabled() {
84 return 'auto' === get_option( 'subscrpt_renewal_process', 'auto' );
85 }
86
87 /**
88 * Get maximum payments for a subscription, checking variation, product, and subscription meta.
89 *
90 * @param int $subscription_id Subscription ID.
91 * @return string|int Maximum payments or empty string if not set.
92 */
93 function subscrpt_get_max_payments( $subscription_id ) {
94 $product_id = get_post_meta( $subscription_id, '_subscrpt_product_id', true );
95 if ( ! $product_id ) {
96 return '';
97 }
98
99 $max_payments = null;
100
101 // Check for variation first
102 $variation_id = get_post_meta( $subscription_id, '_subscrpt_variation_id', true );
103 if ( $variation_id ) {
104 $max_payments = get_post_meta( $variation_id, '_subscrpt_max_no_payment', true );
105 }
106
107 // Fallback to product if variation doesn't have max payments or no variation
108 if ( ! $max_payments ) {
109 $max_payments = get_post_meta( $product_id, '_subscrpt_max_no_payment', true );
110 }
111
112 // Also check subscription's own meta data as final fallback
113 if ( ! $max_payments ) {
114 $max_payments = get_post_meta( $subscription_id, '_subscrpt_max_no_payment', true );
115 }
116
117 return $max_payments ?: '';
118 }
119
120 /**
121 * Count total payments made.
122 *
123 * @param int $subscription_id Subscription ID.
124 * @return int Number of payments made.
125 */
126 function subscrpt_count_payments_made( $subscription_id ) {
127 global $wpdb;
128
129 $table_name = $wpdb->prefix . 'subscrpt_order_relation';
130
131 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
132 $relations = $wpdb->get_results(
133 $wpdb->prepare(
134 "SELECT sr.*, p.post_status, p.post_date
135 FROM $table_name sr
136 INNER JOIN {$wpdb->posts} p ON sr.order_id = p.ID
137 WHERE sr.subscription_id = %d
138 ORDER BY p.post_date ASC",
139 $subscription_id
140 )
141 );
142 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
143
144 // Define all payment-related order types (allow filtering for extensibility)
145 $payment_types = apply_filters( 'subscrpt_payment_order_types', array( 'new', 'renew', 'early-renew' ) );
146
147 // Count successful payments
148 $successful_count = 0;
149 foreach ( $relations as $relation ) {
150 // Count all payment-related types
151 if ( in_array( $relation->type, $payment_types ) ) {
152 // Get the actual WooCommerce order
153 $order = wc_get_order( $relation->order_id );
154 if ( $order ) {
155 // Check if order was paid/successful
156 if ( $order->is_paid() || in_array( $order->get_status(), array( 'completed', 'processing', 'on-hold' ) ) ) {
157 ++$successful_count;
158 }
159 }
160 }
161 }
162
163 return $successful_count;
164 }
165
166 /**
167 * Check if subscription has reached its maximum payment limit.
168 *
169 * @param int $subscription_id Subscription ID.
170 * @return bool True if limit reached, false otherwise.
171 */
172 function subscrpt_is_max_payments_reached( $subscription_id ) {
173 // Get the product ID from subscription
174 $product_id = get_post_meta( $subscription_id, '_subscrpt_product_id', true );
175 if ( ! $product_id ) {
176 return false;
177 }
178
179 // Get maximum payments using helper function
180 $max_payments = subscrpt_get_max_payments( $subscription_id );
181
182 // Allow override of total installments
183 $max_payments = apply_filters( 'subscrpt_split_payment_total_override', $max_payments, $subscription_id, $product_id );
184
185 // If no limit set or unlimited, more payments are allowed
186 if ( ! $max_payments || intval( $max_payments ) <= 0 ) {
187 return false;
188 }
189
190 // Count payments made
191 $payments_made = subscrpt_count_payments_made( $subscription_id );
192
193 // Enhanced completion logic considering failed payments
194 $is_reached = subscrpt_check_enhanced_completion( $subscription_id, $payments_made, $max_payments );
195
196 // Fire action when split payment plan is completed (first time only)
197 if ( $is_reached && ! get_post_meta( $subscription_id, '_subscrpt_split_payment_completed_fired', true ) ) {
198 // Add completion milestone note
199 subscrpt_add_payment_completion_note( $subscription_id, $payments_made, $max_payments );
200
201 // Allow customization of subscription status after completion
202 $expire_status = apply_filters( 'subscrpt_split_payment_expire_status', 'expired', $subscription_id, $payments_made, $max_payments );
203
204 // Update subscription status if different from current
205 $current_status = get_post_status( $subscription_id );
206 if ( $current_status !== $expire_status ) {
207 wp_update_post(
208 array(
209 'ID' => $subscription_id,
210 'post_status' => $expire_status,
211 )
212 );
213 }
214
215 do_action( 'subscrpt_split_payment_completed', $subscription_id, $payments_made, $max_payments );
216 update_post_meta( $subscription_id, '_subscrpt_split_payment_completed_fired', true );
217
218 // Handle split payment access timing if Pro version is active
219 if ( function_exists( 'subscrpt_pro_activated' ) && subscrpt_pro_activated() ) {
220 if ( class_exists( '\SpringDevs\SubscriptionPro\Illuminate\SplitPaymentHandler' ) ) {
221 \SpringDevs\SubscriptionPro\Illuminate\SplitPaymentHandler::handle_split_payment_completion( $subscription_id, $payments_made, $max_payments );
222 }
223 }
224 }
225
226 return $is_reached;
227 }
228
229 /**
230 * Get remaining payments for a subscription.
231 *
232 * @param int $subscription_id Subscription ID.
233 * @return int|string Number of remaining payments or 'unlimited'.
234 */
235 function subscrpt_get_remaining_payments( $subscription_id ) {
236 // Get maximum payments using helper function
237 $max_payments = subscrpt_get_max_payments( $subscription_id );
238
239 // If no limit set or unlimited
240 if ( ! $max_payments || intval( $max_payments ) <= 0 ) {
241 return 'unlimited';
242 }
243
244 // Count payments made
245 $payments_made = subscrpt_count_payments_made( $subscription_id );
246
247 // Calculate remaining
248 $remaining = intval( $max_payments ) - intval( $payments_made );
249
250 return max( 0, $remaining );
251 }
252
253 /**
254 * Get payment type for a subscription (handles variations properly).
255 *
256 * @param int $subscription_id Subscription ID.
257 * @return string Payment type ('split_payment' or 'recurring').
258 */
259 function subscrpt_get_payment_type( $subscription_id ) {
260 $product_id = get_post_meta( $subscription_id, '_subscrpt_product_id', true );
261 $variation_id = get_post_meta( $subscription_id, '_subscrpt_variation_id', true );
262
263 $payment_type = 'recurring'; // Default
264
265 // Check variation first if it exists
266 if ( $variation_id ) {
267 $variation_payment_type = get_post_meta( $variation_id, '_subscrpt_payment_type', true );
268 if ( $variation_payment_type ) {
269 $payment_type = $variation_payment_type;
270 }
271 }
272
273 // Fallback to product if no variation payment type
274 if ( $payment_type === 'recurring' && $product_id ) {
275 $product_payment_type = get_post_meta( $product_id, '_subscrpt_payment_type', true );
276 if ( $product_payment_type ) {
277 $payment_type = $product_payment_type;
278 }
279 }
280
281 // Final fallback: check subscription's own meta data
282 if ( $payment_type === 'recurring' ) {
283 $subscription_payment_type = get_post_meta( $subscription_id, '_subscrpt_payment_type', true );
284 if ( $subscription_payment_type ) {
285 $payment_type = $subscription_payment_type;
286 }
287 }
288
289 return $payment_type;
290 }
291
292 /**
293 * Enhanced completion check considering failed payments and access suspension.
294 *
295 * @param int $subscription_id Subscription ID.
296 * @param int $payments_made Number of successful payments made.
297 * @param int $max_payments Maximum payments required.
298 * @return bool True if subscription should be considered complete.
299 */
300 function subscrpt_check_enhanced_completion( $subscription_id, $payments_made, $max_payments ) {
301 // Standard completion check
302 if ( $payments_made >= $max_payments ) {
303 return true;
304 }
305
306 // Check for access suspension due to payment failures
307 if ( function_exists( '\SpringDevs\SubscriptionPro\Illuminate\PaymentFailureHandler::is_access_suspended' ) ) {
308 $is_suspended = \SpringDevs\SubscriptionPro\Illuminate\PaymentFailureHandler::is_access_suspended( $subscription_id );
309 if ( $is_suspended ) {
310 // If access is suspended, check if we should force completion
311 $force_completion_on_suspension = apply_filters( 'subscrpt_force_completion_on_suspension', false, $subscription_id );
312 if ( $force_completion_on_suspension ) {
313 return true;
314 }
315 }
316 }
317
318 // Check for maximum failure threshold
319 $failure_count = get_post_meta( $subscription_id, '_subscrpt_payment_failure_count', true ) ?: 0;
320 $max_failures_before_completion = apply_filters( 'subscrpt_max_failures_before_completion', 0, $subscription_id );
321
322 if ( $max_failures_before_completion > 0 && $failure_count >= $max_failures_before_completion ) {
323 // Force completion after too many failures
324 return true;
325 }
326
327 // Check for time-based completion (e.g., if too much time has passed)
328 $completion_timeout_days = apply_filters( 'subscrpt_completion_timeout_days', 0, $subscription_id );
329 if ( $completion_timeout_days > 0 ) {
330 $start_date = get_post_meta( $subscription_id, '_subscrpt_start_date', true );
331 if ( $start_date ) {
332 $timeout_timestamp = $start_date + ( $completion_timeout_days * DAY_IN_SECONDS );
333 if ( current_time( 'timestamp' ) >= $timeout_timestamp ) {
334 return true;
335 }
336 }
337 }
338
339 return false;
340 }
341
342 /**
343 * Count total payment attempts (including failed ones) for a subscription.
344 *
345 * @param int $subscription_id Subscription ID.
346 * @return array Array with 'successful', 'failed', and 'total' counts.
347 */
348 function subscrpt_count_all_payment_attempts( $subscription_id ) {
349 global $wpdb;
350
351 $table_name = $wpdb->prefix . 'subscrpt_order_relation';
352
353 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
354 $relations = $wpdb->get_results(
355 $wpdb->prepare(
356 "SELECT sr.*, p.post_status, p.post_date
357 FROM $table_name sr
358 INNER JOIN {$wpdb->posts} p ON sr.order_id = p.ID
359 WHERE sr.subscription_id = %d
360 ORDER BY p.post_date ASC",
361 $subscription_id
362 )
363 );
364 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
365
366 // Define all payment-related order types
367 $payment_types = apply_filters( 'subscrpt_payment_order_types', array( 'new', 'renew', 'early-renew' ) );
368
369 $successful_count = 0;
370 $failed_count = 0;
371
372 foreach ( $relations as $relation ) {
373 if ( in_array( $relation->type, $payment_types ) ) {
374 $order = wc_get_order( $relation->order_id );
375 if ( $order ) {
376 if ( $order->is_paid() || in_array( $order->get_status(), array( 'completed', 'processing', 'on-hold' ) ) ) {
377 ++$successful_count;
378 } elseif ( in_array( $order->get_status(), array( 'failed', 'cancelled' ) ) ) {
379 ++$failed_count;
380 }
381 }
382 }
383 }
384
385 return array(
386 'successful' => $successful_count,
387 'failed' => $failed_count,
388 'total' => $successful_count + $failed_count,
389 );
390 }
391
392 if ( ! function_exists( 'wps_subscription_order_relation_type_cast' ) ) {
393 /**
394 * Return Label against key.
395 *
396 * @param string $key Key to return cast Value.
397 *
398 * @return string
399 */
400 function order_relation_type_cast( string $key ) {
401 // add Deprecated notice
402 _deprecated_function( 'order_relation_type_cast', '1.5.3', 'wps_subscription_order_relation_type_cast' );
403 return wps_subscription_order_relation_type_cast( $key );
404 }
405 /**
406 * Order relation type cast.
407 *
408 * @param string $key Key.
409 *
410 * @return string
411 */
412 function wps_subscription_order_relation_type_cast( string $key ) {
413 $relational_type_keys = apply_filters(
414 'subscrpt_order_relational_types',
415 array(
416 'new' => __( 'New Subscription Order', 'subscription' ),
417 'renew' => __( 'Renewal Order', 'subscription' ),
418 )
419 );
420
421 return isset( $relational_type_keys[ $key ] ) ? $relational_type_keys[ $key ] : '-';
422 }
423 }
424
425 if ( ! function_exists( 'wps_subscription_is_wc_order_hpos_enabled' ) ) {
426 /**
427 * Check if HPOS enabled.
428 */
429 function is_wc_order_hpos_enabled() {
430 // add Deprecated notice
431 _deprecated_function( 'is_wc_order_hpos_enabled', '1.5.3', 'wps_subscription_is_wc_order_hpos_enabled' );
432 return wps_subscription_is_wc_order_hpos_enabled();
433 }
434 /**
435 * Check if HPOS enabled.
436 *
437 * @return bool
438 */
439 function wps_subscription_is_wc_order_hpos_enabled() {
440 return function_exists( 'wc_get_container' ) ?
441 wc_get_container()
442 ->get( CustomOrdersTableController::class )
443 ->custom_orders_table_usage_is_enabled()
444 : false;
445 }
446 }
447
448 if ( ! function_exists( 'sdevs_wp_strtotime' ) ) {
449 /**
450 * Get strtotime with WordPress timezone config.
451 *
452 * @param string $str string.
453 * @param int|null $base_timestamp base timestamp.
454 *
455 * @return int
456 */
457 function sdevs_wp_strtotime( $str, $base_timestamp = null ) {
458 return strtotime( wp_date( 'Y-m-d H:i:s', strtotime( $str, $base_timestamp ) ) );
459 }
460 }
461
462 if ( ! function_exists( 'sdevs_order_status_label' ) ) {
463 /**
464 * Get order status label from slug.
465 *
466 * @param string $status Status.
467 *
468 * @return string
469 */
470 function sdevs_order_status_label( $status ) {
471 $order_statuses = wc_get_order_statuses();
472
473 return ( isset( $order_statuses[ "wc-{$status}" ] ) ? $order_statuses[ "wc-{$status}" ] : $status );
474 }
475 }
476
477 if ( ! function_exists( 'wps_subscription_get_timing_types' ) ) {
478 /**
479 * Get labels.
480 *
481 * @param bool $key_value key_value.
482 *
483 * @return array
484 */
485 function get_timing_types( $key_value = false ): array {
486 // add Deprecated notice
487 _deprecated_function( 'get_timing_types', '1.5.3', 'wps_subscription_get_timing_types' );
488 return wps_subscription_get_timing_types( $key_value );
489 }
490 /**
491 * Get timing types.
492 *
493 * @param bool $key_value Key value.
494 *
495 * @return array
496 */
497 function wps_subscription_get_timing_types( $key_value = false ): array {
498 return $key_value ? array(
499 'days' => 'Daily',
500 'weeks' => 'Weekly',
501 'months' => 'Monthly',
502 'years' => 'Yearly',
503 ) : array(
504 array(
505 'label' => __( 'Day', 'subscription' ),
506 'value' => 'days',
507 ),
508 array(
509 'label' => __( 'Week', 'subscription' ),
510 'value' => 'weeks',
511 ),
512 array(
513 'label' => __( 'Month', 'subscription' ),
514 'value' => 'months',
515 ),
516 array(
517 'label' => __( 'Year', 'subscription' ),
518 'value' => 'years',
519 ),
520 );
521 }
522 }
523
524 /**
525 * Get WC product in subscription wrapper.
526 *
527 * @deprecated 1.8.17 Use SpringDevs\Subscription\Illuminate\Subscription\Subscription::get_subs_product().
528 */
529 function sdevs_get_subscription_product( $product ) {
530 // Deprecated notice.
531 _deprecated_function( 'sdevs_get_subscription_product', '1.8.17', 'SpringDevs\Subscription\Illuminate\Subscription\Subscription::get_subs_product' );
532
533 return Subscription::get_subs_product( $product );
534 }
535
536 /**
537 * Logger
538 *
539 * @param mixed $message Message.
540 * @param bool $should_print Print the output.
541 */
542 function subscrpt_write_log( $message, bool $should_print = false ): void {
543 $logger = wc_get_logger();
544
545 $message = is_array( $message ) || is_object( $message ) ? wp_json_encode( $message ) : $message;
546 $logger->add( 'wp_subscription', $message );
547
548 echo esc_html( $should_print ? $message : '' );
549 }
550
551 /**
552 * Debug Logger
553 *
554 * @param mixed $log logs.
555 */
556 function subscrpt_write_debug_log( $log ): void {
557 if ( defined( 'WP_DEBUG' ) && WP_DEBUG === true ) {
558 if ( is_array( $log ) || is_object( $log ) ) {
559 error_log( print_r( $log, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions
560 } else {
561 error_log( 'wp_subscription: ' . $log ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions
562 }
563 }
564 }
565
566 /**
567 * Add payment completion note for split payment subscriptions.
568 *
569 * @param int $subscription_id Subscription ID.
570 * @param int $payments_made Number of payments made.
571 * @param int $max_payments Maximum number of payments.
572 */
573 function subscrpt_add_payment_completion_note( $subscription_id, $payments_made, $max_payments ) {
574 // Check if this is a split payment subscription
575 if ( ! function_exists( 'subscrpt_get_payment_type' ) ) {
576 return;
577 }
578
579 $payment_type = subscrpt_get_payment_type( $subscription_id );
580 if ( 'split_payment' !== $payment_type ) {
581 return;
582 }
583
584 // Create completion note
585 $completion_note = sprintf(
586 /* translators: %1$d: payments made, %2$d: total payments */
587 __( 'Split payment plan completed successfully! %1$d of %2$d payments received.', 'subscription' ),
588 $payments_made,
589 $max_payments
590 );
591
592 // Add the completion note
593 $comment_id = wp_insert_comment(
594 array(
595 'comment_author' => 'Subscription for WooCommerce',
596 'comment_content' => $completion_note,
597 'comment_post_ID' => $subscription_id,
598 'comment_type' => 'order_note',
599 )
600 );
601 update_comment_meta( $comment_id, '_subscrpt_activity', __( 'Split Payment - Plan Complete', 'subscription' ) );
602 update_comment_meta( $comment_id, '_subscrpt_activity_type', 'split_payment' );
603
604 // Add payment summary note
605 $payment_summary = sprintf(
606 /* translators: %1$d: payments made, %2$d: total payments, %3$s: completion date */
607 __( 'Payment Summary: %1$d of %2$d installments completed on %3$s. All payments received successfully.', 'subscription' ),
608 $payments_made,
609 $max_payments,
610 date_i18n( wc_date_format(), current_time( 'timestamp' ) )
611 );
612
613 $summary_comment_id = wp_insert_comment(
614 array(
615 'comment_author' => 'Subscription for WooCommerce',
616 'comment_content' => $payment_summary,
617 'comment_post_ID' => $subscription_id,
618 'comment_type' => 'order_note',
619 )
620 );
621 update_comment_meta( $summary_comment_id, '_subscrpt_activity', __( 'Payment Summary - Complete', 'subscription' ) );
622 update_comment_meta( $summary_comment_id, '_subscrpt_activity_type', 'split_payment_summary' );
623 }
624