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
subscription / includes / Illuminate / Order.php

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

486 lines 16.7 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 // 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 );
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
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
115 update_post_meta( $subscription_id, '_subscrpt_next_date', $next_date );
116 }
117
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 /**
180 * Add custom column on order item.
181 *
182 * @return void
183 */
184 public function register_custom_column() {
185 ?>
186 <th class="item_recurring sortable" data-sort="float"><?php esc_html_e( 'Recurring', 'subscription' ); ?></th>
187 <?php
188 }
189
190 /**
191 * Display data for custom column.
192 *
193 * @param \WC_Product $product Product Object.
194 * @param \WC_Order_Item $item Order Item.
195 *
196 * @return void
197 */
198 public function add_column_value( $product, $item ) {
199 if ( ! method_exists( $item, 'get_id' ) || ! method_exists( $item, 'get_subtotal' ) ) {
200 return;
201 }
202
203 $subtotal = '-';
204 $subscription_id = Helper::get_subscription_from_order_item_id( $item->get_id() );
205
206 if ( ! $subscription_id ) {
207 echo "<td class='item_recurring' width='15%'>-</td>";
208 return;
209 }
210 $subscription_id = $subscription_id->subscription_id;
211
212 // Strikes the original amount when a discount carries into renewals.
213 $subtotal = Helper::get_subscription_recurring_price_html( $subscription_id, $item );
214 ?>
215 <td class="item_recurring" width="15%">
216 <div class="view">
217 <?php echo wp_kses_post( $subtotal ); ?>
218 </div>
219 </td>
220 <?php
221 }
222
223 public function add_order_item_data( $item_id, $item, $product ) {
224 if ( ! $product ) {
225 return;
226 }
227
228 $item_meta = wc_get_order_item_meta( $item_id, '_subscrpt_meta', true );
229
230 if ( ! $item_meta || ! is_array( $item_meta ) ) {
231 return false;
232 }
233
234 $trial = $item_meta['trial'];
235 $has_trial = isset( $item_meta['trial'] ) && strlen( $item_meta['trial'] ) > 2;
236
237 if ( $has_trial ) {
238 echo '<br/><small> + Got ' . esc_html( $trial ) . ' free trial!</small>';
239 }
240 }
241
242 /**
243 * Take some actions based on order status changed.
244 *
245 * @param int $order_id Order Id.
246 */
247 public function order_status_changed( $order_id ) {
248 $order = wc_get_order( $order_id );
249 $post_status = 'active';
250
251 switch ( $order->get_status() ) {
252 case 'on-hold':
253 case 'pending':
254 $post_status = 'pending';
255 break;
256
257 case 'refunded':
258 case 'failed':
259 case 'cancelled':
260 $post_status = 'cancelled';
261 break;
262
263 default:
264 $post_status = 'active';
265 break;
266 }
267 $post_status = apply_filters( 'subscript_order_status_to_post_status', $post_status, $order );
268
269 global $wpdb;
270 $table_name = $wpdb->prefix . 'subscrpt_order_relation';
271 // @phpcs:ignore
272 $histories = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE order_id=%d', array( $table_name, $order_id ) ) );
273
274 foreach ( $histories as $history ) {
275 if ( 'new' === $history->type || 'renew' === $history->type ) {
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
301 wp_update_post(
302 array(
303 'ID' => $subscription_id,
304 'post_status' => $target_status,
305 )
306 );
307
308 // If possible change order status to completed if it has a trial subscription.
309 $this->maybe_trigger_auto_complete_trial_order( $order_id, $subscription_id );
310
311 // Increment renewal count for completed renewal orders (wps-pro)
312 if ( 'renew' === $history->type && 'active' === $post_status && function_exists( 'subscrpt_pro_activated' ) && subscrpt_pro_activated() ) {
313 if ( class_exists( '\\SpringDevs\\SubscriptionPro\\Illuminate\\LimitChecker' ) ) {
314 \SpringDevs\SubscriptionPro\Illuminate\LimitChecker::increment_renewal_count( $history->subscription_id );
315 }
316 }
317
318 // Add enhanced split payment activity logging
319 $this->add_split_payment_activity_note( $history->subscription_id, $history->type, $post_status, $order );
320
321 Action::write_comment( $target_status, $history->subscription_id );
322 } else {
323 do_action( 'subscrpt_order_status_changed', $order, $history );
324 }
325 }
326 }
327
328 /**
329 * Delete the subscription.
330 *
331 * @param int $order_id Order ID.
332 *
333 * @return void
334 */
335 public function delete_the_subscription( $order_id ) {
336 global $wpdb;
337 $table_name = $wpdb->prefix . 'subscrpt_order_relation';
338
339 $histories = Helper::get_subscriptions_from_order( $order_id );
340 foreach ( (array) $histories as $history ) {
341 $subscription_order_id = get_post_meta( $history->subscription_id, '_subscrpt_order_id', true );
342 if ( (int) $subscription_order_id === $order_id ) {
343 wp_delete_post( $history->subscription_id, true );
344 }
345 }
346
347 // phpcs:ignore
348 $wpdb->delete( $table_name, array( 'order_id' => $order_id ), array( '%d' ) );
349 }
350
351 /**
352 * Add enhanced split payment activity note with payment progress and access information.
353 *
354 * @param int $subscription_id Subscription ID.
355 * @param string $history_type History type (new, renew, etc.).
356 * @param string $post_status Post status.
357 * @param \WC_Order $order WooCommerce order object.
358 */
359 private function add_split_payment_activity_note( $subscription_id, $history_type, $post_status, $order ) {
360 // Only add enhanced notes for active subscriptions
361 if ( 'active' !== $post_status ) {
362 return;
363 }
364
365 // Check if this is a split payment subscription
366 if ( ! function_exists( 'subscrpt_get_payment_type' ) ) {
367 return;
368 }
369
370 $payment_type = subscrpt_get_payment_type( $subscription_id );
371 if ( 'split_payment' !== $payment_type ) {
372 return;
373 }
374
375 // Get payment progress information
376 $max_payments = function_exists( 'subscrpt_get_max_payments' ) ? subscrpt_get_max_payments( $subscription_id ) : 0;
377 $payments_made = function_exists( 'subscrpt_count_payments_made' ) ? subscrpt_count_payments_made( $subscription_id ) : 0;
378 $remaining_payments = function_exists( 'subscrpt_get_remaining_payments' ) ? subscrpt_get_remaining_payments( $subscription_id ) : 0;
379
380 // Determine payment number for this order
381 $payment_number = $payments_made;
382 $order_total = $order->get_total();
383 $order_currency = $order->get_currency();
384
385 // Create enhanced activity note
386 $comment_content = '';
387 $activity_type = '';
388
389 if ( 'new' === $history_type ) {
390 $comment_content = sprintf(
391 /* translators: %1$d: payment number, %2$d: total payments, %3$s: amount, %4$s: currency */
392 __( 'Split payment %1$d of %2$d received (%3$s %4$s). Initial access granted.', 'subscription' ),
393 $payment_number,
394 $max_payments,
395 $order_total,
396 $order_currency
397 );
398 $activity_type = __( 'Split Payment - Initial', 'subscription' );
399 } elseif ( 'renew' === $history_type ) {
400 $comment_content = sprintf(
401 /* translators: %1$d: payment number, %2$d: total payments, %3$s: amount, %4$s: currency, %5$d: remaining */
402 __( 'Split payment %1$d of %2$d received (%3$s %4$s). %5$d payments remaining.', 'subscription' ),
403 $payment_number,
404 $max_payments,
405 $order_total,
406 $order_currency,
407 $remaining_payments
408 );
409 $activity_type = __( 'Split Payment - Installment', 'subscription' );
410 }
411
412 // Add the enhanced activity note
413 if ( $comment_content ) {
414 $comment_id = wp_insert_comment(
415 array(
416 'comment_author' => 'Subscription for WooCommerce',
417 'comment_content' => $comment_content,
418 'comment_post_ID' => $subscription_id,
419 'comment_type' => 'order_note',
420 )
421 );
422 update_comment_meta( $comment_id, '_subscrpt_activity', $activity_type );
423 update_comment_meta( $comment_id, '_subscrpt_activity_type', 'split_payment' );
424
425 // Add order note with split payment context
426 $order_note = sprintf(
427 /* translators: %1$d: payment number, %2$d: total payments, %3$d: subscription id */
428 __( 'Split payment %1$d of %2$d received for subscription #%3$d', 'subscription' ),
429 $payment_number,
430 $max_payments,
431 $subscription_id
432 );
433 $order->add_order_note( $order_note );
434 }
435 }
436
437 /**
438 * Maybe complete the order if it has a trial subscription and is still in processing status.
439 *
440 * @param int $order_id Order ID.
441 * @param int $subscription_id Subscription ID.
442 */
443 public function maybe_trigger_auto_complete_trial_order( $order_id, $subscription_id ) {
444 $order = wc_get_order( $order_id );
445 $order_items = $order->get_items();
446
447 // Only attempt to complete if order is still in processing status.
448 if ( 'processing' !== $order->get_status() ) {
449 return;
450 }
451
452 $is_subs_trial_order = false;
453 foreach ( $order_items as $order_item ) {
454 $subscrpt_meta = $order_item->get_meta( '_subscrpt_meta', true );
455
456 if ( ! empty( $subscrpt_meta ) && isset( $subscrpt_meta['trial'] ) ) {
457 $is_subs_trial_order = true;
458 }
459 }
460
461 if ( $is_subs_trial_order ) {
462 as_enqueue_async_action( 'subscrpt_queue_trial_order_autocomplete', [ 'order_id' => $order_id ] );
463
464 $log_message = "Queued auto complete task for free trial order [ID: {$order_id}]";
465 subscrpt_write_log( $log_message );
466 subscrpt_write_debug_log( $log_message );
467 }
468 }
469
470 /**
471 * Autocomplete subscription trial order.
472 *
473 * @param int $order_id Order ID.
474 */
475 public function auto_complete_subscription_trial_order( $order_id ) {
476 $order = wc_get_order( $order_id );
477 if ( ! $order ) {
478 return;
479 }
480
481 sleep( 3 ); // Adding a small delay to ensure order status is updated before we check it.
482
483 $order->update_status( 'completed', __( 'Subscription order with trial.', 'subscription' ) );
484 }
485 }
486