PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 4.0.5
Tutor LMS – eLearning and online course solution v4.0.5
4.0.5 4.0.4 4.0.3 4.0.2 4.0.1 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 / classes / WooCommerce.php
tutor / classes Last commit date
Addons.php 1 month ago Admin.php 3 weeks ago Ajax.php 1 month ago Announcements.php 1 month ago Assets.php 2 weeks ago Backend_Page_Trait.php 1 year ago BaseController.php 1 year ago Config.php 1 month ago Container.php 1 year ago Course.php 3 days ago Course_Embed.php 3 years ago Course_Filter.php 1 month ago Course_List.php 1 month ago Course_Settings_Tabs.php 1 month ago Course_Widget.php 1 year ago Custom_Validation.php 1 month ago Dashboard.php 1 month ago Earnings.php 10 months ago FormHandler.php 1 month ago Frontend.php 1 month ago Gutenberg.php 1 year ago Icon.php 3 days ago Input.php 1 month ago Instructor.php 1 month ago Instructors_List.php 1 month ago Lesson.php 1 month ago Options_V2.php 1 month ago Permalink.php 1 month ago Post_types.php 1 month ago Private_Course_Access.php 1 month ago Q_And_A.php 1 month ago Question_Answers_List.php 1 year ago Quiz.php 3 days ago QuizBuilder.php 3 days ago Quiz_Attempts_List.php 1 month ago RestAPI.php 1 month ago Reviews.php 1 month ago Rewrite_Rules.php 2 years ago SampleCourse.php 1 month ago Shortcode.php 1 month ago Singleton.php 1 year ago Student.php 1 month ago Students_List.php 1 year ago Taxonomies.php 1 year ago Template.php 1 month ago Theme_Compatibility.php 3 years ago Tools.php 1 year ago Tools_V2.php 2 months ago Tutor.php 3 days ago TutorEDD.php 1 month ago Tutor_Base.php 2 years ago Tutor_Setup.php 1 month ago Upgrader.php 1 month ago User.php 2 weeks ago UserPreference.php 1 month ago Utils.php 3 days ago Video_Stream.php 3 years ago WhatsNew.php 1 month ago Withdraw.php 1 month ago Withdraw_Requests_List.php 1 year ago WooCommerce.php 3 days ago
WooCommerce.php
1223 lines
1 <?php
2 /**
3 * Manage WooCommerce integration
4 *
5 * @package Tutor\WooCommerce
6 * @author Themeum <support@themeum.com>
7 * @link https://themeum.com
8 * @since 1.0.0
9 */
10
11 namespace TUTOR;
12
13 defined( 'ABSPATH' ) || exit;
14
15 use Tutor\Helpers\QueryHelper;
16 use Tutor\Helpers\UrlHelper;
17 use Tutor\Models\EnrollmentModel;
18
19 /**
20 * Handle woocommerce hooks
21 *
22 * @since 1.0.0
23 */
24 class WooCommerce extends Tutor_Base {
25
26 const MONETIZE_BY = 'wc';
27
28 const TUTOR_WC_GUEST_CUSTOMER_ID = '_tutor_wc_guest_customer_id';
29 const WC_STORE_API_DRAFT_ORDER = 'store_api_draft_order';
30 const TUTOR_COURSE_PRODUCT_ID_META = '_tutor_course_product_id';
31
32 /**
33 * Register hooks
34 *
35 * @since 1.0.0
36 */
37 public function __construct() {
38 parent::__construct();
39
40 add_action( 'admin_notices', array( $this, 'notice_on_disabled_wc' ) );
41
42 // Add option settings.
43 add_filter( 'tutor_monetization_options', array( $this, 'tutor_monetization_options' ) );
44
45 $monetize_by = tutor_utils()->get_option( 'monetize_by' );
46 if ( 'wc' !== $monetize_by ) {
47 return;
48 }
49
50 /**
51 * Is Course Purchasable
52 */
53 add_filter( 'is_course_purchasable', array( $this, 'is_course_purchasable' ), 10, 2 );
54 add_filter( 'get_tutor_course_price', array( $this, 'get_tutor_course_price' ), 10, 2 );
55 add_filter( 'tutor_course_sell_by', array( $this, 'tutor_course_sell_by' ) );
56
57 add_filter( 'product_type_options', array( $this, 'add_tutor_type_in_wc_product' ) );
58
59 add_action( 'save_post_' . $this->course_post_type, array( $this, 'save_course_meta' ), 10, 2 );
60 add_action( 'save_post_product', array( $this, 'save_wc_product_meta' ) );
61
62 add_action( 'tutor_course/single/before/enroll', 'wc_print_notices' );
63
64 /**
65 * After place new order
66 */
67 add_action( 'woocommerce_new_order', array( $this, 'course_placing_order_from_admin' ), 10, 3 );
68 add_action( 'woocommerce_new_order_item', array( $this, 'course_placing_order_from_customer' ), 10, 3 );
69 add_action( 'woocommerce_order_status_changed', array( $this, 'handle_customer_order_by_block_checkout' ), 9, 3 );
70
71 /**
72 * Order Status Hook
73 *
74 * Remove course from active courses if an order is cancelled or refunded
75 */
76 add_action( 'woocommerce_order_status_changed', array( $this, 'enrolled_courses_status_change' ), 10, 3 );
77
78 /**
79 * Add Earning Data
80 */
81 add_action( 'woocommerce_new_order_item', array( $this, 'add_earning_data' ), 10, 3 );
82 add_action( 'woocommerce_order_status_changed', array( $this, 'add_earning_data_status_change' ), 10, 3 );
83
84 /**
85 * WC Print Notices After Enroll
86 *
87 * @since 1.3.5
88 */
89 if ( tutor_utils()->has_wc() ) {
90 add_action( 'tutor_course/single/before/inner-wrap', 'wc_print_notices', 10 );
91 add_action( 'tutor_course/single/enrolled/before/inner-wrap', 'wc_print_notices', 10 );
92 }
93
94 /**
95 * Manage WooCommerce plugin dependency
96 *
97 * @since 1.7.8
98 */
99 $woocommerce_path = WP_PLUGIN_DIR . '/woocommerce/woocommerce.php';
100 register_deactivation_hook( $woocommerce_path, array( $this, 'woocommerce_deactivation_handler' ) );
101
102 /**
103 * Redirect student on enrolled courses after course
104 * Enrollment complete
105 *
106 * @since 1.9.0
107 */
108 add_action( 'woocommerce_thankyou', array( $this, 'redirect_to_enrolled_courses' ) );
109
110 /**
111 * Change woo commerce cart product link if it is tutor product
112 */
113 add_filter( 'woocommerce_cart_item_permalink', array( $this, 'tutor_update_product_url' ), 10, 2 );
114 add_filter( 'woocommerce_order_item_permalink', array( $this, 'filter_order_item_permalink_callback' ), 10, 3 );
115
116 /**
117 * On WC product delete clear course linked product
118 *
119 * @since 2.0.7
120 */
121 add_action( 'delete_post', array( $this, 'clear_course_linked_product' ) );
122
123 add_action( 'before_woocommerce_init', array( $this, 'declare_tutor_compatibility_with_hpos' ) );
124
125 add_action( 'woocommerce_order_after_calculate_totals', array( $this, 'add_coupon_to_order' ), 10, 2 );
126
127 add_action( 'woocommerce_guest_session_to_user_id', array( $this, 'enroll_guest_user' ), 10, 2 );
128
129 add_filter( 'woocommerce_shortcode_products_query_results', array( $this, 'filter_products_query_results' ), 10, 2 );
130
131 add_filter( 'woocommerce_shortcode_products_query', array( $this, 'filter_tutor_course_products' ) );
132
133 // @since 4.0.0
134 add_filter( 'tutor_order_history_card_template', fn( $template ) => tutor_get_template( 'dashboard.account.billing.wc-order-history-card' ) );
135 add_filter( 'tutor_order_history_status_options', array( $this, 'filter_order_history_status_options' ), 10, 2 );
136 add_filter( 'tutor_get_orders_by_user_id', array( $this, 'filter_tutor_get_orders_by_user_id' ), 10, 3 );
137 }
138
139 /**
140 * Filter tutor courses from shortcode shop page if enabled.
141 *
142 * @since 3.6.2
143 *
144 * @param array $query_args the query args.
145 *
146 * @return array
147 */
148 public function filter_tutor_course_products( $query_args ) {
149 $hide_course_from_shop_page = (bool) get_tutor_option( 'hide_course_from_shop_page' );
150
151 if ( ! $hide_course_from_shop_page ) {
152 return $query_args;
153 }
154
155 $course_ids = ( new Course() )->get_connected_wc_product_ids();
156
157 $query_args['post__not_in'] = $course_ids;
158
159 return $query_args;
160 }
161
162 /**
163 * Filter woocommerce shop query result to remove scheduled posts.
164 *
165 * @since 3.6.2
166 *
167 * @param \WP_Query $results the query result object.
168 * @param object $wc_shortcode_products_obj the shortcode product class object.
169 *
170 * @return array
171 */
172 public function filter_products_query_results( $results, $wc_shortcode_products_obj ) {
173 $ids = $results->ids ?? array();
174
175 if ( $ids ) {
176 $filtered_ids = array_filter(
177 $ids,
178 function ( $val ) {
179 $course_id = (int) $this->get_post_id_by_meta_key_and_value( self::TUTOR_COURSE_PRODUCT_ID_META, $val );
180 if ( ! $course_id ) {
181 return true;
182 }
183
184 if ( $course_id && 'future' !== get_post_status( $course_id ) ) {
185 return true;
186 }
187 }
188 );
189
190 $results->ids = $filtered_ids;
191 }
192
193 return $results;
194 }
195
196 /**
197 * Enroll students to course after guest checkout.
198 *
199 * @since 3.2.0
200 *
201 * @param int $guest_customer_id the customer id of guest user.
202 * @param int $customer_id the customer id of registered user.
203 *
204 * @return void
205 */
206 public function enroll_guest_user( $guest_customer_id, $customer_id ) {
207 global $wpdb;
208 $course_ids = $wpdb->get_col(
209 $wpdb->prepare(
210 "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key=%s AND meta_value=%s",
211 self::TUTOR_WC_GUEST_CUSTOMER_ID,
212 $guest_customer_id
213 )
214 );
215
216 $order_id = WC()->session->get( self::WC_STORE_API_DRAFT_ORDER, 0 );
217
218 if ( $course_ids && $order_id ) {
219 $wc_order = wc_get_order( $order_id );
220 if ( ! $wc_order ) {
221 return;
222 }
223
224 foreach ( $course_ids as $course_id ) {
225 EnrollmentModel::do_enroll( $course_id, $order_id, $customer_id );
226 }
227 }
228 }
229
230
231 /**
232 * Add manual coupon discount to wc order items.
233 *
234 * @since 3.0.0
235 *
236 * @param bool $taxes whether to consider the taxes.
237 * @param object $order the order object.
238 *
239 * @return void
240 */
241 public function add_coupon_to_order( $taxes, $order ) {
242 global $wpdb;
243 $earning_id = $wpdb->get_var(
244 $wpdb->prepare(
245 "SELECT earning_id FROM {$wpdb->prefix}tutor_earnings WHERE order_id=%d",
246 $order->get_id()
247 )
248 );
249
250 if ( $earning_id ) {
251 $items = $order->get_items();
252 foreach ( $items as $item ) {
253
254 $item = new \WC_Order_Item_Product( $item );
255
256 $product_id = $item->get_product_id();
257 $if_has_course = tutor_utils()->product_belongs_with_course( $product_id );
258 if ( $if_has_course ) {
259 $course_id = $if_has_course->post_id;
260 $user_id = get_post_field( 'post_author', $course_id );
261 $order_status = "wc-{$order->get_status()}";
262 $total_price = $item->get_total();
263
264 list( $admin_amount, $instructor_amount ) = array_values( tutor_split_amounts( $total_price ) );
265
266 $earnings = Earnings::get_instance();
267 $earning_data = apply_filters( 'tutor_new_earning_data', $earnings->prepare_earning_data( $total_price, $course_id, $order->get_id(), $order_status, $admin_amount, $instructor_amount ) );
268
269 $wpdb->update( $wpdb->prefix . 'tutor_earnings', $earning_data, array( 'earning_id' => $earning_id ) );
270 }
271 }
272 }
273 }
274
275 /**
276 * Show admin notice if user disable the WC plugin.
277 *
278 * @since 1.0.0
279 *
280 * @return void
281 */
282 public function notice_on_disabled_wc() {
283 $show = get_option( 'tutor_show_woocommerce_notice' ) && 'free' === tutor_utils()->get_option( 'monetize_by', 'free' );
284
285 if ( $show ) {
286 $message = __( 'Since monetization is currently disabled, your courses are set to free. Enable Tutor LMS monetization to start selling your courses.', 'tutor' );
287 echo '<div class="notice notice-error"><p>' . esc_html( $message ) . '</p></div>';
288 }
289 }
290
291 /**
292 * Check HPOS feature enabled.
293 * WC declared HPOS (Hight Performance Order Storage) feature stable on october 2023 from WC v8.2
294 *
295 * @see https://woo.com/document/high-performance-order-storage
296 *
297 * @since 2.6.0
298 *
299 * @return bool
300 */
301 public static function hpos_enabled() {
302 $hpos = false;
303
304 if ( tutor_utils()->has_wc() && version_compare( WC()->version, '8.2.0', '>=' ) ) {
305 $hpos = 'yes' === get_option( 'woocommerce_custom_orders_table_enabled' );
306 }
307
308 return $hpos;
309 }
310
311 /**
312 * Declare tutor compatibility with WC HPOS feature
313 *
314 * @since 2.6.0
315 *
316 * @return void
317 */
318 public function declare_tutor_compatibility_with_hpos() {
319 if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
320 \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', TUTOR_FILE, true );
321 }
322 }
323
324 /**
325 * On WC product delete, clear course linked product
326 *
327 * @since 2.0.7
328 *
329 * @param int $post_id post id.
330 *
331 * @return void
332 */
333 public function clear_course_linked_product( $post_id ) {
334 if ( get_post_type( $post_id ) === 'product' ) {
335 global $wpdb;
336 $wpdb->query(
337 $wpdb->prepare( "DELETE FROM {$wpdb->postmeta} WHERE meta_key=%s AND meta_value=%d", '_tutor_course_product_id', $post_id )
338 );
339 }
340 }
341
342 /**
343 * Order item callback handler
344 *
345 * @since 1.0.0
346 *
347 * @param string $product_permalink permalink.
348 * @param mixed $item order item.
349 * @param mixed $order orders.
350 *
351 * @return string permalink of course
352 */
353 public function filter_order_item_permalink_callback( $product_permalink, $item, $order ) {
354
355 // For product variations.
356 if ( $item->get_variation_id() > 0 ) {
357 $product = $item->get_product();
358
359 $is_visible = $product && $product->is_visible();
360
361 // Get the instance of the parent variable product Object.
362 $parent_product = wc_get_product( $item->get_product_id() );
363
364 // Return the parent product permalink (if product is visible).
365 return $is_visible ? $parent_product->get_permalink() : '';
366 }
367
368 $course_id = $this->get_post_id_by_meta_key_and_value( '_tutor_course_product_id', $item->get_product_id() );
369
370 return get_permalink( $course_id );
371 }
372
373 /**
374 * Get post id my meta key & value
375 *
376 * @since 1.0.0
377 *
378 * @param mixed $key meta key.
379 * @param mixed $value meta value.
380 *
381 * @return mixed post id on success, false on failure
382 */
383 public function get_post_id_by_meta_key_and_value( $key, $value ) {
384 global $wpdb;
385 $meta = $wpdb->get_results( 'SELECT * FROM `' . $wpdb->postmeta . "` WHERE meta_key='" . esc_sql( $key ) . "' AND meta_value='" . esc_sql( $value ) . "'" );
386 if ( is_array( $meta ) && ! empty( $meta ) && isset( $meta[0] ) ) {
387 $meta = $meta[0];
388 }
389 if ( is_object( $meta ) ) {
390 return $meta->post_id;
391 } else {
392 return false;
393 }
394 }
395
396 /**
397 * Check if course is purchase able
398 *
399 * @since 1.0.0
400 *
401 * @param bool $bool default value.
402 * @param int $course_id course id.
403 *
404 * @return boolean
405 */
406 public function is_course_purchasable( $bool, $course_id ) {
407 if ( ! tutor_utils()->has_wc() ) {
408 return false;
409 }
410
411 $course_id = tutor_utils()->get_post_id( $course_id );
412 $price_type = tutor_utils()->price_type( $course_id );
413 $has_product_id = tutor_utils()->get_course_product_id( $course_id );
414
415 /**
416 * WC course bundle don't have free or paid price types.
417 * Check if the bundle has a WC product attached.
418 */
419 if ( $has_product_id && tutor()->bundle_post_type === get_post_type( $course_id ) ) {
420 return true;
421 }
422
423 if ( Course::PRICE_TYPE_PAID === $price_type && $has_product_id ) {
424 return true;
425 }
426
427 return false;
428 }
429
430 /**
431 * Get course price
432 *
433 * @since 1.0.0
434 *
435 * @param mixed $price course price.
436 * @param int $course_id course id.
437 *
438 * @return string
439 */
440 public function get_tutor_course_price( $price, $course_id ) {
441 $price = null;
442
443 if ( tutor_utils()->is_course_purchasable( $course_id ) ) {
444 if ( tutor_utils()->has_wc() ) {
445 $product_id = tutor_utils()->get_course_product_id( $course_id );
446 $product = wc_get_product( $product_id );
447
448 if ( $product ) {
449 ob_start();
450 ?>
451 <div class="price">
452 <?php echo $product->get_price_html(); //phpcs:ignore ?>
453 </div>
454 <?php
455 return ob_get_clean();
456 }
457 }
458 }
459
460 return $price;
461 }
462
463 /**
464 * Sell by filter handler
465 *
466 * @since 1.0.0
467 *
468 * @return string
469 */
470 public function tutor_course_sell_by() {
471 return 'woocommerce';
472 }
473
474 /**
475 * Add tutor type in WC product
476 *
477 * @since 1.0.0
478 *
479 * @param array $types types.
480 *
481 * @return array
482 */
483 public function add_tutor_type_in_wc_product( $types ) {
484 $types['tutor_product'] = array(
485 'id' => '_tutor_product',
486 'wrapper_class' => 'show_if_simple',
487 'label' => __( 'For Tutor', 'tutor' ),
488 'description' => __( 'This checkmark ensure that you will sell a specific course via this product.', 'tutor' ),
489 'default' => 'no',
490 );
491
492 return $types;
493 }
494
495 /**
496 * Save course meta for attaching WC product
497 *
498 * @since 1.0.0
499 *
500 * @param int $post_ID this is course ID.
501 * @param mixed $post course details.
502 *
503 * @return void
504 */
505 public function save_course_meta( $post_ID, $post ) {
506 do_action( 'save_tutor_course', $post_ID, $post );
507 }
508
509 /**
510 * Save WC product meta
511 *
512 * @since 1.0.0
513 *
514 * @param int $post_ID post id.
515 *
516 * @return void
517 */
518 public function save_wc_product_meta( $post_ID ) {
519 $is_tutor_product = Input::post( '_tutor_product', '' );
520 if ( 'on' === $is_tutor_product ) {
521 update_post_meta( $post_ID, '_tutor_product', 'yes' );
522 } else {
523 delete_post_meta( $post_ID, '_tutor_product' );
524 }
525 }
526
527 /**
528 * Take enrolled course action based on order status change
529 *
530 * Order auto complete
531 *
532 * @param int $order_id wc order id.
533 * @param string $status_from from status.
534 * @param string $status_to to status.
535 *
536 * @return void
537 */
538 public function enrolled_courses_status_change( $order_id, $status_from, $status_to ) {
539 if ( ! tutor_utils()->is_tutor_order( $order_id ) ) {
540 return;
541 }
542
543 $enrolled_ids_with_course = tutor_utils()->get_course_enrolled_ids_by_order_id( $order_id );
544
545 if ( $enrolled_ids_with_course ) {
546 $enrolled_ids = wp_list_pluck( $enrolled_ids_with_course, 'enrolled_id' );
547
548 if ( is_array( $enrolled_ids ) && count( $enrolled_ids ) ) {
549 foreach ( $enrolled_ids as $enrolled_id ) {
550 /**
551 * If order status is processing and payment is not cash on
552 * delivery then mark enrollment as completed.
553 *
554 * Note: Order status processing simply mean customer have done
555 * payment.
556 *
557 * @since v2.0.5
558 */
559 if ( self::should_order_auto_complete( $order_id ) ) {
560 // Mark enrollment as completed.
561 tutor_utils()->course_enrol_status_change( $enrolled_id, 'completed' );
562 // Mark WC order as completed.
563 self::mark_order_complete( $order_id );
564 } else {
565 tutor_utils()->course_enrol_status_change( $enrolled_id, $status_to );
566 }
567
568 // Invoke enrolled hook.
569 if ( 'completed' === $status_to ) {
570 $user_id = get_post_field( 'post_author', $enrolled_id );
571 $course_id = get_post_field( 'post_parent', $enrolled_id );
572
573 $should_fire_hook = apply_filters( 'tutor_should_fire_after_enrolled_for_wc_order', true, $order_id, $enrolled_id );
574 if ( $should_fire_hook ) {
575 do_action( 'tutor_after_enrolled', $course_id, $user_id, $enrolled_id );
576 }
577 }
578 }
579 }
580 }
581 }
582
583 /**
584 * Returning monetization options
585 *
586 * @since v.1.3.5
587 *
588 * @param array $arr attrs.
589 *
590 * @return mixed
591 */
592 public function tutor_monetization_options( $arr ) {
593 $has_wc = tutor_utils()->has_wc();
594 if ( $has_wc ) {
595 $arr[ self::MONETIZE_BY ] = __( 'WooCommerce', 'tutor' );
596 }
597 return $arr;
598 }
599
600 /**
601 * Adding Earning Data processing WooCommerce
602 *
603 * @param int $item_id item id.
604 * @param mixed $item item.
605 * @param int $order_id order id.
606 *
607 * @since 1.1.2
608 */
609 public function add_earning_data( $item_id, $item, $order_id ) {
610
611 if ( 'wc' !== tutor_utils()->get_option( 'monetize_by' ) ) {
612 return;
613 }
614
615 global $wpdb;
616 $item = new \WC_Order_Item_Product( $item );
617
618 $product_id = $item->get_product_id();
619 $order = wc_get_order( $order_id );
620 if ( ! $order ) {
621 return;
622 }
623
624 /**
625 * Prevent adding earning data when order is created with WC block checkout page.
626 *
627 * @since 3.9.14
628 *
629 * @since 4.0.5 Check session before using $order->has_status
630 */
631 if ( WC()->session && $order->has_status( 'checkout-draft' ) ) {
632 return;
633 }
634
635 $order_type = $order->get_type();
636
637 if ( 'shop_subscription' === $order_type ) {
638 return;
639 }
640
641 $if_has_course = tutor_utils()->product_belongs_with_course( $product_id );
642
643 if ( $if_has_course && is_object( $order ) ) {
644 $course_id = $if_has_course->post_id;
645 $user_id = get_post_field( 'post_author', $course_id );
646 $order_status = "wc-{$order->get_status()}";
647
648 /**
649 * Return here if already added this product from this order
650 *
651 * @since v1.9.7
652 */
653 $exist_count = (int) $wpdb->get_var(
654 $wpdb->prepare(
655 "SELECT COUNT(earning_id)
656 FROM {$wpdb->prefix}tutor_earnings
657 WHERE course_id=%d
658 AND order_id=%d
659 AND user_id=%d",
660 $course_id,
661 $order_id,
662 $user_id
663 )
664 );
665
666 if ( $exist_count > 0 ) {
667 return;
668 }
669
670 $total_price = $item->get_total();
671
672 list( $admin_amount, $instructor_amount ) = array_values( tutor_split_amounts( $total_price ) );
673
674 $earnings = Earnings::get_instance();
675 $earning_data = apply_filters( 'tutor_new_earning_data', $earnings->prepare_earning_data( $total_price, $course_id, $order_id, $order_status, $admin_amount, $instructor_amount ) );
676
677 $wpdb->insert( $wpdb->prefix . 'tutor_earnings', $earning_data );
678 }
679 }
680
681 /**
682 * Change Earning data status
683 *
684 * @since 1.0.0
685 *
686 * @param int $order_id wc order id.
687 * @param string $status_from previous status.
688 * @param string $status_to current status.
689 *
690 * @return void
691 */
692 public function add_earning_data_status_change( $order_id, $status_from, $status_to ) {
693 if ( ! tutor_utils()->is_tutor_order( $order_id ) ) {
694 tutor_log( 'not tutor order' );
695 return;
696 }
697
698 /**
699 * If it is auto complete order then make earning status complete
700 * to reflect earning for admin & instructor
701 *
702 * @since 2.0.9
703 */
704 if ( self::should_order_auto_complete( $order_id ) ) {
705 $status_to = 'completed';
706 }
707
708 tutor_utils()->change_earning_status( $order_id, $status_to );
709 }
710
711 /**
712 * Course placing order from admin
713 *
714 * @since v.1.6.7
715 *
716 * @param int $order_id wc order id.
717 *
718 * @return void
719 */
720 public function course_placing_order_from_admin( $order_id ) {
721 if ( ! is_admin() ) {
722 return;
723 }
724
725 $order = wc_get_order( $order_id );
726 /**
727 * Loop though each WC order item.
728 *
729 * @var WC_Order_Item $item WC order item object.
730 */
731 foreach ( $order->get_items() as $item ) {
732 $product_id = $item->get_product_id();
733 $if_has_course = tutor_utils()->product_belongs_with_course( $product_id );
734 if ( $if_has_course ) {
735 $course_id = $if_has_course->post_id;
736 $customer_id = $order->get_customer_id();
737 EnrollmentModel::do_enroll( $course_id, $order_id, $customer_id );
738 }
739 }
740 }
741
742 /**
743 * Handle checkout order item.
744 *
745 * @since 3.9.14
746 *
747 * @param object $order the order object.
748 * @param object $item the order item object.
749 * @param string $context the context of checkout page.
750 *
751 * @return void
752 */
753 private function handle_checkout_order_item( $order, $item, $context = 'classic_checkout' ) {
754 if ( ! $order instanceof \WC_Order || ! $item instanceof \WC_Order_Item_Product ) {
755 return;
756 }
757
758 $order_id = $order->get_id();
759 $is_gift_item = apply_filters( 'tutor_is_gift_item', false, $item );
760 if ( $is_gift_item ) {
761 return;
762 }
763
764 $product_id = $item->get_product_id();
765 $if_has_course = tutor_utils()->product_belongs_with_course( $product_id );
766
767 if ( $if_has_course ) {
768 $should_process = apply_filters( 'tutor_wc_should_process_checkout_order_item', true, $item, $order );
769 if ( ! $should_process ) {
770 return;
771 }
772
773 $course_id = $if_has_course->post_id;
774 $customer_id = $order->get_customer_id();
775
776 // Handle course enrollment.
777 if ( ! $customer_id && WC()->session && WC()->session->has_session() ) {
778 $guest_customer_id = WC()->session->get_customer_unique_id();
779 update_post_meta( $course_id, self::TUTOR_WC_GUEST_CUSTOMER_ID, $guest_customer_id );
780 } else {
781 $has_enrollment = tutor_utils()->is_enrolled( $course_id, $customer_id, true );
782 if ( ! $has_enrollment ) {
783 tutor_utils()->do_enroll( $course_id, $order_id, $customer_id );
784 }
785 }
786
787 if ( 'block_checkout' === $context ) {
788 $this->add_earning_data( $item->get_id(), $item, $order_id );
789 }
790 }
791 }
792
793
794 /**
795 * Course placing order from customer
796 *
797 * @since 1.6.7
798 * @since 3.8.0 Filter hook: tutor_is_gift_item added
799 * @since 4.0.0 Filter hook: tutor_wc_should_process_checkout_order_item added
800 * Prevent duplicate enrollment on checkout.
801 *
802 * @param int $item_id item id.
803 * @param mixed $item order item.
804 * @param int $order_id wc order id.
805 *
806 * @return void
807 */
808 public function course_placing_order_from_customer( $item_id, $item, $order_id ) {
809
810 if ( is_admin() ) {
811 return;
812 }
813
814 $order = wc_get_order( $order_id );
815 if ( ! $order ) {
816 return;
817 }
818
819 /**
820 * Prevent when order is created with WC block checkout page.
821 *
822 * @since 3.9.14
823 *
824 * @since 4.0.5 Check session before using $order->has_status
825 */
826 if ( WC()->session && $order->has_status( 'checkout-draft' ) ) {
827 return;
828 }
829
830 $this->handle_checkout_order_item( $order, $item, 'classic_checkout' );
831 }
832
833 /**
834 * Handle order status change by block checkout page.
835 *
836 * @since 3.9.14
837 *
838 * @param int $order_id WooCommerce order ID.
839 * @param string $status_from Previous order status.
840 * @param string $status_to New order status.
841 *
842 * @return void
843 */
844 public function handle_customer_order_by_block_checkout( $order_id, $status_from, $status_to ) {
845 if ( 'checkout-draft' !== $status_from ) {
846 return;
847 }
848
849 // If transitioning to another draft or invalid status, do nothing.
850 if ( in_array( $status_to, array( 'checkout-draft', 'draft' ), true ) ) {
851 return;
852 }
853
854 $order = wc_get_order( $order_id );
855 if ( ! $order ) {
856 return;
857 }
858
859 foreach ( $order->get_items() as $item ) {
860 $this->handle_checkout_order_item( $order, $item, 'block_checkout' );
861 }
862 }
863
864 /**
865 * Handle disabling WooCommerce monetization on WooCommerce plugin deactivation
866 *
867 * @since 1.7.8
868 *
869 * @return void
870 */
871 public function woocommerce_deactivation_handler() {
872 if ( tutor_utils()->get_option( 'monetize_by' ) === 'wc' ) {
873 tutor_utils()->update_option( 'monetize_by', 'free' );
874 /**
875 * Show a reminder to re-enable Tutor monetization to
876 * monetize courses after re-activating WooCommerce:
877 *
878 * Possible follow-up fix: Only show a notice when
879 * WooCommerce was re-activated after this forced
880 * disabling of WooCommerce monetization took place:
881 */
882 update_option( 'tutor_show_woocommerce_notice', true );
883 }
884 }
885
886 /**
887 * Redirect student on enrolled courses after course
888 * enrollment complete if course is purchasable
889 *
890 * @since 1.0.0
891 *
892 * @param int $order_id wc order id.
893 *
894 * @return void
895 */
896 public function redirect_to_enrolled_courses( $order_id ) {
897 if ( ! tutor_utils()->get_option( 'wc_automatic_order_complete_redirect_to_courses' ) ) {
898 // Since 1.9.1.
899 return;
900 }
901
902 // get woo order details.
903 $order = wc_get_order( $order_id );
904 $tutor_product = false;
905 $url = tutor_utils()->tutor_dashboard_url() . 'courses/';
906
907 /**
908 * Loop though each WC order item.
909 *
910 * @var WC_Order_Item $item WC order item object.
911 */
912 foreach ( $order->get_items() as $item ) {
913 $product_id = $item->get_product_id();
914 // check if product associated with tutor course.
915 $if_has_course = tutor_utils()->product_belongs_with_course( $product_id );
916 if ( $if_has_course ) {
917 $tutor_product = true;
918 }
919 }
920
921 // filter redirection url after woocommerce product purchase.
922 $redirect_url = apply_filters( 'tutor_woocommerce_redirect_url', $url );
923
924 // if tutor product & order status completed.
925 if ( $order->has_status( 'completed' ) && $tutor_product ) {
926 wp_safe_redirect( $redirect_url );
927 exit;
928 }
929 }
930
931 /**
932 * Change product url on cart page if product is tutor course
933 *
934 * @since 1.9.8
935 *
936 * @param string $permalink permalink.
937 * @param mixed $cart_item cart item.
938 *
939 * @return mixed
940 */
941 public function tutor_update_product_url( $permalink, $cart_item ) {
942
943 $woo_product_id = $cart_item['product_id'];
944 $product_meta = get_post_meta( $woo_product_id );
945
946 if ( isset( $product_meta['_tutor_product'] ) && $product_meta['_tutor_product'][0] ) {
947
948 global $wpdb;
949 $table = $wpdb->base_prefix . 'postmeta';
950 $post_id = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM {$table} WHERE meta_key = '_tutor_course_product_id' AND meta_value = %d ", $woo_product_id ) ); //phpcs:ignore
951
952 if ( $post_id ) {
953 $data = get_post_permalink( $post_id );
954 return $data;
955 }
956 }
957 }
958
959 /**
960 * Mark woocommerce order as complete only from the
961 * client side.
962 *
963 * @since 2.0.5
964 *
965 * @param int $order_id wc order id.
966 *
967 * @return bool
968 */
969 public static function mark_order_complete( int $order_id ): bool {
970 if ( is_admin() ) {
971 return false;
972 }
973
974 $order = \wc_get_order( $order_id );
975 $order->set_status( 'completed' );
976 $update = $order->save();
977
978 return (bool) $update;
979 }
980
981 /**
982 * Check if order should auto complete
983 *
984 * @since 2.0.9
985 *
986 * Bank transfer & check payments will consider as manual
987 * payments. Hence, if user pay with bank & check will not
988 * be auto complete
989 *
990 * @since 2.1.4
991 *
992 * @param int $order_id wc order id.
993 *
994 * @return boolean return true if it should auto complete, otherwise false
995 */
996 public static function should_order_auto_complete( int $order_id ): bool {
997 $auto_complete = false;
998
999 $order = wc_get_order( $order_id );
1000 $order_data = is_object( $order ) && method_exists( $order, 'get_data' ) ? $order->get_data() : array();
1001
1002 $payment_method = isset( $order_data['payment_method'] ) ? $order_data['payment_method'] : '';
1003 $monetize_by = tutor_utils()->get_option( 'monetize_by' );
1004
1005 $should_auto_complete = tutor_utils()->get_option( 'tutor_woocommerce_order_auto_complete' );
1006 $is_enabled_auto_complete = 'wc' === $monetize_by && $should_auto_complete ? true : false;
1007
1008 $manual_payments = array( 'cod', 'cheque', 'bacs' );
1009 $order_status = method_exists( $order, 'get_status' ) ? $order->get_status() : '';
1010
1011 if ( 'completed' !== $order_status ) {
1012 $is_tutor_order = tutor_utils()->is_tutor_order( $order->get_id() );
1013
1014 /**
1015 * Is tutor order condition added with other conditions,
1016 * to prevent order other than Tutor get completed
1017 *
1018 * @since 2.1.6
1019 */
1020 if ( ! is_admin() && $is_enabled_auto_complete && 'processing' === $order_status && ! in_array( $payment_method, $manual_payments ) && $is_tutor_order ) {
1021 $auto_complete = true;
1022 }
1023 } else {
1024 $auto_complete = true;
1025 }
1026
1027 return $auto_complete;
1028 }
1029
1030 /**
1031 * Count orders for a user.
1032 *
1033 * @since 4.0.0
1034 *
1035 * @param array $args array of arguments.
1036 *
1037 * @return int return count of orders.
1038 */
1039 public static function get_count( $args = array() ): int {
1040 $query = wc_get_orders(
1041 array(
1042 'customer' => $args['user_id'] ?? 0,
1043 'status' => $args['status'] ?? 'all',
1044 'date_created' => $args['date_created'] ?? null,
1045 'meta_query' => array(
1046 array(
1047 'key' => '_is_tutor_order_for_course',
1048 'compare' => 'EXISTS',
1049 ),
1050 ),
1051 // Limit 1 for query performance.
1052 'limit' => 1,
1053 'paginate' => true,
1054 )
1055 );
1056
1057 return $query->total;
1058 }
1059
1060 /**
1061 * Filter order history status options.
1062 *
1063 * @since 4.0.0
1064 *
1065 * @param array $options the status options.
1066 * @param string $seleted the selected status.
1067 *
1068 * @return array<array{label: string, value: string, count: int, url: string, active: bool}>
1069 */
1070 public function filter_order_history_status_options( $options, $seleted ) {
1071 $url = get_pagenum_link();
1072 $user_id = get_current_user_id();
1073
1074 $statuses = array_merge( array( 'all' => 'All' ), wc_get_order_statuses() );
1075 if ( isset( $statuses['wc-checkout-draft'] ) ) {
1076 unset( $statuses['wc-checkout-draft'] );
1077 }
1078
1079 $start_date = Input::get( 'start_date' );
1080 $end_date = Input::get( 'end_date' );
1081
1082 $options = array();
1083
1084 foreach ( $statuses as $key => $status ) {
1085 $params = array(
1086 'user_id' => $user_id,
1087 'status' => $key,
1088 );
1089
1090 if ( ! empty( $start_date ) && ! empty( $end_date ) ) {
1091 $params['date_created'] = $start_date . '...' . $end_date;
1092 }
1093
1094 $options[] = array(
1095 'label' => $status,
1096 'value' => $key,
1097 'count' => self::get_count( $params ),
1098 'url' => UrlHelper::add_query_params( $url, array( 'data' => $key ) ),
1099 'active' => $key === $seleted || ( empty( $key ) && 'all' === $seleted ),
1100 );
1101 }
1102
1103 return $options;
1104 }
1105
1106 /**
1107 * Filter tutor get orders by user id.
1108 *
1109 * @since 4.0.0
1110 *
1111 * @param object $data data.
1112 * @param int $user_id user id.
1113 * @param array $args array of arguments.
1114 *
1115 * @return object {results: array, total_count: int}
1116 */
1117 public function filter_tutor_get_orders_by_user_id( $data, $user_id, $args ) {
1118 if ( ! $user_id ) {
1119 return $data;
1120 }
1121
1122 global $wpdb;
1123
1124 $period = Input::sanitize( $args['period'] ?? '' );
1125 $status = Input::sanitize( $args['status'] ?? 'all' );
1126 $start_date = Input::sanitize( $args['start_date'] ?? '' );
1127 $end_date = Input::sanitize( $args['end_date'] ?? '' );
1128 $order = QueryHelper::get_valid_sort_order( $args['order'] ?? 'DESC' );
1129 $limit = intval( $args['limit'] ?? 0 );
1130 $offset = intval( $args['offset'] ?? 0 );
1131
1132 $post_type = 'shop_order';
1133 $user_meta = '_customer_user';
1134 $wc_hpos = self::hpos_enabled();
1135 $dt_column = $wc_hpos ? 'date_created_gmt' : 'post_date';
1136
1137 $period_query = '';
1138 if ( ! empty( $period ) ) {
1139 if ( 'today' === $period ) {
1140 $period_query = ' AND DATE(' . $dt_column . ') = CURDATE() ';
1141 } elseif ( 'monthly' === $period ) {
1142 $period_query = ' AND MONTH(' . $dt_column . ') = MONTH(CURDATE()) ';
1143 } else {
1144 $period_query = ' AND YEAR(' . $dt_column . ') = YEAR(CURDATE()) ';
1145 }
1146 }
1147
1148 if ( ! empty( $start_date ) && ! empty( $end_date ) ) {
1149 $period_query = $wpdb->prepare(
1150 //phpcs:ignore -- $dt_column is static value
1151 " AND DATE($dt_column) BETWEEN CAST(%s AS DATE) AND CAST(%s AS DATE) ",
1152 $start_date,
1153 $end_date
1154 );
1155 }
1156
1157 $offset_limit_query = '';
1158 if ( $offset && $limit ) {
1159 $offset_limit_query = "LIMIT {$offset}, {$limit}";
1160 }
1161
1162 $status_query = '';
1163 if ( 'all' !== $status ) {
1164 $status_query = $wc_hpos
1165 ? $wpdb->prepare( 'AND status = %s', $status )
1166 : $wpdb->prepare( 'AND post_status = %s', $status );
1167 }
1168
1169 if ( $wc_hpos ) {
1170 //phpcs:disable
1171 $results = $wpdb->get_results(
1172 $wpdb->prepare(
1173 "SELECT SQL_CALC_FOUND_ROWS orders.id AS ID, orders.status AS post_status, orders.date_created_gmt AS post_date, orders.*
1174 FROM {$wpdb->prefix}wc_orders orders
1175 INNER JOIN {$wpdb->prefix}wc_orders_meta order_meta
1176 ON orders.id = order_meta.order_id
1177 AND order_meta.meta_key = '_is_tutor_order_for_course'
1178 WHERE orders.type = %s
1179 AND orders.customer_id = %d
1180 {$status_query}
1181 {$period_query}
1182 ORDER BY orders.id {$order}
1183 {$offset_limit_query}",
1184 $post_type,
1185 $user_id
1186 )
1187 );
1188 //phpcs:enable
1189 } else {
1190 //phpcs:disable
1191 $results = $wpdb->get_results(
1192 $wpdb->prepare(
1193 "SELECT SQL_CALC_FOUND_ROWS {$wpdb->posts}.*
1194 FROM {$wpdb->posts}
1195 INNER JOIN {$wpdb->postmeta} customer
1196 ON id = customer.post_id
1197 AND customer.meta_key = '{$user_meta}'
1198 INNER JOIN {$wpdb->postmeta} tutor_order
1199 ON id = tutor_order.post_id
1200 AND tutor_order.meta_key = '_is_tutor_order_for_course'
1201 WHERE post_type = %s
1202 AND customer.meta_value = %d
1203 {$status_query}
1204 {$period_query}
1205 ORDER BY {$wpdb->posts}.id {$order}
1206 {$offset_limit_query}
1207 ",
1208 $post_type,
1209 $user_id
1210 )
1211 );
1212 //phpcs:enable
1213 }
1214
1215 $total_count = (int) $wpdb->get_var( 'SELECT FOUND_ROWS()' );
1216
1217 $data->results = $results;
1218 $data->total_count = $total_count;
1219
1220 return $data;
1221 }
1222 }
1223