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/Order.php +302 -19 1.3.1 → 2.4.0 View file →
@@ -18,8 +18,10 @@
18 18 add_action( 'woocommerce_before_order_itemmeta', array( $this, 'add_order_item_data' ), 10, 3 );
19 19 add_action( 'woocommerce_order_status_changed', array( $this, 'order_status_changed' ) );
20 20 add_action( 'woocommerce_before_delete_order', array( $this, 'delete_the_subscription' ) );
21 21 add_action( 'subscrpt_subscription_activated', array( $this, 'generate_dates_for_subscription' ) );
22 +
23 + add_action( 'subscrpt_queue_trial_order_autocomplete', array( $this, 'auto_complete_subscription_trial_order' ) );
22 24 }
23 25
24 26 /**
25 27 * Generate start, next and trial dates.
@@ -34,23 +36,35 @@
34 36
35 37 $order_item_meta = wc_get_order_item_meta( $order_item_id, '_subscrpt_meta' );
36 38 $type = Helper::get_typos( 1, $order_item_meta['type'] );
37 39 $trial = get_post_meta( $subscription_id, '_subscrpt_trial', true );
40 + $recurr_timing = ( $order_item_meta['time'] ?? 1 ) . ' ' . $type;
41 +
38 42 if ( 'new' === $subscription_history->type ) {
39 43 $start_date = time();
40 - $next_date = sdevs_wp_strtotime( 1 . ' ' . $type, $start_date );
44 + $next_date = sdevs_wp_strtotime( $recurr_timing, $start_date );
45 +
41 46 if ( $trial && ! empty( $trial ) ) {
42 47 $trial_started = get_post_meta( $subscription_id, '_subscrpt_trial_started', true );
43 48 $trial_ended = get_post_meta( $subscription_id, '_subscrpt_trial_ended', true );
49 +
44 50 if ( empty( $trial_started ) && empty( $trial_ended ) ) {
45 51 $start_date = sdevs_wp_strtotime( $trial );
52 + $next_date = $start_date;
53 +
46 54 update_post_meta( $subscription_id, '_subscrpt_trial_started', time() );
47 55 update_post_meta( $subscription_id, '_subscrpt_trial_ended', $start_date );
48 56 update_post_meta( $subscription_id, '_subscrpt_trial_mode', 'on' );
49 - $next_date = $start_date;
50 57 }
58 +
59 + if ( ! empty( $trial_ended ) ) {
60 + $start_date = $trial_ended;
61 + $next_date = $start_date;
62 + }
51 63 }
64 +
52 65 update_post_meta( $subscription_id, '_subscrpt_start_date', $start_date );
66 +
53 67 } elseif ( 'renew' === $subscription_history->type ) {
54 68 if ( $trial ) {
55 69 delete_post_meta( $subscription_id, '_subscrpt_trial' );
56 70 delete_post_meta( $subscription_id, '_subscrpt_trial_mode' );
@@ -56,24 +70,114 @@
56 70 delete_post_meta( $subscription_id, '_subscrpt_trial_mode' );
57 71 delete_post_meta( $subscription_id, '_subscrpt_trial_started' );
58 72 delete_post_meta( $subscription_id, '_subscrpt_trial_ended' );
59 73 }
60 - $next_date = sdevs_wp_strtotime( 1 . ' ' . $type, time() );
74 +
75 + $next_date = $this->get_anchored_next_date( $subscription_id, $recurr_timing, (int) $subscription_history->order_id );
76 +
61 77 } elseif ( 'early-renew' === $subscription_history->type ) {
62 - $next_date = sdevs_wp_strtotime( 1 . ' ' . $type, get_post_meta( $subscription_id, '_subscrpt_next_date', true ) );
63 -
64 78 if ( $trial ) {
65 - $trial_mode = get_post_meta( $subscription_id, '_subscrpt_trial_mode', true );
66 - if ( 'on' === $trial_mode ) {
67 - update_post_meta( $subscription_id, '_subscrpt_trial_mode', 'extended' );
68 - $next_date = sdevs_wp_strtotime( 1 . ' ' . $type, get_post_meta( $subscription_id, '_subscrpt_trial_ended', true ) );
69 - }
79 + delete_post_meta( $subscription_id, '_subscrpt_trial' );
80 + delete_post_meta( $subscription_id, '_subscrpt_trial_mode' );
81 + delete_post_meta( $subscription_id, '_subscrpt_trial_started' );
82 + delete_post_meta( $subscription_id, '_subscrpt_trial_ended' );
70 83 }
84 +
85 + // Extend from the current due date so the days already paid for are kept.
86 + $next_date = $this->get_anchored_next_date( $subscription_id, $recurr_timing, (int) $subscription_history->order_id );
71 87 }
88 +
89 + // Split payment: no next date after the final installment.
90 + if (
91 + in_array( $subscription_history->type, array( 'renew', 'early-renew' ), true )
92 + && function_exists( 'subscrpt_is_max_payments_reached' )
93 + && subscrpt_is_max_payments_reached( $subscription_id )
94 + ) {
95 + return;
96 + }
97 +
98 + /**
99 + * Filter the subscription next payment date before it is saved.
100 + *
101 + * General-purpose hook (not scoped to split payment) for adjusting the
102 + * computed next renewal date. Note `$next_date` may be null for history
103 + * types other than 'new'/'renew'/'early-renew' (e.g. switch orders).
104 + *
105 + * The deprecated `subscrpt_split_payment_next_due_date` filter is bridged
106 + * onto this hook in LegacyCompat.php for backward compatibility.
107 + *
108 + * @param int|null $next_date Computed next payment timestamp, or null.
109 + * @param int $subscription_id Subscription ID.
110 + * @param string $recurr_timing Recurring timing string (e.g. "1 month").
111 + * @param string $type Subscription history type.
112 + */
113 + $next_date = apply_filters( 'subscrpt_subscription_next_date', $next_date, $subscription_id, $recurr_timing, $subscription_history->type );
114 +
72 115 update_post_meta( $subscription_id, '_subscrpt_next_date', $next_date );
73 116 }
74 117
75 118 /**
119 + * Calculate a renewal's next payment date, anchored to the previous due date.
120 + *
121 + * Computing from time() instead makes every cycle inherit however late the
122 + * renewal was actually processed — with an hourly cron a due date of 02:00:05
123 + * is missed by the 02:00:03 run and lands at 03:00, and that hour is carried
124 + * into every following cycle until a whole billing period is skipped. Anchoring
125 + * to the stored due date keeps the billing time-of-day stable instead.
126 + *
127 + * When payment arrives more than one period late (manual renewal, cron outage),
128 + * the anchor is stepped forward period by period so the returned date is always
129 + * in the future — otherwise the subscription would be due again immediately.
130 + *
131 + * Because this runs on every activating status transition of the same renewal
132 + * order (pending → processing → completed), the order that last moved the date
133 + * is recorded so repeat transitions do not advance the cycle again.
134 + *
135 + * @param int $subscription_id Subscription ID.
136 + * @param string $recurr_timing Recurring timing string (e.g. "1 month").
137 + * @param int $renewal_order_id Renewal order driving this activation.
138 + *
139 + * @return int Next payment timestamp.
140 + */
141 + private function get_anchored_next_date( $subscription_id, $recurr_timing, $renewal_order_id = 0 ) {
142 + $now = time();
143 + $anchor = (int) get_post_meta( $subscription_id, '_subscrpt_next_date', true );
144 +
145 + if ( $anchor <= 0 ) {
146 + return sdevs_wp_strtotime( $recurr_timing, $now );
147 + }
148 +
149 + // This renewal order already advanced the date; keep it where it is.
150 + $dated_by = (int) get_post_meta( $subscription_id, '_subscrpt_next_date_set_by_order', true );
151 + if ( $renewal_order_id && $renewal_order_id === $dated_by ) {
152 + return $anchor;
153 + }
154 +
155 + if ( $renewal_order_id ) {
156 + update_post_meta( $subscription_id, '_subscrpt_next_date_set_by_order', $renewal_order_id );
157 + }
158 +
159 + $next_date = sdevs_wp_strtotime( $recurr_timing, $anchor );
160 +
161 + // Step forward until the date is in the future. Bail out if the timing string does not advance.
162 + $guard = 0;
163 + while ( $next_date <= $now && $guard < 1000 ) {
164 + $stepped = sdevs_wp_strtotime( $recurr_timing, $next_date );
165 + if ( $stepped <= $next_date ) {
166 + break;
167 + }
168 + $next_date = $stepped;
169 + ++$guard;
170 + }
171 +
172 + if ( $next_date <= $now ) {
173 + return sdevs_wp_strtotime( $recurr_timing, $now );
174 + }
175 +
176 + return $next_date;
177 + }
178 +
179 + /**
76 180 * Add custom column on order item.
77 181 *
78 182 * @return void
79 183 */
@@ -78,9 +182,9 @@
78 182 * @return void
79 183 */
80 184 public function register_custom_column() {
81 185 ?>
82 - <th class="item_recurring sortable" data-sort="float"><?php esc_html_e( 'Recurring', 'sdevs_subscrpt' ); ?></th>
186 + <th class="item_recurring sortable" data-sort="float"><?php esc_html_e( 'Recurring', 'subscription' ); ?></th>
83 187 <?php
84 188 }
85 189
86 190 /**
@@ -96,12 +200,18 @@
96 200 return;
97 201 }
98 202
99 203 $subtotal = '-';
100 - $item_id = $item->get_id();
101 - $subscription_id = Helper::get_subscription_from_order_item_id( $item->get_id() )->subscription_id;
102 - $price = get_post_meta( $subscription_id, '_subscrpt_price', true );
103 - $subtotal = Helper::format_price_with_order_item( $price, $item_id );
204 + $subscription_id = Helper::get_subscription_from_order_item_id( $item->get_id() );
205 +
206 + if ( ! $subscription_id ) {
207 + echo "<td class='item_recurring' width='15%'>-</td>";
208 + return;
209 + }
210 + $subscription_id = $subscription_id->subscription_id;
211 +
212 + // Strikes the original amount when a discount carries into renewals.
213 + $subtotal = Helper::get_subscription_recurring_price_html( $subscription_id, $item );
104 214 ?>
105 215 <td class="item_recurring" width="15%">
106 216 <div class="view">
107 217 <?php echo wp_kses_post( $subtotal ); ?>
@@ -124,9 +234,9 @@
124 234 $trial = $item_meta['trial'];
125 235 $has_trial = isset( $item_meta['trial'] ) && strlen( $item_meta['trial'] ) > 2;
126 236
127 237 if ( $has_trial ) {
128 - echo '<br/><small> + Got ' . $trial . ' free trial!</small>';
238 + echo '<br/><small> + Got ' . esc_html( $trial ) . ' free trial!</small>';
129 239 }
130 240 }
131 241
132 242 /**
@@ -162,16 +272,54 @@
162 272 $histories = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE order_id=%d', array( $table_name, $order_id ) ) );
163 273
164 274 foreach ( $histories as $history ) {
165 275 if ( 'new' === $history->type || 'renew' === $history->type ) {
276 + $subscription_id = $history->subscription_id;
277 +
278 + // Renewals ignore the intermediate `processing` state for renewal orders.
279 + if ( 'renew' === $history->type && 'processing' === $order->get_status() ) {
280 + continue;
281 + }
282 +
283 + // Capture the status before the max-payments check below may flip it.
284 + $current_status = get_post_status( $subscription_id );
285 +
286 + // Split payment: complete instead of re-activate after the final installment.
287 + $target_status = $post_status;
288 + if (
289 + 'active' === $target_status
290 + && function_exists( 'subscrpt_is_max_payments_reached' )
291 + && subscrpt_is_max_payments_reached( $subscription_id )
292 + ) {
293 + $target_status = 'completed';
294 + }
295 +
296 + // Skip no-op transitions so the note and side effects don't duplicate.
297 + if ( $current_status === $target_status ) {
298 + continue;
299 + }
300 +
166 301 wp_update_post(
167 302 array(
168 - 'ID' => $history->subscription_id,
169 - 'post_status' => $post_status,
303 + 'ID' => $subscription_id,
304 + 'post_status' => $target_status,
170 305 )
171 306 );
172 307
173 - Action::write_comment( $post_status, $history->subscription_id );
308 + // If possible change order status to completed if it has a trial subscription.
309 + $this->maybe_trigger_auto_complete_trial_order( $order_id, $subscription_id );
310 +
311 + // Increment renewal count for completed renewal orders (wps-pro)
312 + if ( 'renew' === $history->type && 'active' === $post_status && function_exists( 'subscrpt_pro_activated' ) && subscrpt_pro_activated() ) {
313 + if ( class_exists( '\\SpringDevs\\SubscriptionPro\\Illuminate\\LimitChecker' ) ) {
314 + \SpringDevs\SubscriptionPro\Illuminate\LimitChecker::increment_renewal_count( $history->subscription_id );
315 + }
316 + }
317 +
318 + // Add enhanced split payment activity logging
319 + $this->add_split_payment_activity_note( $history->subscription_id, $history->type, $post_status, $order );
320 +
321 + Action::write_comment( $target_status, $history->subscription_id );
174 322 } else {
175 323 do_action( 'subscrpt_order_status_changed', $order, $history );
176 324 }
177 325 }
@@ -197,6 +345,141 @@
197 345 }
198 346
199 347 // phpcs:ignore
200 348 $wpdb->delete( $table_name, array( 'order_id' => $order_id ), array( '%d' ) );
349 + }
350 +
351 + /**
352 + * Add enhanced split payment activity note with payment progress and access information.
353 + *
354 + * @param int $subscription_id Subscription ID.
355 + * @param string $history_type History type (new, renew, etc.).
356 + * @param string $post_status Post status.
357 + * @param \WC_Order $order WooCommerce order object.
358 + */
359 + private function add_split_payment_activity_note( $subscription_id, $history_type, $post_status, $order ) {
360 + // Only add enhanced notes for active subscriptions
361 + if ( 'active' !== $post_status ) {
362 + return;
363 + }
364 +
365 + // Check if this is a split payment subscription
366 + if ( ! function_exists( 'subscrpt_get_payment_type' ) ) {
367 + return;
368 + }
369 +
370 + $payment_type = subscrpt_get_payment_type( $subscription_id );
371 + if ( 'split_payment' !== $payment_type ) {
372 + return;
373 + }
374 +
375 + // Get payment progress information
376 + $max_payments = function_exists( 'subscrpt_get_max_payments' ) ? subscrpt_get_max_payments( $subscription_id ) : 0;
377 + $payments_made = function_exists( 'subscrpt_count_payments_made' ) ? subscrpt_count_payments_made( $subscription_id ) : 0;
378 + $remaining_payments = function_exists( 'subscrpt_get_remaining_payments' ) ? subscrpt_get_remaining_payments( $subscription_id ) : 0;
379 +
380 + // Determine payment number for this order
381 + $payment_number = $payments_made;
382 + $order_total = $order->get_total();
383 + $order_currency = $order->get_currency();
384 +
385 + // Create enhanced activity note
386 + $comment_content = '';
387 + $activity_type = '';
388 +
389 + if ( 'new' === $history_type ) {
390 + $comment_content = sprintf(
391 + /* translators: %1$d: payment number, %2$d: total payments, %3$s: amount, %4$s: currency */
392 + __( 'Split payment %1$d of %2$d received (%3$s %4$s). Initial access granted.', 'subscription' ),
393 + $payment_number,
394 + $max_payments,
395 + $order_total,
396 + $order_currency
397 + );
398 + $activity_type = __( 'Split Payment - Initial', 'subscription' );
399 + } elseif ( 'renew' === $history_type ) {
400 + $comment_content = sprintf(
401 + /* translators: %1$d: payment number, %2$d: total payments, %3$s: amount, %4$s: currency, %5$d: remaining */
402 + __( 'Split payment %1$d of %2$d received (%3$s %4$s). %5$d payments remaining.', 'subscription' ),
403 + $payment_number,
404 + $max_payments,
405 + $order_total,
406 + $order_currency,
407 + $remaining_payments
408 + );
409 + $activity_type = __( 'Split Payment - Installment', 'subscription' );
410 + }
411 +
412 + // Add the enhanced activity note
413 + if ( $comment_content ) {
414 + $comment_id = wp_insert_comment(
415 + array(
416 + 'comment_author' => 'Subscription for WooCommerce',
417 + 'comment_content' => $comment_content,
418 + 'comment_post_ID' => $subscription_id,
419 + 'comment_type' => 'order_note',
420 + )
421 + );
422 + update_comment_meta( $comment_id, '_subscrpt_activity', $activity_type );
423 + update_comment_meta( $comment_id, '_subscrpt_activity_type', 'split_payment' );
424 +
425 + // Add order note with split payment context
426 + $order_note = sprintf(
427 + /* translators: %1$d: payment number, %2$d: total payments, %3$d: subscription id */
428 + __( 'Split payment %1$d of %2$d received for subscription #%3$d', 'subscription' ),
429 + $payment_number,
430 + $max_payments,
431 + $subscription_id
432 + );
433 + $order->add_order_note( $order_note );
434 + }
435 + }
436 +
437 + /**
438 + * Maybe complete the order if it has a trial subscription and is still in processing status.
439 + *
440 + * @param int $order_id Order ID.
441 + * @param int $subscription_id Subscription ID.
442 + */
443 + public function maybe_trigger_auto_complete_trial_order( $order_id, $subscription_id ) {
444 + $order = wc_get_order( $order_id );
445 + $order_items = $order->get_items();
446 +
447 + // Only attempt to complete if order is still in processing status.
448 + if ( 'processing' !== $order->get_status() ) {
449 + return;
450 + }
451 +
452 + $is_subs_trial_order = false;
453 + foreach ( $order_items as $order_item ) {
454 + $subscrpt_meta = $order_item->get_meta( '_subscrpt_meta', true );
455 +
456 + if ( ! empty( $subscrpt_meta ) && isset( $subscrpt_meta['trial'] ) ) {
457 + $is_subs_trial_order = true;
458 + }
459 + }
460 +
461 + if ( $is_subs_trial_order ) {
462 + as_enqueue_async_action( 'subscrpt_queue_trial_order_autocomplete', [ 'order_id' => $order_id ] );
463 +
464 + $log_message = "Queued auto complete task for free trial order [ID: {$order_id}]";
465 + subscrpt_write_log( $log_message );
466 + subscrpt_write_debug_log( $log_message );
467 + }
468 + }
469 +
470 + /**
471 + * Autocomplete subscription trial order.
472 + *
473 + * @param int $order_id Order ID.
474 + */
475 + public function auto_complete_subscription_trial_order( $order_id ) {
476 + $order = wc_get_order( $order_id );
477 + if ( ! $order ) {
478 + return;
479 + }
480 +
481 + sleep( 3 ); // Adding a small delay to ensure order status is updated before we check it.
482 +
483 + $order->update_status( 'completed', __( 'Subscription order with trial.', 'subscription' ) );
201 484 }
202 485 }