PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 3.9.0
Tutor LMS – eLearning and online course solution v3.9.0
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 9 months ago BillingController.php 1 year ago CartController.php 1 year ago CheckoutController.php 8 months ago CouponController.php 11 months ago Ecommerce.php 1 year ago EmailController.php 11 months ago HooksHandler.php 9 months ago OptionKeys.php 1 year ago OrderActivitiesController.php 1 year ago OrderController.php 9 months ago PaymentHandler.php 9 months ago Settings.php 9 months ago Tax.php 9 months ago currency.php 1 year ago
HooksHandler.php
561 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\Course;
14 use Tutor\Models\OrderActivitiesModel;
15 use TUTOR\Earnings;
16 use Tutor\Helpers\QueryHelper;
17 use TUTOR\Input;
18 use Tutor\Models\CartModel;
19 use Tutor\Models\OrderMetaModel;
20 use Tutor\Models\OrderModel;
21 use TutorPro\CourseBundle\CustomPosts\CourseBundle;
22 use TutorPro\CourseBundle\Models\BundleModel;
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 }
96
97 /**
98 * Clear order menu badge count
99 *
100 * @since 3.0.0
101 *
102 * @return void
103 */
104 public function clear_order_badge_count() {
105 delete_transient( OrderModel::TRANSIENT_ORDER_BADGE_COUNT );
106 }
107
108 /**
109 * Store order activity before bulk action.
110 *
111 * @since 3.0.0
112 *
113 * @param string $bulk_action The bulk action being performed.
114 * @param array $bulk_ids The IDs of the orders being acted upon.
115 *
116 * @return void
117 */
118 public function after_order_bulk_action( $bulk_action, $bulk_ids ) {
119 $order_status = $this->order_model->get_order_status_by_payment_status( $bulk_action );
120
121 $cancel_reason = Input::post( 'cancel_reason', '' );
122 foreach ( $bulk_ids as $order_id ) {
123 try {
124 $this->manage_earnings_and_enrollments( $order_status, $order_id );
125 $data = (object) array(
126 'order_id' => $order_id,
127 'meta_key' => $this->order_activities_model::META_KEY_HISTORY,
128 'meta_value' => "Order mark as {$bulk_action} {$cancel_reason}",
129 );
130 $this->order_activities_model->add_order_meta( $data );
131 } catch ( \Throwable $th ) {
132 // Log message with line & file.
133 error_log( $th->getMessage() . ' in ' . $th->getFile() . ' at line ' . $th->getLine() );
134 }
135 }
136 }
137
138 /**
139 * Alter course sell by value
140 *
141 * @since 3.0.0
142 *
143 * @param mixed $sell_by Default sell by.
144 *
145 * @return mixed
146 */
147 public function alter_course_sell_by( $sell_by ) {
148 if ( tutor_utils()->is_monetize_by_tutor() ) {
149 $sell_by = Ecommerce::MONETIZE_BY;
150 }
151
152 return $sell_by;
153 }
154
155 /**
156 * Alter course price to show price on the course
157 * entry box
158 *
159 * @since 3.0.0
160 *
161 * @param mixed $price Course price.
162 * @param int $course_id Course id.
163 *
164 * @return mixed
165 */
166 public function alter_course_price( $price, $course_id ) {
167 $price_type = tutor_utils()->price_type( $course_id );
168 if ( tutor_utils()->is_monetize_by_tutor() && Course::PRICE_TYPE_PAID === $price_type ) {
169 $price = tutor_get_course_formatted_price_html( $course_id, false );
170 }
171
172 return $price;
173 }
174
175 /**
176 * Handle payment updated webhook
177 *
178 * @since 3.0.0
179 *
180 * @param object $res Response data.
181 * {order_id, transaction_id, payment_status, payment_method, redirectUrl}.
182 *
183 * @return void
184 */
185 public function handle_payment_updated_webhook( $res ) {
186 $order_id = $res->id;
187 $new_payment_status = $res->payment_status;
188 $transaction_id = $res->transaction_id;
189
190 $order_details = $this->order_model->get_order_by_id( $order_id );
191 if ( $order_details ) {
192 $prev_payment_status = $order_details->payment_status;
193
194 $order_data = array(
195 'order_status' => $order_details->order_status,
196 'payment_status' => $new_payment_status,
197 'payment_payloads' => $res->payment_payload,
198 'transaction_id' => $transaction_id,
199 'earnings' => $res->earnings,
200 'fees' => $res->fees,
201 'updated_at_gmt' => current_time( 'mysql', true ),
202 );
203
204 switch ( $new_payment_status ) {
205 case $this->order_model::PAYMENT_PAID:
206 $order_data['order_status'] = $this->order_model::ORDER_COMPLETED;
207 break;
208 case $this->order_model::PAYMENT_FAILED:
209 case $this->order_model::PAYMENT_REFUNDED:
210 $order_data['order_status'] = $this->order_model::ORDER_CANCELLED;
211 break;
212 }
213
214 $update = $this->order_model->update_order( $order_id, $order_data );
215 if ( $update ) {
216 // Provide hook after update order.
217 do_action( 'tutor_order_payment_status_changed', $order_id, $prev_payment_status, $new_payment_status );
218 }
219 }
220
221 }
222
223 /**
224 * Update enrollment & earnings based on payment status
225 *
226 * @since 3.0.0
227 *
228 * @param int $order_id Order id.
229 * @param string $prev_payment_status previous payment status.
230 * @param string $new_payment_status new payment status.
231 *
232 * @return void
233 */
234 public function handle_payment_status_changed( $order_id, $prev_payment_status, $new_payment_status ) {
235
236 $order_status = $this->order_model->get_order_status_by_payment_status( $new_payment_status );
237
238 $cancel_reason = Input::post( 'cancel_reason' );
239 $remove_enrollment = Input::post( 'is_remove_enrolment', false, Input::TYPE_BOOL );
240
241 // Store activity.
242 $data = (object) array(
243 'order_id' => $order_id,
244 'meta_key' => $this->order_activities_model::META_KEY_HISTORY,
245 'meta_value' => 'Order marked as ' . $new_payment_status,
246 );
247
248 if ( $cancel_reason ) {
249 $meta_value = array(
250 'message' => 'Order marked as ' . $new_payment_status,
251 'cancel_reason' => $cancel_reason,
252 );
253 $data->meta_value = json_encode( $meta_value );
254 }
255
256 $this->order_activities_model->add_order_meta( $data );
257
258 if ( $remove_enrollment ) {
259 $order_status = OrderModel::ORDER_CANCELLED;
260 }
261
262 $this->manage_earnings_and_enrollments( $order_status, $order_id );
263
264 // Store coupon usage.
265 $this->coupon_ctrl->store_coupon_usage( $order_id );
266 }
267
268 /**
269 * Handle new order placement
270 *
271 * Clear cart items, managing enrollment & earnings
272 *
273 * @since 3.0.0
274 *
275 * @param int $order_id Order id.
276 *
277 * @return void
278 */
279 public function handle_order_placement_success( int $order_id ) {
280 $order_data = $this->order_model->get_order_by_id( $order_id );
281 if ( $order_data ) {
282 $user_id = $order_data->student->id;
283
284 ( new CartModel() )->clear_user_cart( $user_id );
285
286 // Manage enrollment & earnings.
287 $order = ( new OrderModel() )->get_order_by_id( $order_id );
288 $payment_status = $order->payment_status;
289
290 $order_status = $this->order_model->get_order_status_by_payment_status( $payment_status );
291
292 $this->manage_earnings_and_enrollments( $order_status, $order_id );
293 }
294 }
295
296 /**
297 * Check if order is bundle order
298 *
299 * @since 3.0.0
300 *
301 * @param object $order order object.
302 * @param int $object_id object id.
303 *
304 * @return boolean
305 */
306 private function is_bundle_order( $order, $object_id ) {
307 return tutor_utils()->is_addon_enabled( 'course-bundle' )
308 && in_array( $order->order_type, array( $this->order_model::TYPE_SINGLE_ORDER, $this->order_model::TYPE_SUBSCRIPTION ), true )
309 && 'course-bundle' === get_post_type( $object_id );
310 }
311
312 /**
313 * Manage earnings after order bulk action
314 *
315 * @since 3.0.0
316 *
317 * @param string $order_status Order status.
318 * @param int $order_id Order ID.
319 *
320 * @return void
321 */
322 public function manage_earnings_and_enrollments( string $order_status, int $order_id ) {
323 $earnings = Earnings::get_instance();
324 $order = $this->order_model->get_order_by_id( $order_id );
325 $student_id = $order->student->id;
326
327 $enrollment_status = ( OrderModel::ORDER_COMPLETED === $order_status ? 'completed' : ( OrderModel::ORDER_INCOMPLETE === $order->order_status ? 'pending' : 'cancel' ) );
328
329 foreach ( $order->items as $item ) {
330 $object_id = $item->id; // It could be course/bundle/plan id.
331 $is_gift_item = apply_filters( 'tutor_is_gift_item', false, $item->primary_id );
332 if ( $is_gift_item ) {
333 continue;
334 }
335
336 if ( $this->order_model::TYPE_SINGLE_ORDER !== $order->order_type ) {
337 /**
338 * Do not process enrollment for membership plan.
339 *
340 * @since 3.2.0
341 */
342 $plan_info = apply_filters( 'tutor_get_plan_info', null, $object_id );
343 if ( $plan_info && isset( $plan_info->is_membership_plan ) && $plan_info->is_membership_plan ) {
344 continue;
345 } else {
346 $object_id = apply_filters( 'tutor_subscription_course_by_plan', $item->id, $order );
347 }
348
349 /**
350 * Do not process enrollment for subscription order refund.
351 * It will be handled by subscription controller's handle_order_refund method.
352 *
353 * @since 3.3.0
354 */
355 if ( Input::has( 'is_cancel_subscription' ) ) {
356 continue;
357 }
358 }
359
360 $has_enrollment = tutor_utils()->is_enrolled( $object_id, $student_id, false );
361 if ( $has_enrollment ) {
362 // Update enrollment status based on order status.
363 $update = tutor_utils()->update_enrollments( $enrollment_status, array( $has_enrollment->ID ) );
364 if ( $update ) {
365 if ( $this->is_bundle_order( $order, $object_id ) && $this->order_model->is_single_order( $order ) ) {
366 if ( 'completed' === $enrollment_status ) {
367 BundleModel::enroll_to_bundle_courses( $object_id, $student_id );
368 } else {
369 BundleModel::disenroll_from_bundle_courses( $object_id, $student_id );
370 }
371 }
372
373 /**
374 * For subscription, renewal no need to update order id.
375 */
376 if ( $this->order_model->is_single_order( $order ) ) {
377 update_post_meta( $has_enrollment->ID, '_tutor_enrolled_by_order_id', $order_id );
378
379 /**
380 * Update enrollment expiry date if it is set in a course.
381 */
382 if ( tutor()->course_post_type === get_post_type( $object_id ) ) {
383 $is_set_enrollment_expiry = (int) get_tutor_course_settings( $object_id, 'enrollment_expiry' );
384 $enrollment_expiry_enabled = (bool) get_tutor_option( 'enrollment_expiry_enabled' );
385 if ( $enrollment_expiry_enabled && $is_set_enrollment_expiry ) {
386 global $wpdb;
387 QueryHelper::update(
388 $wpdb->posts,
389 array(
390 'post_date' => current_time( 'mysql' ),
391 'post_date_gmt' => current_time( 'mysql', true ),
392 ),
393 array(
394 'ID' => $has_enrollment->ID,
395 'post_type' => tutor()->enrollment_post_type,
396 )
397 );
398 }
399 }
400
401 if ( OrderModel::ORDER_COMPLETED === $order_status ) {
402 do_action( 'tutor_after_enrolled', $object_id, $student_id, $has_enrollment->ID );
403 }
404 }
405
406 if ( 'completed' === $enrollment_status ) {
407 do_action( 'tutor_order_enrolled', $order, $has_enrollment->ID );
408 }
409 }
410 } else {
411 if ( $order->order_status === $this->order_model::ORDER_COMPLETED ) {
412 // Insert enrollment.
413 add_filter( 'tutor_enroll_data', fn( $enroll_data) => array_merge( $enroll_data, array( 'post_status' => 'completed' ) ) );
414
415 $enrollment_id = tutor_utils()->do_enroll( $object_id, $order_id, $student_id );
416 if ( $enrollment_id ) {
417 if ( $this->is_bundle_order( $order, $object_id ) && $this->order_model->is_single_order( $order ) ) {
418 BundleModel::enroll_to_bundle_courses( $object_id, $student_id );
419 }
420 update_post_meta( $enrollment_id, '_tutor_enrolled_by_order_id', $order_id );
421
422 do_action( 'tutor_order_enrolled', $order, $enrollment_id );
423 } else {
424 // Log error message with student id and course id.
425 error_log( "Error updating enrollment for student {$student_id} and course {$object_id}" );
426 }
427 }
428 }
429 }
430
431 // Update earnings.
432 $earnings->prepare_order_earnings( $order_id );
433 $earnings->remove_before_store_earnings();
434 }
435
436 /**
437 * Update order data for the free checkout
438 *
439 * @since 3.4.0
440 *
441 * @param array $order_data Order data.
442 *
443 * @return array
444 */
445 public function update_order_data( array $order_data ) {
446 if ( empty( $order_data['total_price'] ) && OrderModel::TYPE_SINGLE_ORDER === $order_data['order_type'] ) {
447 $order_data['order_status'] = OrderModel::ORDER_COMPLETED;
448 $order_data['payment_status'] = OrderModel::PAYMENT_PAID;
449 $order_data['payment_method'] = 'free';
450 }
451 return $order_data;
452 }
453
454 /**
455 * Enroll user to the course when free checkout
456 *
457 * @since 3.4.0
458 *
459 * @param array $order_data Order data.
460 *
461 * @return array
462 */
463 public function handle_free_checkout( array $order_data ) {
464 if ( empty( $order_data['total_price'] ) && OrderModel::TYPE_SINGLE_ORDER === $order_data['order_type'] ) {
465 $order_id = $order_data['id'];
466 $user_id = $order_data['user_id'];
467 $items = $order_data['items'];
468 foreach ( $items as $item ) {
469 add_filter( 'tutor_enroll_data', fn( $enroll_data) => array_merge( $enroll_data, array( 'post_status' => 'completed' ) ) );
470
471 $enrolled_id = tutor_utils()->do_enroll( $item['item_id'], $order_data['id'], $user_id );
472 if ( $enrolled_id && tutor_utils()->is_addon_enabled( 'course-bundle' ) && get_post_type( $item['item_id'] ) === CourseBundle::POST_TYPE ) {
473 BundleModel::enroll_to_bundle_courses( $item['item_id'], $user_id );
474 }
475 }
476
477 // Store coupon usage.
478 $this->coupon_ctrl->store_coupon_usage( $order_id );
479 }
480 return $order_data;
481 }
482
483 /**
484 * Redirect user to the course after free checkout when item is 1.
485 * If user checkout multiple items and keep the default behavior.
486 *
487 * @since 3.4.0
488 *
489 * @param string $url Default redirect url.
490 * @param string $status Order placement status.
491 * @param integer $order_id Order id.
492 *
493 * @return string
494 */
495 public function redirect_to_the_course( string $url, string $status, int $order_id ):string {
496 $user_id = get_current_user_id();
497 if ( OrderModel::ORDER_PLACEMENT_SUCCESS === $status ) {
498 $order = $this->order_model->get_order_by_id( $order_id );
499 if ( $order && count( $order->items ) === 1 && empty( $order->total_price ) && OrderModel::TYPE_SINGLE_ORDER === $order->order_type ) {
500
501 // Firing hook to clear cart.
502 do_action( 'tutor_order_placement_success', $order_id );
503
504 // Clear the alert message.
505 delete_transient( CheckoutController::PAY_NOW_ALERT_MSG_TRANSIENT_KEY . $user_id );
506 delete_transient( CheckoutController::PAY_NOW_ERROR_TRANSIENT_KEY . $user_id );
507 $course_id = $order->items[0]->id;
508 $url = get_the_permalink( $course_id );
509 }
510 }
511 return $url;
512 }
513
514 /**
515 * Store billing address for an order when order is placed.
516 *
517 * @since 3.5.0
518 *
519 * @param array $order_data order data.
520 *
521 * @return void
522 */
523 public function store_billing_address_for_order( array $order_data ) {
524 $order_id = $order_data['id'];
525 $user_id = $order_data['user_id'];
526 $billing_info = ( new BillingController( false ) )->get_billing_info( $user_id );
527
528 /**
529 * JSON_UNESCAPED_UNICODE is used to ensure that the billing info is stored in a readable format.
530 * This is important for languages that use non-ASCII characters like ñ, á, é, í, ó, ú, ü, etc.
531 *
532 * @since 3.7.1
533 */
534 $meta_value = '{}';
535 if ( $billing_info ) {
536 $meta_value = wp_json_encode( $billing_info, JSON_UNESCAPED_UNICODE );
537 } else {
538 /**
539 * Store user data as billing info
540 * If user has no billing info during order like manual enrollment from CSV.
541 */
542 $user_data = get_userdata( $user_id );
543 $meta_value = wp_json_encode(
544 array(
545 'billing_first_name' => $user_data->first_name,
546 'billing_last_name' => $user_data->last_name,
547 'billing_email' => $user_data->user_email,
548 ),
549 JSON_UNESCAPED_UNICODE
550 );
551 }
552
553 OrderMetaModel::add_meta(
554 $order_id,
555 OrderModel::META_KEY_BILLING_ADDRESS,
556 $meta_value
557 );
558 }
559 }
560
561