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