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