PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / 1.11.1
Subscriptions for WooCommerce with Stripe Recurring Payments v1.11.1
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.11.1, at includes/functions.php

648 lines 19.7 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 * Resolve a relative date string against a base timestamp, in site timezone.
451 *
452 * The relative interval is applied to the site-local wall clock (so "+1 month"
453 * keeps the same local time across DST changes), and a real UTC timestamp is
454 * returned.
455 *
456 * Do not reimplement this as strtotime( wp_date( ... ) ): wp_date() renders the
457 * site-local wall clock while strtotime() parses it as UTC (WP sets PHP's default
458 * timezone to UTC), so the site's UTC offset gets added on every call. For
459 * recurring dates that compounds — a daily subscription on a UTC+7 site renews
460 * every 31 hours and skips a calendar day every few renewals.
461 *
462 * @param string $str string.
463 * @param int|null $base_timestamp base timestamp.
464 *
465 * @return int
466 */
467 function sdevs_wp_strtotime( $str, $base_timestamp = null ) {
468 $base = null === $base_timestamp ? time() : (int) $base_timestamp;
469
470 try {
471 $date = new DateTime( '@' . $base );
472 $modified = $date->setTimezone( wp_timezone() )->modify( $str );
473
474 if ( $modified instanceof DateTime ) {
475 return $modified->getTimestamp();
476 }
477 } catch ( Exception $e ) {
478 // Unparsable string — fall through to strtotime().
479 return strtotime( $str, $base );
480 }
481
482 return strtotime( $str, $base );
483 }
484 }
485
486 if ( ! function_exists( 'sdevs_order_status_label' ) ) {
487 /**
488 * Get order status label from slug.
489 *
490 * @param string $status Status.
491 *
492 * @return string
493 */
494 function sdevs_order_status_label( $status ) {
495 $order_statuses = wc_get_order_statuses();
496
497 return ( isset( $order_statuses[ "wc-{$status}" ] ) ? $order_statuses[ "wc-{$status}" ] : $status );
498 }
499 }
500
501 if ( ! function_exists( 'wps_subscription_get_timing_types' ) ) {
502 /**
503 * Get labels.
504 *
505 * @param bool $key_value key_value.
506 *
507 * @return array
508 */
509 function get_timing_types( $key_value = false ): array {
510 // add Deprecated notice
511 _deprecated_function( 'get_timing_types', '1.5.3', 'wps_subscription_get_timing_types' );
512 return wps_subscription_get_timing_types( $key_value );
513 }
514 /**
515 * Get timing types.
516 *
517 * @param bool $key_value Key value.
518 *
519 * @return array
520 */
521 function wps_subscription_get_timing_types( $key_value = false ): array {
522 return $key_value ? array(
523 'days' => 'Daily',
524 'weeks' => 'Weekly',
525 'months' => 'Monthly',
526 'years' => 'Yearly',
527 ) : array(
528 array(
529 'label' => __( 'Day', 'subscription' ),
530 'value' => 'days',
531 ),
532 array(
533 'label' => __( 'Week', 'subscription' ),
534 'value' => 'weeks',
535 ),
536 array(
537 'label' => __( 'Month', 'subscription' ),
538 'value' => 'months',
539 ),
540 array(
541 'label' => __( 'Year', 'subscription' ),
542 'value' => 'years',
543 ),
544 );
545 }
546 }
547
548 /**
549 * Get WC product in subscription wrapper.
550 *
551 * @deprecated 1.8.17 Use SpringDevs\Subscription\Illuminate\Subscription\Subscription::get_subs_product().
552 */
553 function sdevs_get_subscription_product( $product ) {
554 // Deprecated notice.
555 _deprecated_function( 'sdevs_get_subscription_product', '1.8.17', 'SpringDevs\Subscription\Illuminate\Subscription\Subscription::get_subs_product' );
556
557 return Subscription::get_subs_product( $product );
558 }
559
560 /**
561 * Logger
562 *
563 * @param mixed $message Message.
564 * @param bool $should_print Print the output.
565 */
566 function subscrpt_write_log( $message, bool $should_print = false ): void {
567 $logger = wc_get_logger();
568
569 $message = is_array( $message ) || is_object( $message ) ? wp_json_encode( $message ) : $message;
570 $logger->add( 'wp_subscription', $message );
571
572 echo esc_html( $should_print ? $message : '' );
573 }
574
575 /**
576 * Debug Logger
577 *
578 * @param mixed $log logs.
579 */
580 function subscrpt_write_debug_log( $log ): void {
581 if ( defined( 'WP_DEBUG' ) && WP_DEBUG === true ) {
582 if ( is_array( $log ) || is_object( $log ) ) {
583 error_log( print_r( $log, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions
584 } else {
585 error_log( 'wp_subscription: ' . $log ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions
586 }
587 }
588 }
589
590 /**
591 * Add payment completion note for split payment subscriptions.
592 *
593 * @param int $subscription_id Subscription ID.
594 * @param int $payments_made Number of payments made.
595 * @param int $max_payments Maximum number of payments.
596 */
597 function subscrpt_add_payment_completion_note( $subscription_id, $payments_made, $max_payments ) {
598 // Check if this is a split payment subscription
599 if ( ! function_exists( 'subscrpt_get_payment_type' ) ) {
600 return;
601 }
602
603 $payment_type = subscrpt_get_payment_type( $subscription_id );
604 if ( 'split_payment' !== $payment_type ) {
605 return;
606 }
607
608 // Create completion note
609 $completion_note = sprintf(
610 /* translators: %1$d: payments made, %2$d: total payments */
611 __( 'Split payment plan completed successfully! %1$d of %2$d payments received.', 'subscription' ),
612 $payments_made,
613 $max_payments
614 );
615
616 // Add the completion note
617 $comment_id = wp_insert_comment(
618 array(
619 'comment_author' => 'Subscription for WooCommerce',
620 'comment_content' => $completion_note,
621 'comment_post_ID' => $subscription_id,
622 'comment_type' => 'order_note',
623 )
624 );
625 update_comment_meta( $comment_id, '_subscrpt_activity', __( 'Split Payment - Plan Complete', 'subscription' ) );
626 update_comment_meta( $comment_id, '_subscrpt_activity_type', 'split_payment' );
627
628 // Add payment summary note
629 $payment_summary = sprintf(
630 /* translators: %1$d: payments made, %2$d: total payments, %3$s: completion date */
631 __( 'Payment Summary: %1$d of %2$d installments completed on %3$s. All payments received successfully.', 'subscription' ),
632 $payments_made,
633 $max_payments,
634 date_i18n( wc_date_format(), current_time( 'timestamp' ) )
635 );
636
637 $summary_comment_id = wp_insert_comment(
638 array(
639 'comment_author' => 'Subscription for WooCommerce',
640 'comment_content' => $payment_summary,
641 'comment_post_ID' => $subscription_id,
642 'comment_type' => 'order_note',
643 )
644 );
645 update_comment_meta( $summary_comment_id, '_subscrpt_activity', __( 'Payment Summary - Complete', 'subscription' ) );
646 update_comment_meta( $summary_comment_id, '_subscrpt_activity_type', 'split_payment_summary' );
647 }
648