PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 4.0.0
Tutor LMS – eLearning and online course solution v4.0.0
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 day ago Admin.php 1 day ago Ajax.php 1 day ago Announcements.php 1 day ago Assets.php 1 day ago Backend_Page_Trait.php 1 year ago BaseController.php 1 year ago Config.php 1 day ago Container.php 11 months ago Course.php 1 day ago Course_Embed.php 3 years ago Course_Filter.php 1 day ago Course_List.php 1 day ago Course_Settings_Tabs.php 1 day ago Course_Widget.php 1 year ago Custom_Validation.php 1 day ago Dashboard.php 1 day ago Earnings.php 9 months ago FormHandler.php 1 day ago Frontend.php 1 day ago Gutenberg.php 1 year ago Icon.php 1 day ago Input.php 1 day ago Instructor.php 1 day ago Instructors_List.php 1 day ago Lesson.php 1 day ago Options_V2.php 1 day ago Permalink.php 1 day ago Post_types.php 2 days ago Private_Course_Access.php 1 day ago Q_And_A.php 1 day ago Question_Answers_List.php 11 months ago Quiz.php 1 day ago QuizBuilder.php 1 day ago Quiz_Attempts_List.php 1 day ago RestAPI.php 2 years ago Reviews.php 1 day ago Rewrite_Rules.php 2 years ago SampleCourse.php 1 day ago Shortcode.php 1 day ago Singleton.php 1 year ago Student.php 1 day ago Students_List.php 1 year ago Taxonomies.php 1 year ago Template.php 1 day ago Theme_Compatibility.php 3 years ago Tools.php 1 year ago Tools_V2.php 4 weeks ago Tutor.php 1 day ago TutorEDD.php 1 day ago Tutor_Base.php 2 years ago Tutor_Setup.php 1 day ago Upgrader.php 1 day ago User.php 1 day ago UserPreference.php 1 day ago Utils.php 1 day ago Video_Stream.php 3 years ago WhatsNew.php 10 months ago Withdraw.php 1 day ago Withdraw_Requests_List.php 11 months ago WooCommerce.php 1 day ago
WooCommerce.php
1218 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 if ( $order->has_status( 'checkout-draft' ) ) {
630 return;
631 }
632
633 $order_type = $order->get_type();
634
635 if ( 'shop_subscription' === $order_type ) {
636 return;
637 }
638
639 $if_has_course = tutor_utils()->product_belongs_with_course( $product_id );
640
641 if ( $if_has_course && is_object( $order ) ) {
642 $course_id = $if_has_course->post_id;
643 $user_id = get_post_field( 'post_author', $course_id );
644 $order_status = "wc-{$order->get_status()}";
645
646 /**
647 * Return here if already added this product from this order
648 *
649 * @since v1.9.7
650 */
651 $exist_count = (int) $wpdb->get_var(
652 $wpdb->prepare(
653 "SELECT COUNT(earning_id)
654 FROM {$wpdb->prefix}tutor_earnings
655 WHERE course_id=%d
656 AND order_id=%d
657 AND user_id=%d",
658 $course_id,
659 $order_id,
660 $user_id
661 )
662 );
663
664 if ( $exist_count > 0 ) {
665 return;
666 }
667
668 $total_price = $item->get_total();
669
670 list( $admin_amount, $instructor_amount ) = array_values( tutor_split_amounts( $total_price ) );
671
672 $earnings = Earnings::get_instance();
673 $earning_data = apply_filters( 'tutor_new_earning_data', $earnings->prepare_earning_data( $total_price, $course_id, $order_id, $order_status, $admin_amount, $instructor_amount ) );
674
675 $wpdb->insert( $wpdb->prefix . 'tutor_earnings', $earning_data );
676 }
677 }
678
679 /**
680 * Change Earning data status
681 *
682 * @since 1.0.0
683 *
684 * @param int $order_id wc order id.
685 * @param string $status_from previous status.
686 * @param string $status_to current status.
687 *
688 * @return void
689 */
690 public function add_earning_data_status_change( $order_id, $status_from, $status_to ) {
691 if ( ! tutor_utils()->is_tutor_order( $order_id ) ) {
692 tutor_log( 'not tutor order' );
693 return;
694 }
695
696 /**
697 * If it is auto complete order then make earning status complete
698 * to reflect earning for admin & instructor
699 *
700 * @since 2.0.9
701 */
702 if ( self::should_order_auto_complete( $order_id ) ) {
703 $status_to = 'completed';
704 }
705
706 tutor_utils()->change_earning_status( $order_id, $status_to );
707 }
708
709 /**
710 * Course placing order from admin
711 *
712 * @since v.1.6.7
713 *
714 * @param int $order_id wc order id.
715 *
716 * @return void
717 */
718 public function course_placing_order_from_admin( $order_id ) {
719 if ( ! is_admin() ) {
720 return;
721 }
722
723 $order = wc_get_order( $order_id );
724 /**
725 * Loop though each WC order item.
726 *
727 * @var WC_Order_Item $item WC order item object.
728 */
729 foreach ( $order->get_items() as $item ) {
730 $product_id = $item->get_product_id();
731 $if_has_course = tutor_utils()->product_belongs_with_course( $product_id );
732 if ( $if_has_course ) {
733 $course_id = $if_has_course->post_id;
734 $customer_id = $order->get_customer_id();
735 EnrollmentModel::do_enroll( $course_id, $order_id, $customer_id );
736 }
737 }
738 }
739
740 /**
741 * Handle checkout order item.
742 *
743 * @since 3.9.14
744 *
745 * @param object $order the order object.
746 * @param object $item the order item object.
747 * @param string $context the context of checkout page.
748 *
749 * @return void
750 */
751 private function handle_checkout_order_item( $order, $item, $context = 'classic_checkout' ) {
752 if ( ! $order instanceof \WC_Order || ! $item instanceof \WC_Order_Item_Product ) {
753 return;
754 }
755
756 $order_id = $order->get_id();
757 $is_gift_item = apply_filters( 'tutor_is_gift_item', false, $item );
758 if ( $is_gift_item ) {
759 return;
760 }
761
762 $product_id = $item->get_product_id();
763 $if_has_course = tutor_utils()->product_belongs_with_course( $product_id );
764
765 if ( $if_has_course ) {
766 $should_process = apply_filters( 'tutor_wc_should_process_checkout_order_item', true, $item, $order );
767 if ( ! $should_process ) {
768 return;
769 }
770
771 $course_id = $if_has_course->post_id;
772 $customer_id = $order->get_customer_id();
773
774 // Handle course enrollment.
775 if ( ! $customer_id && WC()->session && WC()->session->has_session() ) {
776 $guest_customer_id = WC()->session->get_customer_unique_id();
777 update_post_meta( $course_id, self::TUTOR_WC_GUEST_CUSTOMER_ID, $guest_customer_id );
778 } else {
779 $has_enrollment = tutor_utils()->is_enrolled( $course_id, $customer_id, true );
780 if ( ! $has_enrollment ) {
781 tutor_utils()->do_enroll( $course_id, $order_id, $customer_id );
782 }
783 }
784
785 if ( 'block_checkout' === $context ) {
786 $this->add_earning_data( $item->get_id(), $item, $order_id );
787 }
788 }
789 }
790
791
792 /**
793 * Course placing order from customer
794 *
795 * @since 1.6.7
796 * @since 3.8.0 Filter hook: tutor_is_gift_item added
797 * @since 4.0.0 Filter hook: tutor_wc_should_process_checkout_order_item added
798 * Prevent duplicate enrollment on checkout.
799 *
800 * @param int $item_id item id.
801 * @param mixed $item order item.
802 * @param int $order_id wc order id.
803 *
804 * @return void
805 */
806 public function course_placing_order_from_customer( $item_id, $item, $order_id ) {
807 if ( is_admin() ) {
808 return;
809 }
810
811 $order = wc_get_order( $order_id );
812 if ( ! $order ) {
813 return;
814 }
815
816 /**
817 * Prevent when order is created with WC block checkout page.
818 *
819 * @since 3.9.14
820 */
821 if ( $order->has_status( 'checkout-draft' ) ) {
822 return;
823 }
824
825 $this->handle_checkout_order_item( $order, $item, 'classic_checkout' );
826 }
827
828 /**
829 * Handle order status change by block checkout page.
830 *
831 * @since 3.9.14
832 *
833 * @param int $order_id WooCommerce order ID.
834 * @param string $status_from Previous order status.
835 * @param string $status_to New order status.
836 *
837 * @return void
838 */
839 public function handle_customer_order_by_block_checkout( $order_id, $status_from, $status_to ) {
840 if ( 'checkout-draft' !== $status_from ) {
841 return;
842 }
843
844 // If transitioning to another draft or invalid status, do nothing.
845 if ( in_array( $status_to, array( 'checkout-draft', 'draft' ), true ) ) {
846 return;
847 }
848
849 $order = wc_get_order( $order_id );
850 if ( ! $order ) {
851 return;
852 }
853
854 foreach ( $order->get_items() as $item ) {
855 $this->handle_checkout_order_item( $order, $item, 'block_checkout' );
856 }
857 }
858
859 /**
860 * Handle disabling WooCommerce monetization on WooCommerce plugin deactivation
861 *
862 * @since 1.7.8
863 *
864 * @return void
865 */
866 public function woocommerce_deactivation_handler() {
867 if ( tutor_utils()->get_option( 'monetize_by' ) === 'wc' ) {
868 tutor_utils()->update_option( 'monetize_by', 'free' );
869 /**
870 * Show a reminder to re-enable Tutor monetization to
871 * monetize courses after re-activating WooCommerce:
872 *
873 * Possible follow-up fix: Only show a notice when
874 * WooCommerce was re-activated after this forced
875 * disabling of WooCommerce monetization took place:
876 */
877 update_option( 'tutor_show_woocommerce_notice', true );
878 }
879 }
880
881 /**
882 * Redirect student on enrolled courses after course
883 * enrollment complete if course is purchasable
884 *
885 * @since 1.0.0
886 *
887 * @param int $order_id wc order id.
888 *
889 * @return void
890 */
891 public function redirect_to_enrolled_courses( $order_id ) {
892 if ( ! tutor_utils()->get_option( 'wc_automatic_order_complete_redirect_to_courses' ) ) {
893 // Since 1.9.1.
894 return;
895 }
896
897 // get woo order details.
898 $order = wc_get_order( $order_id );
899 $tutor_product = false;
900 $url = tutor_utils()->tutor_dashboard_url() . 'courses/';
901
902 /**
903 * Loop though each WC order item.
904 *
905 * @var WC_Order_Item $item WC order item object.
906 */
907 foreach ( $order->get_items() as $item ) {
908 $product_id = $item->get_product_id();
909 // check if product associated with tutor course.
910 $if_has_course = tutor_utils()->product_belongs_with_course( $product_id );
911 if ( $if_has_course ) {
912 $tutor_product = true;
913 }
914 }
915
916 // filter redirection url after woocommerce product purchase.
917 $redirect_url = apply_filters( 'tutor_woocommerce_redirect_url', $url );
918
919 // if tutor product & order status completed.
920 if ( $order->has_status( 'completed' ) && $tutor_product ) {
921 wp_safe_redirect( $redirect_url );
922 exit;
923 }
924 }
925
926 /**
927 * Change product url on cart page if product is tutor course
928 *
929 * @since 1.9.8
930 *
931 * @param string $permalink permalink.
932 * @param mixed $cart_item cart item.
933 *
934 * @return mixed
935 */
936 public function tutor_update_product_url( $permalink, $cart_item ) {
937
938 $woo_product_id = $cart_item['product_id'];
939 $product_meta = get_post_meta( $woo_product_id );
940
941 if ( isset( $product_meta['_tutor_product'] ) && $product_meta['_tutor_product'][0] ) {
942
943 global $wpdb;
944 $table = $wpdb->base_prefix . 'postmeta';
945 $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
946
947 if ( $post_id ) {
948 $data = get_post_permalink( $post_id );
949 return $data;
950 }
951 }
952 }
953
954 /**
955 * Mark woocommerce order as complete only from the
956 * client side.
957 *
958 * @since 2.0.5
959 *
960 * @param int $order_id wc order id.
961 *
962 * @return bool
963 */
964 public static function mark_order_complete( int $order_id ): bool {
965 if ( is_admin() ) {
966 return false;
967 }
968
969 $order = \wc_get_order( $order_id );
970 $order->set_status( 'completed' );
971 $update = $order->save();
972
973 return (bool) $update;
974 }
975
976 /**
977 * Check if order should auto complete
978 *
979 * @since 2.0.9
980 *
981 * Bank transfer & check payments will consider as manual
982 * payments. Hence, if user pay with bank & check will not
983 * be auto complete
984 *
985 * @since 2.1.4
986 *
987 * @param int $order_id wc order id.
988 *
989 * @return boolean return true if it should auto complete, otherwise false
990 */
991 public static function should_order_auto_complete( int $order_id ): bool {
992 $auto_complete = false;
993
994 $order = wc_get_order( $order_id );
995 $order_data = is_object( $order ) && method_exists( $order, 'get_data' ) ? $order->get_data() : array();
996
997 $payment_method = isset( $order_data['payment_method'] ) ? $order_data['payment_method'] : '';
998 $monetize_by = tutor_utils()->get_option( 'monetize_by' );
999
1000 $should_auto_complete = tutor_utils()->get_option( 'tutor_woocommerce_order_auto_complete' );
1001 $is_enabled_auto_complete = 'wc' === $monetize_by && $should_auto_complete ? true : false;
1002
1003 $manual_payments = array( 'cod', 'cheque', 'bacs' );
1004 $order_status = method_exists( $order, 'get_status' ) ? $order->get_status() : '';
1005
1006 if ( 'completed' !== $order_status ) {
1007 $is_tutor_order = tutor_utils()->is_tutor_order( $order->get_id() );
1008
1009 /**
1010 * Is tutor order condition added with other conditions,
1011 * to prevent order other than Tutor get completed
1012 *
1013 * @since 2.1.6
1014 */
1015 if ( ! is_admin() && $is_enabled_auto_complete && 'processing' === $order_status && ! in_array( $payment_method, $manual_payments ) && $is_tutor_order ) {
1016 $auto_complete = true;
1017 }
1018 } else {
1019 $auto_complete = true;
1020 }
1021
1022 return $auto_complete;
1023 }
1024
1025 /**
1026 * Count orders for a user.
1027 *
1028 * @since 4.0.0
1029 *
1030 * @param array $args array of arguments.
1031 *
1032 * @return int return count of orders.
1033 */
1034 public static function get_count( $args = array() ): int {
1035 $query = wc_get_orders(
1036 array(
1037 'customer' => $args['user_id'] ?? 0,
1038 'status' => $args['status'] ?? 'all',
1039 'date_created' => $args['date_created'] ?? null,
1040 'meta_query' => array(
1041 array(
1042 'key' => '_is_tutor_order_for_course',
1043 'compare' => 'EXISTS',
1044 ),
1045 ),
1046 // Limit 1 for query performance.
1047 'limit' => 1,
1048 'paginate' => true,
1049 )
1050 );
1051
1052 return $query->total;
1053 }
1054
1055 /**
1056 * Filter order history status options.
1057 *
1058 * @since 4.0.0
1059 *
1060 * @param array $options the status options.
1061 * @param string $seleted the selected status.
1062 *
1063 * @return array<array{label: string, value: string, count: int, url: string, active: bool}>
1064 */
1065 public function filter_order_history_status_options( $options, $seleted ) {
1066 $url = get_pagenum_link();
1067 $user_id = get_current_user_id();
1068
1069 $statuses = array_merge( array( 'all' => 'All' ), wc_get_order_statuses() );
1070 if ( isset( $statuses['wc-checkout-draft'] ) ) {
1071 unset( $statuses['wc-checkout-draft'] );
1072 }
1073
1074 $start_date = Input::get( 'start_date' );
1075 $end_date = Input::get( 'end_date' );
1076
1077 $options = array();
1078
1079 foreach ( $statuses as $key => $status ) {
1080 $params = array(
1081 'user_id' => $user_id,
1082 'status' => $key,
1083 );
1084
1085 if ( ! empty( $start_date ) && ! empty( $end_date ) ) {
1086 $params['date_created'] = $start_date . '...' . $end_date;
1087 }
1088
1089 $options[] = array(
1090 'label' => $status,
1091 'value' => $key,
1092 'count' => self::get_count( $params ),
1093 'url' => UrlHelper::add_query_params( $url, array( 'data' => $key ) ),
1094 'active' => $key === $seleted || ( empty( $key ) && 'all' === $seleted ),
1095 );
1096 }
1097
1098 return $options;
1099 }
1100
1101 /**
1102 * Filter tutor get orders by user id.
1103 *
1104 * @since 4.0.0
1105 *
1106 * @param object $data data.
1107 * @param int $user_id user id.
1108 * @param array $args array of arguments.
1109 *
1110 * @return object {results: array, total_count: int}
1111 */
1112 public function filter_tutor_get_orders_by_user_id( $data, $user_id, $args ) {
1113 if ( ! $user_id ) {
1114 return $data;
1115 }
1116
1117 global $wpdb;
1118
1119 $period = Input::sanitize( $args['period'] ?? '' );
1120 $status = Input::sanitize( $args['status'] ?? 'all' );
1121 $start_date = Input::sanitize( $args['start_date'] ?? '' );
1122 $end_date = Input::sanitize( $args['end_date'] ?? '' );
1123 $order = QueryHelper::get_valid_sort_order( $args['order'] ?? 'DESC' );
1124 $limit = intval( $args['limit'] ?? 0 );
1125 $offset = intval( $args['offset'] ?? 0 );
1126
1127 $post_type = 'shop_order';
1128 $user_meta = '_customer_user';
1129 $wc_hpos = self::hpos_enabled();
1130 $dt_column = $wc_hpos ? 'date_created_gmt' : 'post_date';
1131
1132 $period_query = '';
1133 if ( ! empty( $period ) ) {
1134 if ( 'today' === $period ) {
1135 $period_query = ' AND DATE(' . $dt_column . ') = CURDATE() ';
1136 } elseif ( 'monthly' === $period ) {
1137 $period_query = ' AND MONTH(' . $dt_column . ') = MONTH(CURDATE()) ';
1138 } else {
1139 $period_query = ' AND YEAR(' . $dt_column . ') = YEAR(CURDATE()) ';
1140 }
1141 }
1142
1143 if ( ! empty( $start_date ) && ! empty( $end_date ) ) {
1144 $period_query = $wpdb->prepare(
1145 //phpcs:ignore -- $dt_column is static value
1146 " AND DATE($dt_column) BETWEEN CAST(%s AS DATE) AND CAST(%s AS DATE) ",
1147 $start_date,
1148 $end_date
1149 );
1150 }
1151
1152 $offset_limit_query = '';
1153 if ( $offset && $limit ) {
1154 $offset_limit_query = "LIMIT {$offset}, {$limit}";
1155 }
1156
1157 $status_query = '';
1158 if ( 'all' !== $status ) {
1159 $status_query = $wc_hpos
1160 ? $wpdb->prepare( 'AND status = %s', $status )
1161 : $wpdb->prepare( 'AND post_status = %s', $status );
1162 }
1163
1164 if ( $wc_hpos ) {
1165 //phpcs:disable
1166 $results = $wpdb->get_results(
1167 $wpdb->prepare(
1168 "SELECT SQL_CALC_FOUND_ROWS orders.id AS ID, orders.status AS post_status, orders.date_created_gmt AS post_date, orders.*
1169 FROM {$wpdb->prefix}wc_orders orders
1170 INNER JOIN {$wpdb->prefix}wc_orders_meta order_meta
1171 ON orders.id = order_meta.order_id
1172 AND order_meta.meta_key = '_is_tutor_order_for_course'
1173 WHERE orders.type = %s
1174 AND orders.customer_id = %d
1175 {$status_query}
1176 {$period_query}
1177 ORDER BY orders.id {$order}
1178 {$offset_limit_query}",
1179 $post_type,
1180 $user_id
1181 )
1182 );
1183 //phpcs:enable
1184 } else {
1185 //phpcs:disable
1186 $results = $wpdb->get_results(
1187 $wpdb->prepare(
1188 "SELECT SQL_CALC_FOUND_ROWS {$wpdb->posts}.*
1189 FROM {$wpdb->posts}
1190 INNER JOIN {$wpdb->postmeta} customer
1191 ON id = customer.post_id
1192 AND customer.meta_key = '{$user_meta}'
1193 INNER JOIN {$wpdb->postmeta} tutor_order
1194 ON id = tutor_order.post_id
1195 AND tutor_order.meta_key = '_is_tutor_order_for_course'
1196 WHERE post_type = %s
1197 AND customer.meta_value = %d
1198 {$status_query}
1199 {$period_query}
1200 ORDER BY {$wpdb->posts}.id {$order}
1201 {$offset_limit_query}
1202 ",
1203 $post_type,
1204 $user_id
1205 )
1206 );
1207 //phpcs:enable
1208 }
1209
1210 $total_count = (int) $wpdb->get_var( 'SELECT FOUND_ROWS()' );
1211
1212 $data->results = $results;
1213 $data->total_count = $total_count;
1214
1215 return $data;
1216 }
1217 }
1218