PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 3.4.1
Tutor LMS – eLearning and online course solution v3.4.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
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
488 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\OrderModel;
20 use TutorPro\CourseBundle\CustomPosts\CourseBundle;
21 use TutorPro\CourseBundle\Models\BundleModel;
22
23 /**
24 * Handle custom hooks
25 */
26 class HooksHandler {
27
28 /**
29 * OrderModel
30 *
31 * @since 3.0.0
32 *
33 * @var OrderModel
34 */
35 private $order_model;
36
37 /**
38 * OrderActivitiesModel
39 *
40 * @since 3.0.0
41 *
42 * @var OrderActivitiesModel
43 */
44 private $order_activities_model;
45
46 /**
47 * Register hooks & resolve props
48 *
49 * @since 3.0.0
50 */
51 public function __construct() {
52 $this->order_activities_model = new OrderActivitiesModel();
53 $this->order_model = new OrderModel();
54
55 // Register hooks.
56 add_filter( 'tutor_course_sell_by', array( $this, 'alter_course_sell_by' ) );
57 add_filter( 'get_tutor_course_price', array( $this, 'alter_course_price' ), 10, 2 );
58
59 // Order hooks.
60 add_action( 'tutor_order_payment_updated', array( $this, 'handle_payment_updated_webhook' ) );
61
62 add_action( 'tutor_order_payment_status_changed', array( $this, 'handle_payment_status_changed' ), 10, 3 );
63
64 add_action( 'tutor_order_placement_success', array( $this, 'handle_order_placement_success' ) );
65
66 /**
67 * Clear order menu badge count
68 *
69 * @since 3.0.0
70 */
71 add_action( 'tutor_order_placed', array( $this, 'clear_order_badge_count' ) );
72 add_action( 'tutor_order_payment_status_changed', array( $this, 'clear_order_badge_count' ) );
73 add_action( 'tutor_before_order_bulk_action', array( $this, 'clear_order_badge_count' ) );
74 add_filter( 'tutor_before_order_create', array( $this, 'update_order_data' ) );
75 add_action( 'tutor_order_placed', array( $this, 'handle_free_checkout' ) );
76 add_filter( 'tutor_redirect_url_after_checkout', array( $this, 'redirect_to_the_course' ), 10, 3 );
77 }
78
79 /**
80 * Clear order menu badge count
81 *
82 * @since 3.0.0
83 *
84 * @return void
85 */
86 public function clear_order_badge_count() {
87 delete_transient( OrderModel::TRANSIENT_ORDER_BADGE_COUNT );
88 }
89
90 /**
91 * Store order activity before bulk action.
92 *
93 * @since 3.0.0
94 *
95 * @param string $bulk_action The bulk action being performed.
96 * @param array $bulk_ids The IDs of the orders being acted upon.
97 *
98 * @return void
99 */
100 public function after_order_bulk_action( $bulk_action, $bulk_ids ) {
101 $order_status = $this->order_model->get_order_status_by_payment_status( $bulk_action );
102
103 $cancel_reason = Input::post( 'cancel_reason', '' );
104 foreach ( $bulk_ids as $order_id ) {
105 try {
106 $this->manage_earnings_and_enrollments( $order_status, $order_id );
107 $data = (object) array(
108 'order_id' => $order_id,
109 'meta_key' => $this->order_activities_model::META_KEY_HISTORY,
110 'meta_value' => "Order mark as {$bulk_action} {$cancel_reason}",
111 );
112 $this->order_activities_model->add_order_meta( $data );
113 } catch ( \Throwable $th ) {
114 // Log message with line & file.
115 error_log( $th->getMessage() . ' in ' . $th->getFile() . ' at line ' . $th->getLine() );
116 }
117 }
118 }
119
120 /**
121 * Alter course sell by value
122 *
123 * @since 3.0.0
124 *
125 * @param mixed $sell_by Default sell by.
126 *
127 * @return mixed
128 */
129 public function alter_course_sell_by( $sell_by ) {
130 if ( tutor_utils()->is_monetize_by_tutor() ) {
131 $sell_by = Ecommerce::MONETIZE_BY;
132 }
133
134 return $sell_by;
135 }
136
137 /**
138 * Alter course price to show price on the course
139 * entry box
140 *
141 * @since 3.0.0
142 *
143 * @param mixed $price Course price.
144 * @param int $course_id Course id.
145 *
146 * @return mixed
147 */
148 public function alter_course_price( $price, $course_id ) {
149 $price_type = tutor_utils()->price_type( $course_id );
150 if ( tutor_utils()->is_monetize_by_tutor() && Course::PRICE_TYPE_PAID === $price_type ) {
151 $price = tutor_get_course_formatted_price_html( $course_id, false );
152 }
153
154 return $price;
155 }
156
157 /**
158 * Handle payment updated webhook
159 *
160 * @since 3.0.0
161 *
162 * @param object $res Response data.
163 * {order_id, transaction_id, payment_status, payment_method, redirectUrl}.
164 *
165 * @return void
166 */
167 public function handle_payment_updated_webhook( $res ) {
168 $order_id = $res->id;
169 $new_payment_status = $res->payment_status;
170 $transaction_id = $res->transaction_id;
171
172 $order_details = $this->order_model->get_order_by_id( $order_id );
173 if ( $order_details ) {
174 $prev_payment_status = $order_details->payment_status;
175
176 $order_data = array(
177 'order_status' => $order_details->order_status,
178 'payment_status' => $new_payment_status,
179 'payment_payloads' => $res->payment_payload,
180 'transaction_id' => $transaction_id,
181 'earnings' => $res->earnings,
182 'fees' => $res->fees,
183 'updated_at_gmt' => current_time( 'mysql', true ),
184 );
185
186 switch ( $new_payment_status ) {
187 case $this->order_model::PAYMENT_PAID:
188 $order_data['order_status'] = $this->order_model::ORDER_COMPLETED;
189 break;
190 case $this->order_model::PAYMENT_FAILED:
191 case $this->order_model::PAYMENT_REFUNDED:
192 $order_data['order_status'] = $this->order_model::ORDER_CANCELLED;
193 break;
194 }
195
196 $update = $this->order_model->update_order( $order_id, $order_data );
197 if ( $update ) {
198 // Provide hook after update order.
199 do_action( 'tutor_order_payment_status_changed', $order_id, $prev_payment_status, $new_payment_status );
200 }
201 }
202
203 }
204
205 /**
206 * Update enrollment & earnings based on payment status
207 *
208 * @since 3.0.0
209 *
210 * @param int $order_id Order id.
211 * @param string $prev_payment_status previous payment status.
212 * @param string $new_payment_status new payment status.
213 *
214 * @return void
215 */
216 public function handle_payment_status_changed( $order_id, $prev_payment_status, $new_payment_status ) {
217
218 $order_status = $this->order_model->get_order_status_by_payment_status( $new_payment_status );
219
220 $cancel_reason = Input::post( 'cancel_reason' );
221 $remove_enrollment = Input::post( 'is_remove_enrolment', false, Input::TYPE_BOOL );
222
223 // Store activity.
224 $data = (object) array(
225 'order_id' => $order_id,
226 'meta_key' => $this->order_activities_model::META_KEY_HISTORY,
227 'meta_value' => 'Order marked as ' . $new_payment_status,
228 );
229
230 if ( $cancel_reason ) {
231 $meta_value = array(
232 'message' => 'Order marked as ' . $new_payment_status,
233 'cancel_reason' => $cancel_reason,
234 );
235 $data->meta_value = json_encode( $meta_value );
236 }
237
238 $this->order_activities_model->add_order_meta( $data );
239
240 if ( $remove_enrollment ) {
241 $order_status = OrderModel::ORDER_CANCELLED;
242 }
243
244 $this->manage_earnings_and_enrollments( $order_status, $order_id );
245
246 // Store coupon usage.
247 ( new CouponController( false ) )->store_coupon_usage( $order_id );
248 }
249
250 /**
251 * Handle new order placement
252 *
253 * Clear cart items, managing enrollment & earnings
254 *
255 * @since 3.0.0
256 *
257 * @param int $order_id Order id.
258 *
259 * @return void
260 */
261 public function handle_order_placement_success( int $order_id ) {
262 $order_data = $this->order_model->get_order_by_id( $order_id );
263 if ( $order_data ) {
264 $user_id = $order_data->student->id;
265
266 ( new CartModel() )->clear_user_cart( $user_id );
267
268 // Manage enrollment & earnings.
269 $order = ( new OrderModel() )->get_order_by_id( $order_id );
270 $payment_status = $order->payment_status;
271
272 $order_status = $this->order_model->get_order_status_by_payment_status( $payment_status );
273
274 $this->manage_earnings_and_enrollments( $order_status, $order_id );
275 }
276 }
277
278 /**
279 * Check if order is bundle order
280 *
281 * @since 3.0.0
282 *
283 * @param object $order order object.
284 * @param int $object_id object id.
285 *
286 * @return boolean
287 */
288 private function is_bundle_order( $order, $object_id ) {
289 return tutor_utils()->is_addon_enabled( 'course-bundle' )
290 && in_array( $order->order_type, array( $this->order_model::TYPE_SINGLE_ORDER, $this->order_model::TYPE_SUBSCRIPTION ), true )
291 && 'course-bundle' === get_post_type( $object_id );
292 }
293
294 /**
295 * Manage earnings after order bulk action
296 *
297 * @since 3.0.0
298 *
299 * @param string $order_status Order status.
300 * @param int $order_id Order ID.
301 *
302 * @return void
303 */
304 public function manage_earnings_and_enrollments( string $order_status, int $order_id ) {
305 $earnings = Earnings::get_instance();
306 $order = $this->order_model->get_order_by_id( $order_id );
307 $student_id = $order->student->id;
308
309 $enrollment_status = ( OrderModel::ORDER_COMPLETED === $order_status ? 'completed' : ( OrderModel::ORDER_INCOMPLETE === $order->order_status ? 'pending' : 'cancel' ) );
310
311 foreach ( $order->items as $item ) {
312 $object_id = $item->id; // It could be course/bundle/plan id.
313 if ( $this->order_model::TYPE_SINGLE_ORDER !== $order->order_type ) {
314 /**
315 * Do not process enrollment for membership plan.
316 *
317 * @since 3.2.0
318 */
319 $plan_info = apply_filters( 'tutor_get_plan_info', new \stdClass(), $object_id );
320 if ( $plan_info && isset( $plan_info->is_membership_plan ) && $plan_info->is_membership_plan ) {
321 continue;
322 } else {
323 $object_id = apply_filters( 'tutor_subscription_course_by_plan', $item->id, $order );
324 }
325
326 /**
327 * Do not process enrollment for subscription order refund.
328 * It will be handled by subscription controller's handle_order_refund method.
329 *
330 * @since 3.3.0
331 */
332 if ( Input::has( 'is_cancel_subscription' ) ) {
333 continue;
334 }
335 }
336
337 $has_enrollment = tutor_utils()->is_enrolled( $object_id, $student_id, false );
338 if ( $has_enrollment ) {
339 // Update enrollment status based on order status.
340 $update = tutor_utils()->update_enrollments( $enrollment_status, array( $has_enrollment->ID ) );
341 if ( $update ) {
342 if ( $this->is_bundle_order( $order, $object_id ) && $this->order_model->is_single_order( $order ) ) {
343 if ( 'completed' === $enrollment_status ) {
344 BundleModel::enroll_to_bundle_courses( $object_id, $student_id );
345 } else {
346 BundleModel::disenroll_from_bundle_courses( $object_id, $student_id );
347 }
348 }
349
350 /**
351 * For subscription, renewal no need to update order id.
352 */
353 if ( $this->order_model->is_single_order( $order ) ) {
354 update_post_meta( $has_enrollment->ID, '_tutor_enrolled_by_order_id', $order_id );
355
356 /**
357 * Update enrollment expiry date if it is set in a course.
358 */
359 if ( tutor()->course_post_type === get_post_type( $object_id ) ) {
360 $is_set_enrollment_expiry = (int) get_tutor_course_settings( $object_id, 'enrollment_expiry' );
361 $enrollment_expiry_enabled = (bool) get_tutor_option( 'enrollment_expiry_enabled' );
362 if ( $enrollment_expiry_enabled && $is_set_enrollment_expiry ) {
363 global $wpdb;
364 QueryHelper::update(
365 $wpdb->posts,
366 array(
367 'post_date' => current_time( 'mysql' ),
368 'post_date_gmt' => current_time( 'mysql', true ),
369 ),
370 array(
371 'ID' => $has_enrollment->ID,
372 'post_type' => tutor()->enrollment_post_type,
373 )
374 );
375 }
376 }
377
378 if ( OrderModel::ORDER_COMPLETED === $order_status ) {
379 do_action( 'tutor_after_enrolled', $object_id, $student_id, $has_enrollment->ID );
380 }
381 }
382
383 if ( 'completed' === $enrollment_status ) {
384 do_action( 'tutor_order_enrolled', $order, $has_enrollment->ID );
385 }
386 }
387 } else {
388 if ( $order->order_status === $this->order_model::ORDER_COMPLETED ) {
389 // Insert enrollment.
390 add_filter( 'tutor_enroll_data', fn( $enroll_data) => array_merge( $enroll_data, array( 'post_status' => 'completed' ) ) );
391
392 $enrollment_id = tutor_utils()->do_enroll( $object_id, $order_id, $student_id );
393 if ( $enrollment_id ) {
394 if ( $this->is_bundle_order( $order, $object_id ) && $this->order_model->is_single_order( $order ) ) {
395 BundleModel::enroll_to_bundle_courses( $object_id, $student_id );
396 }
397 update_post_meta( $enrollment_id, '_tutor_enrolled_by_order_id', $order_id );
398
399 do_action( 'tutor_order_enrolled', $order, $enrollment_id );
400 } else {
401 // Log error message with student id and course id.
402 error_log( "Error updating enrollment for student {$student_id} and course {$object_id}" );
403 }
404 }
405 }
406 }
407
408 // Update earnings.
409 $earnings->prepare_order_earnings( $order_id );
410 $earnings->remove_before_store_earnings();
411 }
412
413 /**
414 * Update order data for the free checkout
415 *
416 * @since 3.4.0
417 *
418 * @param array $order_data Order data.
419 *
420 * @return array
421 */
422 public function update_order_data( array $order_data ) {
423 if ( empty( $order_data['total_price'] ) && OrderModel::TYPE_SINGLE_ORDER === $order_data['order_type'] ) {
424 $order_data['order_status'] = OrderModel::ORDER_COMPLETED;
425 $order_data['payment_status'] = OrderModel::PAYMENT_PAID;
426 $order_data['payment_method'] = 'free';
427 }
428 return $order_data;
429 }
430
431 /**
432 * Enroll user to the course when free checkout
433 *
434 * @since 3.4.0
435 *
436 * @param array $order_data Order data.
437 *
438 * @return array
439 */
440 public function handle_free_checkout( array $order_data ) {
441 if ( empty( $order_data['total_price'] ) && OrderModel::TYPE_SINGLE_ORDER === $order_data['order_type'] ) {
442 $user_id = $order_data['user_id'];
443 $items = $order_data['items'];
444 foreach ( $items as $item ) {
445 add_filter( 'tutor_enroll_data', fn( $enroll_data) => array_merge( $enroll_data, array( 'post_status' => 'completed' ) ) );
446
447 $enrolled_id = tutor_utils()->do_enroll( $item['item_id'], $order_data['id'], $user_id );
448 if ( $enrolled_id && tutor_utils()->is_addon_enabled( 'course-bundle' ) && get_post_type( $item['item_id'] ) === CourseBundle::POST_TYPE ) {
449 BundleModel::enroll_to_bundle_courses( $item['item_id'], $user_id );
450 }
451 }
452 }
453 return $order_data;
454 }
455
456 /**
457 * Redirect user to the course after free checkout when item is 1.
458 * If user checkout multiple items and keep the default behavior.
459 *
460 * @since 3.4.0
461 *
462 * @param string $url Default redirect url.
463 * @param string $status Order placement status.
464 * @param integer $order_id Order id.
465 *
466 * @return string
467 */
468 public function redirect_to_the_course( string $url, string $status, int $order_id ):string {
469 $user_id = get_current_user_id();
470 if ( OrderModel::ORDER_PLACEMENT_SUCCESS === $status ) {
471 $order = $this->order_model->get_order_by_id( $order_id );
472 if ( $order && count( $order->items ) === 1 && empty( $order->total_price ) && OrderModel::TYPE_SINGLE_ORDER === $order->order_type ) {
473
474 // Firing hook to clear cart.
475 do_action( 'tutor_order_placement_success', $order_id );
476
477 // Clear the alert message.
478 delete_transient( CheckoutController::PAY_NOW_ALERT_MSG_TRANSIENT_KEY . $user_id );
479 delete_transient( CheckoutController::PAY_NOW_ERROR_TRANSIENT_KEY . $user_id );
480 $course_id = $order->items[0]->id;
481 $url = get_the_permalink( $course_id );
482 }
483 }
484 return $url;
485 }
486 }
487
488