Current File : /home/escuelai/public_html/wp-content/plugins/learnpress/inc/Models/CourseModel.php
<?php

/**
 * Class Course Model
 * Purpose: Use to map property separate table learnpress_course
 * Field json for store all value of single course.
 * Another fields for query list courses faster
 *
 * @package LearnPress/Classes
 * @version 1.0.3
 * @since 4.2.6.9
 */

namespace LearnPress\Models;

use Exception;
use LearnPress\Models\UserItems\UserCourseModel;
use LP_Admin_Editor_Course;
use LP_Course_Cache;
use LP_Course_DB;
use LP_Course_Item;
use LP_Course_JSON_DB;
use LP_Course_JSON_Filter;
use LP_Database;
use LP_Datetime;
use LP_Helper;
use LP_Lesson;
use LP_Post_Type_Filter;
use LP_Section_CURD;
use LP_Settings;
use stdClass;
use Throwable;
use WP_Error;
use WP_Post;

class CourseModel {
	/**
	 * Auto increment, Primary key
	 *
	 * @var int
	 */
	public $ID = 0;
	/**
	 * @var string author id, foreign key
	 */
	public $post_author = 0;
	/**
	 * @var string post date gmt
	 */
	public $post_date_gmt = null;
	/**
	 * @var string post content
	 */
	public $post_content = '';
	/**
	 * @var string Post title
	 */
	public $post_title = '';
	/**
	 * @var string Post Status (publish, draft, ...)
	 */
	public $post_status = '';
	/**
	 * @var string Post name (slug for link)
	 */
	public $post_name = '';
	/**
	 * @var float price only using for filter courses, don't use for course detail
	 * Because price can change by date if set schedule sale
	 */
	public $price_to_sort = 0;
	public $is_sale       = 0;
	/**
	 * @var string JSON Store all data a single course
	 */
	public $json = null; // Only set when save, don't set when get
	/**
	 * @var string lang of Course
	 */
	public $lang = null;
	/********** Field not on table **********/
	/**
	 * @var stdClass all meta data
	 */
	public $meta_data = null;
	public $image_url = null;
	public $permalink = '';
	public $categories;
	public $tags;
	private $price; // Not save in database, must auto reload calculate
	private $passing_condition = '';
	public $post_excerpt       = '';
	/**
	 * @var int ID of first item
	 */
	public $first_item_id;
	/**
	 * @var null|object info total items {'count_items': 20, 'lp_lesson': 10, 'lp_quiz': 10, ...}
	 */
	public $total_items;
	/**
	 * @var array list sections items
	 */
	public $sections_items;

	/**
	 * If data get from database, map to object.
	 * Else create new object to save data to database.
	 *
	 * @param array|object|mixed $data
	 */
	public function __construct( $data = null ) {
		if ( $data ) {
			$this->map_to_object( $data );
		}

		if ( is_null( $this->meta_data ) ) {
			$this->meta_data = new stdClass();
		}
	}

	/**
	 * Map array, object data to CourseModel.
	 * Use for data get from database.
	 *
	 * @param array|object|mixed $data
	 *
	 * @return CourseModel
	 */
	public function map_to_object( $data ): CourseModel {
		foreach ( $data as $key => $value ) {
			if ( property_exists( $this, $key ) ) {
				$this->{$key} = $value;
			}
		}

		return $this;
	}

	/**
	 * Get course id
	 *
	 * @return int
	 */
	public function get_id(): int {
		return $this->ID;
	}

	public function get_title(): string {
		$course_post = new CoursePostModel( $this );

		return $course_post->get_the_title();
	}

	/**
	 * Get image url
	 * if not check get from Post
	 *
	 * @param string|int[] $size
	 *
	 * @return string
	 * @since 4.2.6.9
	 * @version 1.0.1
	 */
	public function get_image_url( $size = 'post-thumbnail' ): string {
		if ( isset( $this->image_url ) ) {
			return $this->image_url;
		}

		$post      = new CoursePostModel( $this );
		$image_url = $post->get_image_url( $size );

		$this->image_url = $image_url;

		return $image_url;
	}

	/**
	 * Get author model
	 * Check has data on table learnpress_courses return
	 * if not check get from Post
	 *
	 * @return UserModel|false
	 */
	public function get_author_model() {
		$post = new CoursePostModel( $this );
		return $post->get_author_model();
	}

	/**
	 * Get status of course
	 *
	 * @return string
	 * @since 4.2.7.3
	 * @version 1.0.0
	 */
	public function get_status(): string {
		return $this->post_status;
	}

	/**
	 * Get categories
	 * Check has data on table learnpress_courses return
	 * if not check get from Post
	 *
	 * @return array
	 */
	public function get_categories(): array {
		if ( isset( $this->categories ) ) {
			return $this->categories;
		}

		$post       = new PostModel( $this );
		$categories = $post->get_categories();

		$this->categories = $categories;

		return $this->categories;
	}

	/**
	 * Get tags of course.
	 *
	 * @return array
	 * @since 4.2.7.2
	 * @version 1.0.0
	 */
	public function get_tags(): array {
		if ( isset( $this->tags ) ) {
			return $this->tags;
		}

		$post = new PostModel( $this );
		$tags = $post->get_tags();

		$this->tags = $tags;

		return $this->tags;
	}

	/**
	 * Get price
	 *
	 * @return float
	 */
	public function get_price(): float {
		/*if ( ! empty( $this->price ) ) {
			return $this->price;
		}*/

		if ( $this->has_sale_price() ) {
			$price = $this->get_sale_price();
		} else {
			$price = $this->get_regular_price();
		}

		$this->price                                        = (float) $price;
		$this->meta_data->{CoursePostModel::META_KEY_PRICE} = (float) $price;

		return apply_filters( 'learnPress/course/price', (float) $price, $this->get_id() );
	}

	/**
	 * Get regular price
	 * Check has data on table learnpress_courses return
	 * if not check get from Post
	 * Value can be string empty if not set
	 *
	 * @return float|string
	 */
	public function get_regular_price() {
		$key = CoursePostModel::META_KEY_REGULAR_PRICE;
		if ( $this->meta_data && isset( $this->meta_data->{$key} ) ) {
			return $this->meta_data->{$key};
		}

		$coursePost              = new CoursePostModel( $this );
		$this->meta_data->{$key} = $coursePost->get_regular_price();

		return $this->meta_data->{$key};
	}

	/**
	 * Get sale price
	 * Sale price can is string empty if not set
	 * Sale price set if is number >= 0
	 * Check has data on table learnpress_courses return
	 * if not check get from Post
	 *
	 * @return float|string
	 */
	public function get_sale_price() {
		$key = CoursePostModel::META_KEY_SALE_PRICE;
		if ( $this->meta_data && isset( $this->meta_data->{$key} ) ) {
			return $this->meta_data->{$key};
		}

		$coursePost              = new CoursePostModel( $this );
		$sale_price              = $coursePost->get_sale_price();
		$this->meta_data->{$key} = $sale_price;

		return $this->meta_data->{$key};
	}

	/**
	 * Check course has 'sale price'
	 *
	 * @return bool
	 */
	public function has_sale_price(): bool {
		$has_sale_price = false;
		$regular_price  = $this->get_regular_price();
		$sale_price     = $this->get_sale_price();
		$start_date     = $this->get_sale_start();
		$end_date       = $this->get_sale_end();

		if ( $sale_price !== '' && (float) $regular_price > (float) $sale_price ) {
			$has_sale_price = true;
		}

		// Check in days sale
		if ( $has_sale_price && ! empty( $start_date ) && ! empty( $end_date ) ) {
			$nowObj = new LP_Datetime();
			// Compare via timezone WP
			$nowStr = $nowObj->toSql( true );
			$now    = strtotime( $nowStr );
			$end    = strtotime( $end_date );
			$start  = strtotime( $start_date );

			$has_sale_price = $now >= $start && $now <= $end;
		}

		return apply_filters( 'learnPress/course/has-sale-price', $has_sale_price, $this );
	}

	/**
	 * Get date sale start
	 *
	 * @return mixed
	 */
	public function get_sale_start() {
		return $this->get_meta_value_by_key( CoursePostModel::META_KEY_SALE_START );
	}

	/**
	 * Get date sale end
	 *
	 * @return mixed
	 */
	public function get_sale_end() {
		return $this->get_meta_value_by_key( CoursePostModel::META_KEY_SALE_END );
	}

	/**
	 * Check if a course is Free
	 *
	 * @return bool
	 */
	public function is_free(): bool {
		return apply_filters( 'learnPress/course/is-free', $this->get_price() == 0, $this );
	}

	/**
	 * Check if a course is enabled Offline
	 *
	 * @return bool
	 */
	public function is_offline(): bool {
		return $this->get_meta_value_by_key( CoursePostModel::META_KEY_OFFLINE_COURSE, 'no' ) === 'yes';
	}

	/**
	 * Check option "Block course when expire" enable.
	 *
	 * @return bool
	 * @since 4.2.7.3
	 * @version 1.0.0
	 */
	public function enable_block_when_expire(): bool {
		return $this->get_meta_value_by_key( CoursePostModel::META_KEY_BLOCK_EXPIRE_DURATION, 'no' ) === 'yes';
	}

	/**
	 * Check option "Block course when finished" enable.
	 *
	 * @return bool
	 * @since 4.2.7.6
	 * @version 1.0.0
	 */
	public function enable_block_when_finished(): bool {
		return $this->get_meta_value_by_key( CoursePostModel::META_KEY_BLOCK_FINISH, 'no' ) === 'yes';
	}

	/**
	 * Get first item of course
	 *
	 * @return int
	 */
	public function get_first_item_id(): int {
		if ( isset( $this->first_item_id ) ) {
			return $this->first_item_id;
		}

		try {
			$this->first_item_id = LP_Course_DB::getInstance()->get_first_item_id( $this->get_id() );
		} catch ( Throwable $e ) {
			$this->first_item_id = 0;
		}

		return $this->first_item_id;
	}

	/**
	 * Get total items of course
	 *
	 * @return null|object
	 */
	public function get_total_items() {
		if ( isset( $this->total_items ) ) {
			return $this->total_items;
		}

		try {
			$this->total_items = LP_Course_DB::getInstance()->get_total_items( $this->get_id() );
		} catch ( Throwable $e ) {
			$this->total_items = null;
		}

		return $this->total_items;
	}

	/**
	 * Get total sections of course
	 *
	 * @return int
	 * @since 4.2.7.6
	 * @version 1.0.0
	 */
	public function get_total_sections(): int {
		$section_items = $this->get_section_items();

		return count( $section_items );
	}

	/**
	 * Get total items of course
	 *
	 * @return array
	 */
	public function get_section_items(): array {
		if ( isset( $this->sections_items ) ) {
			return $this->sections_items;
		}

		try {
			$this->sections_items = $this->get_sections_and_items_course_from_db_and_sort();
		} catch ( Throwable $e ) {
			$this->sections_items = [];
		}

		return $this->sections_items;
	}

	/**
	 * Get section id of item
	 *
	 * @param int $item_id
	 *
	 * @return int
	 * @since 4.2.8
	 * @version 1.0.0
	 */
	public function get_section_of_item( int $item_id ): int {
		$section_id = 0;

		$section_items = $this->get_section_items();
		foreach ( $section_items as $section ) {
			foreach ( $section->items as $item ) {
				$item_id_check = (int) ( $item->item_id ?? $item->id ?? 0 );
				if ( $item_id_check === $item_id ) {
					$section_id = $section->section_id ?? $section->id ?? 0;
					break;
				}
			}
		}

		return (int) $section_id;
	}

	/**
	 * Get course Evaluation type.
	 *
	 * @return string
	 * @since 4.2.7.3
	 * @version 1.0.1
	 */
	public function get_evaluation_type(): string {
		return $this->get_meta_value_by_key( CoursePostModel::META_KEY_EVALUATION_TYPE, 'evaluate_lesson' );
	}

	/**
	 * Get course passing condition value.
	 *
	 * @return float
	 * @since 4.2.7.3
	 * @version 1.0.0
	 */
	public function get_passing_condition(): float {
		return (float) $this->get_meta_value_by_key( CoursePostModel::META_KEY_PASSING_CONDITION, 80 );
	}

	/**
	 * Get final quiz id
	 *
	 * @return int
	 */
	public function get_final_quiz(): int {
		$key = '_lp_final_quiz';
		if ( ! empty( $this->meta_data->{$key} ) ) {
			return $this->meta_data->$key;
		}

		$final_quiz = 0;

		// Not use array_reverse, it's make change object
		$section_items = $this->get_section_items();
		$found         = 0;
		for ( $i = count( $section_items ); $i > 0; $i-- ) {
			$section = $section_items[ $i - 1 ];
			for ( $j = count( $section->items ); $j > 0; $j-- ) {
				$item = $section->items[ $j - 1 ];
				if ( learn_press_get_post_type( $item->id ) === LP_QUIZ_CPT ) {
					$final_quiz = $item->id;
					$found      = 1;
					break;
				}
			}

			if ( $found ) {
				break;
			}
		}

		$evaluation_type = $this->get_evaluation_type();
		if ( $evaluation_type === 'evaluate_final_quiz' ) {
			if ( isset( $final_quiz ) ) {
				update_post_meta( $this->ID, $key, $final_quiz );
			} else {
				delete_post_meta( $this->ID, $key );
			}
		}

		$this->meta_data->{$key} = $final_quiz;

		return $final_quiz;
	}

	/**
	 * Get all sections and items from database, then handle sort
	 * Only call when data change or not set
	 *
	 * @return array
	 * @since 4.1.6.9
	 * @version 1.0.4
	 * @author tungnx
	 */
	private function get_sections_and_items_course_from_db_and_sort(): array {
		$sections_items = [];
		$course_id      = $this->get_id();
		$lp_course_db   = LP_Course_DB::getInstance();

		try {
			$sections_results       = $lp_course_db->get_sections( $course_id );
			$sections_items_results = $lp_course_db->get_full_sections_and_items_course( $course_id );
			$count_items            = count( $sections_items_results );
			$index_items_last       = $count_items - 1;
			$section_current        = 0;

			/**
			 * @var $section_order_plus int
			 * @var $item_order_plus int
			 * To fixed case: section order start from 0, item order start from 0
			 */
			$section_order_plus = 0;
			$item_order_plus    = 0;
			foreach ( $sections_items_results as $index => $sections_item ) {
				$section_new   = $sections_item->section_id;
				$section_order = (int) $sections_item->section_order;
				if ( $index === 0 && $section_order === 0 ) {
					$section_order_plus = 1;
				}

				$section_order += $section_order_plus;
				$item           = new stdClass();
				$item->id       = $sections_item->item_id;
				$item->item_id  = $sections_item->item_id;
				$item_order     = (int) $sections_item->item_order;
				if ( $index === 0 && $item_order === 0 ) {
					$item_order_plus = 1;
				}

				$item_order      += $item_order_plus;
				$item->order      = $item_order;
				$item->item_order = $item_order;
				$item->type       = $sections_item->item_type;
				$item->item_type  = $sections_item->item_type;
				$item_tmp         = LP_Course_Item::get_item( $item->id );
				if ( $item_tmp ) {
					$item->title   = html_entity_decode( $item_tmp->get_title() );
					$item->preview = $item_tmp->is_preview();
				}

				if ( $section_new != $section_current ) {
					$sections_items[ $section_new ]                      = new stdClass();
					$sections_items[ $section_new ]->id                  = $section_new; // old field will be deprecated in future
					$sections_items[ $section_new ]->section_id          = $section_new; // new field
					$sections_items[ $section_new ]->order               = $section_order; // old field will be deprecated in future
					$sections_items[ $section_new ]->section_order       = $section_order; // new field
					$sections_items[ $section_new ]->title               = html_entity_decode( $sections_item->section_name ); // old field will be deprecated in future
					$sections_items[ $section_new ]->section_name        = html_entity_decode( $sections_item->section_name ); // new field
					$sections_items[ $section_new ]->description         = html_entity_decode( $sections_item->section_description ); // old field will be deprecated in future
					$sections_items[ $section_new ]->section_description = html_entity_decode( $sections_item->section_description ); // new field
					$sections_items[ $section_new ]->items               = [];

					// Sort item by item_order
					if ( $section_current != 0 ) {
						usort(
							$sections_items[ $section_current ]->items,
							function ( $item1, $item2 ) {
								return $item1->item_order - $item2->item_order;
							}
						);
					}

					$section_current = $section_new;
				}

				$sections_items[ $section_new ]->items[ $item->item_id ] = $item;

				if ( $index_items_last === $index ) {
					usort(
						$sections_items[ $section_current ]->items,
						function ( $item1, $item2 ) {
							return $item1->item_order - $item2->item_order;
						}
					);
				}
			}

			// Check case if section empty items
			foreach ( $sections_results as $section ) {
				$section_id = $section->section_id;
				if ( isset( $sections_items[ $section_id ] ) ) {
					continue;
				}

				$section_obj                      = new stdClass();
				$section_obj->id                  = $section_id;
				$section_obj->section_id          = $section_id;
				$section_obj->order               = $section->section_order;
				$section_obj->section_order       = $section->section_order;
				$section_obj->title               = html_entity_decode( $section->section_name );
				$section_obj->section_name        = html_entity_decode( $section->section_name );
				$section_obj->description         = html_entity_decode( $section->section_description );
				$section_obj->section_description = html_entity_decode( $section->section_description );
				$section_obj->items               = [];
				$sections_items[ $section_id ]    = $section_obj;
			}

			// Sort section by section_order
			usort(
				$sections_items,
				function ( $section1, $section2 ) {
					return $section1->section_order - $section2->section_order;
				}
			);
		} catch ( Throwable $e ) {
			error_log( $e->getMessage() );
		}

		return $sections_items;
	}

	/**
	 * Get permalink course
	 *
	 * @return string
	 */
	public function get_permalink(): string {
		if ( ! empty( $this->permalink ) ) {
			return $this->permalink;
		}

		try {
			$coursePostModel = new CoursePostModel( $this );
			$this->permalink = $coursePostModel->get_permalink();
		} catch ( Throwable $e ) {
			$this->permalink = '';
		}

		return $this->permalink;
	}

	/**
	 * Get value option No enroll requirement
	 *
	 * @return mixed
	 */
	public function get_no_enroll_requirement() {
		return $this->get_meta_value_by_key( CoursePostModel::META_KEY_NO_REQUIRED_ENROLL, 'no' );
	}

	/**
	 * Get description of Course
	 *
	 * @return string
	 */
	public function get_description(): string {
		$course_post = new CoursePostModel( $this );

		return $course_post->get_the_content();
	}

	/**
	 * Get short description of Course
	 *
	 * @return string
	 */
	public function get_short_description(): string {
		$course_post = new CoursePostModel( $this );

		return $course_post->get_the_excerpt();
	}

	/**
	 * Get value option No enroll requirement
	 *
	 * @return bool
	 */
	public function has_no_enroll_requirement(): bool {
		return $this->get_no_enroll_requirement() === 'yes';
	}

	/**
	 * Get value from meta data by key
	 *
	 * @param string $key
	 * @param mixed|false $default_value
	 *
	 * @return false|mixed
	 * @since 4.2.6.9
	 * @version 1.0.1
	 */
	public function get_meta_value_by_key( string $key, $default_value = false ) {
		if ( $this->meta_data instanceof stdClass && isset( $this->meta_data->{$key} ) ) {
			$value = maybe_unserialize( $this->meta_data->{$key} );
		} else {
			$coursePost = new CoursePostModel( $this );
			$value      = $coursePost->get_meta_value_by_key( $key, $default_value );
		}

		$this->meta_data->{$key} = $value;

		return $value;
	}

	/**
	 * Check course is in stock
	 * True is in stock, False is out of stock
	 *
	 * @return mixed
	 * @since 3.0.0
	 * @version 1.0.1
	 */
	public function is_in_stock() {
		$in_stock    = true;
		$max_allowed = (int) $this->get_meta_value_by_key( CoursePostModel::META_KEY_MAX_STUDENTS, 0 );

		if ( $max_allowed ) {
			$in_stock = $max_allowed > $this->get_total_user_enrolled_or_purchased();
		}

		return apply_filters( 'learn-press/is-in-stock', $in_stock, $this->get_id() );
	}

	/**
	 * Check course is enable repurchase
	 *
	 * @return bool
	 * @since 4.2.7.2
	 * @version 1.0.1
	 */
	public function enable_allow_repurchase(): bool {
		$enable = $this->get_meta_value_by_key( CoursePostModel::META_KEY_ALLOW_COURSE_REPURCHASE, 'no' );

		return 'yes' === $enable;
	}

	/**
	 * Type repurchase
	 *
	 * @return string
	 * @since 4.2.7.3
	 * @version 1.0.0
	 */
	public function get_type_repurchase(): string {
		return $this->get_meta_value_by_key( CoursePostModel::META_KEY_COURSE_REPURCHASE_OPTION, 'reset' );
	}

	/**
	 * Get external link
	 *
	 * @return string
	 */
	public function get_external_link(): string {
		return esc_url_raw(
			$this->get_meta_value_by_key( CoursePostModel::META_KEY_EXTERNAL_LINK_BY_COURSE, '' )
		);
	}

	/**
	 * Get item's link
	 * @move from LP_Abstract_Course
	 *
	 * @param int $item_id
	 * @param string $item_type
	 *
	 * @return string
	 * @since 3.0.0
	 * @version 1.0.2
	 */
	public function get_item_link( int $item_id, string $item_type = '' ): string {
		if ( empty( $item_type ) ) {
			$item_type = get_post_type( $item_id );
		}
		$course_permalink = trailingslashit( $this->get_permalink() );
		$item_slug        = get_post_field( 'post_name', $item_id );

		$slug_prefixes = apply_filters(
			'learn-press/course/custom-item-prefixes',
			array(
				LP_QUIZ_CPT   => sanitize_title_with_dashes( LP_Settings::get_option( 'quiz_slug', 'quizzes' ) ),
				LP_LESSON_CPT => sanitize_title_with_dashes( LP_Settings::get_option( 'lesson_slug', 'lessons' ) ),
			),
			$this->get_id()
		);

		$slug_prefix = trailingslashit( $slug_prefixes[ $item_type ] ?? '' );
		$item_link   = trailingslashit( $course_permalink . $slug_prefix . $item_slug );

		return apply_filters( 'learn-press/course/item-link', $item_link, $item_id, $this );
	}

	/**
	 * Get total user enrolled, purchased or finished
	 *
	 * @move from LP_Abstract_Course
	 * @return int
	 * @version 1.0.1
	 * @since 4.1.4
	 */
	public function get_total_user_enrolled_or_purchased(): int {
		$total           = 0;
		$lp_course_cache = new LP_Course_Cache( true );

		try {
			$total = $lp_course_cache->get_total_students_enrolled_or_purchased( $this->get_id() );
			if ( false !== $total ) {
				return $total;
			}

			$lp_course_db = LP_Course_DB::getInstance();
			$total        = $lp_course_db->get_total_user_enrolled_or_purchased( $this->get_id() );
			$lp_course_cache->set_total_students_enrolled_or_purchased( $this->get_id(), $total );
		} catch ( Throwable $e ) {
			error_log( $e->getMessage() );
		}

		return $total;
	}

	/**
	 * Get fake students.
	 *
	 * @return int
	 */
	public function get_fake_students(): int {
		return (int) $this->get_meta_value_by_key( CoursePostModel::META_KEY_STUDENTS, 0 );
	}

	/**
	 * Count number of students enrolled course.
	 * Check global settings `enrolled_students_number`
	 * and add the fake value if both are set.
	 *
	 * @return int
	 * @move from LP_Abstract_Course
	 */
	public function count_students(): int {
		$total  = $this->get_total_user_enrolled_or_purchased();
		$total += $this->get_fake_students();

		return $total;
	}

	/**
	 * Count total items in Course
	 * item_type empty will return all items if exists.
	 *
	 * @param string $item_type
	 *
	 * @return int
	 * @since 4.2.7.3
	 * @version 1.0.1
	 */
	public function count_items( string $item_type = '' ): int {
		$count = 0;

		$total_items = $this->get_total_items();
		if ( empty( $item_type ) ) {
			$count = $total_items->count_items ?? 0;
		} elseif ( isset( $total_items->{$item_type} ) ) {
			return $total_items->{$item_type};
		}

		return $count;
	}

	/**
	 * Get Duration of course
	 * Timestamp in second
	 *
	 * @return int
	 */
	public function get_duration(): string {
		return $this->get_meta_value_by_key( CoursePostModel::META_KEY_DURATION, '0' );
	}

	/**
	 * Check user can enroll course.
	 * @move from can_enroll_course method of LP_User class, since 4.1.1
	 *
	 * @param UserModel|false $user
	 *
	 * @return bool|WP_Error
	 * @since 4.2.7.3
	 * @version 1.0.1
	 */
	public function can_enroll( $user ) {
		$can_enroll = true;
		$error_code = '';

		$user_id = 0;
		if ( $user instanceof UserModel ) {
			$user_id = $user->get_id();
		}

		try {
			if ( ! in_array( $this->post_status, [ 'publish', 'private' ] ) ) {
				$error_code = 'course_not_publish';
				throw new Exception( __( 'The course is not public', 'learnpress' ) );
			}

			$userCourseModel           = UserCourseModel::find( $user_id, $this->get_id(), true );
			$enable_no_required_enroll = $this->has_no_enroll_requirement();
			$out_of_stock              = ! $this->is_in_stock();

			// Case user can retake course.
			if ( $userCourseModel && $userCourseModel->can_retake() ) {
				$error_code = 'course_can_retry';
				throw new Exception( esc_html__( 'Course can retake.', 'learnpress' ) );
			}

			// Case course is out of stock, show message when user is not login or user_item not exits
			if ( $out_of_stock &&
				( ! $user || ! $userCourseModel || ! $userCourseModel->has_enrolled_or_finished() ) ) {
				$error_code = 'course_out_of_stock';
				throw new Exception( __( 'The course is full of students.', 'learnpress' ) );
			}

			// Case user is logged in and user_item exists
			if ( $userCourseModel && $user ) {
				if ( $userCourseModel->has_enrolled() ) {
					$error_code = 'course_is_enrolled';
					throw new Exception( __( 'This course is already enrolled!', 'learnpress' ) );
				} elseif ( $userCourseModel->has_finished() ) {
					$error_code = 'course_is_finished';
					throw new Exception( __( 'The course is finished.', 'learnpress' ) );
				}
			}

			if ( $enable_no_required_enroll ) {
				if ( ! $user ) {
					$error_code = 'course_is_no_required_enroll_not_login';
					throw new Exception(
						__( 'Enrollment in the course is not mandatory. You can access course for learning now.', 'learnpress' )
					);
				} else {

				}
			} else {
				if ( ! empty( $this->get_external_link() )
					&& ( ! $userCourseModel || $userCourseModel->get_status() === LP_USER_COURSE_CANCEL )
					&& ! $this->is_offline() ) {
					$error_code = 'course_is_external';
					throw new Exception( __( 'The course is external', 'learnpress' ) );
				}

				if ( ! $this->is_free() ) {
					if ( ! $user ) {
						$error_code = 'course_is_not_purchased_not_login';
						throw new Exception( __( 'The course is not purchased.', 'learnpress' ) );
					} elseif ( ! $userCourseModel || ! $userCourseModel->has_purchased() ) {
						$error_code = 'course_is_not_purchased';
						throw new Exception( __( 'The course is not purchased.', 'learnpress' ) );
					}
				}
			}
		} catch ( Throwable $e ) {
			if ( empty( $error_code ) ) {
				$error_code = 'course_can_not_enroll';
			}
			$can_enroll = new WP_Error( $error_code, $e->getMessage() );
		}

		// Hook old
		if ( has_filter( 'learn-press/user/can-enroll-course' ) ) {
			$output          = new stdClass();
			$output->check   = true;
			$output->message = '';
			if ( $can_enroll instanceof WP_Error ) {
				$output->check   = false;
				$output->message = $can_enroll->get_error_message();
			}

			$course_old = learn_press_get_course( $this->get_id() );
			$user_old   = learn_press_get_user( $user_id );
			$output     = apply_filters( 'learn-press/user/can-enroll-course', $output, $course_old, false, $user_old );
			if ( $output === false ) {
				$can_enroll = new WP_Error( '', '' );
			} elseif ( ! $output->check && $output->message ) {
				$can_enroll = new WP_Error( 'error_custom', $output->message );
			}
			//_deprecated_function( 'The learn-press/user/can-enroll-course filter', '4.2.7.3', 'learn-press/user/can-enroll/course' );
		}

		return apply_filters( 'learn-press/user/can-enroll/course', $can_enroll, $this, $user );
	}

	/**
	 * Check user can purchase course.
	 * @move from can_purchase_course method of LP_User class, since 4.0.8
	 * @use LP_User::can_purchase_course
	 *
	 * @param UserModel|false $user
	 *
	 * @return bool|WP_Error
	 * @since 4.2.7.3
	 * @version 1.0.0
	 */
	public function can_purchase( $user ) {
		$can_purchase = true;
		$error_code   = '';

		$user_id = 0;
		if ( $user instanceof UserModel ) {
			$user_id = $user->get_id();
		}

		try {
			$can_enroll = $this->can_enroll( $user );
			if ( $can_enroll instanceof WP_Error ) {
				$error_code_return = [
					'course_is_not_purchased_not_login',
					'course_is_not_purchased',
					'course_is_enrolled',
					'course_is_finished',
				];
				if ( ! in_array( $can_enroll->get_error_code(), $error_code_return ) ) {
					$error_code = $can_enroll->get_error_code();
					throw new Exception( $can_enroll->get_error_message() );
				}
			}

			if ( $this->is_free() ) {
				$error_code = 'course_is_free';
				throw new Exception( __( 'The course is free.', 'learnpress' ) );
			}

			$enable_no_required_enroll = $this->has_no_enroll_requirement();
			if ( $enable_no_required_enroll ) {
				$error_code = 'course_is_no_required_enroll';
				throw new Exception(
					__( 'Enrollment in the course is not mandatory. You can access course for learning now.', 'learnpress' )
				);
			}

			$userCourseModel = UserCourseModel::find( $user_id, $this->get_id(), true );
			if ( $user ) {
				if ( $userCourseModel ) {
					if ( $userCourseModel->has_purchased() ) {
						$error_code = 'course_purchased';
						throw new Exception( __( 'Course is purchased', 'learnpress' ) );
					}

					if ( $this->enable_allow_repurchase() ) {
						if ( $userCourseModel->has_enrolled() && $userCourseModel->timestamp_remaining_duration() !== 0 ) {
							$error_code = 'course_is_enrolled';
							throw new Exception( 'This course is already enrolled!' );
						}
					} else {
						if ( $userCourseModel->has_enrolled_or_finished() ) {
							$error_code = 'course_is_enrolled_or_finished';
							throw new Exception( __( 'Course is enrolled or finished', 'learnpress' ) );
						}
					}
				}
			}
		} catch ( Throwable $e ) {
			if ( empty( $error_code ) ) {
				$error_code = 'course_can_not_purchase';
			}
			$can_purchase = new WP_Error( $error_code, $e->getMessage() );
		}

		// Hook old
		if ( has_filter( 'learn-press/user/can-purchase-course' ) ) {
			$can_purchase = apply_filters( 'learn-press/user/can-purchase-course', $can_purchase, $user_id, $this->get_id() );
			if ( $can_purchase === false ) {
				$can_purchase = new WP_Error( '', '' );
			}
			//_deprecated_function( 'The learn-press/user/can-purchase-course filter', '4.2.7.3', 'learn-press/user/can-purchase/course' );
		}

		return apply_filters( 'learn-press/user/can-purchase/course', $can_purchase, $this, $user );
	}

	/**
	 * Check user is author or co-in of course.
	 *
	 * @param UserModel $userModel
	 *
	 * @return bool
	 * @since 4.2.7.6
	 * @version 1.0.0
	 */
	public function check_user_is_author( UserModel $userModel ): bool {
		$is_author = false;

		if ( $userModel->get_id() === $this->post_author ) {
			$is_author = true;
		}

		return apply_filters( 'learn-press/course/is-author', $is_author, $this, $userModel );
	}

	/**
	 * Get item model assigned to this course
	 *
	 * @return mixed|false|null|WP_Post
	 * @since v4.2.7.6
	 * @version 1.0.1
	 */
	public function get_item_model( int $item_id, string $item_type ) {
		try {
			$item = false;

			switch ( $item_type ) {
				case LP_LESSON_CPT:
					$item = LessonPostModel::find( $item_id, true );
					break;
				case LP_QUIZ_CPT:
					$item = QuizPostModel::find( $item_id, true );
					break;
				case LP_QUESTION_CPT:
					break;
				default:
					$item = apply_filters( 'learn-press/course/get-item-model', $item, $item_id, $item_type, $this );
					break;
			}

			// If not defined class, get post default
			if ( ! $item ) {
				$filter            = new LP_Post_Type_Filter();
				$filter->ID        = $item_id;
				$filter->post_type = $item_type;
				$item              = PostModel::get_item_model_from_db( $filter );
			}
		} catch ( Exception $e ) {
			error_log( __METHOD__ . ': ' . $e->getMessage() );
		}

		return $item;
	}

	/**
	 * Get item model if query success.
	 * If not exists, return false.
	 * If exists, return PostModel.
	 *
	 * @param LP_Course_JSON_Filter $filter
	 *
	 * @return CourseModel|false|static
	 * @since 4.2.6.9
	 * @version 1.0.2
	 */
	public static function get_item_model_from_db( LP_Course_JSON_Filter $filter ) {
		$course_model = false;

		try {
			$filter->only_fields = [ 'json', 'post_content' ];

			$course_rs = self::get_course_from_db( $filter );
			if ( $course_rs instanceof stdClass && isset( $course_rs->json ) ) {
				$course_obj   = LP_Helper::json_decode( $course_rs->json );
				$course_model = new static( $course_obj );
				//$course_model->json         = $course_rs->json;
				$course_model->post_content = $course_rs->post_content;
				$course_model->get_author_model();
			}
		} catch ( Throwable $e ) {
			error_log( __METHOD__ . ': ' . $e->getMessage() );
		}

		return $course_model;
	}

	/**
	 * Get course by ID
	 *
	 * @param int $course_id
	 * @param bool $check_cache
	 *
	 * @return false|CourseModel|static
	 */
	public static function find( int $course_id, bool $check_cache = false ) {
		$filter_course     = new LP_Course_JSON_Filter();
		$filter_course->ID = $course_id;
		$key_cache         = "courseModel/find/id/{$course_id}";
		$lp_course_cache   = new LP_Course_Cache();

		// Check cache
		if ( $check_cache ) {
			$course_model = $lp_course_cache->get_cache( $key_cache );
			if ( $course_model instanceof CourseModel ) {
				return $course_model;
			}
		}

		// Query database no cache.
		$course_model = self::get_item_model_from_db( $filter_course );
		if ( false === $course_model ) { // Find on table posts
			$course_rs = CoursePostModel::find( $course_id );
			if ( $course_rs instanceof CoursePostModel ) {
				$course_model = new static( $course_rs );
			}
		}

		// Set cache
		if ( $course_model instanceof CourseModel ) {
			$lp_course_cache->set_cache( $key_cache, $course_model );
		}

		return $course_model;
	}

	/**
	 * Get course from table learnpress_courses
	 *
	 * @return array|object|stdClass|null
	 * @throws Exception
	 */
	private static function get_course_from_db( LP_Course_JSON_Filter $filter ) {
		$lp_course_json_db = LP_Course_JSON_DB::getInstance();
		$lp_course_json_db->get_query_single_row( $filter );
		$query_single_row = $lp_course_json_db->get_courses( $filter );

		return $lp_course_json_db->wpdb->get_row( $query_single_row );
	}

	/**
	 * Save course data to table learnpress_courses.
	 *
	 * @throws Exception
	 * @since 4.2.6.9
	 * @version 1.0.1
	 */
	public function save(): CourseModel {
		$lp_course_json_db = LP_Course_JSON_DB::getInstance();

		$data = [];

		$courseObjToJSON = clone $this;
		unset( $courseObjToJSON->post_content );
		unset( $courseObjToJSON->json );
		$this->json = json_encode( $courseObjToJSON, JSON_UNESCAPED_UNICODE );
		foreach ( get_object_vars( $this ) as $property => $value ) {
			$data[ $property ] = $value;
		}

		if ( ! isset( $data['ID'] ) ) {
			throw new Exception( 'Course ID is invalid!' );
		}

		$filter              = new LP_Course_JSON_Filter();
		$filter->ID          = $this->ID;
		$filter->only_fields = [ 'ID' ];
		$course_rs           = self::get_course_from_db( $filter );
		// Check if exists course id.
		if ( empty( $course_rs ) ) { // Insert data.
			$lp_course_json_db->insert_data( $data );
		} else { // Update data.
			$lp_course_json_db->update_data( $data );
		}

		// Clear cache
		$this->clean_caches();

		return $this;
	}

	/**
	 * Delete row
	 *
	 * @throws Exception
	 */
	public function delete() {
		$lp_course_json_db  = LP_Course_JSON_DB::getInstance();
		$filter             = new LP_Course_JSON_Filter();
		$filter->where[]    = $lp_course_json_db->wpdb->prepare( 'AND ID = %d', $this->ID );
		$filter->collection = $lp_course_json_db->tb_lp_courses;
		$lp_course_json_db->delete_execute( $filter );

		// Clear cache
		$this->clean_caches();
	}

	/**
	 * Clean caches
	 *
	 * @since 4.2.7.4
	 * @version 1.0.0
	 * @return void
	 */
	public function clean_caches() {
		$key_cache       = "courseModel/find/id/{$this->ID}";
		$lp_course_cache = new LP_Course_Cache();
		$lp_course_cache->clear( $key_cache );
	}

	/**
	 * Return course's items support.
	 * To replace learn_press_course_get_support_item_types()
	 * Should add hook on addons before use this function.
	 *
	 * @return array
	 * @since 4.2.7.4
	 * @version 1.0.1
	 */
	public static function item_types_support(): array {
		$item_types = [
			LP_LESSON_CPT,
			LP_QUIZ_CPT,
		];

		// Hook old
		if ( has_filter( 'learn-press/course-item-type' ) ) {
			$item_types = apply_filters( 'learn-press/course-item-type', $item_types );
		}

		$item_types = apply_filters( 'learn-press/course/item-types-support', $item_types );

		// set types unique
		return array_unique( $item_types );
	}
}