PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / 2.2.0
Subscriptions for WooCommerce with Stripe Recurring Payments v2.2.0
2.4.0 2.3.0 2.2.0 2.1.0 2.0.0 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 All 65 releases
← All changes | includes/Illuminate/Helper.php +1266 -89 1.3.1 → 2.2.0 View file →
@@ -1,8 +1,21 @@
1 1 <?php
2 +/**
3 + * Subscription helper utilities.
4 + *
5 + * @package SpringDevs\Subscription\Illuminate
6 + */
2 7
3 8 namespace SpringDevs\Subscription\Illuminate;
4 9
10 +use SpringDevs\Subscription\Illuminate\Gateways\Stripe\Stripe;
11 +use SpringDevs\Subscription\Illuminate\Subscription\Subscription;
12 +
13 +// HPOS: This file is compatible with WooCommerce High-Performance Order Storage (HPOS).
14 +// All WooCommerce order data is accessed via WooCommerce CRUD methods (wc_get_order, wc_get_orders, etc.).
15 +// All direct post meta access is for subscription data only, not WooCommerce order data.
16 +// If you add new order data access, use WooCommerce CRUD for HPOS compatibility.
17 +
5 18 /**
6 19 * Class Helper || Some Helper Methods
7 20 *
8 21 * @package SpringDevs\Subscription\Illuminate
@@ -7,29 +20,74 @@
7 20 *
8 21 * @package SpringDevs\Subscription\Illuminate
9 22 */
10 23 class Helper {
11 -
12 24 /**
13 25 * Get type's singular or plural from time_per.
14 26 *
15 27 * @param int $number timing_per.
16 28 * @param string $typo timing_option.
29 + * @param bool $translate Whether to translate the output.
30 + * @return string
31 + */
32 + public static function get_typos( $number, $typo, $translate = false ) {
33 + switch ( strtolower( $typo ) ) {
34 + case 'day':
35 + case 'days':
36 + return $translate
37 + ? _n( 'day', 'days', $number, 'subscription' )
38 + : ( (int) $number === 1 ? 'day' : 'days' );
39 +
40 + case 'week':
41 + case 'weeks':
42 + return $translate
43 + ? _n( 'week', 'weeks', $number, 'subscription' )
44 + : ( (int) $number === 1 ? 'week' : 'weeks' );
45 +
46 + case 'month':
47 + case 'months':
48 + return $translate
49 + ? _n( 'month', 'months', $number, 'subscription' )
50 + : ( (int) $number === 1 ? 'month' : 'months' );
51 +
52 + case 'year':
53 + case 'years':
54 + return $translate
55 + ? _n( 'year', 'years', $number, 'subscription' )
56 + : ( (int) $number === 1 ? 'year' : 'years' );
57 +
58 + default:
59 + return $typo;
60 + }
61 + }
62 +
63 + /**
64 + * Get verbose status from status slug.
17 65 *
18 - * @return string
66 + * @param string $status Status.
67 + * @param bool $return_all Whether to return all statuses or a single status.
68 + *
69 + * @return string|array Verbose status label, or the full status map when $return_all is true.
19 70 */
20 - public static function get_typos( $number, $typo ) {
21 - if ( 1 === (int) $number && 'days' === $typo ) {
22 - return __( 'day', 'sdevs_subscrpt' );
23 - } elseif ( 1 === (int) $number && 'weeks' === $typo ) {
24 - return __( 'week', 'sdevs_subscrpt' );
25 - } elseif ( 1 === (int) $number && 'months' === $typo ) {
26 - return __( 'month', 'sdevs_subscrpt' );
27 - } elseif ( 1 === (int) $number && 'years' === $typo ) {
28 - return __( 'year', 'sdevs_subscrpt' );
29 - } else {
30 - return $typo;
71 + public static function get_verbose_status( $status, $return_all = false ) {
72 + $statuses = array(
73 + 'pending' => __( 'Pending', 'subscription' ),
74 + 'active' => __( 'Active', 'subscription' ),
75 + 'on-hold' => __( 'On Hold', 'subscription' ),
76 + 'expired' => __( 'Expired', 'subscription' ),
77 + 'completed' => __( 'Completed', 'subscription' ),
78 + 'pe_cancelled' => __( 'Pending Cancellation', 'subscription' ),
79 + 'cancelled' => __( 'Cancelled', 'subscription' ),
80 + 'draft' => __( 'Draft', 'subscription' ),
81 + 'trash' => __( 'Trash', 'subscription' ),
82 + );
83 +
84 + if ( $return_all ) {
85 + return $statuses;
31 86 }
87 +
88 + $status = strtolower( $status );
89 + return isset( $statuses[ $status ] ) ? $statuses[ $status ] : '';
32 90 }
33 91
34 92 /**
35 93 * Generate start date
@@ -64,8 +122,107 @@
64 122 return wp_date( get_option( 'date_format' ), strtotime( $time, $start_date ) );
65 123 }
66 124
67 125 /**
126 + * Get Subscriptions
127 + *
128 + * Args:
129 + * - status => [ any, active, pending, expired, pe_cancelled, cancelled, trash ]
130 + * - user_id => user_id, -1 for all users.
131 + * - posts_per_page => limit number of subscriptions.
132 + * - return => return data: ids, post, subscription_data
133 + *
134 + * @param array $args Args.
135 + */
136 + public static function get_subscriptions( array $args = array() ) {
137 + $default_args = array(
138 + 'post_type' => 'subscrpt_order',
139 + 'post_status' => 'active',
140 + 'author' => get_current_user_id(),
141 + 'posts_per_page' => -1,
142 + 'fields' => 'all',
143 + 'return' => 'post',
144 + );
145 +
146 + // Normalize some args.
147 + if ( isset( $args['status'] ) ) {
148 + $args['post_status'] = $args['status'];
149 + unset( $args['status'] );
150 + }
151 + if ( isset( $args['user_id'] ) ) {
152 + $args['author'] = $args['user_id'];
153 + unset( $args['user_id'] );
154 + }
155 +
156 + // Merge default args with provided args.
157 + $final_args = wp_parse_args( $args, $default_args );
158 +
159 + if ( isset( $args['author'] ) ) {
160 + if ( $args['author'] === -1 ) {
161 + unset( $final_args['author'] );
162 + } else {
163 + $final_args['author'] = (int) $args['author'];
164 + }
165 + }
166 +
167 + if ( isset( $args['product_id'] ) ) {
168 + $final_args['meta_query'] = array(
169 + array(
170 + 'key' => '_subscrpt_product_id',
171 + 'value' => (int) $args['product_id'],
172 + ),
173 + );
174 + unset( $final_args['product_id'] );
175 + }
176 +
177 + // Fields check
178 + $only_ids = false;
179 + if ( $final_args['fields'] === 'ids' || $final_args['return'] === 'ids' ) {
180 + $final_args['fields'] = 'all';
181 + $only_ids = true;
182 + }
183 +
184 + // Status check
185 + $statuses = $final_args['post_status'];
186 + $final_args['post_status'] = 'any';
187 +
188 + // Get all subscriptions.
189 + $subscriptions = get_posts( $final_args );
190 +
191 + // Fallback filtering.
192 + // ? Sometime status filtering not works properly. So, we need to filter manually.
193 + $filtered_subscriptions = [];
194 +
195 + // Filter by status.
196 + foreach ( $subscriptions as $subscription ) {
197 + if ( ( is_array( $statuses ) && in_array( 'any', $statuses, true ) ) || $statuses === 'any' ) {
198 + $filtered_subscriptions[] = $subscription;
199 + continue;
200 + }
201 +
202 + if ( ( is_array( $statuses ) && in_array( $subscription->post_status, $statuses, true ) ) || $subscription->post_status === $statuses ) {
203 + $filtered_subscriptions[] = $subscription;
204 + }
205 + }
206 +
207 + // Final filtering (only ids, post, or full data)
208 + $subscriptions = [];
209 + foreach ( $filtered_subscriptions as $subscription ) {
210 + if ( $only_ids ) {
211 + $subscriptions[] = $subscription->ID;
212 + } elseif ( $final_args['return'] === 'subscription_data' ) {
213 + $subs_id = $subscription->ID;
214 + $subscription_data = self::get_subscription_data( $subs_id );
215 + $subscriptions[] = $subscription_data;
216 + } else {
217 + $subscriptions[] = $subscription;
218 + }
219 + }
220 +
221 + return $subscriptions;
222 + }
223 +
224 + /**
68 225 * Check subscription exists by product ID.
69 226 *
70 227 * @param int $product_id Product ID.
71 228 * @param string|array $status Status.
@@ -77,21 +234,14 @@
77 234 return false;
78 235 }
79 236
80 237 $args = array(
81 - 'post_type' => 'subscrpt_order',
82 238 'post_status' => $status,
83 239 'fields' => 'ids',
84 - 'meta_query' => array(
85 - array(
86 - 'key' => '_subscrpt_product_id',
87 - 'value' => $product_id,
88 - ),
89 - ),
90 - 'author' => get_current_user_id(),
240 + 'product_id' => $product_id,
91 241 );
92 242
93 - $posts = get_posts( $args );
243 + $posts = self::get_subscriptions( $args );
94 244 return count( $posts ) > 0 ? $posts[0] : false;
95 245 }
96 246
97 247 /**
@@ -212,9 +362,9 @@
212 362 return false;
213 363 }
214 364
215 365 $time = 1 === (int) $item_meta['time'] ? null : $item_meta['time'] . '-';
216 - $type = self::get_typos( $item_meta['time'], $item_meta['type'] );
366 + $type = self::get_typos( $item_meta['time'], $item_meta['type'], true );
217 367
218 368 $formatted_price = wc_price(
219 369 $price,
220 370 array(
@@ -219,16 +369,17 @@
219 369 $price,
220 370 array(
221 371 'currency' => $order->get_currency(),
222 372 )
223 - ) . ' / ' . $time . $type;
373 + ) . ' / ' . $time . ucfirst( $type );
224 374
225 375 if ( $display_trial ) {
226 - $trial = $item_meta['trial'];
227 376 $has_trial = isset( $item_meta['trial'] ) && strlen( $item_meta['trial'] ) > 2;
377 + $trial = $item_meta['trial'] ?? '';
228 378
229 379 if ( $has_trial ) {
230 - $trial_html = '<br/><small> + Got ' . $trial . ' free trial!</small>';
380 + // translators: %s: trial period.
381 + $trial_html = '<br/><small> ' . sprintf( __( '+ %s free trial!', 'subscription' ), $trial ) . '</small>';
231 382 $formatted_price .= $trial_html;
232 383 }
233 384 }
234 385
@@ -273,21 +424,44 @@
273 424 public static function process_order_renewal( $subscription_id, $order_id, $order_item_id ) {
274 425 global $wpdb;
275 426 $history_table = $wpdb->prefix . 'subscrpt_order_relation';
276 427
428 + // Check if this is a split payment subscription
429 + $payment_type = function_exists( 'subscrpt_get_payment_type' ) ? subscrpt_get_payment_type( $subscription_id ) : 'recurring';
430 + $max_payments = function_exists( 'subscrpt_get_max_payments' ) ? subscrpt_get_max_payments( $subscription_id ) : 0;
431 + $payments_made = function_exists( 'subscrpt_count_payments_made' ) ? subscrpt_count_payments_made( $subscription_id ) : 0;
432 +
433 + $comment_content = '';
434 + $activity_type = '';
435 +
436 + if ( 'split_payment' === $payment_type && $max_payments ) {
437 + $comment_content = sprintf(
438 + /* translators: %1$s: order id, %2$d: payment number, %3$d: total payments */
439 + __( 'Split payment installment %2$d of %3$d. Order %1$s created for subscription.', 'subscription' ),
440 + $order_id,
441 + $payments_made + 1, // +1 because this is a new renewal
442 + $max_payments
443 + );
444 + $activity_type = __( 'Split Payment - Renewal', 'subscription' );
445 + } else {
446 + $comment_content = sprintf(
447 + /* translators: order id. */
448 + __( 'The order %s has been created for the subscription', 'subscription' ),
449 + $order_id
450 + );
451 + $activity_type = __( 'Renewal Order', 'subscription' );
452 + }
453 +
277 454 $comment_id = wp_insert_comment(
278 455 array(
279 456 'comment_author' => 'Subscription for WooCommerce',
280 - 'comment_content' => sprintf(
281 - // translators: order id.
282 - __( 'The order %s has been created for the subscription', 'sdevs_subscrpt' ),
283 - $order_id
284 - ),
457 + 'comment_content' => $comment_content,
285 458 'comment_post_ID' => $subscription_id,
286 459 'comment_type' => 'order_note',
287 460 )
288 461 );
289 - update_comment_meta( $comment_id, '_subscrpt_activity', __( 'Renewal Order', 'sdevs_subscrpt' ) );
462 + update_comment_meta( $comment_id, '_subscrpt_activity', $activity_type );
463 + update_comment_meta( $comment_id, '_subscrpt_activity_type', 'renewal_order' );
290 464
291 465 $wpdb->insert(
292 466 $history_table,
293 467 array(
@@ -296,8 +470,11 @@
296 470 'order_item_id' => $order_item_id,
297 471 'type' => 'renew',
298 472 )
299 473 );
474 +
475 + // Fire action when split payment is renewed
476 + do_action( 'subscrpt_split_payment_renewed', $subscription_id, $order_id, $order_item_id );
300 477 }
301 478
302 479 /**
303 480 * Process new subscription on order.
@@ -311,12 +488,31 @@
311 488 public static function process_new_subscription_order( $order_item, $post_status, $product ) {
312 489 global $wpdb;
313 490 $history_table = $wpdb->prefix . 'subscrpt_order_relation';
314 491
492 + // Prepare split payment arguments
493 + $split_payment_args = array(
494 + 'product_id' => $product->get_id(),
495 + 'order_id' => $order_item->get_order_id(),
496 + 'order_item_id' => $order_item->get_id(),
497 + 'post_status' => $post_status,
498 + 'max_payments' => $product->get_meta( '_subscrpt_max_no_payment' ),
499 + 'timing_per' => $product->get_meta( '_subscrpt_timing_per' ),
500 + 'timing_option' => $product->get_meta( '_subscrpt_timing_option' ),
501 + 'price' => $product->get_price(),
502 + );
503 +
504 + // Allow modification of split payment arguments
505 + $split_payment_args = apply_filters( 'subscrpt_split_payment_args', $split_payment_args, $order_item, $product );
506 +
507 + // Own the subscription from the order's customer, not the current user.
508 + $parent_order = wc_get_order( $order_item->get_order_id() );
509 +
315 510 $args = array(
316 511 'post_title' => 'Subscription',
317 512 'post_type' => 'subscrpt_order',
318 - 'post_status' => $post_status,
513 + 'post_status' => $split_payment_args['post_status'],
514 + 'post_author' => $parent_order ? (int) $parent_order->get_customer_id() : get_current_user_id(),
319 515 );
320 516 $subscription_id = wp_insert_post( $args );
321 517 wp_update_post(
322 518 array(
@@ -323,21 +519,43 @@
323 519 'ID' => $subscription_id,
324 520 'post_title' => "Subscription #{$subscription_id}",
325 521 )
326 522 );
523 + // Check if this is a split payment subscription
524 + $payment_type = $product->get_meta( '_subscrpt_payment_type' );
525 + $payment_type = $payment_type ? $payment_type : 'recurring';
526 + $max_payments = $product->get_meta( '_subscrpt_max_no_payment' );
527 +
528 + $comment_content = '';
529 + $activity_type = '';
530 +
531 + if ( 'split_payment' === $payment_type && $max_payments ) {
532 + $comment_content = sprintf(
533 + /* translators: %1$s: order id, %2$d: max payments */
534 + __( 'Split payment subscription created successfully. Order: %1$s. Total installments: %2$d.', 'subscription' ),
535 + $order_item->get_order_id(),
536 + $max_payments
537 + );
538 + $activity_type = __( 'Split Payment - New Subscription', 'subscription' );
539 + } else {
540 + $comment_content = sprintf(
541 + /* translators: Order Id. */
542 + __( 'Subscription successfully created. Order is %s', 'subscription' ),
543 + $order_item->get_order_id()
544 + );
545 + $activity_type = __( 'New Subscription', 'subscription' );
546 + }
547 +
327 548 $comment_id = wp_insert_comment(
328 549 array(
329 550 'comment_author' => 'Subscription for WooCommerce',
330 - 'comment_content' => sprintf(
331 - // translators: Order Id.
332 - __( 'Subscription successfully created. order is %s', 'sdevs_subscrpt' ),
333 - $order_item->get_order_id()
334 - ),
551 + 'comment_content' => $comment_content,
335 552 'comment_post_ID' => $subscription_id,
336 553 'comment_type' => 'order_note',
337 554 )
338 555 );
339 - update_comment_meta( $comment_id, '_subscrpt_activity', __( 'New Subscription', 'sdevs_subscrpt' ) );
556 + update_comment_meta( $comment_id, '_subscrpt_activity', $activity_type );
557 + update_comment_meta( $comment_id, '_subscrpt_activity_type', 'subs_created' );
340 558
341 559 update_post_meta( $subscription_id, '_subscrpt_product_id', $product->get_id() );
342 560
343 561 $wpdb->insert(
@@ -349,12 +567,415 @@
349 567 'type' => 'new',
350 568 )
351 569 );
352 570
571 + // Fire action when split payment plan is created
572 + do_action( 'subscrpt_split_payment_created', $subscription_id, $split_payment_args, $order_item );
573 +
353 574 return $subscription_id;
354 575 }
355 576
356 577 /**
578 + * Resolve applied coupon discounts for a single cart item, split by whether they recur.
579 + *
580 + * WooCommerce computes a per-coupon, per-item discount breakdown while calculating cart
581 + * totals but never persists it — only the aggregated per-coupon and per-item sums survive.
582 + * Replaying the coupons through a fresh WC_Discounts instance recovers that breakdown,
583 + * which is what lets recurring and one-time discounts be told apart for one cart line.
584 + *
585 + * Amounts are returned in the same space WC_Discounts works in, i.e. `get_price() * qty`,
586 + * so they are tax-inclusive only when the store's prices include tax.
587 + *
588 + * @param string $cart_item_key Cart item key.
589 + *
590 + * @return array{recurring:float,non_recurring:float,total:float,recurring_limit:int}
591 + */
592 + public static function get_cart_item_coupon_discounts( $cart_item_key ) {
593 + $empty = array(
594 + 'recurring' => 0.0,
595 + 'non_recurring' => 0.0,
596 + 'total' => 0.0,
597 + 'recurring_limit' => 0,
598 + );
599 +
600 + if ( ! function_exists( 'WC' ) || ! WC()->cart ) {
601 + return $empty;
602 + }
603 +
604 + $coupons = WC()->cart->get_coupons();
605 + if ( empty( $coupons ) ) {
606 + return $empty;
607 + }
608 +
609 + // Memoized per cart state: recurring totals are read several times per request, and
610 + // replaying coupons re-runs coupon validation, which hits the database.
611 + static $cache = array();
612 +
613 + $cache_key = md5( WC()->cart->get_cart_hash() . '|' . implode( ',', array_keys( $coupons ) ) );
614 +
615 + if ( ! isset( $cache[ $cache_key ] ) ) {
616 + // Replay in cart order so stacked coupons resolve exactly as WC_Cart_Totals resolved them.
617 + $discounts = new \WC_Discounts( WC()->cart );
618 + foreach ( $coupons as $coupon ) {
619 + $discounts->apply_coupon( $coupon );
620 + }
621 + $cache[ $cache_key ] = $discounts->get_discounts();
622 + }
623 +
624 + $result = $empty;
625 + $limits = array();
626 +
627 + foreach ( $cache[ $cache_key ] as $coupon_code => $item_discounts ) {
628 + $amount = (float) ( $item_discounts[ $cart_item_key ] ?? 0 );
629 + if ( 0 >= $amount ) {
630 + continue;
631 + }
632 +
633 + $coupon = $coupons[ $coupon_code ] ?? new \WC_Coupon( $coupon_code );
634 +
635 + /**
636 + * Filters whether a coupon's discount also applies to subscription renewals.
637 + *
638 + * The free plugin has no recurring-coupon concept, so discounts apply to the
639 + * first payment only unless an extension — the pro plugin — says otherwise.
640 + *
641 + * @param bool $is_recurring Whether the discount recurs. Default false.
642 + * @param \WC_Coupon $coupon Coupon object.
643 + * @param string $cart_item_key Cart item key the discount applies to.
644 + */
645 + $is_recurring = (bool) apply_filters( 'subscrpt_coupon_is_recurring', false, $coupon, $cart_item_key );
646 +
647 + if ( ! $is_recurring ) {
648 + $result['non_recurring'] += $amount;
649 + continue;
650 + }
651 +
652 + /**
653 + * Filters how many payments a recurring coupon's discount covers.
654 + *
655 + * @param int $limit Number of payments, including the initial one. 0 means unlimited.
656 + * @param \WC_Coupon $coupon Coupon object.
657 + * @param string $cart_item_key Cart item key the discount applies to.
658 + */
659 + $limit = (int) apply_filters( 'subscrpt_coupon_recurring_limit', 0, $coupon, $cart_item_key );
660 +
661 + // A limit of one covers the initial payment only, so it never reaches a renewal —
662 + // whatever the coupon is flagged as, its effect here is a one-time discount.
663 + if ( 1 === $limit ) {
664 + $result['non_recurring'] += $amount;
665 + continue;
666 + }
667 +
668 + $result['recurring'] += $amount;
669 +
670 + if ( $limit > 0 ) {
671 + $limits[] = $limit;
672 + }
673 + }
674 +
675 + $result['total'] = $result['recurring'] + $result['non_recurring'];
676 +
677 + // The earliest limit to expire is when the discounted figure stops being true.
678 + $result['recurring_limit'] = empty( $limits ) ? 0 : min( $limits );
679 +
680 + return $result;
681 + }
682 +
683 + /**
684 + * Build the discount-aware recurring price figures and markup for one cart item.
685 + *
686 + * A recurring coupon lowers what every renewal costs, so it is folded into the recurring
687 + * figures. A one-time coupon lowers only what is paid today, so the recurring figures keep
688 + * the full price and the caller discloses the first-payment amount separately.
689 + *
690 + * Discounts come back from WC_Discounts in `get_price() * qty` space, so they are converted
691 + * to the tax-inclusive display space by ratio rather than by re-deriving tax.
692 + *
693 + * @param array $cart_item Cart item.
694 + * @param string $cart_item_key Cart item key.
695 + * @param string $type_label Human readable timing label, e.g. "Month".
696 + *
697 + * @return array
698 + */
699 + public static function build_cart_recurring_price_data( $cart_item, $cart_item_key, $type_label ) {
700 + $product = $cart_item['data'];
701 + $quantity = (int) $cart_item['quantity'];
702 + $per_cost = (float) ( $cart_item['subscription']['per_cost'] ?? 0 );
703 +
704 + $full_total = (float) wc_get_price_including_tax( $product, [ 'qty' => $quantity ] );
705 + $discounts = self::get_cart_item_coupon_discounts( $cart_item_key );
706 +
707 + // Same basis WC_Discounts used, so the discount and the basis are directly comparable.
708 + $basis = (float) $product->get_price() * $quantity;
709 + $recurring_ratio = $basis > 0 ? ( $basis - $discounts['recurring'] ) / $basis : 1.0;
710 + $first_ratio = $basis > 0 ? ( $basis - $discounts['total'] ) / $basis : 1.0;
711 +
712 + $total = $full_total * $recurring_ratio;
713 + $timing_html = "<span class='wpsubs-subscription-timing'>&nbsp;/&nbsp;{$type_label}</span>";
714 +
715 + $has_recurring_discount = $discounts['recurring'] > 0;
716 + $full_price_html = wc_price( $full_total ) . $timing_html;
717 + $price_html = $has_recurring_discount
718 + ? '<del aria-hidden="true">' . wc_price( $full_total ) . '</del> <ins>' . wc_price( $total ) . '</ins>' . $timing_html
719 + : $full_price_html;
720 +
721 + return array(
722 + 'price_html' => $price_html,
723 + 'full_price_html' => $full_price_html,
724 + 'price' => $per_cost * $recurring_ratio,
725 + 'full_price' => $per_cost,
726 + 'total' => $total,
727 + 'full_total' => $full_total,
728 + 'first_total' => $full_total * $first_ratio,
729 + 'has_recurring_discount' => $has_recurring_discount,
730 + 'has_one_time_discount' => $discounts['non_recurring'] > 0,
731 + 'recurring_limit' => $discounts['recurring_limit'],
732 + );
733 + }
734 +
735 + /**
736 + * Resolve the discount that still applies to a subscription's future renewals.
737 + *
738 + * Only coupons flagged as recurring survive into renewal orders, and only while their
739 + * recurring limit holds — this mirrors the skip conditions in the pro plugin's
740 + * `Coupon::maybe_add_coupon_to_renewal_order()`, so what is displayed matches what the
741 + * next renewal order will actually be charged.
742 + *
743 + * A subscription order always holds exactly one line item (enforced by
744 + * `Frontend\Cart::validate_cart_items()`), so each coupon line's whole discount belongs
745 + * to that item.
746 + *
747 + * @param int $subscription_id Subscription ID.
748 + * @param \WC_Order|null $order Source order. Resolved from the subscription when omitted.
749 + * @param \WC_Order_Item|null $order_item Source order item. Used to rebase the discount when the
750 + * recurring price has since drifted, e.g. after a switch.
751 + *
752 + * @return array{amount:float,limit:int,exhausted:bool}
753 + */
754 + public static function get_subscription_recurring_discount( $subscription_id, $order = null, $order_item = null ) {
755 + $result = array(
756 + 'amount' => 0.0,
757 + 'limit' => 0,
758 + 'exhausted' => false,
759 + );
760 +
761 + // Memoized per request: list views and the single view each resolve the same
762 + // subscription two or three times, and a coupon'd subscription costs a query.
763 + static $cache = array();
764 +
765 + $cache_key = $subscription_id . '|' . ( $order_item ? $order_item->get_id() : 0 );
766 +
767 + if ( isset( $cache[ $cache_key ] ) ) {
768 + return $cache[ $cache_key ];
769 + }
770 +
771 + if ( ! $order ) {
772 + $order_item_id = get_post_meta( $subscription_id, '_subscrpt_order_item_id', true );
773 + $order = $order_item_id ? wc_get_order( wc_get_order_id_by_order_item_id( $order_item_id ) ) : null;
774 + }
775 +
776 + if ( ! $order ) {
777 + $cache[ $cache_key ] = $result;
778 + return $result;
779 + }
780 +
781 + $coupon_lines = $order->get_items( 'coupon' );
782 + if ( empty( $coupon_lines ) ) {
783 + $cache[ $cache_key ] = $result;
784 + return $result;
785 + }
786 +
787 + /*
788 + * Position the next renewal will take in this subscription's order sequence.
789 + *
790 + * Note this is deliberately one more than the current order count, and so is NOT the
791 + * same expression pro's Coupon::maybe_add_coupon_to_renewal_order() evaluates: that
792 + * runs after the new order's relation row is already inserted, so its count includes
793 + * the order being created. Both mean "is this order still within the limit".
794 + */
795 + $next_order_position = count( self::get_related_orders( (int) $subscription_id ) ) + 1;
796 + $limits = array();
797 +
798 + foreach ( $coupon_lines as $coupon_line ) {
799 + $coupon = new \WC_Coupon( $coupon_line->get_code() );
800 +
801 + /** This filter is documented in includes/Illuminate/Helper.php */
802 + if ( ! apply_filters( 'subscrpt_coupon_is_recurring', false, $coupon, '' ) ) {
803 + continue;
804 + }
805 +
806 + /** This filter is documented in includes/Illuminate/Helper.php */
807 + $limit = (int) apply_filters( 'subscrpt_coupon_recurring_limit', 0, $coupon, '' );
808 +
809 + if ( $limit > 0 ) {
810 + $limits[] = $limit;
811 +
812 + // The next renewal is past the limit, so it will be charged full price.
813 + if ( $next_order_position > $limit ) {
814 + $result['exhausted'] = true;
815 + continue;
816 + }
817 + }
818 +
819 + $result['amount'] += (float) $coupon_line->get_discount();
820 + }
821 +
822 + $result['limit'] = empty( $limits ) ? 0 : min( $limits );
823 +
824 + // Rebase onto the current recurring price when it no longer matches what was discounted.
825 + $discounted_subtotal = $order_item ? (float) $order_item->get_subtotal() : 0.0;
826 + $recurring_subtotal = $order_item ? (float) self::get_subscription_total( $subscription_id ) * max( 1, (int) $order_item->get_quantity() ) : 0.0;
827 +
828 + if ( $result['amount'] > 0 && $discounted_subtotal > 0 && abs( $discounted_subtotal - $recurring_subtotal ) > 0.01 ) {
829 + $result['amount'] = $result['amount'] * ( $recurring_subtotal / $discounted_subtotal );
830 + }
831 +
832 + $cache[ $cache_key ] = $result;
833 +
834 + return $result;
835 + }
836 +
837 + /**
838 + * Build the figures the My Account subscription views display.
839 + *
840 + * The recurring price in `_subscrpt_price` is always the undiscounted product price, so the
841 + * renewal figure has to be derived: full price, minus whatever discount recurs, plus tax on
842 + * the discounted amount. Tax is scaled from the order item's own tax ratio rather than
843 + * recalculated, which keeps the figures consistent with the order they came from.
844 + *
845 + * @param int $subscription_id Subscription ID.
846 + * @param \WC_Order_Item|null $order_item Source order item.
847 + *
848 + * @return array{full_excl:float,discount:float,discount_tax:float,tax:float,total:float,has_discount:bool}
849 + */
850 + public static function get_subscription_display_totals( $subscription_id, $order_item = null ) {
851 + $quantity = $order_item ? max( 1, (int) $order_item->get_quantity() ) : 1;
852 + $full_excl = (float) self::get_subscription_total( $subscription_id ) * $quantity;
853 +
854 + $item_subtotal = $order_item ? (float) $order_item->get_subtotal() : 0.0;
855 + $item_tax = $order_item ? (float) $order_item->get_subtotal_tax() : 0.0;
856 + $tax_ratio = $item_subtotal > 0 ? $item_tax / $item_subtotal : 0.0;
857 +
858 + $order = $order_item ? wc_get_order( $order_item->get_order_id() ) : null;
859 + $discount = self::get_subscription_recurring_discount( $subscription_id, $order, $order_item );
860 + $discount = min( (float) $discount['amount'], $full_excl );
861 +
862 + $discount_tax = $discount * $tax_ratio;
863 + $tax = ( $full_excl * $tax_ratio ) - $discount_tax;
864 +
865 + return array(
866 + 'full_excl' => $full_excl,
867 + 'discount' => $discount,
868 + 'discount_tax' => $discount_tax,
869 + 'tax' => $tax,
870 + 'total' => $full_excl - $discount + $tax,
871 + 'has_discount' => $discount > 0,
872 + );
873 + }
874 +
875 + /**
876 + * Resolve the pieces every recurring-amount display needs.
877 + *
878 + * @param int $subscription_id Subscription ID.
879 + * @param \WC_Order_Item|null $order_item Source order item.
880 + *
881 + * @return array|false {discounted:string,full:string,has_discount:bool}, or false when the
882 + * order item is missing or carries no subscription meta.
883 + */
884 + protected static function get_subscription_recurring_price_parts( $subscription_id, $order_item = null ) {
885 + if ( ! $order_item ) {
886 + return false;
887 + }
888 +
889 + $totals = self::get_subscription_display_totals( $subscription_id, $order_item );
890 + $discounted = self::format_price_with_order_item( $totals['total'], $order_item->get_id() );
891 +
892 + if ( ! $discounted ) {
893 + return false;
894 + }
895 +
896 + // Undiscounted amount including its own tax.
897 + $full = $totals['full_excl'] + $totals['tax'] + $totals['discount_tax'];
898 + $order = wc_get_order( $order_item->get_order_id() );
899 +
900 + return array(
901 + 'discounted' => $discounted,
902 + 'full' => wc_price(
903 + $full,
904 + array(
905 + 'currency' => $order ? $order->get_currency() : '',
906 + )
907 + ),
908 + 'has_discount' => $totals['has_discount'],
909 + );
910 + }
911 +
912 + /**
913 + * Formatted recurring amount for a subscription, striking the original when a discount recurs.
914 + *
915 + * Produces the same `<del>` / `<ins>` shape the cart's recurring totals use, so a customer
916 + * sees one consistent treatment of a recurring discount from cart through to order details.
917 + * Use `get_subscription_recurring_price_text()` anywhere the output may reach a plain-text
918 + * context, such as an email that renders in both HTML and plain.
919 + *
920 + * @param int $subscription_id Subscription ID.
921 + * @param \WC_Order_Item|null $order_item Source order item.
922 + * @param array $args Optional. 'del_style' is an inline style for the struck-through
923 + * amount — email clients strip stylesheets, so email callers
924 + * must pass one.
925 + *
926 + * @return string|false Formatted price, or false when the order item has no subscription meta.
927 + */
928 + public static function get_subscription_recurring_price_html( $subscription_id, $order_item = null, $args = array() ) {
929 + $parts = self::get_subscription_recurring_price_parts( $subscription_id, $order_item );
930 +
931 + if ( ! $parts ) {
932 + return false;
933 + }
934 +
935 + if ( ! $parts['has_discount'] ) {
936 + return $parts['discounted'];
937 + }
938 +
939 + $del_style = $args['del_style'] ?? '';
940 + $del_attributes = $del_style ? ' style="' . esc_attr( $del_style ) . '"' : '';
941 +
942 + return '<del aria-hidden="true"' . $del_attributes . '>' . $parts['full'] . '</del> <ins>' . $parts['discounted'] . '</ins>';
943 + }
944 +
945 + /**
946 + * Formatted recurring amount for a subscription, as markup-free text.
947 + *
948 + * For contexts that cannot render `<del>` — plain-text emails above all, where stripping the
949 + * tags would leave two bare amounts side by side and no way to tell which is charged.
950 + *
951 + * @param int $subscription_id Subscription ID.
952 + * @param \WC_Order_Item|null $order_item Source order item.
953 + *
954 + * @return string|false Formatted price, or false when the order item has no subscription meta.
955 + */
956 + public static function get_subscription_recurring_price_text( $subscription_id, $order_item = null ) {
957 + $parts = self::get_subscription_recurring_price_parts( $subscription_id, $order_item );
958 +
959 + if ( ! $parts ) {
960 + return false;
961 + }
962 +
963 + $discounted = wp_strip_all_tags( $parts['discounted'] );
964 +
965 + if ( ! $parts['has_discount'] ) {
966 + return $discounted;
967 + }
968 +
969 + return sprintf(
970 + // translators: 1: discounted recurring amount, 2: original amount before the discount.
971 + __( '%1$s (discounted from %2$s)', 'subscription' ),
972 + $discounted,
973 + wp_strip_all_tags( $parts['full'] )
974 + );
975 + }
976 +
977 + /**
357 978 * Get recurrings items from cart items.
358 979 *
359 980 * @param array $cart_items Cart items.
360 981 *
@@ -359,58 +980,133 @@
359 980 * @param array $cart_items Cart items.
360 981 *
361 982 * @return array
362 983 */
363 - public static function get_recurrs_for_cart( $cart_items ) {
984 + public static function get_recurrs_from_cart( $cart_items ) {
364 985 $recurrs = array();
365 986 foreach ( $cart_items as $key => $cart_item ) {
366 987 $product = $cart_item['data'];
367 988 if ( $product->is_type( 'simple' ) && isset( $cart_item['subscription'] ) ) {
368 989 $cart_subscription = $cart_item['subscription'];
369 - $type = $cart_subscription['type'];
990 + // Cadence word must respect the frequency (plan items store the raw
991 + // plural interval, e.g. "months"): singular for 1, plural + count above.
992 + $sub_time = max( 1, (int) ( $cart_subscription['time'] ?? 1 ) );
993 + $type = ( 1 === $sub_time ? '' : $sub_time . ' ' ) . ucfirst( self::get_typos( $sub_time, $cart_subscription['type'] ) );
994 + $price_data = self::build_cart_recurring_price_data( $cart_item, $key, $type );
370 995
371 - $price_html = wc_price( $cart_subscription['per_cost'] * $cart_item['quantity'] ) . '/ ' . $type;
372 - $recurrs[ $key ] = array(
373 - 'trial_status' => ! is_null( $cart_subscription['trial'] ),
374 - 'price_html' => $price_html,
375 - 'start_date' => self::start_date( $cart_subscription['trial'] ),
376 - 'next_date' => self::next_date( ( $cart_subscription['time'] ?? 1 ) . ' ' . $cart_subscription['type'], $cart_subscription['trial'] ),
377 - 'can_user_cancel' => $cart_item['data']->get_meta( '_subscrpt_user_cancel' ),
996 + $recurrs[ $key ] = array_merge(
997 + $price_data,
998 + array(
999 + 'trial_status' => ! is_null( $cart_subscription['trial'] ),
1000 + 'start_date' => self::start_date( $cart_subscription['trial'] ),
1001 + 'next_date' => self::next_date( ( $cart_subscription['time'] ?? 1 ) . ' ' . $cart_subscription['type'], $cart_subscription['trial'] ),
1002 + 'can_user_cancel' => $cart_item['data']->get_meta( '_subscrpt_user_cancel' ),
1003 + 'max_no_payment' => ! empty( $cart_item['subscrpt_max_no_payment'] )
1004 + ? (int) $cart_item['subscrpt_max_no_payment']
1005 + : $cart_item['data']->get_meta( '_subscrpt_max_no_payment' ),
1006 + // Exact plan total for split items (the entered price the split is
1007 + // divided from); null for classic split items which have no plan total.
1008 + 'split_total' => isset( $cart_item['subscrpt_split_total'] ) ? (float) $cart_item['subscrpt_split_total'] : null,
1009 + 'quantity' => (int) $cart_item['quantity'],
1010 + )
378 1011 );
379 1012 }
380 1013 }
381 1014
382 - return apply_filters( 'subscrpt_cart_recurring_items', $recurrs, $cart_items );
1015 + return apply_filters( 'wpsubs_cart_recurring_items', $recurrs, $cart_items );
383 1016 }
384 1017
385 1018 /**
1019 + * Check if the order has subscription item.
1020 + *
1021 + * @param \WC_Order|int $order Order object.
1022 + */
1023 + public static function order_has_subscription_item( $order ) {
1024 + if ( is_int( $order ) ) {
1025 + $order = wc_get_order( $order );
1026 + }
1027 +
1028 + $is_subscription_order = false;
1029 + foreach ( $order->get_items() as $item ) {
1030 + $item_data = $item->get_data() ?? array();
1031 + $item_product_id = $item_data['product_id'] ?? 0;
1032 + $item_variation_id = $item_data['variation_id'] ?? 0;
1033 +
1034 + $product_id = $item_variation_id ? $item_variation_id : $item_product_id;
1035 + $product = Subscription::get_subs_product( $product_id );
1036 +
1037 + if ( $product && $product->is_enabled() ) {
1038 + $is_subscription_order = true;
1039 + break;
1040 + }
1041 + }
1042 + return $is_subscription_order;
1043 + }
1044 +
1045 + /**
386 1046 * Create renewal order when subscription expired. [wip]
387 1047 *
388 1048 * @param int $subscription_id Subscription ID.
1049 + * @return false|\WC_Order Renewal order object or false on failure.
389 1050 * @throws \WC_Data_Exception Exception.
390 1051 * @throws \Exception Exception.
391 1052 */
392 1053 public static function create_renewal_order( $subscription_id ) {
1054 + // Check if maximum payment limit has been reached
1055 + if ( subscrpt_is_max_payments_reached( $subscription_id ) ) {
1056 + // Mark subscription as expired due to limit reached
1057 + Action::status( 'expired', $subscription_id );
1058 +
1059 + error_log( "WPS: Maximum payment limit reached for subscription #{$subscription_id}. No renewal order created." );
1060 + return false;
1061 + }
1062 +
393 1063 $order_item_id = get_post_meta( $subscription_id, '_subscrpt_order_item_id', true );
394 1064 $order_id = wc_get_order_id_by_order_item_id( $order_item_id );
395 1065 $old_order = self::check_order_for_renewal( $order_id );
396 1066
397 1067 if ( ! $old_order ) {
398 - return;
1068 + // 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.
1069 + foreach ( self::get_related_orders( $subscription_id ) as $row ) {
1070 + $candidate = wc_get_order( (int) ( $row->order_id ?? 0 ) );
1071 + if ( $candidate && 'completed' === $candidate->get_status() ) {
1072 + $old_order = $candidate;
1073 + $order_item_id = (int) ( $row->order_item_id ?? 0 );
1074 + break;
1075 + }
1076 + }
399 1077 }
400 1078
1079 + if ( ! $old_order ) {
1080 + subscrpt_write_log( "Old order not found for renewal. Skipping creating renewal order. [ Subscription ID: {$subscription_id} ]" );
1081 + return false;
1082 + }
1083 +
401 1084 $order_item = $old_order->get_item( $order_item_id );
402 - $subscription_price = get_post_meta( $subscription_id, '_subscrpt_price', true );
403 - $product_args = array(
1085 + $subscription_price = (float) get_post_meta( $subscription_id, '_subscrpt_price', true );
1086 + $qty = $order_item->get_quantity();
1087 +
1088 + // Subtract tax from per-unit subscription price if prices include tax. WC_Order will calculate tax on line total.
1089 + if ( wc_prices_include_tax() ) {
1090 + $product = ( $order_item instanceof \WC_Order_Item_Product ) ? $order_item->get_product() : null;
1091 + $tax_class = $product ? $product->get_tax_class() : '';
1092 + $tax_rates = \WC_Tax::get_rates( $tax_class );
1093 + $taxes = \WC_Tax::calc_inclusive_tax( $subscription_price, $tax_rates );
1094 + $subscription_price = $subscription_price - array_sum( $taxes );
1095 + }
1096 +
1097 + $line_total = $subscription_price * $qty;
1098 + $product_args = array(
404 1099 'name' => $order_item->get_name(),
405 - 'subtotal' => $subscription_price,
406 - 'total' => $subscription_price,
1100 + 'subtotal' => $line_total,
1101 + 'total' => $line_total,
407 1102 );
408 1103
409 1104 // creating new order.
410 1105 $new_order_data = self::create_new_order_for_renewal( $old_order, $order_item, $product_args );
411 1106 if ( ! $new_order_data ) {
412 - return;
1107 + subscrpt_write_log( "Failed to create renewal order. [ Subscription ID: {$subscription_id} ]" );
1108 + return false;
413 1109 }
414 1110 $new_order = $new_order_data['order'];
415 1111 $new_order_item_id = $new_order_data['order_item_id'];
416 1112
@@ -418,13 +1114,17 @@
418 1114 update_post_meta( $subscription_id, '_subscrpt_order_id', $new_order->get_id() );
419 1115 update_post_meta( $subscription_id, '_subscrpt_order_item_id', $new_order_item_id );
420 1116
421 1117 self::clone_order_metadata( $new_order, $old_order );
422 - self::clone_stripe_metadata_for_renewal( $subscription_id, $old_order, $new_order );
423 1118
1119 + // Allow modification of the renewal order before saving.
1120 + $new_order = apply_filters( 'subscrpt_before_saving_renewal_order', $new_order, $old_order, $subscription_id );
1121 +
1122 + // Save the new order.
424 1123 $new_order->calculate_totals();
425 1124 $new_order->save();
426 - if ( ! is_admin() ) {
1125 +
1126 + if ( ! is_admin() && function_exists( 'wc_add_notice' ) && WC()->session ) {
427 1127 $message = 'Renewal Order(#' . $new_order->get_id() . ') Created.';
428 1128 if ( $new_order->has_status( 'pending' ) ) {
429 1129 $message .= 'Please <a href="' . $new_order->get_checkout_payment_url() . '">Pay now</a>';
430 1130 }
@@ -431,11 +1131,188 @@
431 1131 wc_add_notice( $message, 'success' );
432 1132 }
433 1133
434 1134 do_action( 'subscrpt_after_create_renew_order', $new_order, $old_order, $subscription_id, false );
1135 +
1136 + return $new_order;
435 1137 }
436 1138
437 1139 /**
1140 + * Get subscription total price.
1141 + *
1142 + * @param int $subscription_id Subscription ID.
1143 + * @return float
1144 + */
1145 + public static function get_subscription_total( $subscription_id ) {
1146 + return (float) get_post_meta( $subscription_id, '_subscrpt_price', true );
1147 + }
1148 +
1149 + /**
1150 + * Get subscription status.
1151 + *
1152 + * @param int $subscription_id Subscription ID.
1153 + * @return string
1154 + */
1155 + public static function get_subscription_status( $subscription_id ) {
1156 + return get_post_status( $subscription_id );
1157 + }
1158 +
1159 + /**
1160 + * Check if subscription has status.
1161 + *
1162 + * @param int $subscription_id Subscription ID.
1163 + * @param string $status Status to check.
1164 + * @return bool
1165 + */
1166 + public static function subscription_has_status( $subscription_id, $status ) {
1167 + return self::get_subscription_status( $subscription_id ) === $status;
1168 + }
1169 +
1170 + /**
1171 + * Check if subscription needs payment.
1172 + *
1173 + * @param int $subscription_id Subscription ID.
1174 + * @return bool
1175 + */
1176 + public static function subscription_needs_payment( $subscription_id ) {
1177 + return true; // Always true for now
1178 + }
1179 +
1180 + /**
1181 + * Get product period (timing option).
1182 + *
1183 + * @param int $product_id Product ID.
1184 + * @return string
1185 + */
1186 + public static function get_product_period( $product_id ) {
1187 + $product = wc_get_product( $product_id );
1188 + return $product ? $product->get_meta( '_subscrpt_timing_option' ) : '';
1189 + }
1190 +
1191 + /**
1192 + * Get product interval (timing per).
1193 + *
1194 + * @param int $product_id Product ID.
1195 + * @return int
1196 + */
1197 + public static function get_product_interval( $product_id ) {
1198 + $product = wc_get_product( $product_id );
1199 + return $product ? (int) $product->get_meta( '_subscrpt_timing_per' ) : 1;
1200 + }
1201 +
1202 + /**
1203 + * Get product length (max payments).
1204 + *
1205 + * @param int $product_id Product ID.
1206 + * @return int
1207 + */
1208 + public static function get_product_length( $product_id ) {
1209 + $product = wc_get_product( $product_id );
1210 + return $product ? (int) $product->get_meta( '_subscrpt_max_no_payment' ) : 0;
1211 + }
1212 +
1213 + /**
1214 + * Get product trial length.
1215 + *
1216 + * @param int $product_id Product ID.
1217 + * @return int
1218 + */
1219 + public static function get_product_trial_length( $product_id ) {
1220 + $product = wc_get_product( $product_id );
1221 + return $product ? (int) $product->get_meta( '_subscrpt_trial_timing_per' ) : 0;
1222 + }
1223 +
1224 + /**
1225 + * Get product signup fee.
1226 + *
1227 + * @param int $product_id Product ID.
1228 + * @return float
1229 + */
1230 + public static function get_product_signup_fee( $product_id ) {
1231 + $product = wc_get_product( $product_id );
1232 + return $product ? (float) $product->get_meta( '_subscrpt_signup_fee' ) : 0.0;
1233 + }
1234 +
1235 + /**
1236 + * Get first renewal payment time.
1237 + *
1238 + * @param int $product_id Product ID.
1239 + * @return int Timestamp
1240 + */
1241 + public static function get_first_renewal_payment_time( $product_id ) {
1242 + $product = wc_get_product( $product_id );
1243 + if ( ! $product ) {
1244 + return 0;
1245 + }
1246 +
1247 + $trial_period = $product->get_meta( '_subscrpt_trial_timing_per' );
1248 + $trial_option = $product->get_meta( '_subscrpt_trial_timing_option' );
1249 +
1250 + if ( ! empty( $trial_period ) && ! empty( $trial_option ) ) {
1251 + return strtotime( "+{$trial_period} {$trial_option}" );
1252 + }
1253 +
1254 + return 0;
1255 + }
1256 +
1257 + /**
1258 + * Update subscription next payment date.
1259 + *
1260 + * @param int $subscription_id Subscription ID.
1261 + * @param string $new_date New Date string.
1262 + * @return void
1263 + */
1264 + public static function update_subscription_next_payment_date( $subscription_id, $new_date ) {
1265 + update_post_meta( $subscription_id, '_subscrpt_next_date', strtotime( $new_date ) );
1266 + }
1267 +
1268 + /**
1269 + * Cancel subscription.
1270 + *
1271 + * @param int $subscription_id Subscription ID.
1272 + * @return void
1273 + */
1274 + public static function cancel_subscription( $subscription_id ) {
1275 + Action::status( 'cancelled', $subscription_id );
1276 + }
1277 +
1278 + /**
1279 + * Pause subscription.
1280 + *
1281 + * @param int $subscription_id Subscription ID.
1282 + * @return void
1283 + */
1284 + public static function pause_subscription( $subscription_id ) {
1285 + Action::status( 'on-hold', $subscription_id );
1286 + }
1287 +
1288 + /**
1289 + * Resume subscription.
1290 + *
1291 + * @param int $subscription_id Subscription ID.
1292 + * @return void
1293 + */
1294 + public static function resume_subscription( $subscription_id ) {
1295 + Action::status( 'active', $subscription_id );
1296 + }
1297 +
1298 + /**
1299 + * Mark subscription payment as complete.
1300 + *
1301 + * @param int $subscription_id Subscription ID.
1302 + * @param string $payment_id Payment/Transaction ID.
1303 + * @return void
1304 + */
1305 + public static function subscription_payment_complete( $subscription_id, $payment_id ) {
1306 + if ( 'active' !== get_post_status( $subscription_id ) ) {
1307 + Action::status( 'active', $subscription_id );
1308 + }
1309 +
1310 + // Allow payment gateways to add their own comments/notes
1311 + do_action( 'subscrpt_subscription_payment_completed', $subscription_id, $payment_id );
1312 + }
1313 +
1314 + /**
438 1315 * Clone stripe metadata from old order.
439 1316 *
440 1317 * @param int $subscription_id Subscription Id.
441 1318 * @param \WC_Order $old_order Old Order Object.
@@ -443,15 +1320,39 @@
443 1320 *
444 1321 * @return void
445 1322 */
446 1323 public static function clone_stripe_metadata_for_renewal( $subscription_id, $old_order, $new_order ) {
447 - $is_auto_renew = get_post_meta( $subscription_id, '_subscrpt_auto_renew', true );
448 - $stripe_enabled = ( 'stripe' === $old_order->get_payment_method() && in_array( $is_auto_renew, array( 1, '1' ), true ) && subscrpt_is_auto_renew_enabled() && '1' === get_option( 'subscrpt_stripe_auto_renew', '1' ) );
1324 + $is_auto_renew = get_post_meta( $subscription_id, '_subscrpt_auto_renew', true );
1325 + if ( empty( $is_auto_renew ) && subscrpt_is_auto_renew_enabled() ) {
1326 + $is_auto_renew = true;
1327 + update_post_meta( $subscription_id, '_subscrpt_auto_renew', true );
1328 + }
1329 +
1330 + $is_auto_renew = get_post_meta( $subscription_id, '_subscrpt_auto_renew', true );
1331 + $is_auto_renew = in_array( $is_auto_renew, array( 1, '1' ), true );
1332 +
1333 + $is_global_auto_renew = get_option( 'wp_subscription_stripe_auto_renew', '1' );
1334 + $is_global_auto_renew = in_array( $is_global_auto_renew, array( 1, '1' ), true );
1335 +
1336 + $stripe_supported_methods = Stripe::WPSUBS_SUPPORTED_METHODS;
1337 + $old_method = $old_order->get_payment_method();
1338 + $is_stripe_pm = ! empty( $old_method ) && in_array( $old_method, $stripe_supported_methods, true );
1339 +
1340 + $has_stripe_meta = ! empty( $old_order->get_meta( '_stripe_customer_id' ) ) || ! empty( $old_order->get_meta( '_stripe_source_id' ) );
1341 +
1342 + $stripe_enabled = ( ( $is_stripe_pm || $has_stripe_meta ) && $is_auto_renew && $is_global_auto_renew && subscrpt_is_auto_renew_enabled() );
1343 +
449 1344 if ( $stripe_enabled ) {
450 1345 $new_order->update_meta_data( '_stripe_customer_id', $old_order->get_meta( '_stripe_customer_id' ) );
451 1346 $new_order->update_meta_data( '_stripe_source_id', $old_order->get_meta( '_stripe_source_id' ) );
452 1347 $new_order->set_payment_method( $old_order->get_payment_method() );
453 1348 $new_order->set_payment_method_title( $old_order->get_payment_method_title() );
1349 +
1350 + // Add debug log.
1351 + subscrpt_write_debug_log( "Stripe metadata cloned for renewal order #{$new_order->get_id()} from old order #{$old_order->get_id()}" );
1352 + } else {
1353 + subscrpt_write_log( "Stripe metadata not processed. Auto renewal may fail. [ Renewal order #{$new_order->get_id()}, Old order #{$old_order->get_id()} ]" );
1354 + subscrpt_write_debug_log( "Stripe metadata did not clone for renewal order #{$new_order->get_id()} from old order #{$old_order->get_id()}" );
454 1355 }
455 1356 }
456 1357
457 1358 /**
@@ -478,26 +1379,180 @@
478 1379
479 1380 $comment_id = wp_insert_comment(
480 1381 array(
481 1382 'comment_author' => 'Subscription for WooCommerce',
482 - 'comment_content' => sprintf( 'Subscription Renewal order successfully created. order is %s', $new_order_id ),
1383 + 'comment_content' => sprintf( 'Subscription Renewal order successfully created. Order #%s', $new_order_id ),
483 1384 'comment_post_ID' => $subscription_id,
484 1385 'comment_type' => 'order_note',
485 1386 )
486 1387 );
487 1388 update_comment_meta( $comment_id, '_subscrpt_activity', 'Renewal Order' );
1389 + update_comment_meta( $comment_id, '_subscrpt_activity_type', 'renewal_order' );
488 1390 }
489 1391
490 1392 /**
1393 + * Get a subscription data.
1394 + *
1395 + * @param int $subscription_id Subscription ID.
1396 + * @return array|null
1397 + */
1398 + public static function get_subscription_data( int $subscription_id ): ?array {
1399 + if ( empty( get_post_meta( $subscription_id ) ) ) {
1400 + return null;
1401 + }
1402 +
1403 + $subs_post = get_post( $subscription_id );
1404 + $user_id = ! empty( $subs_post ) ? (int) $subs_post->post_author : 0;
1405 +
1406 + $product_id = get_post_meta( $subscription_id, '_subscrpt_product_id', true );
1407 + $product_id = ! empty( $product_id ) ? (int) $product_id : 0;
1408 +
1409 + $variation_id = get_post_meta( $subscription_id, '_subscrpt_variation_id', true );
1410 + $variation_id = ! empty( $variation_id ) ? (int) $variation_id : 0;
1411 +
1412 + $chk_product_id = $variation_id ? $variation_id : $product_id;
1413 +
1414 + $status = get_post_status( $subscription_id ); // pending, active, cancelled, pe_cancelled, expired
1415 + $price = get_post_meta( $subscription_id, '_subscrpt_price', true );
1416 +
1417 + $signup_fee = get_post_meta( $subscription_id, '_subscrpt_signup_fee', true );
1418 + $signup_fee = ! empty( $signup_fee ) ? $signup_fee : 0;
1419 +
1420 + $order_id = get_post_meta( $subscription_id, '_subscrpt_order_id', true );
1421 + $order_item_id = get_post_meta( $subscription_id, '_subscrpt_order_item_id', true );
1422 +
1423 + $can_user_cancel = in_array( get_post_meta( $subscription_id, '_subscrpt_user_cancel', true ), array( 1, '1', 'true', 'yes' ), true );
1424 +
1425 + $start_datetime = (int) get_post_meta( $subscription_id, '_subscrpt_start_date', true );
1426 + $start_date = ! empty( $start_datetime ) ? gmdate( DATE_RFC2822, $start_datetime ) : null;
1427 +
1428 + $next_datetime = (int) get_post_meta( $subscription_id, '_subscrpt_next_date', true );
1429 + $next_date = ! empty( $next_datetime ) ? gmdate( DATE_RFC2822, $next_datetime ) : null;
1430 +
1431 + $timing_per = get_post_meta( $subscription_id, '_subscrpt_timing_per', true );
1432 + $timing_per = empty( $timing_per ) ? get_post_meta( $chk_product_id, '_subscrpt_timing_per', true ) : $timing_per;
1433 +
1434 + $timing_option = get_post_meta( $subscription_id, '_subscrpt_timing_option', true );
1435 + $timing_option = empty( $timing_option ) ? get_post_meta( $chk_product_id, '_subscrpt_timing_option', true ) : $timing_option;
1436 +
1437 + $trial_timing_per = get_post_meta( $subscription_id, '_subscrpt_trial_timing_per', true );
1438 + $trial_timing_per = empty( $trial_timing_per ) ? get_post_meta( $chk_product_id, '_subscrpt_trial_timing_per', true ) : $trial_timing_per;
1439 +
1440 + $trial_timing_option = get_post_meta( $subscription_id, '_subscrpt_trial_timing_option', true );
1441 + $trial_timing_option = empty( $trial_timing_option ) ? get_post_meta( $chk_product_id, '_subscrpt_trial_timing_option', true ) : $trial_timing_option;
1442 +
1443 + $is_auto_renew = in_array( get_post_meta( $subscription_id, '_subscrpt_auto_renew', true ), array( 1, '1', 'true', 'yes' ), true );
1444 + $is_auto_renew = ! empty( $is_auto_renew ) ? $is_auto_renew : subscrpt_is_auto_renew_enabled();
1445 +
1446 + $default_grace_period = (int) get_option( 'subscrpt_default_payment_grace_period', '7' );
1447 + $default_grace_period = subscrpt_pro_activated() ? $default_grace_period : 0;
1448 + $grace_end_datetime = $next_datetime + ( $default_grace_period * DAY_IN_SECONDS );
1449 + $grace_end_date = gmdate( DATE_RFC2822, $grace_end_datetime );
1450 + $grace_remaining_days = ceil( max( 0, $grace_end_datetime - time() ) / DAY_IN_SECONDS );
1451 +
1452 + $subscription_data = array(
1453 + 'id' => $subscription_id,
1454 + 'status' => $status,
1455 + 'schedule' => array(
1456 + 'timing_per' => $timing_per,
1457 + 'timing_option' => $timing_option,
1458 + ),
1459 + 'price' => $price,
1460 + 'signup_fee' => $signup_fee,
1461 + 'start_date' => $start_date,
1462 + 'next_date' => $next_date,
1463 + 'product' => array(
1464 + 'product_id' => $product_id,
1465 + 'variation_id' => $variation_id,
1466 + ),
1467 + 'order' => array(
1468 + 'order_id' => $order_id,
1469 + 'order_item_id' => $order_item_id,
1470 + ),
1471 + 'can_user_cancel' => $can_user_cancel,
1472 + 'is_auto_renew' => (bool) $is_auto_renew,
1473 + 'user_id' => $user_id,
1474 + );
1475 +
1476 + if ( ! empty( $trial_timing_per ) ) {
1477 + $subscription_data['trial'] = array(
1478 + 'timing_per' => $trial_timing_per,
1479 + 'timing_option' => $trial_timing_option,
1480 + );
1481 + }
1482 +
1483 + if (
1484 + ! in_array( strtolower( $status ), array( 'cancelled', 'pending', 'completed' ), true )
1485 + && ! empty( $next_date )
1486 + && $next_datetime - time() <= 0
1487 + && (int) $default_grace_period > 0
1488 + ) {
1489 + $subscription_data['grace_period'] = array(
1490 + 'remaining_days' => $grace_remaining_days,
1491 + 'end_date' => $grace_end_date,
1492 + );
1493 + }
1494 +
1495 + return $subscription_data;
1496 + }
1497 +
1498 + /**
1499 + * Get related orders of a subscription.
1500 + *
1501 + * @param int $subscription_id Subscription ID.
1502 + * @return array
1503 + */
1504 + public static function get_related_orders( int $subscription_id ): array {
1505 + global $wpdb;
1506 + $table_name = $wpdb->prefix . 'subscrpt_order_relation';
1507 +
1508 + // @phpcs:ignore
1509 + $order_histories = $wpdb->get_results(
1510 + $wpdb->prepare(
1511 + 'SELECT order_id, order_item_id, type FROM %i WHERE subscription_id=%d ORDER BY id DESC',
1512 + array(
1513 + $table_name,
1514 + $subscription_id,
1515 + )
1516 + )
1517 + );
1518 +
1519 + return $order_histories;
1520 + }
1521 +
1522 + /**
1523 + * Get parent order from subscription.
1524 + *
1525 + * @param int $subscription_id Subscription ID.
1526 + */
1527 + public static function get_parent_order( int $subscription_id ) {
1528 + $related_orders = self::get_related_orders( $subscription_id );
1529 + $last_order = end( $related_orders );
1530 +
1531 + if ( ! $last_order || strtolower( $last_order->type ?? '' ) !== 'new' ) {
1532 + foreach ( $related_orders as $order ) {
1533 + if ( strtolower( $order->type ?? '' ) === 'new' ) {
1534 + $last_order = $order;
1535 + break;
1536 + }
1537 + }
1538 + }
1539 +
1540 + $parent_order_id = $last_order->order_id ?? 0;
1541 + $parent_order = wc_get_order( $parent_order_id );
1542 + return $parent_order;
1543 + }
1544 +
1545 + /**
491 1546 * Create new order for renewal.
492 1547 *
493 - * @param \WC_Order $old_order Old Order Object.
494 - * @param \WC_Order_Item $order_item Old Order Item Object.
495 - * @param array $product_args Product args for add product.
1548 + * @param \WC_Order $old_order Old Order Object.
1549 + * @param \WC_Order_Item_Product $order_item Old Order Item Object.
1550 + * @param array $product_args Product args for add product.
496 1551 *
497 1552 * @return array|false
498 1553 */
499 - public static function create_new_order_for_renewal( \WC_Order $old_order, \WC_Order_Item $order_item, array $product_args ) {
1554 + public static function create_new_order_for_renewal( \WC_Order $old_order, \WC_Order_Item_Product $order_item, array $product_args ) {
500 1555 $product = $order_item->get_product();
501 1556 $user_id = $old_order->get_user_id();
502 1557 $new_order = wc_create_order(
503 1558 array(
@@ -525,8 +1580,11 @@
525 1580 'trial' => null,
526 1581 )
527 1582 );
528 1583
1584 + // Add debug log.
1585 + subscrpt_write_debug_log( "Renewal order #{$new_order->get_id()} created for old order #{$old_order->get_id()}" );
1586 +
529 1587 return array(
530 1588 'order' => $new_order,
531 1589 'order_item_id' => $new_order_item_id,
532 1590 );
@@ -541,10 +1599,10 @@
541 1599 */
542 1600 public static function check_order_for_renewal( $old_order_id ) {
543 1601 $old_order = wc_get_order( $old_order_id );
544 1602 if ( ! $old_order || 'completed' !== $old_order->get_status() ) {
545 - if ( ! is_admin() ) {
546 - return wc_add_notice( __( 'Subscription renewal isn\'t possible due to previous order not completed or deletion.', 'sdevs_subscrpt' ), 'error' );
1603 + if ( ! is_admin() && function_exists( 'wc_add_notice' ) && WC()->session ) {
1604 + return wc_add_notice( __( 'Subscription renewal isn\'t possible due to previous order not completed or deletion.', 'subscription' ), 'error' );
547 1605 }
548 1606 return false;
549 1607 }
550 1608
@@ -551,8 +1609,112 @@
551 1609 return $old_order;
552 1610 }
553 1611
554 1612 /**
1613 + * Get delivery info from order.
1614 + *
1615 + * @param \WC_Order $order Order object.
1616 + * @return array
1617 + */
1618 + public static function get_delivery_info_from_order( \WC_Order $order ) {
1619 + $customer_id = $order->get_customer_id();
1620 + $customer = new \WC_Customer( $customer_id );
1621 + $email = $customer->get_email();
1622 +
1623 + // Billing info (get from order first, if empty get from customer).
1624 + $billing_first_name = ! empty( $order->get_billing_first_name() ) ? $order->get_billing_first_name() : $customer->get_billing_first_name();
1625 + $billing_last_name = ! empty( $order->get_billing_last_name() ) ? $order->get_billing_last_name() : $customer->get_billing_last_name();
1626 + $billing_email = ! empty( $order->get_billing_email() ) ? $order->get_billing_email() : $customer->get_billing_email();
1627 + $billing_phone = ! empty( $order->get_billing_phone() ) ? $order->get_billing_phone() : $customer->get_billing_phone();
1628 + $billing_company = ! empty( $order->get_billing_company() ) ? $order->get_billing_company() : $customer->get_billing_company();
1629 +
1630 + $billing_city = ! empty( $order->get_billing_city() ) ? $order->get_billing_city() : $customer->get_billing_city();
1631 + $billing_state = ! empty( $order->get_billing_state() ) ? $order->get_billing_state() : $customer->get_billing_state();
1632 + $billing_country = ! empty( $order->get_billing_country() ) ? $order->get_billing_country() : $customer->get_billing_country();
1633 + $billing_postcode = ! empty( $order->get_billing_postcode() ) ? $order->get_billing_postcode() : $customer->get_billing_postcode();
1634 + $billing_address_1 = ! empty( $order->get_billing_address_1() ) ? $order->get_billing_address_1() : $customer->get_billing_address_1();
1635 + $billing_address_2 = ! empty( $order->get_billing_address_2() ) ? $order->get_billing_address_2() : $customer->get_billing_address_2();
1636 +
1637 + // Shipping info (get from order first, if empty get from customer).
1638 + $shipping_first_name = ! empty( $order->get_shipping_first_name() ) ? $order->get_shipping_first_name() : $customer->get_shipping_first_name();
1639 + $shipping_last_name = ! empty( $order->get_shipping_last_name() ) ? $order->get_shipping_last_name() : $customer->get_shipping_last_name();
1640 + $shipping_phone = ! empty( $order->get_shipping_phone() ) ? $order->get_shipping_phone() : $customer->get_shipping_phone();
1641 + $shipping_company = ! empty( $order->get_shipping_company() ) ? $order->get_shipping_company() : $customer->get_shipping_company();
1642 +
1643 + $shipping_city = ! empty( $order->get_shipping_city() ) ? $order->get_shipping_city() : $customer->get_shipping_city();
1644 + $shipping_state = ! empty( $order->get_shipping_state() ) ? $order->get_shipping_state() : $customer->get_shipping_state();
1645 + $shipping_country = ! empty( $order->get_shipping_country() ) ? $order->get_shipping_country() : $customer->get_shipping_country();
1646 + $shipping_postcode = ! empty( $order->get_shipping_postcode() ) ? $order->get_shipping_postcode() : $customer->get_shipping_postcode();
1647 + $shipping_address_1 = ! empty( $order->get_shipping_address_1() ) ? $order->get_shipping_address_1() : $customer->get_shipping_address_1();
1648 + $shipping_address_2 = ! empty( $order->get_shipping_address_2() ) ? $order->get_shipping_address_2() : $customer->get_shipping_address_2();
1649 +
1650 + $order_meta = [
1651 + 'customer_id' => $order->get_customer_id(),
1652 + 'email' => $email,
1653 + 'billing' => [
1654 + 'first_name' => $billing_first_name,
1655 + 'last_name' => $billing_last_name,
1656 + 'email' => $billing_email,
1657 + 'phone' => $billing_phone,
1658 + 'company' => $billing_company,
1659 + 'city' => $billing_city,
1660 + 'state' => $billing_state,
1661 + 'country' => $billing_country,
1662 + 'postcode' => $billing_postcode,
1663 + 'address_1' => $billing_address_1,
1664 + 'address_2' => $billing_address_2,
1665 + ],
1666 + 'shipping' => [
1667 + 'first_name' => $shipping_first_name,
1668 + 'last_name' => $shipping_last_name,
1669 + 'phone' => $shipping_phone,
1670 + 'company' => $shipping_company,
1671 + 'city' => $shipping_city,
1672 + 'state' => $shipping_state,
1673 + 'country' => $shipping_country,
1674 + 'postcode' => $shipping_postcode,
1675 + 'address_1' => $shipping_address_1,
1676 + 'address_2' => $shipping_address_2,
1677 + ],
1678 + ];
1679 +
1680 + return $order_meta;
1681 + }
1682 +
1683 + /**
1684 + * Set delivery info to order.
1685 + *
1686 + * @param \WC_Order $order Order object.
1687 + * @param array $order_meta Order meta data.
1688 + */
1689 + public static function set_delivery_info_to_order( \WC_Order $order, array $order_meta ) {
1690 + // Set Billing Info.
1691 + $order->set_billing_first_name( $order_meta['billing']['first_name'] ?? '' );
1692 + $order->set_billing_last_name( $order_meta['billing']['last_name'] ?? '' );
1693 + $order->set_billing_email( $order_meta['billing']['email'] ?? '' );
1694 + $order->set_billing_phone( $order_meta['billing']['phone'] ?? '' );
1695 + $order->set_billing_company( $order_meta['billing']['company'] ?? '' );
1696 + $order->set_billing_city( $order_meta['billing']['city'] ?? '' );
1697 + $order->set_billing_state( $order_meta['billing']['state'] ?? '' );
1698 + $order->set_billing_country( $order_meta['billing']['country'] ?? '' );
1699 + $order->set_billing_postcode( $order_meta['billing']['postcode'] ?? '' );
1700 + $order->set_billing_address_1( $order_meta['billing']['address_1'] ?? '' );
1701 + $order->set_billing_address_2( $order_meta['billing']['address_2'] ?? '' );
1702 +
1703 + // Set Shipping Info.
1704 + $order->set_shipping_first_name( $order_meta['shipping']['first_name'] ?? '' );
1705 + $order->set_shipping_last_name( $order_meta['shipping']['last_name'] ?? '' );
1706 + $order->set_shipping_phone( $order_meta['shipping']['phone'] ?? '' );
1707 + $order->set_shipping_company( $order_meta['shipping']['company'] ?? '' );
1708 + $order->set_shipping_city( $order_meta['shipping']['city'] ?? '' );
1709 + $order->set_shipping_state( $order_meta['shipping']['state'] ?? '' );
1710 + $order->set_shipping_country( $order_meta['shipping']['country'] ?? '' );
1711 + $order->set_shipping_postcode( $order_meta['shipping']['postcode'] ?? '' );
1712 + $order->set_shipping_address_1( $order_meta['shipping']['address_1'] ?? '' );
1713 + $order->set_shipping_address_2( $order_meta['shipping']['address_2'] ?? '' );
1714 + }
1715 +
1716 + /**
555 1717 * Save meta-data from old order
556 1718 *
557 1719 * @param \WC_Order $new_order new order object.
558 1720 * @param \WC_Order $old_order old order object.
@@ -559,33 +1721,48 @@
559 1721 *
560 1722 * @return void
561 1723 */
562 1724 public static function clone_order_metadata( $new_order, $old_order ) {
1725 + // Set customer and currency info.
563 1726 $new_order->set_customer_id( $old_order->get_customer_id() );
564 1727 $new_order->set_currency( $old_order->get_currency() );
565 1728
566 - // 3 Add Billing Fields
567 - $customer = new \WC_Customer( $old_order->get_customer_id() );
568 - $new_order->set_billing_city( $customer->get_billing_city() );
569 - $new_order->set_billing_state( $customer->get_billing_state() );
570 - $new_order->set_billing_postcode( $customer->get_billing_postcode() );
571 - $new_order->set_billing_email( $customer->get_billing_email() );
572 - $new_order->set_billing_phone( $customer->get_billing_phone() );
573 - $new_order->set_billing_address_1( $customer->get_billing_address_1() );
574 - $new_order->set_billing_address_2( $customer->get_billing_address_2() );
575 - $new_order->set_billing_country( $customer->get_billing_country() );
576 - $new_order->set_billing_first_name( $customer->get_billing_first_name() );
577 - $new_order->set_billing_last_name( $customer->get_billing_last_name() );
578 - $new_order->set_billing_company( $customer->get_billing_company() );
1729 + // Get delivery info from old order.
1730 + $order_meta = self::get_delivery_info_from_order( $old_order );
579 1731
580 - // 4 Add Shipping Fields
581 - $new_order->set_shipping_country( $customer->get_shipping_country() );
582 - $new_order->set_shipping_first_name( $customer->get_shipping_first_name() );
583 - $new_order->set_shipping_last_name( $customer->get_shipping_last_name() );
584 - $new_order->set_shipping_company( $customer->get_shipping_company() );
585 - $new_order->set_shipping_address_1( $customer->get_shipping_address_1() );
586 - $new_order->set_shipping_address_2( $customer->get_shipping_address_2() );
587 - $new_order->set_shipping_city( $customer->get_shipping_city() );
588 - $new_order->set_shipping_state( $customer->get_shipping_state() );
589 - $new_order->set_shipping_postcode( $customer->get_shipping_postcode() );
1732 + // Check for any missing information.
1733 + $missing_billing_info = true;
1734 + foreach ( $order_meta['billing'] as $key => $value ) {
1735 + if ( ! empty( $value ) ) {
1736 + $missing_billing_info = false;
1737 + break;
1738 + }
1739 + }
1740 + $missing_shipping_info = true;
1741 + foreach ( $order_meta['shipping'] as $key => $value ) {
1742 + if ( ! empty( $value ) ) {
1743 + $missing_shipping_info = false;
1744 + break;
1745 + }
1746 + }
1747 + $missing_info = $missing_billing_info || $missing_shipping_info;
1748 +
1749 + // Get info from the parent order if missing.
1750 + if ( $missing_info ) {
1751 + $subscription = self::get_subscriptions_from_order( $old_order->get_id() );
1752 + $subscription = reset( $subscription );
1753 + $subscription_id = ! empty( $subscription ) ? $subscription->subscription_id : 0;
1754 +
1755 + subscrpt_write_log( "Missing delivery info in old order #{$old_order->get_id()} for subscription #{$subscription_id}. Trying to get from parent order." );
1756 +
1757 + $parent_order = self::get_parent_order( $subscription_id );
1758 + if ( ! empty( $parent_order ) ) {
1759 + $order_meta = self::get_delivery_info_from_order( $parent_order );
1760 + }
1761 + }
1762 +
1763 + // Set delivery info to new order.
1764 + self::set_delivery_info_to_order( $new_order, $order_meta );
590 1765 }
591 1766 }
1767 +
1768 +// HPOS: All order data access below uses WooCommerce CRUD and is HPOS compatible.