PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / 2.0.0
Subscriptions for WooCommerce with Stripe Recurring Payments v2.0.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 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 2.0.0, at includes/Illuminate/Order.php

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