PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / 2.1.0
Subscriptions for WooCommerce with Stripe Recurring Payments v2.1.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 +1259 -89 1.4.0 → 2.1.0 View file →
@@ -1,8 +1,16 @@
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 +
5 13 // HPOS: This file is compatible with WooCommerce High-Performance Order Storage (HPOS).
6 14 // All WooCommerce order data is accessed via WooCommerce CRUD methods (wc_get_order, wc_get_orders, etc.).
7 15 // All direct post meta access is for subscription data only, not WooCommerce order data.
8 16 // If you add new order data access, use WooCommerce CRUD for HPOS compatibility.
@@ -12,29 +20,74 @@
12 20 *
13 21 * @package SpringDevs\Subscription\Illuminate
14 22 */
15 23 class Helper {
16 -
17 24 /**
18 25 * Get type's singular or plural from time_per.
19 26 *
20 27 * @param int $number timing_per.
21 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.
22 65 *
23 - * @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.
24 70 */
25 - public static function get_typos( $number, $typo ) {
26 - if ( 1 === (int) $number && 'days' === $typo ) {
27 - return __( 'day', 'sdevs_subscrpt' );
28 - } elseif ( 1 === (int) $number && 'weeks' === $typo ) {
29 - return __( 'week', 'sdevs_subscrpt' );
30 - } elseif ( 1 === (int) $number && 'months' === $typo ) {
31 - return __( 'month', 'sdevs_subscrpt' );
32 - } elseif ( 1 === (int) $number && 'years' === $typo ) {
33 - return __( 'year', 'sdevs_subscrpt' );
34 - } else {
35 - 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;
36 86 }
87 +
88 + $status = strtolower( $status );
89 + return isset( $statuses[ $status ] ) ? $statuses[ $status ] : '';
37 90 }
38 91
39 92 /**
40 93 * Generate start date
@@ -69,8 +122,107 @@
69 122 return wp_date( get_option( 'date_format' ), strtotime( $time, $start_date ) );
70 123 }
71 124
72 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 + /**
73 225 * Check subscription exists by product ID.
74 226 *
75 227 * @param int $product_id Product ID.
76 228 * @param string|array $status Status.
@@ -82,21 +234,14 @@
82 234 return false;
83 235 }
84 236
85 237 $args = array(
86 - 'post_type' => 'subscrpt_order',
87 238 'post_status' => $status,
88 239 'fields' => 'ids',
89 - 'meta_query' => array(
90 - array(
91 - 'key' => '_subscrpt_product_id',
92 - 'value' => $product_id,
93 - ),
94 - ),
95 - 'author' => get_current_user_id(),
240 + 'product_id' => $product_id,
96 241 );
97 242
98 - $posts = get_posts( $args );
243 + $posts = self::get_subscriptions( $args );
99 244 return count( $posts ) > 0 ? $posts[0] : false;
100 245 }
101 246
102 247 /**
@@ -217,9 +362,9 @@
217 362 return false;
218 363 }
219 364
220 365 $time = 1 === (int) $item_meta['time'] ? null : $item_meta['time'] . '-';
221 - $type = self::get_typos( $item_meta['time'], $item_meta['type'] );
366 + $type = self::get_typos( $item_meta['time'], $item_meta['type'], true );
222 367
223 368 $formatted_price = wc_price(
224 369 $price,
225 370 array(
@@ -224,16 +369,17 @@
224 369 $price,
225 370 array(
226 371 'currency' => $order->get_currency(),
227 372 )
228 - ) . ' / ' . $time . $type;
373 + ) . ' / ' . $time . ucfirst( $type );
229 374
230 375 if ( $display_trial ) {
231 - $trial = $item_meta['trial'];
232 376 $has_trial = isset( $item_meta['trial'] ) && strlen( $item_meta['trial'] ) > 2;
377 + $trial = $item_meta['trial'] ?? '';
233 378
234 379 if ( $has_trial ) {
235 - $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>';
236 382 $formatted_price .= $trial_html;
237 383 }
238 384 }
239 385
@@ -278,21 +424,44 @@
278 424 public static function process_order_renewal( $subscription_id, $order_id, $order_item_id ) {
279 425 global $wpdb;
280 426 $history_table = $wpdb->prefix . 'subscrpt_order_relation';
281 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 +
282 454 $comment_id = wp_insert_comment(
283 455 array(
284 456 'comment_author' => 'Subscription for WooCommerce',
285 - 'comment_content' => sprintf(
286 - // translators: order id.
287 - __( 'The order %s has been created for the subscription', 'sdevs_subscrpt' ),
288 - $order_id
289 - ),
457 + 'comment_content' => $comment_content,
290 458 'comment_post_ID' => $subscription_id,
291 459 'comment_type' => 'order_note',
292 460 )
293 461 );
294 - 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' );
295 464
296 465 $wpdb->insert(
297 466 $history_table,
298 467 array(
@@ -301,8 +470,11 @@
301 470 'order_item_id' => $order_item_id,
302 471 'type' => 'renew',
303 472 )
304 473 );
474 +
475 + // Fire action when split payment is renewed
476 + do_action( 'subscrpt_split_payment_renewed', $subscription_id, $order_id, $order_item_id );
305 477 }
306 478
307 479 /**
308 480 * Process new subscription on order.
@@ -316,12 +488,31 @@
316 488 public static function process_new_subscription_order( $order_item, $post_status, $product ) {
317 489 global $wpdb;
318 490 $history_table = $wpdb->prefix . 'subscrpt_order_relation';
319 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 +
320 510 $args = array(
321 511 'post_title' => 'Subscription',
322 512 'post_type' => 'subscrpt_order',
323 - '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(),
324 515 );
325 516 $subscription_id = wp_insert_post( $args );
326 517 wp_update_post(
327 518 array(
@@ -328,21 +519,43 @@
328 519 'ID' => $subscription_id,
329 520 'post_title' => "Subscription #{$subscription_id}",
330 521 )
331 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 +
332 548 $comment_id = wp_insert_comment(
333 549 array(
334 550 'comment_author' => 'Subscription for WooCommerce',
335 - 'comment_content' => sprintf(
336 - // translators: Order Id.
337 - __( 'Subscription successfully created. order is %s', 'sdevs_subscrpt' ),
338 - $order_item->get_order_id()
339 - ),
551 + 'comment_content' => $comment_content,
340 552 'comment_post_ID' => $subscription_id,
341 553 'comment_type' => 'order_note',
342 554 )
343 555 );
344 - 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' );
345 558
346 559 update_post_meta( $subscription_id, '_subscrpt_product_id', $product->get_id() );
347 560
348 561 $wpdb->insert(
@@ -354,12 +567,415 @@
354 567 'type' => 'new',
355 568 )
356 569 );
357 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 +
358 574 return $subscription_id;
359 575 }
360 576
361 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 + /**
362 978 * Get recurrings items from cart items.
363 979 *
364 980 * @param array $cart_items Cart items.
365 981 *
@@ -364,58 +980,133 @@
364 980 * @param array $cart_items Cart items.
365 981 *
366 982 * @return array
367 983 */
368 - public static function get_recurrs_for_cart( $cart_items ) {
984 + public static function get_recurrs_from_cart( $cart_items ) {
369 985 $recurrs = array();
370 986 foreach ( $cart_items as $key => $cart_item ) {
371 987 $product = $cart_item['data'];
372 988 if ( $product->is_type( 'simple' ) && isset( $cart_item['subscription'] ) ) {
373 989 $cart_subscription = $cart_item['subscription'];
374 - $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 );
375 995
376 - $price_html = wc_price( $cart_subscription['per_cost'] * $cart_item['quantity'] ) . '/ ' . $type;
377 - $recurrs[ $key ] = array(
378 - 'trial_status' => ! is_null( $cart_subscription['trial'] ),
379 - 'price_html' => $price_html,
380 - 'start_date' => self::start_date( $cart_subscription['trial'] ),
381 - 'next_date' => self::next_date( ( $cart_subscription['time'] ?? 1 ) . ' ' . $cart_subscription['type'], $cart_subscription['trial'] ),
382 - '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 + )
383 1011 );
384 1012 }
385 1013 }
386 1014
387 - return apply_filters( 'subscrpt_cart_recurring_items', $recurrs, $cart_items );
1015 + return apply_filters( 'wpsubs_cart_recurring_items', $recurrs, $cart_items );
388 1016 }
389 1017
390 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 + /**
391 1046 * Create renewal order when subscription expired. [wip]
392 1047 *
393 1048 * @param int $subscription_id Subscription ID.
1049 + * @return false|\WC_Order Renewal order object or false on failure.
394 1050 * @throws \WC_Data_Exception Exception.
395 1051 * @throws \Exception Exception.
396 1052 */
397 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 +
398 1063 $order_item_id = get_post_meta( $subscription_id, '_subscrpt_order_item_id', true );
399 1064 $order_id = wc_get_order_id_by_order_item_id( $order_item_id );
400 1065 $old_order = self::check_order_for_renewal( $order_id );
401 1066
402 1067 if ( ! $old_order ) {
403 - 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 + }
404 1077 }
405 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 +
406 1084 $order_item = $old_order->get_item( $order_item_id );
407 - $subscription_price = get_post_meta( $subscription_id, '_subscrpt_price', true );
408 - $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(
409 1099 'name' => $order_item->get_name(),
410 - 'subtotal' => $subscription_price,
411 - 'total' => $subscription_price,
1100 + 'subtotal' => $line_total,
1101 + 'total' => $line_total,
412 1102 );
413 1103
414 1104 // creating new order.
415 1105 $new_order_data = self::create_new_order_for_renewal( $old_order, $order_item, $product_args );
416 1106 if ( ! $new_order_data ) {
417 - return;
1107 + subscrpt_write_log( "Failed to create renewal order. [ Subscription ID: {$subscription_id} ]" );
1108 + return false;
418 1109 }
419 1110 $new_order = $new_order_data['order'];
420 1111 $new_order_item_id = $new_order_data['order_item_id'];
421 1112
@@ -423,13 +1114,17 @@
423 1114 update_post_meta( $subscription_id, '_subscrpt_order_id', $new_order->get_id() );
424 1115 update_post_meta( $subscription_id, '_subscrpt_order_item_id', $new_order_item_id );
425 1116
426 1117 self::clone_order_metadata( $new_order, $old_order );
427 - self::clone_stripe_metadata_for_renewal( $subscription_id, $old_order, $new_order );
428 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.
429 1123 $new_order->calculate_totals();
430 1124 $new_order->save();
431 - if ( ! is_admin() && function_exists( 'wc_add_notice' ) ) {
1125 +
1126 + if ( ! is_admin() && function_exists( 'wc_add_notice' ) && WC()->session ) {
432 1127 $message = 'Renewal Order(#' . $new_order->get_id() . ') Created.';
433 1128 if ( $new_order->has_status( 'pending' ) ) {
434 1129 $message .= 'Please <a href="' . $new_order->get_checkout_payment_url() . '">Pay now</a>';
435 1130 }
@@ -436,11 +1131,188 @@
436 1131 wc_add_notice( $message, 'success' );
437 1132 }
438 1133
439 1134 do_action( 'subscrpt_after_create_renew_order', $new_order, $old_order, $subscription_id, false );
1135 +
1136 + return $new_order;
440 1137 }
441 1138
442 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 + /**
443 1315 * Clone stripe metadata from old order.
444 1316 *
445 1317 * @param int $subscription_id Subscription Id.
446 1318 * @param \WC_Order $old_order Old Order Object.
@@ -448,15 +1320,39 @@
448 1320 *
449 1321 * @return void
450 1322 */
451 1323 public static function clone_stripe_metadata_for_renewal( $subscription_id, $old_order, $new_order ) {
452 - $is_auto_renew = get_post_meta( $subscription_id, '_subscrpt_auto_renew', true );
453 - $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 +
454 1344 if ( $stripe_enabled ) {
455 1345 $new_order->update_meta_data( '_stripe_customer_id', $old_order->get_meta( '_stripe_customer_id' ) );
456 1346 $new_order->update_meta_data( '_stripe_source_id', $old_order->get_meta( '_stripe_source_id' ) );
457 1347 $new_order->set_payment_method( $old_order->get_payment_method() );
458 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()}" );
459 1355 }
460 1356 }
461 1357
462 1358 /**
@@ -483,26 +1379,180 @@
483 1379
484 1380 $comment_id = wp_insert_comment(
485 1381 array(
486 1382 'comment_author' => 'Subscription for WooCommerce',
487 - '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 ),
488 1384 'comment_post_ID' => $subscription_id,
489 1385 'comment_type' => 'order_note',
490 1386 )
491 1387 );
492 1388 update_comment_meta( $comment_id, '_subscrpt_activity', 'Renewal Order' );
1389 + update_comment_meta( $comment_id, '_subscrpt_activity_type', 'renewal_order' );
493 1390 }
494 1391
495 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 + /**
496 1546 * Create new order for renewal.
497 1547 *
498 - * @param \WC_Order $old_order Old Order Object.
499 - * @param \WC_Order_Item $order_item Old Order Item Object.
500 - * @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.
501 1551 *
502 1552 * @return array|false
503 1553 */
504 - 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 ) {
505 1555 $product = $order_item->get_product();
506 1556 $user_id = $old_order->get_user_id();
507 1557 $new_order = wc_create_order(
508 1558 array(
@@ -530,8 +1580,11 @@
530 1580 'trial' => null,
531 1581 )
532 1582 );
533 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 +
534 1587 return array(
535 1588 'order' => $new_order,
536 1589 'order_item_id' => $new_order_item_id,
537 1590 );
@@ -546,10 +1599,10 @@
546 1599 */
547 1600 public static function check_order_for_renewal( $old_order_id ) {
548 1601 $old_order = wc_get_order( $old_order_id );
549 1602 if ( ! $old_order || 'completed' !== $old_order->get_status() ) {
550 - if ( ! is_admin() && function_exists( 'wc_add_notice' ) ) {
551 - 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' );
552 1605 }
553 1606 return false;
554 1607 }
555 1608
@@ -556,8 +1609,112 @@
556 1609 return $old_order;
557 1610 }
558 1611
559 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 + /**
560 1717 * Save meta-data from old order
561 1718 *
562 1719 * @param \WC_Order $new_order new order object.
563 1720 * @param \WC_Order $old_order old order object.
@@ -564,35 +1721,48 @@
564 1721 *
565 1722 * @return void
566 1723 */
567 1724 public static function clone_order_metadata( $new_order, $old_order ) {
1725 + // Set customer and currency info.
568 1726 $new_order->set_customer_id( $old_order->get_customer_id() );
569 1727 $new_order->set_currency( $old_order->get_currency() );
570 1728
571 - // 3 Add Billing Fields
572 - $customer = new \WC_Customer( $old_order->get_customer_id() );
573 - $new_order->set_billing_city( $customer->get_billing_city() );
574 - $new_order->set_billing_state( $customer->get_billing_state() );
575 - $new_order->set_billing_postcode( $customer->get_billing_postcode() );
576 - $new_order->set_billing_email( $customer->get_billing_email() );
577 - $new_order->set_billing_phone( $customer->get_billing_phone() );
578 - $new_order->set_billing_address_1( $customer->get_billing_address_1() );
579 - $new_order->set_billing_address_2( $customer->get_billing_address_2() );
580 - $new_order->set_billing_country( $customer->get_billing_country() );
581 - $new_order->set_billing_first_name( $customer->get_billing_first_name() );
582 - $new_order->set_billing_last_name( $customer->get_billing_last_name() );
583 - $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 );
584 1731
585 - // 4 Add Shipping Fields
586 - $new_order->set_shipping_country( $customer->get_shipping_country() );
587 - $new_order->set_shipping_first_name( $customer->get_shipping_first_name() );
588 - $new_order->set_shipping_last_name( $customer->get_shipping_last_name() );
589 - $new_order->set_shipping_company( $customer->get_shipping_company() );
590 - $new_order->set_shipping_address_1( $customer->get_shipping_address_1() );
591 - $new_order->set_shipping_address_2( $customer->get_shipping_address_2() );
592 - $new_order->set_shipping_city( $customer->get_shipping_city() );
593 - $new_order->set_shipping_state( $customer->get_shipping_state() );
594 - $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 );
595 1765 }
596 1766 }
597 1767
598 1768 // HPOS: All order data access below uses WooCommerce CRUD and is HPOS compatible.