PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 3.9.7
Tutor LMS – eLearning and online course solution v3.9.7
4.0.0 3.9.15 3.9.14 3.9.13 3.9.12 3.9.11 trunk 1.0.0 1.0.0-alpha 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.2.0 1.2.1 1.2.11 1.2.12 1.2.13 1.2.20 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 1.7.6 1.7.7 1.7.8 1.7.9 1.8.0 1.8.1 1.8.10 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7 1.8.8 1.8.9 1.9.0 1.9.1 1.9.10 1.9.11 1.9.12 1.9.13 1.9.14 1.9.15 1.9.16 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.1 2.0.10 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1.0 2.1.1 2.1.10 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.3.0 2.4.0 2.5.0 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 3.0.0 3.0.1 3.0.2 3.1.0 3.2.0 3.2.1 3.2.2 3.2.3 3.3.0 3.3.1 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.7.0 3.7.1 3.7.2 3.7.3 3.7.4 3.8.0 3.8.1 3.8.2 3.8.3 3.9.0 3.9.1 3.9.10 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9
tutor / ecommerce / HooksHandler.php
tutor / ecommerce Last commit date
Cart 10 months ago PaymentGateways 8 months ago AdminMenu.php 10 months ago BillingController.php 1 year ago CartController.php 1 year ago CheckoutController.php 7 months ago CouponController.php 5 months ago Ecommerce.php 1 year ago EmailController.php 11 months ago HooksHandler.php 4 months ago OptionKeys.php 1 year ago OrderActivitiesController.php 1 year ago OrderController.php 6 months ago PaymentHandler.php 9 months ago Settings.php 9 months ago Tax.php 9 months ago currency.php 5 months ago
HooksHandler.php
569 lines
1 <?php
2 /**
3 * Handle ecommerce hooks
4 *
5 * @package Tutor\Ecommerce
6 * @author Themeum <support@themeum.com>
7 * @link https://themeum.com
8 * @since 3.0.0
9 */
10
11 namespace Tutor\Ecommerce;
12
13 use TUTOR\Input;
14 use TUTOR\Course;
15 use TUTOR\Earnings;
16 use Tutor\Models\CartModel;
17 use Tutor\Models\OrderModel;
18 use Tutor\Helpers\QueryHelper;
19 use Tutor\Models\OrderMetaModel;
20 use Tutor\Models\OrderActivitiesModel;
21 use TutorPro\CourseBundle\Models\BundleModel;
22 use TutorPro\CourseBundle\CustomPosts\CourseBundle;
23
24 /**
25 * Handle custom hooks
26 */
27 class HooksHandler {
28
29 /**
30 * OrderModel
31 *
32 * @since 3.0.0
33 *
34 * @var OrderModel
35 */
36 private $order_model;
37
38 /**
39 * OrderActivitiesModel
40 *
41 * @since 3.0.0
42 *
43 * @var OrderActivitiesModel
44 */
45 private $order_activities_model;
46
47 /**
48 * Coupon controller instance
49 *
50 * @since 3.5.0
51 *
52 * @var CouponController
53 */
54 private $coupon_ctrl;
55
56 /**
57 * Register hooks & resolve props
58 *
59 * @since 3.0.0
60 */
61 public function __construct() {
62 $this->order_activities_model = new OrderActivitiesModel();
63 $this->order_model = new OrderModel();
64 $this->coupon_ctrl = new CouponController( false );
65
66 // Register hooks.
67 add_filter( 'tutor_course_sell_by', array( $this, 'alter_course_sell_by' ) );
68 add_filter( 'get_tutor_course_price', array( $this, 'alter_course_price' ), 10, 2 );
69
70 // Order hooks.
71 add_action( 'tutor_order_payment_updated', array( $this, 'handle_payment_updated_webhook' ) );
72
73 add_action( 'tutor_order_payment_status_changed', array( $this, 'handle_payment_status_changed' ), 10, 3 );
74
75 add_action( 'tutor_order_placement_success', array( $this, 'handle_order_placement_success' ) );
76
77 /**
78 * Clear order menu badge count
79 *
80 * @since 3.0.0
81 */
82 add_action( 'tutor_order_placed', array( $this, 'clear_order_badge_count' ) );
83 add_action( 'tutor_order_payment_status_changed', array( $this, 'clear_order_badge_count' ) );
84 add_action( 'tutor_before_order_bulk_action', array( $this, 'clear_order_badge_count' ) );
85 add_filter( 'tutor_before_order_create', array( $this, 'update_order_data' ) );
86 add_action( 'tutor_order_placed', array( $this, 'handle_free_checkout' ) );
87 add_filter( 'tutor_redirect_url_after_checkout', array( $this, 'redirect_to_the_course' ), 10, 3 );
88
89 /**
90 * Store customer billing information for each order.
91 *
92 * @since 3.5.0
93 */
94 add_action( 'tutor_order_placed', array( $this, 'store_billing_address_for_order' ) );
95 add_action( 'tutor_order_updated', array( $this, 'store_billing_address_for_order' ) );
96 }
97
98 /**
99 * Clear order menu badge count
100 *
101 * @since 3.0.0
102 *
103 * @return void
104 */
105 public function clear_order_badge_count() {
106 delete_transient( OrderModel::TRANSIENT_ORDER_BADGE_COUNT );
107 }
108
109 /**
110 * Store order activity before bulk action.
111 *
112 * @since 3.0.0
113 *
114 * @param string $bulk_action The bulk action being performed.
115 * @param array $bulk_ids The IDs of the orders being acted upon.
116 *
117 * @return void
118 */
119 public function after_order_bulk_action( $bulk_action, $bulk_ids ) {
120 $order_status = $this->order_model->get_order_status_by_payment_status( $bulk_action );
121
122 $cancel_reason = Input::post( 'cancel_reason', '' );
123 foreach ( $bulk_ids as $order_id ) {
124 try {
125 $this->manage_earnings_and_enrollments( $order_status, $order_id );
126 $data = (object) array(
127 'order_id' => $order_id,
128 'meta_key' => $this->order_activities_model::META_KEY_HISTORY,
129 'meta_value' => "Order mark as {$bulk_action} {$cancel_reason}",
130 );
131 $this->order_activities_model->add_order_meta( $data );
132 } catch ( \Throwable $th ) {
133 // Log message with line & file.
134 error_log( $th->getMessage() . ' in ' . $th->getFile() . ' at line ' . $th->getLine() );
135 }
136 }
137 }
138
139 /**
140 * Alter course sell by value
141 *
142 * @since 3.0.0
143 *
144 * @param mixed $sell_by Default sell by.
145 *
146 * @return mixed
147 */
148 public function alter_course_sell_by( $sell_by ) {
149 if ( tutor_utils()->is_monetize_by_tutor() ) {
150 $sell_by = Ecommerce::MONETIZE_BY;
151 }
152
153 return $sell_by;
154 }
155
156 /**
157 * Alter course price to show price on the course
158 * entry box
159 *
160 * @since 3.0.0
161 *
162 * @param mixed $price Course price.
163 * @param int $course_id Course id.
164 *
165 * @return mixed
166 */
167 public function alter_course_price( $price, $course_id ) {
168 $price_type = tutor_utils()->price_type( $course_id );
169 if ( tutor_utils()->is_monetize_by_tutor() && Course::PRICE_TYPE_PAID === $price_type ) {
170 $price = tutor_get_course_formatted_price_html( $course_id, false );
171 }
172
173 return $price;
174 }
175
176 /**
177 * Handle payment updated webhook
178 *
179 * @since 3.0.0
180 *
181 * @param object $res Response data.
182 * {order_id, transaction_id, payment_status, payment_method, redirectUrl}.
183 *
184 * @return void
185 */
186 public function handle_payment_updated_webhook( $res ) {
187 $order_id = $res->id;
188 $new_payment_status = $res->payment_status;
189 $transaction_id = $res->transaction_id;
190
191 $order_details = $this->order_model->get_order_by_id( $order_id );
192
193 /**
194 * Ignore canceled/failed webhooks for old/failed payment session to avoid unenrolling paid users.
195 *
196 * @since 3.9.7
197 */
198 $is_valid_paid_order = OrderModel::ORDER_COMPLETED === $order_details && OrderModel::PAYMENT_PAID === $order_details->payment_status;
199
200 if ( $order_details && ! $is_valid_paid_order ) {
201 $prev_payment_status = $order_details->payment_status;
202
203 $order_data = array(
204 'order_status' => $order_details->order_status,
205 'payment_status' => $new_payment_status,
206 'payment_method' => $res->payment_method,
207 'payment_payloads' => $res->payment_payload,
208 'transaction_id' => $transaction_id,
209 'earnings' => $res->earnings,
210 'fees' => $res->fees,
211 'updated_at_gmt' => current_time( 'mysql', true ),
212 );
213
214 switch ( $new_payment_status ) {
215 case $this->order_model::PAYMENT_PAID:
216 $order_data['order_status'] = $this->order_model::ORDER_COMPLETED;
217 break;
218 case $this->order_model::PAYMENT_FAILED:
219 case $this->order_model::PAYMENT_REFUNDED:
220 $order_data['order_status'] = $this->order_model::ORDER_CANCELLED;
221 break;
222 }
223
224 $update = $this->order_model->update_order( $order_id, $order_data );
225 if ( $update ) {
226 // Provide hook after update order.
227 do_action( 'tutor_order_payment_status_changed', $order_id, $prev_payment_status, $new_payment_status );
228 }
229 }
230 }
231
232 /**
233 * Update enrollment & earnings based on payment status
234 *
235 * @since 3.0.0
236 *
237 * @param int $order_id Order id.
238 * @param string $prev_payment_status previous payment status.
239 * @param string $new_payment_status new payment status.
240 *
241 * @return void
242 */
243 public function handle_payment_status_changed( $order_id, $prev_payment_status, $new_payment_status ) {
244
245 $order_status = $this->order_model->get_order_status_by_payment_status( $new_payment_status );
246
247 $cancel_reason = Input::post( 'cancel_reason' );
248 $remove_enrollment = Input::post( 'is_remove_enrolment', false, Input::TYPE_BOOL );
249
250 // Store activity.
251 $data = (object) array(
252 'order_id' => $order_id,
253 'meta_key' => $this->order_activities_model::META_KEY_HISTORY,
254 'meta_value' => 'Order marked as ' . $new_payment_status,
255 );
256
257 if ( $cancel_reason ) {
258 $meta_value = array(
259 'message' => 'Order marked as ' . $new_payment_status,
260 'cancel_reason' => $cancel_reason,
261 );
262 $data->meta_value = json_encode( $meta_value );
263 }
264
265 $this->order_activities_model->add_order_meta( $data );
266
267 if ( $remove_enrollment ) {
268 $order_status = OrderModel::ORDER_CANCELLED;
269 }
270
271 $this->manage_earnings_and_enrollments( $order_status, $order_id );
272
273 // Store coupon usage.
274 $this->coupon_ctrl->store_coupon_usage( $order_id );
275 }
276
277 /**
278 * Handle new order placement
279 *
280 * Clear cart items, managing enrollment & earnings
281 *
282 * @since 3.0.0
283 *
284 * @param int $order_id Order id.
285 *
286 * @return void
287 */
288 public function handle_order_placement_success( int $order_id ) {
289 $order_data = $this->order_model->get_order_by_id( $order_id );
290 if ( $order_data ) {
291 $user_id = $order_data->student->id;
292
293 ( new CartModel() )->clear_user_cart( $user_id );
294
295 // Manage enrollment & earnings.
296 $order = ( new OrderModel() )->get_order_by_id( $order_id );
297 $payment_status = $order->payment_status;
298
299 $order_status = $this->order_model->get_order_status_by_payment_status( $payment_status );
300
301 $this->manage_earnings_and_enrollments( $order_status, $order_id );
302 }
303 }
304
305 /**
306 * Check if order is bundle order
307 *
308 * @since 3.0.0
309 *
310 * @param object $order order object.
311 * @param int $object_id object id.
312 *
313 * @return boolean
314 */
315 private function is_bundle_order( $order, $object_id ) {
316 return tutor_utils()->is_addon_enabled( 'course-bundle' )
317 && in_array( $order->order_type, array( $this->order_model::TYPE_SINGLE_ORDER, $this->order_model::TYPE_SUBSCRIPTION ), true )
318 && 'course-bundle' === get_post_type( $object_id );
319 }
320
321 /**
322 * Manage earnings after order bulk action
323 *
324 * @since 3.0.0
325 *
326 * @param string $order_status Order status.
327 * @param int $order_id Order ID.
328 *
329 * @return void
330 */
331 public function manage_earnings_and_enrollments( string $order_status, int $order_id ) {
332 $earnings = Earnings::get_instance();
333 $order = $this->order_model->get_order_by_id( $order_id );
334 $student_id = $order->student->id;
335
336 $enrollment_status = ( OrderModel::ORDER_COMPLETED === $order_status ? 'completed' : ( OrderModel::ORDER_INCOMPLETE === $order->order_status ? 'pending' : 'cancel' ) );
337
338 foreach ( $order->items as $item ) {
339 $object_id = $item->id; // It could be course/bundle/plan id.
340 $is_gift_item = apply_filters( 'tutor_is_gift_item', false, $item->primary_id );
341 if ( $is_gift_item ) {
342 continue;
343 }
344
345 if ( $this->order_model::TYPE_SINGLE_ORDER !== $order->order_type ) {
346 /**
347 * Do not process enrollment for membership plan.
348 *
349 * @since 3.2.0
350 */
351 $plan_info = apply_filters( 'tutor_get_plan_info', null, $object_id );
352 if ( $plan_info && isset( $plan_info->is_membership_plan ) && $plan_info->is_membership_plan ) {
353 continue;
354 } else {
355 $object_id = apply_filters( 'tutor_subscription_course_by_plan', $item->id, $order );
356 }
357
358 /**
359 * Do not process enrollment for subscription order refund.
360 * It will be handled by subscription controller's handle_order_refund method.
361 *
362 * @since 3.3.0
363 */
364 if ( Input::has( 'is_cancel_subscription' ) ) {
365 continue;
366 }
367 }
368
369 $has_enrollment = tutor_utils()->is_enrolled( $object_id, $student_id, false );
370 if ( $has_enrollment ) {
371 // Update enrollment status based on order status.
372 $update = tutor_utils()->update_enrollments( $enrollment_status, array( $has_enrollment->ID ) );
373 if ( $update ) {
374 if ( $this->is_bundle_order( $order, $object_id ) && $this->order_model->is_single_order( $order ) ) {
375 if ( 'completed' === $enrollment_status ) {
376 BundleModel::enroll_to_bundle_courses( $object_id, $student_id );
377 } else {
378 BundleModel::disenroll_from_bundle_courses( $object_id, $student_id );
379 }
380 }
381
382 /**
383 * For subscription, renewal no need to update order id.
384 */
385 if ( $this->order_model->is_single_order( $order ) ) {
386 update_post_meta( $has_enrollment->ID, '_tutor_enrolled_by_order_id', $order_id );
387
388 /**
389 * Update enrollment expiry date if it is set in a course.
390 */
391 if ( tutor()->course_post_type === get_post_type( $object_id ) ) {
392 $is_set_enrollment_expiry = (int) get_tutor_course_settings( $object_id, 'enrollment_expiry' );
393 $enrollment_expiry_enabled = (bool) get_tutor_option( 'enrollment_expiry_enabled' );
394 if ( $enrollment_expiry_enabled && $is_set_enrollment_expiry ) {
395 global $wpdb;
396 QueryHelper::update(
397 $wpdb->posts,
398 array(
399 'post_date' => current_time( 'mysql' ),
400 'post_date_gmt' => current_time( 'mysql', true ),
401 ),
402 array(
403 'ID' => $has_enrollment->ID,
404 'post_type' => tutor()->enrollment_post_type,
405 )
406 );
407 }
408 }
409
410 if ( OrderModel::ORDER_COMPLETED === $order_status ) {
411 do_action( 'tutor_after_enrolled', $object_id, $student_id, $has_enrollment->ID );
412 }
413 }
414
415 if ( 'completed' === $enrollment_status ) {
416 do_action( 'tutor_order_enrolled', $order, $has_enrollment->ID );
417 }
418 }
419 } else {
420 if ( $order->order_status === $this->order_model::ORDER_COMPLETED ) {
421 // Insert enrollment.
422 add_filter( 'tutor_enroll_data', fn( $enroll_data) => array_merge( $enroll_data, array( 'post_status' => 'completed' ) ) );
423
424 $enrollment_id = tutor_utils()->do_enroll( $object_id, $order_id, $student_id );
425 if ( $enrollment_id ) {
426 if ( $this->is_bundle_order( $order, $object_id ) && $this->order_model->is_single_order( $order ) ) {
427 BundleModel::enroll_to_bundle_courses( $object_id, $student_id );
428 }
429 update_post_meta( $enrollment_id, '_tutor_enrolled_by_order_id', $order_id );
430
431 do_action( 'tutor_order_enrolled', $order, $enrollment_id );
432 } else {
433 // Log error message with student id and course id.
434 error_log( "Error updating enrollment for student {$student_id} and course {$object_id}" );
435 }
436 }
437 }
438 }
439
440 // Update earnings.
441 $earnings->prepare_order_earnings( $order_id );
442 $earnings->remove_before_store_earnings();
443 }
444
445 /**
446 * Update order data for the free checkout
447 *
448 * @since 3.4.0
449 *
450 * @param array $order_data Order data.
451 *
452 * @return array
453 */
454 public function update_order_data( array $order_data ) {
455 if ( empty( $order_data['total_price'] ) && OrderModel::TYPE_SINGLE_ORDER === $order_data['order_type'] ) {
456 $order_data['order_status'] = OrderModel::ORDER_COMPLETED;
457 $order_data['payment_status'] = OrderModel::PAYMENT_PAID;
458 $order_data['payment_method'] = 'free';
459 }
460 return $order_data;
461 }
462
463 /**
464 * Enroll user to the course when free checkout
465 *
466 * @since 3.4.0
467 *
468 * @param array $order_data Order data.
469 *
470 * @return array
471 */
472 public function handle_free_checkout( array $order_data ) {
473 if ( empty( $order_data['total_price'] ) && OrderModel::TYPE_SINGLE_ORDER === $order_data['order_type'] ) {
474 $order_id = $order_data['id'];
475 $user_id = $order_data['user_id'];
476 $items = $order_data['items'];
477 foreach ( $items as $item ) {
478 add_filter( 'tutor_enroll_data', fn( $enroll_data ) => array_merge( $enroll_data, array( 'post_status' => 'completed' ) ) );
479
480 $enrolled_id = tutor_utils()->do_enroll( $item['item_id'], $order_data['id'], $user_id );
481 if ( $enrolled_id && tutor_utils()->is_addon_enabled( 'course-bundle' ) && get_post_type( $item['item_id'] ) === CourseBundle::POST_TYPE ) {
482 BundleModel::enroll_to_bundle_courses( $item['item_id'], $user_id );
483 }
484 }
485
486 // Store coupon usage.
487 $this->coupon_ctrl->store_coupon_usage( $order_id );
488 }
489 return $order_data;
490 }
491
492 /**
493 * Redirect user to the course after free checkout when item is 1.
494 * If user checkout multiple items and keep the default behavior.
495 *
496 * @since 3.4.0
497 *
498 * @param string $url Default redirect url.
499 * @param string $status Order placement status.
500 * @param integer $order_id Order id.
501 *
502 * @return string
503 */
504 public function redirect_to_the_course( string $url, string $status, int $order_id ): string {
505 $user_id = get_current_user_id();
506 if ( OrderModel::ORDER_PLACEMENT_SUCCESS === $status ) {
507 $order = $this->order_model->get_order_by_id( $order_id );
508 if ( $order && count( $order->items ) === 1 && empty( $order->total_price ) && OrderModel::TYPE_SINGLE_ORDER === $order->order_type ) {
509
510 // Firing hook to clear cart.
511 do_action( 'tutor_order_placement_success', $order_id );
512
513 // Clear the alert message.
514 delete_transient( CheckoutController::PAY_NOW_ALERT_MSG_TRANSIENT_KEY . $user_id );
515 delete_transient( CheckoutController::PAY_NOW_ERROR_TRANSIENT_KEY . $user_id );
516 $course_id = $order->items[0]->id;
517 $url = get_the_permalink( $course_id );
518 }
519 }
520 return $url;
521 }
522
523 /**
524 * Store billing address for an order when order is placed.
525 *
526 * @since 3.5.0
527 *
528 * @param array $order_data order data.
529 *
530 * @return void
531 */
532 public function store_billing_address_for_order( array $order_data ) {
533 $order_id = $order_data['id'];
534 $user_id = $order_data['user_id'];
535 $billing_info = ( new BillingController( false ) )->get_billing_info( $user_id );
536
537 /**
538 * JSON_UNESCAPED_UNICODE is used to ensure that the billing info is stored in a readable format.
539 * This is important for languages that use non-ASCII characters like ñ, á, é, í, ó, ú, ü, etc.
540 *
541 * @since 3.7.1
542 */
543 $meta_value = '{}';
544 if ( $billing_info ) {
545 $meta_value = wp_json_encode( $billing_info, JSON_UNESCAPED_UNICODE );
546 } else {
547 /**
548 * Store user data as billing info
549 * If user has no billing info during order like manual enrollment from CSV.
550 */
551 $user_data = get_userdata( $user_id );
552 $meta_value = wp_json_encode(
553 array(
554 'billing_first_name' => $user_data->first_name,
555 'billing_last_name' => $user_data->last_name,
556 'billing_email' => $user_data->user_email,
557 ),
558 JSON_UNESCAPED_UNICODE
559 );
560 }
561
562 OrderMetaModel::add_meta(
563 $order_id,
564 OrderModel::META_KEY_BILLING_ADDRESS,
565 $meta_value
566 );
567 }
568 }
569