PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / 2.1.0
Subscriptions for WooCommerce with Stripe Recurring Payments v2.1.0
2.4.0 2.3.0 2.2.0 2.1.0 2.0.0 1.11.2 1.11.1 1.11.0 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 1.10.2 1.10.1 1.10.0 1.9.6 1.9.5 trunk 1.3.0 1.3.1 1.3.2 1.4.0 All 65 releases
← All changes | includes/Illuminate/Order.php +99 -92 1.10.8 → 2.1.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' );
@@ -84,8 +84,17 @@
84 84
85 85 $next_date = sdevs_wp_strtotime( $recurr_timing, time() );
86 86 }
87 87
88 + // Split payment: no next date after the final installment.
89 + if (
90 + in_array( $subscription_history->type, array( 'renew', 'early-renew' ), true )
91 + && function_exists( 'subscrpt_is_max_payments_reached' )
92 + && subscrpt_is_max_payments_reached( $subscription_id )
93 + ) {
94 + return;
95 + }
96 +
88 97 /**
89 98 * Filter the subscription next payment date before it is saved.
90 99 *
91 100 * General-purpose hook (not scoped to split payment) for adjusting the
@@ -105,8 +114,69 @@
105 114 update_post_meta( $subscription_id, '_subscrpt_next_date', $next_date );
106 115 }
107 116
108 117 /**
118 + * Calculate a renewal's next payment date, anchored to the previous due date.
119 + *
120 + * Computing from time() instead makes every cycle inherit however late the
121 + * renewal was actually processed — with an hourly cron a due date of 02:00:05
122 + * is missed by the 02:00:03 run and lands at 03:00, and that hour is carried
123 + * into every following cycle until a whole billing period is skipped. Anchoring
124 + * to the stored due date keeps the billing time-of-day stable instead.
125 + *
126 + * When payment arrives more than one period late (manual renewal, cron outage),
127 + * the anchor is stepped forward period by period so the returned date is always
128 + * in the future — otherwise the subscription would be due again immediately.
129 + *
130 + * Because this runs on every activating status transition of the same renewal
131 + * order (pending → processing → completed), the order that last moved the date
132 + * is recorded so repeat transitions do not advance the cycle again.
133 + *
134 + * @param int $subscription_id Subscription ID.
135 + * @param string $recurr_timing Recurring timing string (e.g. "1 month").
136 + * @param int $renewal_order_id Renewal order driving this activation.
137 + *
138 + * @return int Next payment timestamp.
139 + */
140 + private function get_anchored_next_date( $subscription_id, $recurr_timing, $renewal_order_id = 0 ) {
141 + $now = time();
142 + $anchor = (int) get_post_meta( $subscription_id, '_subscrpt_next_date', true );
143 +
144 + if ( $anchor <= 0 ) {
145 + return sdevs_wp_strtotime( $recurr_timing, $now );
146 + }
147 +
148 + // This renewal order already advanced the date; keep it where it is.
149 + $dated_by = (int) get_post_meta( $subscription_id, '_subscrpt_next_date_set_by_order', true );
150 + if ( $renewal_order_id && $renewal_order_id === $dated_by ) {
151 + return $anchor;
152 + }
153 +
154 + if ( $renewal_order_id ) {
155 + update_post_meta( $subscription_id, '_subscrpt_next_date_set_by_order', $renewal_order_id );
156 + }
157 +
158 + $next_date = sdevs_wp_strtotime( $recurr_timing, $anchor );
159 +
160 + // Step forward until the date is in the future. Bail out if the timing string does not advance.
161 + $guard = 0;
162 + while ( $next_date <= $now && $guard < 1000 ) {
163 + $stepped = sdevs_wp_strtotime( $recurr_timing, $next_date );
164 + if ( $stepped <= $next_date ) {
165 + break;
166 + }
167 + $next_date = $stepped;
168 + ++$guard;
169 + }
170 +
171 + if ( $next_date <= $now ) {
172 + return sdevs_wp_strtotime( $recurr_timing, $now );
173 + }
174 +
175 + return $next_date;
176 + }
177 +
178 + /**
109 179 * Add custom column on order item.
110 180 *
111 181 * @return void
112 182 */
@@ -129,9 +199,8 @@
129 199 return;
130 200 }
131 201
132 202 $subtotal = '-';
133 - $item_id = $item->get_id();
134 203 $subscription_id = Helper::get_subscription_from_order_item_id( $item->get_id() );
135 204
136 205 if ( ! $subscription_id ) {
137 206 echo "<td class='item_recurring' width='15%'>-</td>";
@@ -138,10 +207,10 @@
138 207 return;
139 208 }
140 209 $subscription_id = $subscription_id->subscription_id;
141 210
142 - $price = get_post_meta( $subscription_id, '_subscrpt_price', true );
143 - $subtotal = Helper::format_price_with_order_item( $price, $item_id );
211 + // Strikes the original amount when a discount carries into renewals.
212 + $subtotal = Helper::get_subscription_recurring_price_html( $subscription_id, $item );
144 213 ?>
145 214 <td class="item_recurring" width="15%">
146 215 <div class="view">
147 216 <?php echo wp_kses_post( $subtotal ); ?>
@@ -203,12 +272,36 @@
203 272
204 273 foreach ( $histories as $history ) {
205 274 if ( 'new' === $history->type || 'renew' === $history->type ) {
206 275 $subscription_id = $history->subscription_id;
276 +
277 + // Renewals ignore the intermediate `processing` state for renewal orders.
278 + if ( 'renew' === $history->type && 'processing' === $order->get_status() ) {
279 + continue;
280 + }
281 +
282 + // Capture the status before the max-payments check below may flip it.
283 + $current_status = get_post_status( $subscription_id );
284 +
285 + // Split payment: complete instead of re-activate after the final installment.
286 + $target_status = $post_status;
287 + if (
288 + 'active' === $target_status
289 + && function_exists( 'subscrpt_is_max_payments_reached' )
290 + && subscrpt_is_max_payments_reached( $subscription_id )
291 + ) {
292 + $target_status = 'completed';
293 + }
294 +
295 + // Skip no-op transitions so the note and side effects don't duplicate.
296 + if ( $current_status === $target_status ) {
297 + continue;
298 + }
299 +
207 300 wp_update_post(
208 301 array(
209 302 'ID' => $subscription_id,
210 - 'post_status' => $post_status,
303 + 'post_status' => $target_status,
211 304 )
212 305 );
213 306
214 307 // If possible change order status to completed if it has a trial subscription.
@@ -223,9 +316,9 @@
223 316
224 317 // Add enhanced split payment activity logging
225 318 $this->add_split_payment_activity_note( $history->subscription_id, $history->type, $post_status, $order );
226 319
227 - Action::write_comment( $post_status, $history->subscription_id );
320 + Action::write_comment( $target_status, $history->subscription_id );
228 321 } else {
229 322 do_action( 'subscrpt_order_status_changed', $order, $history );
230 323 }
231 324 }
@@ -336,94 +429,8 @@
336 429 $max_payments,
337 430 $subscription_id
338 431 );
339 432 $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 433 }
427 434 }
428 435
429 436 /**