PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / 1.11.2
Subscriptions for WooCommerce with Stripe Recurring Payments v1.11.2
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 1.4.1 1.4.2 1.5.0 1.5.1 All 61 releases
subscription / includes / Illuminate / Order.php

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

538 lines 18.3 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 $subscription_id = Helper::get_subscription_from_order_item_id( $item->get_id() );
195
196 if ( ! $subscription_id ) {
197 echo "<td class='item_recurring' width='15%'>-</td>";
198 return;
199 }
200 $subscription_id = $subscription_id->subscription_id;
201
202 // Strikes the original amount when a discount carries into renewals.
203 $subtotal = Helper::get_subscription_recurring_price_html( $subscription_id, $item );
204 ?>
205 <td class="item_recurring" width="15%">
206 <div class="view">
207 <?php echo wp_kses_post( $subtotal ); ?>
208 </div>
209 </td>
210 <?php
211 }
212
213 public function add_order_item_data( $item_id, $item, $product ) {
214 if ( ! $product ) {
215 return;
216 }
217
218 $item_meta = wc_get_order_item_meta( $item_id, '_subscrpt_meta', true );
219
220 if ( ! $item_meta || ! is_array( $item_meta ) ) {
221 return false;
222 }
223
224 $trial = $item_meta['trial'];
225 $has_trial = isset( $item_meta['trial'] ) && strlen( $item_meta['trial'] ) > 2;
226
227 if ( $has_trial ) {
228 echo '<br/><small> + Got ' . esc_html( $trial ) . ' free trial!</small>';
229 }
230 }
231
232 /**
233 * Take some actions based on order status changed.
234 *
235 * @param int $order_id Order Id.
236 */
237 public function order_status_changed( $order_id ) {
238 $order = wc_get_order( $order_id );
239 $post_status = 'active';
240
241 switch ( $order->get_status() ) {
242 case 'on-hold':
243 case 'pending':
244 $post_status = 'pending';
245 break;
246
247 case 'refunded':
248 case 'failed':
249 case 'cancelled':
250 $post_status = 'cancelled';
251 break;
252
253 default:
254 $post_status = 'active';
255 break;
256 }
257 $post_status = apply_filters( 'subscript_order_status_to_post_status', $post_status, $order );
258
259 global $wpdb;
260 $table_name = $wpdb->prefix . 'subscrpt_order_relation';
261 // @phpcs:ignore
262 $histories = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE order_id=%d', array( $table_name, $order_id ) ) );
263
264 foreach ( $histories as $history ) {
265 if ( 'new' === $history->type || 'renew' === $history->type ) {
266 $subscription_id = $history->subscription_id;
267 wp_update_post(
268 array(
269 'ID' => $subscription_id,
270 'post_status' => $post_status,
271 )
272 );
273
274 // If possible change order status to completed if it has a trial subscription.
275 $this->maybe_trigger_auto_complete_trial_order( $order_id, $subscription_id );
276
277 // Increment renewal count for completed renewal orders (wps-pro)
278 if ( 'renew' === $history->type && 'active' === $post_status && function_exists( 'subscrpt_pro_activated' ) && subscrpt_pro_activated() ) {
279 if ( class_exists( '\\SpringDevs\\SubscriptionPro\\Illuminate\\LimitChecker' ) ) {
280 \SpringDevs\SubscriptionPro\Illuminate\LimitChecker::increment_renewal_count( $history->subscription_id );
281 }
282 }
283
284 // Add enhanced split payment activity logging
285 $this->add_split_payment_activity_note( $history->subscription_id, $history->type, $post_status, $order );
286
287 Action::write_comment( $post_status, $history->subscription_id );
288 } else {
289 do_action( 'subscrpt_order_status_changed', $order, $history );
290 }
291 }
292 }
293
294 /**
295 * Delete the subscription.
296 *
297 * @param int $order_id Order ID.
298 *
299 * @return void
300 */
301 public function delete_the_subscription( $order_id ) {
302 global $wpdb;
303 $table_name = $wpdb->prefix . 'subscrpt_order_relation';
304
305 $histories = Helper::get_subscriptions_from_order( $order_id );
306 foreach ( (array) $histories as $history ) {
307 $subscription_order_id = get_post_meta( $history->subscription_id, '_subscrpt_order_id', true );
308 if ( (int) $subscription_order_id === $order_id ) {
309 wp_delete_post( $history->subscription_id, true );
310 }
311 }
312
313 // phpcs:ignore
314 $wpdb->delete( $table_name, array( 'order_id' => $order_id ), array( '%d' ) );
315 }
316
317 /**
318 * Add enhanced split payment activity note with payment progress and access information.
319 *
320 * @param int $subscription_id Subscription ID.
321 * @param string $history_type History type (new, renew, etc.).
322 * @param string $post_status Post status.
323 * @param \WC_Order $order WooCommerce order object.
324 */
325 private function add_split_payment_activity_note( $subscription_id, $history_type, $post_status, $order ) {
326 // Only add enhanced notes for active subscriptions
327 if ( 'active' !== $post_status ) {
328 return;
329 }
330
331 // Check if this is a split payment subscription
332 if ( ! function_exists( 'subscrpt_get_payment_type' ) ) {
333 return;
334 }
335
336 $payment_type = subscrpt_get_payment_type( $subscription_id );
337 if ( 'split_payment' !== $payment_type ) {
338 return;
339 }
340
341 // Get payment progress information
342 $max_payments = function_exists( 'subscrpt_get_max_payments' ) ? subscrpt_get_max_payments( $subscription_id ) : 0;
343 $payments_made = function_exists( 'subscrpt_count_payments_made' ) ? subscrpt_count_payments_made( $subscription_id ) : 0;
344 $remaining_payments = function_exists( 'subscrpt_get_remaining_payments' ) ? subscrpt_get_remaining_payments( $subscription_id ) : 0;
345
346 // Determine payment number for this order
347 $payment_number = $payments_made;
348 $order_total = $order->get_total();
349 $order_currency = $order->get_currency();
350
351 // Create enhanced activity note
352 $comment_content = '';
353 $activity_type = '';
354
355 if ( 'new' === $history_type ) {
356 $comment_content = sprintf(
357 /* translators: %1$d: payment number, %2$d: total payments, %3$s: amount, %4$s: currency */
358 __( 'Split payment %1$d of %2$d received (%3$s %4$s). Initial access granted.', 'subscription' ),
359 $payment_number,
360 $max_payments,
361 $order_total,
362 $order_currency
363 );
364 $activity_type = __( 'Split Payment - Initial', 'subscription' );
365 } elseif ( 'renew' === $history_type ) {
366 $comment_content = sprintf(
367 /* translators: %1$d: payment number, %2$d: total payments, %3$s: amount, %4$s: currency, %5$d: remaining */
368 __( 'Split payment %1$d of %2$d received (%3$s %4$s). %5$d payments remaining.', 'subscription' ),
369 $payment_number,
370 $max_payments,
371 $order_total,
372 $order_currency,
373 $remaining_payments
374 );
375 $activity_type = __( 'Split Payment - Installment', 'subscription' );
376 }
377
378 // Add the enhanced activity note
379 if ( $comment_content ) {
380 $comment_id = wp_insert_comment(
381 array(
382 'comment_author' => 'Subscription for WooCommerce',
383 'comment_content' => $comment_content,
384 'comment_post_ID' => $subscription_id,
385 'comment_type' => 'order_note',
386 )
387 );
388 update_comment_meta( $comment_id, '_subscrpt_activity', $activity_type );
389 update_comment_meta( $comment_id, '_subscrpt_activity_type', 'split_payment' );
390
391 // Add order note with split payment context
392 $order_note = sprintf(
393 /* translators: %1$d: payment number, %2$d: total payments, %3$d: subscription id */
394 __( 'Split payment %1$d of %2$d received for subscription #%3$d', 'subscription' ),
395 $payment_number,
396 $max_payments,
397 $subscription_id
398 );
399 $order->add_order_note( $order_note );
400 }
401
402 // Add payment milestone notes for significant progress
403 $this->add_payment_milestone_notes( $subscription_id, $payments_made, $max_payments );
404 }
405
406 /**
407 * Add payment milestone notes for significant progress (25%, 50%, 75%, 100%).
408 *
409 * @param int $subscription_id Subscription ID.
410 * @param int $payments_made Number of payments made.
411 * @param int $max_payments Maximum number of payments.
412 */
413 private function add_payment_milestone_notes( $subscription_id, $payments_made, $max_payments ) {
414 if ( ! $max_payments || $max_payments <= 0 ) {
415 return;
416 }
417
418 $percentage = round( ( $payments_made / $max_payments ) * 100 );
419 $milestone_key = '_subscrpt_milestone_' . $percentage . '_logged';
420
421 // Check if this milestone has already been logged
422 if ( get_post_meta( $subscription_id, $milestone_key, true ) ) {
423 return;
424 }
425
426 $milestone_note = '';
427 $activity_type = '';
428
429 switch ( $percentage ) {
430 case 25:
431 $milestone_note = sprintf(
432 /* translators: %1$d: payments made, %2$d: total payments */
433 __( 'Payment milestone reached: %1$d of %2$d payments completed (25%%).', 'subscription' ),
434 $payments_made,
435 $max_payments
436 );
437 $activity_type = __( 'Payment Milestone - 25%', 'subscription' );
438 break;
439
440 case 50:
441 $milestone_note = sprintf(
442 /* translators: %1$d: payments made, %2$d: total payments */
443 __( 'Payment milestone reached: %1$d of %2$d payments completed (50%%). Halfway there!', 'subscription' ),
444 $payments_made,
445 $max_payments
446 );
447 $activity_type = __( 'Payment Milestone - 50%', 'subscription' );
448 break;
449
450 case 75:
451 $milestone_note = sprintf(
452 /* translators: %1$d: payments made, %2$d: total payments */
453 __( 'Payment milestone reached: %1$d of %2$d payments completed (75%%). Almost complete!', 'subscription' ),
454 $payments_made,
455 $max_payments
456 );
457 $activity_type = __( 'Payment Milestone - 75%', 'subscription' );
458 break;
459
460 case 100:
461 $milestone_note = sprintf(
462 /* translators: %1$d: payments made, %2$d: total payments */
463 __( 'Payment milestone reached: %1$d of %2$d payments completed (100%%). Split payment plan complete!', 'subscription' ),
464 $payments_made,
465 $max_payments
466 );
467 $activity_type = __( 'Payment Milestone - 100%', 'subscription' );
468 break;
469 }
470
471 // Add milestone note if applicable
472 if ( $milestone_note ) {
473 $comment_id = wp_insert_comment(
474 array(
475 'comment_author' => 'Subscription for WooCommerce',
476 'comment_content' => $milestone_note,
477 'comment_post_ID' => $subscription_id,
478 'comment_type' => 'order_note',
479 )
480 );
481 update_comment_meta( $comment_id, '_subscrpt_activity', $activity_type );
482 update_comment_meta( $comment_id, '_subscrpt_activity_type', 'split_payment' );
483
484 // Mark milestone as logged
485 update_post_meta( $subscription_id, $milestone_key, current_time( 'timestamp' ) );
486 }
487 }
488
489 /**
490 * Maybe complete the order if it has a trial subscription and is still in processing status.
491 *
492 * @param int $order_id Order ID.
493 * @param int $subscription_id Subscription ID.
494 */
495 public function maybe_trigger_auto_complete_trial_order( $order_id, $subscription_id ) {
496 $order = wc_get_order( $order_id );
497 $order_items = $order->get_items();
498
499 // Only attempt to complete if order is still in processing status.
500 if ( 'processing' !== $order->get_status() ) {
501 return;
502 }
503
504 $is_subs_trial_order = false;
505 foreach ( $order_items as $order_item ) {
506 $subscrpt_meta = $order_item->get_meta( '_subscrpt_meta', true );
507
508 if ( ! empty( $subscrpt_meta ) && isset( $subscrpt_meta['trial'] ) ) {
509 $is_subs_trial_order = true;
510 }
511 }
512
513 if ( $is_subs_trial_order ) {
514 as_enqueue_async_action( 'subscrpt_queue_trial_order_autocomplete', [ 'order_id' => $order_id ] );
515
516 $log_message = "Queued auto complete task for free trial order [ID: {$order_id}]";
517 subscrpt_write_log( $log_message );
518 subscrpt_write_debug_log( $log_message );
519 }
520 }
521
522 /**
523 * Autocomplete subscription trial order.
524 *
525 * @param int $order_id Order ID.
526 */
527 public function auto_complete_subscription_trial_order( $order_id ) {
528 $order = wc_get_order( $order_id );
529 if ( ! $order ) {
530 return;
531 }
532
533 sleep( 3 ); // Adding a small delay to ensure order status is updated before we check it.
534
535 $order->update_status( 'completed', __( 'Subscription order with trial.', 'subscription' ) );
536 }
537 }
538