PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / 1.10.9
Subscriptions for WooCommerce with Stripe Recurring Payments v1.10.9
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
subscription / includes / Illuminate / Order.php

Order.php in Subscriptions for WooCommerce with Stripe Recurring Payments 1.10.9, at includes/Illuminate/Order.php

539 lines 18.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace SpringDevs\Subscription\Illuminate;
4
5 /**
6 * Class Order
7 *
8 * @package SpringDevs\Subscription\Illuminate
9 */
10 class Order {
11
12 /**
13 * Initialize the class.
14 */
15 public function __construct() {
16 add_action( 'woocommerce_admin_order_item_headers', array( $this, 'register_custom_column' ) );
17 add_action( 'woocommerce_admin_order_item_values', array( $this, 'add_column_value' ), 10, 2 );
18 add_action( 'woocommerce_before_order_itemmeta', array( $this, 'add_order_item_data' ), 10, 3 );
19 add_action( 'woocommerce_order_status_changed', array( $this, 'order_status_changed' ) );
20 add_action( 'woocommerce_before_delete_order', array( $this, 'delete_the_subscription' ) );
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' ) );
24 }
25
26 /**
27 * Generate start, next and trial dates.
28 *
29 * @param int $subscription_id Subscription Id.
30 *
31 * @return void
32 */
33 public function generate_dates_for_subscription( $subscription_id ) {
34 $order_item_id = get_post_meta( $subscription_id, '_subscrpt_order_item_id', true );
35 $subscription_history = Helper::get_subscription_from_order_item_id( $order_item_id );
36
37 $order_item_meta = wc_get_order_item_meta( $order_item_id, '_subscrpt_meta' );
38 $type = Helper::get_typos( 1, $order_item_meta['type'] );
39 $trial = get_post_meta( $subscription_id, '_subscrpt_trial', true );
40 $recurr_timing = ( $order_item_meta['time'] ?? 1 ) . ' ' . $type;
41
42 if ( 'new' === $subscription_history->type ) {
43 $start_date = time();
44 $next_date = sdevs_wp_strtotime( $recurr_timing, $start_date );
45
46 if ( $trial && ! empty( $trial ) ) {
47 $trial_started = get_post_meta( $subscription_id, '_subscrpt_trial_started', true );
48 $trial_ended = get_post_meta( $subscription_id, '_subscrpt_trial_ended', true );
49
50 if ( empty( $trial_started ) && empty( $trial_ended ) ) {
51 $start_date = sdevs_wp_strtotime( $trial );
52 $next_date = $start_date;
53
54 update_post_meta( $subscription_id, '_subscrpt_trial_started', time() );
55 update_post_meta( $subscription_id, '_subscrpt_trial_ended', $start_date );
56 update_post_meta( $subscription_id, '_subscrpt_trial_mode', 'on' );
57 }
58
59 if ( ! empty( $trial_ended ) ) {
60 $start_date = $trial_ended;
61 $next_date = $start_date;
62 }
63 }
64
65 update_post_meta( $subscription_id, '_subscrpt_start_date', $start_date );
66
67 } elseif ( 'renew' === $subscription_history->type ) {
68 if ( $trial ) {
69 delete_post_meta( $subscription_id, '_subscrpt_trial' );
70 delete_post_meta( $subscription_id, '_subscrpt_trial_mode' );
71 delete_post_meta( $subscription_id, '_subscrpt_trial_started' );
72 delete_post_meta( $subscription_id, '_subscrpt_trial_ended' );
73 }
74
75 $next_date = $this->get_anchored_next_date( $subscription_id, $recurr_timing, (int) $subscription_history->order_id );
76
77 } elseif ( 'early-renew' === $subscription_history->type ) {
78 if ( $trial ) {
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' );
83 }
84
85 $next_date = sdevs_wp_strtotime( $recurr_timing, time() );
86 }
87
88 /**
89 * Filter the subscription next payment date before it is saved.
90 *
91 * General-purpose hook (not scoped to split payment) for adjusting the
92 * computed next renewal date. Note `$next_date` may be null for history
93 * types other than 'new'/'renew'/'early-renew' (e.g. switch orders).
94 *
95 * The deprecated `subscrpt_split_payment_next_due_date` filter is bridged
96 * onto this hook in LegacyCompat.php for backward compatibility.
97 *
98 * @param int|null $next_date Computed next payment timestamp, or null.
99 * @param int $subscription_id Subscription ID.
100 * @param string $recurr_timing Recurring timing string (e.g. "1 month").
101 * @param string $type Subscription history type.
102 */
103 $next_date = apply_filters( 'subscrpt_subscription_next_date', $next_date, $subscription_id, $recurr_timing, $subscription_history->type );
104
105 update_post_meta( $subscription_id, '_subscrpt_next_date', $next_date );
106 }
107
108 /**
109 * Calculate a renewal's next payment date, anchored to the previous due date.
110 *
111 * Computing from time() instead makes every cycle inherit however late the
112 * renewal was actually processed — with an hourly cron a due date of 02:00:05
113 * is missed by the 02:00:03 run and lands at 03:00, and that hour is carried
114 * into every following cycle until a whole billing period is skipped. Anchoring
115 * to the stored due date keeps the billing time-of-day stable instead.
116 *
117 * When payment arrives more than one period late (manual renewal, cron outage),
118 * the anchor is stepped forward period by period so the returned date is always
119 * in the future — otherwise the subscription would be due again immediately.
120 *
121 * Because this runs on every activating status transition of the same renewal
122 * order (pending → processing → completed), the order that last moved the date
123 * is recorded so repeat transitions do not advance the cycle again.
124 *
125 * @param int $subscription_id Subscription ID.
126 * @param string $recurr_timing Recurring timing string (e.g. "1 month").
127 * @param int $renewal_order_id Renewal order driving this activation.
128 *
129 * @return int Next payment timestamp.
130 */
131 private function get_anchored_next_date( $subscription_id, $recurr_timing, $renewal_order_id = 0 ) {
132 $now = time();
133 $anchor = (int) get_post_meta( $subscription_id, '_subscrpt_next_date', true );
134
135 if ( $anchor <= 0 ) {
136 return sdevs_wp_strtotime( $recurr_timing, $now );
137 }
138
139 // This renewal order already advanced the date; keep it where it is.
140 $dated_by = (int) get_post_meta( $subscription_id, '_subscrpt_next_date_set_by_order', true );
141 if ( $renewal_order_id && $renewal_order_id === $dated_by ) {
142 return $anchor;
143 }
144
145 if ( $renewal_order_id ) {
146 update_post_meta( $subscription_id, '_subscrpt_next_date_set_by_order', $renewal_order_id );
147 }
148
149 $next_date = sdevs_wp_strtotime( $recurr_timing, $anchor );
150
151 // Step forward until the date is in the future. Bail out if the timing string does not advance.
152 $guard = 0;
153 while ( $next_date <= $now && $guard < 1000 ) {
154 $stepped = sdevs_wp_strtotime( $recurr_timing, $next_date );
155 if ( $stepped <= $next_date ) {
156 break;
157 }
158 $next_date = $stepped;
159 ++$guard;
160 }
161
162 if ( $next_date <= $now ) {
163 return sdevs_wp_strtotime( $recurr_timing, $now );
164 }
165
166 return $next_date;
167 }
168
169 /**
170 * Add custom column on order item.
171 *
172 * @return void
173 */
174 public function register_custom_column() {
175 ?>
176 <th class="item_recurring sortable" data-sort="float"><?php esc_html_e( 'Recurring', 'subscription' ); ?></th>
177 <?php
178 }
179
180 /**
181 * Display data for custom column.
182 *
183 * @param \WC_Product $product Product Object.
184 * @param \WC_Order_Item $item Order Item.
185 *
186 * @return void
187 */
188 public function add_column_value( $product, $item ) {
189 if ( ! method_exists( $item, 'get_id' ) || ! method_exists( $item, 'get_subtotal' ) ) {
190 return;
191 }
192
193 $subtotal = '-';
194 $item_id = $item->get_id();
195 $subscription_id = Helper::get_subscription_from_order_item_id( $item->get_id() );
196
197 if ( ! $subscription_id ) {
198 echo "<td class='item_recurring' width='15%'>-</td>";
199 return;
200 }
201 $subscription_id = $subscription_id->subscription_id;
202
203 $price = get_post_meta( $subscription_id, '_subscrpt_price', true );
204 $subtotal = Helper::format_price_with_order_item( $price, $item_id );
205 ?>
206 <td class="item_recurring" width="15%">
207 <div class="view">
208 <?php echo wp_kses_post( $subtotal ); ?>
209 </div>
210 </td>
211 <?php
212 }
213
214 public function add_order_item_data( $item_id, $item, $product ) {
215 if ( ! $product ) {
216 return;
217 }
218
219 $item_meta = wc_get_order_item_meta( $item_id, '_subscrpt_meta', true );
220
221 if ( ! $item_meta || ! is_array( $item_meta ) ) {
222 return false;
223 }
224
225 $trial = $item_meta['trial'];
226 $has_trial = isset( $item_meta['trial'] ) && strlen( $item_meta['trial'] ) > 2;
227
228 if ( $has_trial ) {
229 echo '<br/><small> + Got ' . esc_html( $trial ) . ' free trial!</small>';
230 }
231 }
232
233 /**
234 * Take some actions based on order status changed.
235 *
236 * @param int $order_id Order Id.
237 */
238 public function order_status_changed( $order_id ) {
239 $order = wc_get_order( $order_id );
240 $post_status = 'active';
241
242 switch ( $order->get_status() ) {
243 case 'on-hold':
244 case 'pending':
245 $post_status = 'pending';
246 break;
247
248 case 'refunded':
249 case 'failed':
250 case 'cancelled':
251 $post_status = 'cancelled';
252 break;
253
254 default:
255 $post_status = 'active';
256 break;
257 }
258 $post_status = apply_filters( 'subscript_order_status_to_post_status', $post_status, $order );
259
260 global $wpdb;
261 $table_name = $wpdb->prefix . 'subscrpt_order_relation';
262 // @phpcs:ignore
263 $histories = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE order_id=%d', array( $table_name, $order_id ) ) );
264
265 foreach ( $histories as $history ) {
266 if ( 'new' === $history->type || 'renew' === $history->type ) {
267 $subscription_id = $history->subscription_id;
268 wp_update_post(
269 array(
270 'ID' => $subscription_id,
271 'post_status' => $post_status,
272 )
273 );
274
275 // If possible change order status to completed if it has a trial subscription.
276 $this->maybe_trigger_auto_complete_trial_order( $order_id, $subscription_id );
277
278 // Increment renewal count for completed renewal orders (wps-pro)
279 if ( 'renew' === $history->type && 'active' === $post_status && function_exists( 'subscrpt_pro_activated' ) && subscrpt_pro_activated() ) {
280 if ( class_exists( '\\SpringDevs\\SubscriptionPro\\Illuminate\\LimitChecker' ) ) {
281 \SpringDevs\SubscriptionPro\Illuminate\LimitChecker::increment_renewal_count( $history->subscription_id );
282 }
283 }
284
285 // Add enhanced split payment activity logging
286 $this->add_split_payment_activity_note( $history->subscription_id, $history->type, $post_status, $order );
287
288 Action::write_comment( $post_status, $history->subscription_id );
289 } else {
290 do_action( 'subscrpt_order_status_changed', $order, $history );
291 }
292 }
293 }
294
295 /**
296 * Delete the subscription.
297 *
298 * @param int $order_id Order ID.
299 *
300 * @return void
301 */
302 public function delete_the_subscription( $order_id ) {
303 global $wpdb;
304 $table_name = $wpdb->prefix . 'subscrpt_order_relation';
305
306 $histories = Helper::get_subscriptions_from_order( $order_id );
307 foreach ( (array) $histories as $history ) {
308 $subscription_order_id = get_post_meta( $history->subscription_id, '_subscrpt_order_id', true );
309 if ( (int) $subscription_order_id === $order_id ) {
310 wp_delete_post( $history->subscription_id, true );
311 }
312 }
313
314 // phpcs:ignore
315 $wpdb->delete( $table_name, array( 'order_id' => $order_id ), array( '%d' ) );
316 }
317
318 /**
319 * Add enhanced split payment activity note with payment progress and access information.
320 *
321 * @param int $subscription_id Subscription ID.
322 * @param string $history_type History type (new, renew, etc.).
323 * @param string $post_status Post status.
324 * @param \WC_Order $order WooCommerce order object.
325 */
326 private function add_split_payment_activity_note( $subscription_id, $history_type, $post_status, $order ) {
327 // Only add enhanced notes for active subscriptions
328 if ( 'active' !== $post_status ) {
329 return;
330 }
331
332 // Check if this is a split payment subscription
333 if ( ! function_exists( 'subscrpt_get_payment_type' ) ) {
334 return;
335 }
336
337 $payment_type = subscrpt_get_payment_type( $subscription_id );
338 if ( 'split_payment' !== $payment_type ) {
339 return;
340 }
341
342 // Get payment progress information
343 $max_payments = function_exists( 'subscrpt_get_max_payments' ) ? subscrpt_get_max_payments( $subscription_id ) : 0;
344 $payments_made = function_exists( 'subscrpt_count_payments_made' ) ? subscrpt_count_payments_made( $subscription_id ) : 0;
345 $remaining_payments = function_exists( 'subscrpt_get_remaining_payments' ) ? subscrpt_get_remaining_payments( $subscription_id ) : 0;
346
347 // Determine payment number for this order
348 $payment_number = $payments_made;
349 $order_total = $order->get_total();
350 $order_currency = $order->get_currency();
351
352 // Create enhanced activity note
353 $comment_content = '';
354 $activity_type = '';
355
356 if ( 'new' === $history_type ) {
357 $comment_content = sprintf(
358 /* translators: %1$d: payment number, %2$d: total payments, %3$s: amount, %4$s: currency */
359 __( 'Split payment %1$d of %2$d received (%3$s %4$s). Initial access granted.', 'subscription' ),
360 $payment_number,
361 $max_payments,
362 $order_total,
363 $order_currency
364 );
365 $activity_type = __( 'Split Payment - Initial', 'subscription' );
366 } elseif ( 'renew' === $history_type ) {
367 $comment_content = sprintf(
368 /* translators: %1$d: payment number, %2$d: total payments, %3$s: amount, %4$s: currency, %5$d: remaining */
369 __( 'Split payment %1$d of %2$d received (%3$s %4$s). %5$d payments remaining.', 'subscription' ),
370 $payment_number,
371 $max_payments,
372 $order_total,
373 $order_currency,
374 $remaining_payments
375 );
376 $activity_type = __( 'Split Payment - Installment', 'subscription' );
377 }
378
379 // Add the enhanced activity note
380 if ( $comment_content ) {
381 $comment_id = wp_insert_comment(
382 array(
383 'comment_author' => 'Subscription for WooCommerce',
384 'comment_content' => $comment_content,
385 'comment_post_ID' => $subscription_id,
386 'comment_type' => 'order_note',
387 )
388 );
389 update_comment_meta( $comment_id, '_subscrpt_activity', $activity_type );
390 update_comment_meta( $comment_id, '_subscrpt_activity_type', 'split_payment' );
391
392 // Add order note with split payment context
393 $order_note = sprintf(
394 /* translators: %1$d: payment number, %2$d: total payments, %3$d: subscription id */
395 __( 'Split payment %1$d of %2$d received for subscription #%3$d', 'subscription' ),
396 $payment_number,
397 $max_payments,
398 $subscription_id
399 );
400 $order->add_order_note( $order_note );
401 }
402
403 // Add payment milestone notes for significant progress
404 $this->add_payment_milestone_notes( $subscription_id, $payments_made, $max_payments );
405 }
406
407 /**
408 * Add payment milestone notes for significant progress (25%, 50%, 75%, 100%).
409 *
410 * @param int $subscription_id Subscription ID.
411 * @param int $payments_made Number of payments made.
412 * @param int $max_payments Maximum number of payments.
413 */
414 private function add_payment_milestone_notes( $subscription_id, $payments_made, $max_payments ) {
415 if ( ! $max_payments || $max_payments <= 0 ) {
416 return;
417 }
418
419 $percentage = round( ( $payments_made / $max_payments ) * 100 );
420 $milestone_key = '_subscrpt_milestone_' . $percentage . '_logged';
421
422 // Check if this milestone has already been logged
423 if ( get_post_meta( $subscription_id, $milestone_key, true ) ) {
424 return;
425 }
426
427 $milestone_note = '';
428 $activity_type = '';
429
430 switch ( $percentage ) {
431 case 25:
432 $milestone_note = sprintf(
433 /* translators: %1$d: payments made, %2$d: total payments */
434 __( 'Payment milestone reached: %1$d of %2$d payments completed (25%%).', 'subscription' ),
435 $payments_made,
436 $max_payments
437 );
438 $activity_type = __( 'Payment Milestone - 25%', 'subscription' );
439 break;
440
441 case 50:
442 $milestone_note = sprintf(
443 /* translators: %1$d: payments made, %2$d: total payments */
444 __( 'Payment milestone reached: %1$d of %2$d payments completed (50%%). Halfway there!', 'subscription' ),
445 $payments_made,
446 $max_payments
447 );
448 $activity_type = __( 'Payment Milestone - 50%', 'subscription' );
449 break;
450
451 case 75:
452 $milestone_note = sprintf(
453 /* translators: %1$d: payments made, %2$d: total payments */
454 __( 'Payment milestone reached: %1$d of %2$d payments completed (75%%). Almost complete!', 'subscription' ),
455 $payments_made,
456 $max_payments
457 );
458 $activity_type = __( 'Payment Milestone - 75%', 'subscription' );
459 break;
460
461 case 100:
462 $milestone_note = sprintf(
463 /* translators: %1$d: payments made, %2$d: total payments */
464 __( 'Payment milestone reached: %1$d of %2$d payments completed (100%%). Split payment plan complete!', 'subscription' ),
465 $payments_made,
466 $max_payments
467 );
468 $activity_type = __( 'Payment Milestone - 100%', 'subscription' );
469 break;
470 }
471
472 // Add milestone note if applicable
473 if ( $milestone_note ) {
474 $comment_id = wp_insert_comment(
475 array(
476 'comment_author' => 'Subscription for WooCommerce',
477 'comment_content' => $milestone_note,
478 'comment_post_ID' => $subscription_id,
479 'comment_type' => 'order_note',
480 )
481 );
482 update_comment_meta( $comment_id, '_subscrpt_activity', $activity_type );
483 update_comment_meta( $comment_id, '_subscrpt_activity_type', 'split_payment' );
484
485 // Mark milestone as logged
486 update_post_meta( $subscription_id, $milestone_key, current_time( 'timestamp' ) );
487 }
488 }
489
490 /**
491 * Maybe complete the order if it has a trial subscription and is still in processing status.
492 *
493 * @param int $order_id Order ID.
494 * @param int $subscription_id Subscription ID.
495 */
496 public function maybe_trigger_auto_complete_trial_order( $order_id, $subscription_id ) {
497 $order = wc_get_order( $order_id );
498 $order_items = $order->get_items();
499
500 // Only attempt to complete if order is still in processing status.
501 if ( 'processing' !== $order->get_status() ) {
502 return;
503 }
504
505 $is_subs_trial_order = false;
506 foreach ( $order_items as $order_item ) {
507 $subscrpt_meta = $order_item->get_meta( '_subscrpt_meta', true );
508
509 if ( ! empty( $subscrpt_meta ) && isset( $subscrpt_meta['trial'] ) ) {
510 $is_subs_trial_order = true;
511 }
512 }
513
514 if ( $is_subs_trial_order ) {
515 as_enqueue_async_action( 'subscrpt_queue_trial_order_autocomplete', [ 'order_id' => $order_id ] );
516
517 $log_message = "Queued auto complete task for free trial order [ID: {$order_id}]";
518 subscrpt_write_log( $log_message );
519 subscrpt_write_debug_log( $log_message );
520 }
521 }
522
523 /**
524 * Autocomplete subscription trial order.
525 *
526 * @param int $order_id Order ID.
527 */
528 public function auto_complete_subscription_trial_order( $order_id ) {
529 $order = wc_get_order( $order_id );
530 if ( ! $order ) {
531 return;
532 }
533
534 sleep( 3 ); // Adding a small delay to ensure order status is updated before we check it.
535
536 $order->update_status( 'completed', __( 'Subscription order with trial.', 'subscription' ) );
537 }
538 }
539