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 +117 -95 1.10.4 → 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,18 +81,103 @@
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
88 - // Allow filtering of next due date logic
89 - $next_date = apply_filters( 'subscrpt_split_payment_next_due_date', $next_date, $subscription_id, $recurr_timing, $subscription_history->type );
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 + }
90 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 +
91 115 update_post_meta( $subscription_id, '_subscrpt_next_date', $next_date );
92 116 }
93 117
94 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 + /**
95 180 * Add custom column on order item.
96 181 *
97 182 * @return void
98 183 */
@@ -115,9 +200,8 @@
115 200 return;
116 201 }
117 202
118 203 $subtotal = '-';
119 - $item_id = $item->get_id();
120 204 $subscription_id = Helper::get_subscription_from_order_item_id( $item->get_id() );
121 205
122 206 if ( ! $subscription_id ) {
123 207 echo "<td class='item_recurring' width='15%'>-</td>";
@@ -124,10 +208,10 @@
124 208 return;
125 209 }
126 210 $subscription_id = $subscription_id->subscription_id;
127 211
128 - $price = get_post_meta( $subscription_id, '_subscrpt_price', true );
129 - $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 );
130 214 ?>
131 215 <td class="item_recurring" width="15%">
132 216 <div class="view">
133 217 <?php echo wp_kses_post( $subtotal ); ?>
@@ -189,12 +273,36 @@
189 273
190 274 foreach ( $histories as $history ) {
191 275 if ( 'new' === $history->type || 'renew' === $history->type ) {
192 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 +
193 301 wp_update_post(
194 302 array(
195 303 'ID' => $subscription_id,
196 - 'post_status' => $post_status,
304 + 'post_status' => $target_status,
197 305 )
198 306 );
199 307
200 308 // If possible change order status to completed if it has a trial subscription.
@@ -209,9 +317,9 @@
209 317
210 318 // Add enhanced split payment activity logging
211 319 $this->add_split_payment_activity_note( $history->subscription_id, $history->type, $post_status, $order );
212 320
213 - Action::write_comment( $post_status, $history->subscription_id );
321 + Action::write_comment( $target_status, $history->subscription_id );
214 322 } else {
215 323 do_action( 'subscrpt_order_status_changed', $order, $history );
216 324 }
217 325 }
@@ -322,94 +430,8 @@
322 430 $max_payments,
323 431 $subscription_id
324 432 );
325 433 $order->add_order_note( $order_note );
326 - }
327 -
328 - // Add payment milestone notes for significant progress
329 - $this->add_payment_milestone_notes( $subscription_id, $payments_made, $max_payments );
330 - }
331 -
332 - /**
333 - * Add payment milestone notes for significant progress (25%, 50%, 75%, 100%).
334 - *
335 - * @param int $subscription_id Subscription ID.
336 - * @param int $payments_made Number of payments made.
337 - * @param int $max_payments Maximum number of payments.
338 - */
339 - private function add_payment_milestone_notes( $subscription_id, $payments_made, $max_payments ) {
340 - if ( ! $max_payments || $max_payments <= 0 ) {
341 - return;
342 - }
343 -
344 - $percentage = round( ( $payments_made / $max_payments ) * 100 );
345 - $milestone_key = '_subscrpt_milestone_' . $percentage . '_logged';
346 -
347 - // Check if this milestone has already been logged
348 - if ( get_post_meta( $subscription_id, $milestone_key, true ) ) {
349 - return;
350 - }
351 -
352 - $milestone_note = '';
353 - $activity_type = '';
354 -
355 - switch ( $percentage ) {
356 - case 25:
357 - $milestone_note = sprintf(
358 - /* translators: %1$d: payments made, %2$d: total payments */
359 - __( 'Payment milestone reached: %1$d of %2$d payments completed (25%%).', 'subscription' ),
360 - $payments_made,
361 - $max_payments
362 - );
363 - $activity_type = __( 'Payment Milestone - 25%', 'subscription' );
364 - break;
365 -
366 - case 50:
367 - $milestone_note = sprintf(
368 - /* translators: %1$d: payments made, %2$d: total payments */
369 - __( 'Payment milestone reached: %1$d of %2$d payments completed (50%%). Halfway there!', 'subscription' ),
370 - $payments_made,
371 - $max_payments
372 - );
373 - $activity_type = __( 'Payment Milestone - 50%', 'subscription' );
374 - break;
375 -
376 - case 75:
377 - $milestone_note = sprintf(
378 - /* translators: %1$d: payments made, %2$d: total payments */
379 - __( 'Payment milestone reached: %1$d of %2$d payments completed (75%%). Almost complete!', 'subscription' ),
380 - $payments_made,
381 - $max_payments
382 - );
383 - $activity_type = __( 'Payment Milestone - 75%', 'subscription' );
384 - break;
385 -
386 - case 100:
387 - $milestone_note = sprintf(
388 - /* translators: %1$d: payments made, %2$d: total payments */
389 - __( 'Payment milestone reached: %1$d of %2$d payments completed (100%%). Split payment plan complete!', 'subscription' ),
390 - $payments_made,
391 - $max_payments
392 - );
393 - $activity_type = __( 'Payment Milestone - 100%', 'subscription' );
394 - break;
395 - }
396 -
397 - // Add milestone note if applicable
398 - if ( $milestone_note ) {
399 - $comment_id = wp_insert_comment(
400 - array(
401 - 'comment_author' => 'Subscription for WooCommerce',
402 - 'comment_content' => $milestone_note,
403 - 'comment_post_ID' => $subscription_id,
404 - 'comment_type' => 'order_note',
405 - )
406 - );
407 - update_comment_meta( $comment_id, '_subscrpt_activity', $activity_type );
408 - update_comment_meta( $comment_id, '_subscrpt_activity_type', 'split_payment' );
409 -
410 - // Mark milestone as logged
411 - update_post_meta( $subscription_id, $milestone_key, current_time( 'timestamp' ) );
412 434 }
413 435 }
414 436
415 437 /**