PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / 2.3.0
Subscriptions for WooCommerce with Stripe Recurring Payments v2.3.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 +101 -93 1.10.6 → 2.3.0 View file →
@@ -71,9 +71,9 @@
71 71 delete_post_meta( $subscription_id, '_subscrpt_trial_started' );
72 72 delete_post_meta( $subscription_id, '_subscrpt_trial_ended' );
73 73 }
74 74
75 - $next_date = sdevs_wp_strtotime( $recurr_timing, time() );
75 + $next_date = $this->get_anchored_next_date( $subscription_id, $recurr_timing, (int) $subscription_history->order_id );
76 76
77 77 } elseif ( 'early-renew' === $subscription_history->type ) {
78 78 if ( $trial ) {
79 79 delete_post_meta( $subscription_id, '_subscrpt_trial' );
@@ -81,11 +81,21 @@
81 81 delete_post_meta( $subscription_id, '_subscrpt_trial_started' );
82 82 delete_post_meta( $subscription_id, '_subscrpt_trial_ended' );
83 83 }
84 84
85 - $next_date = sdevs_wp_strtotime( $recurr_timing, time() );
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 );
86 87 }
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 +
88 98 /**
89 99 * Filter the subscription next payment date before it is saved.
90 100 *
91 101 * General-purpose hook (not scoped to split payment) for adjusting the
@@ -105,8 +115,69 @@
105 115 update_post_meta( $subscription_id, '_subscrpt_next_date', $next_date );
106 116 }
107 117
108 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 + /**
109 180 * Add custom column on order item.
110 181 *
111 182 * @return void
112 183 */
@@ -129,9 +200,8 @@
129 200 return;
130 201 }
131 202
132 203 $subtotal = '-';
133 - $item_id = $item->get_id();
134 204 $subscription_id = Helper::get_subscription_from_order_item_id( $item->get_id() );
135 205
136 206 if ( ! $subscription_id ) {
137 207 echo "<td class='item_recurring' width='15%'>-</td>";
@@ -138,10 +208,10 @@
138 208 return;
139 209 }
140 210 $subscription_id = $subscription_id->subscription_id;
141 211
142 - $price = get_post_meta( $subscription_id, '_subscrpt_price', true );
143 - $subtotal = Helper::format_price_with_order_item( $price, $item_id );
212 + // Strikes the original amount when a discount carries into renewals.
213 + $subtotal = Helper::get_subscription_recurring_price_html( $subscription_id, $item );
144 214 ?>
145 215 <td class="item_recurring" width="15%">
146 216 <div class="view">
147 217 <?php echo wp_kses_post( $subtotal ); ?>
@@ -203,12 +273,36 @@
203 273
204 274 foreach ( $histories as $history ) {
205 275 if ( 'new' === $history->type || 'renew' === $history->type ) {
206 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 +
207 301 wp_update_post(
208 302 array(
209 303 'ID' => $subscription_id,
210 - 'post_status' => $post_status,
304 + 'post_status' => $target_status,
211 305 )
212 306 );
213 307
214 308 // If possible change order status to completed if it has a trial subscription.
@@ -223,9 +317,9 @@
223 317
224 318 // Add enhanced split payment activity logging
225 319 $this->add_split_payment_activity_note( $history->subscription_id, $history->type, $post_status, $order );
226 320
227 - Action::write_comment( $post_status, $history->subscription_id );
321 + Action::write_comment( $target_status, $history->subscription_id );
228 322 } else {
229 323 do_action( 'subscrpt_order_status_changed', $order, $history );
230 324 }
231 325 }
@@ -336,94 +430,8 @@
336 430 $max_payments,
337 431 $subscription_id
338 432 );
339 433 $order->add_order_note( $order_note );
340 - }
341 -
342 - // Add payment milestone notes for significant progress
343 - $this->add_payment_milestone_notes( $subscription_id, $payments_made, $max_payments );
344 - }
345 -
346 - /**
347 - * Add payment milestone notes for significant progress (25%, 50%, 75%, 100%).
348 - *
349 - * @param int $subscription_id Subscription ID.
350 - * @param int $payments_made Number of payments made.
351 - * @param int $max_payments Maximum number of payments.
352 - */
353 - private function add_payment_milestone_notes( $subscription_id, $payments_made, $max_payments ) {
354 - if ( ! $max_payments || $max_payments <= 0 ) {
355 - return;
356 - }
357 -
358 - $percentage = round( ( $payments_made / $max_payments ) * 100 );
359 - $milestone_key = '_subscrpt_milestone_' . $percentage . '_logged';
360 -
361 - // Check if this milestone has already been logged
362 - if ( get_post_meta( $subscription_id, $milestone_key, true ) ) {
363 - return;
364 - }
365 -
366 - $milestone_note = '';
367 - $activity_type = '';
368 -
369 - switch ( $percentage ) {
370 - case 25:
371 - $milestone_note = sprintf(
372 - /* translators: %1$d: payments made, %2$d: total payments */
373 - __( 'Payment milestone reached: %1$d of %2$d payments completed (25%%).', 'subscription' ),
374 - $payments_made,
375 - $max_payments
376 - );
377 - $activity_type = __( 'Payment Milestone - 25%', 'subscription' );
378 - break;
379 -
380 - case 50:
381 - $milestone_note = sprintf(
382 - /* translators: %1$d: payments made, %2$d: total payments */
383 - __( 'Payment milestone reached: %1$d of %2$d payments completed (50%%). Halfway there!', 'subscription' ),
384 - $payments_made,
385 - $max_payments
386 - );
387 - $activity_type = __( 'Payment Milestone - 50%', 'subscription' );
388 - break;
389 -
390 - case 75:
391 - $milestone_note = sprintf(
392 - /* translators: %1$d: payments made, %2$d: total payments */
393 - __( 'Payment milestone reached: %1$d of %2$d payments completed (75%%). Almost complete!', 'subscription' ),
394 - $payments_made,
395 - $max_payments
396 - );
397 - $activity_type = __( 'Payment Milestone - 75%', 'subscription' );
398 - break;
399 -
400 - case 100:
401 - $milestone_note = sprintf(
402 - /* translators: %1$d: payments made, %2$d: total payments */
403 - __( 'Payment milestone reached: %1$d of %2$d payments completed (100%%). Split payment plan complete!', 'subscription' ),
404 - $payments_made,
405 - $max_payments
406 - );
407 - $activity_type = __( 'Payment Milestone - 100%', 'subscription' );
408 - break;
409 - }
410 -
411 - // Add milestone note if applicable
412 - if ( $milestone_note ) {
413 - $comment_id = wp_insert_comment(
414 - array(
415 - 'comment_author' => 'Subscription for WooCommerce',
416 - 'comment_content' => $milestone_note,
417 - 'comment_post_ID' => $subscription_id,
418 - 'comment_type' => 'order_note',
419 - )
420 - );
421 - update_comment_meta( $comment_id, '_subscrpt_activity', $activity_type );
422 - update_comment_meta( $comment_id, '_subscrpt_activity_type', 'split_payment' );
423 -
424 - // Mark milestone as logged
425 - update_post_meta( $subscription_id, $milestone_key, current_time( 'timestamp' ) );
426 434 }
427 435 }
428 436
429 437 /**