PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 3.6.1
Tutor LMS – eLearning and online course solution v3.6.1
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 1 year ago PaymentGateways 1 year ago AdminMenu.php 1 year ago BillingController.php 1 year ago CartController.php 1 year ago CheckoutController.php 1 year ago CouponController.php 1 year ago Ecommerce.php 1 year ago EmailController.php 1 year ago HooksHandler.php 1 year ago OptionKeys.php 1 year ago OrderActivitiesController.php 1 year ago OrderController.php 1 year ago PaymentHandler.php 1 year ago Settings.php 1 year ago Tax.php 1 year ago currency.php 1 year ago
HooksHandler.php
549 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 if ( $this->order_model::TYPE_SINGLE_ORDER !== $order->order_type ) {
332 /**
333 * Do not process enrollment for membership plan.
334 *
335 * @since 3.2.0
336 */
337 $plan_info = apply_filters( 'tutor_get_plan_info', null, $object_id );
338 if ( $plan_info && isset( $plan_info->is_membership_plan ) && $plan_info->is_membership_plan ) {
339 continue;
340 } else {
341 $object_id = apply_filters( 'tutor_subscription_course_by_plan', $item->id, $order );
342 }
343
344 /**
345 * Do not process enrollment for subscription order refund.
346 * It will be handled by subscription controller's handle_order_refund method.
347 *
348 * @since 3.3.0
349 */
350 if ( Input::has( 'is_cancel_subscription' ) ) {
351 continue;
352 }
353 }
354
355 $has_enrollment = tutor_utils()->is_enrolled( $object_id, $student_id, false );
356 if ( $has_enrollment ) {
357 // Update enrollment status based on order status.
358 $update = tutor_utils()->update_enrollments( $enrollment_status, array( $has_enrollment->ID ) );
359 if ( $update ) {
360 if ( $this->is_bundle_order( $order, $object_id ) && $this->order_model->is_single_order( $order ) ) {
361 if ( 'completed' === $enrollment_status ) {
362 BundleModel::enroll_to_bundle_courses( $object_id, $student_id );
363 } else {
364 BundleModel::disenroll_from_bundle_courses( $object_id, $student_id );
365 }
366 }
367
368 /**
369 * For subscription, renewal no need to update order id.
370 */
371 if ( $this->order_model->is_single_order( $order ) ) {
372 update_post_meta( $has_enrollment->ID, '_tutor_enrolled_by_order_id', $order_id );
373
374 /**
375 * Update enrollment expiry date if it is set in a course.
376 */
377 if ( tutor()->course_post_type === get_post_type( $object_id ) ) {
378 $is_set_enrollment_expiry = (int) get_tutor_course_settings( $object_id, 'enrollment_expiry' );
379 $enrollment_expiry_enabled = (bool) get_tutor_option( 'enrollment_expiry_enabled' );
380 if ( $enrollment_expiry_enabled && $is_set_enrollment_expiry ) {
381 global $wpdb;
382 QueryHelper::update(
383 $wpdb->posts,
384 array(
385 'post_date' => current_time( 'mysql' ),
386 'post_date_gmt' => current_time( 'mysql', true ),
387 ),
388 array(
389 'ID' => $has_enrollment->ID,
390 'post_type' => tutor()->enrollment_post_type,
391 )
392 );
393 }
394 }
395
396 if ( OrderModel::ORDER_COMPLETED === $order_status ) {
397 do_action( 'tutor_after_enrolled', $object_id, $student_id, $has_enrollment->ID );
398 }
399 }
400
401 if ( 'completed' === $enrollment_status ) {
402 do_action( 'tutor_order_enrolled', $order, $has_enrollment->ID );
403 }
404 }
405 } else {
406 if ( $order->order_status === $this->order_model::ORDER_COMPLETED ) {
407 // Insert enrollment.
408 add_filter( 'tutor_enroll_data', fn( $enroll_data) => array_merge( $enroll_data, array( 'post_status' => 'completed' ) ) );
409
410 $enrollment_id = tutor_utils()->do_enroll( $object_id, $order_id, $student_id );
411 if ( $enrollment_id ) {
412 if ( $this->is_bundle_order( $order, $object_id ) && $this->order_model->is_single_order( $order ) ) {
413 BundleModel::enroll_to_bundle_courses( $object_id, $student_id );
414 }
415 update_post_meta( $enrollment_id, '_tutor_enrolled_by_order_id', $order_id );
416
417 do_action( 'tutor_order_enrolled', $order, $enrollment_id );
418 } else {
419 // Log error message with student id and course id.
420 error_log( "Error updating enrollment for student {$student_id} and course {$object_id}" );
421 }
422 }
423 }
424 }
425
426 // Update earnings.
427 $earnings->prepare_order_earnings( $order_id );
428 $earnings->remove_before_store_earnings();
429 }
430
431 /**
432 * Update order data for the free checkout
433 *
434 * @since 3.4.0
435 *
436 * @param array $order_data Order data.
437 *
438 * @return array
439 */
440 public function update_order_data( array $order_data ) {
441 if ( empty( $order_data['total_price'] ) && OrderModel::TYPE_SINGLE_ORDER === $order_data['order_type'] ) {
442 $order_data['order_status'] = OrderModel::ORDER_COMPLETED;
443 $order_data['payment_status'] = OrderModel::PAYMENT_PAID;
444 $order_data['payment_method'] = 'free';
445 }
446 return $order_data;
447 }
448
449 /**
450 * Enroll user to the course when free checkout
451 *
452 * @since 3.4.0
453 *
454 * @param array $order_data Order data.
455 *
456 * @return array
457 */
458 public function handle_free_checkout( array $order_data ) {
459 if ( empty( $order_data['total_price'] ) && OrderModel::TYPE_SINGLE_ORDER === $order_data['order_type'] ) {
460 $order_id = $order_data['id'];
461 $user_id = $order_data['user_id'];
462 $items = $order_data['items'];
463 foreach ( $items as $item ) {
464 add_filter( 'tutor_enroll_data', fn( $enroll_data) => array_merge( $enroll_data, array( 'post_status' => 'completed' ) ) );
465
466 $enrolled_id = tutor_utils()->do_enroll( $item['item_id'], $order_data['id'], $user_id );
467 if ( $enrolled_id && tutor_utils()->is_addon_enabled( 'course-bundle' ) && get_post_type( $item['item_id'] ) === CourseBundle::POST_TYPE ) {
468 BundleModel::enroll_to_bundle_courses( $item['item_id'], $user_id );
469 }
470 }
471
472 // Store coupon usage.
473 $this->coupon_ctrl->store_coupon_usage( $order_id );
474 }
475 return $order_data;
476 }
477
478 /**
479 * Redirect user to the course after free checkout when item is 1.
480 * If user checkout multiple items and keep the default behavior.
481 *
482 * @since 3.4.0
483 *
484 * @param string $url Default redirect url.
485 * @param string $status Order placement status.
486 * @param integer $order_id Order id.
487 *
488 * @return string
489 */
490 public function redirect_to_the_course( string $url, string $status, int $order_id ):string {
491 $user_id = get_current_user_id();
492 if ( OrderModel::ORDER_PLACEMENT_SUCCESS === $status ) {
493 $order = $this->order_model->get_order_by_id( $order_id );
494 if ( $order && count( $order->items ) === 1 && empty( $order->total_price ) && OrderModel::TYPE_SINGLE_ORDER === $order->order_type ) {
495
496 // Firing hook to clear cart.
497 do_action( 'tutor_order_placement_success', $order_id );
498
499 // Clear the alert message.
500 delete_transient( CheckoutController::PAY_NOW_ALERT_MSG_TRANSIENT_KEY . $user_id );
501 delete_transient( CheckoutController::PAY_NOW_ERROR_TRANSIENT_KEY . $user_id );
502 $course_id = $order->items[0]->id;
503 $url = get_the_permalink( $course_id );
504 }
505 }
506 return $url;
507 }
508
509 /**
510 * Store billing address for an order when order is placed.
511 *
512 * @since 3.5.0
513 *
514 * @param array $order_data order data.
515 *
516 * @return void
517 */
518 public function store_billing_address_for_order( array $order_data ) {
519 $order_id = $order_data['id'];
520 $user_id = $order_data['user_id'];
521 $billing_info = ( new BillingController( false ) )->get_billing_info( $user_id );
522
523 $meta_value = '{}';
524 if ( $billing_info ) {
525 $meta_value = wp_json_encode( $billing_info );
526 } else {
527 /**
528 * Store user data as billing info
529 * If user has no billing info during order like manual enrollment from CSV.
530 */
531 $user_data = get_userdata( $user_id );
532 $meta_value = wp_json_encode(
533 array(
534 'billing_first_name' => $user_data->first_name,
535 'billing_last_name' => $user_data->last_name,
536 'billing_email' => $user_data->user_email,
537 )
538 );
539 }
540
541 OrderMetaModel::add_meta(
542 $order_id,
543 OrderModel::META_KEY_BILLING_ADDRESS,
544 $meta_value
545 );
546 }
547 }
548
549