PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 3.2.0
Tutor LMS – eLearning and online course solution v3.2.0
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 / models / CouponModel.php
tutor / models Last commit date
BillingModel.php 1 year ago CartModel.php 1 year ago CouponModel.php 1 year ago CourseModel.php 1 year ago LessonModel.php 1 year ago OrderActivitiesModel.php 1 year ago OrderMetaModel.php 1 year ago OrderModel.php 1 year ago QuizModel.php 1 year ago UserModel.php 1 year ago WithdrawModel.php 1 year ago
CouponModel.php
1082 lines
1 <?php
2 /**
3 * Coupon Model
4 *
5 * @package Tutor\Models
6 * @author Themeum <support@themeum.com>
7 * @link https://themeum.com
8 * @since 3.0.0
9 */
10
11 namespace Tutor\Models;
12
13 use TUTOR\Course;
14 use Tutor\Ecommerce\Settings;
15 use Tutor\Ecommerce\Tax;
16 use Tutor\Helpers\QueryHelper;
17
18 /**
19 * Coupon model class
20 */
21 class CouponModel {
22
23 /**
24 * Coupon status
25 *
26 * @since 3.0.0
27 *
28 * @var string
29 */
30 const STATUS_ACTIVE = 'active';
31 const STATUS_INACTIVE = 'inactive';
32 const STATUS_TRASH = 'trash';
33
34 /**
35 * Coupon type
36 *
37 * @since 3.0.0
38 *
39 * @var string
40 */
41 const TYPE_CODE = 'code';
42 const TYPE_AUTOMATIC = 'automatic';
43
44 /**
45 * Coupon applies to
46 *
47 * @since 3.0.0
48 *
49 * @var string
50 */
51 const APPLIES_TO_ALL_COURSES_AND_BUNDLES = 'all_courses_and_bundles';
52 const APPLIES_TO_ALL_COURSES = 'all_courses';
53 const APPLIES_TO_ALL_BUNDLES = 'all_bundles';
54 const APPLIES_TO_SPECIFIC_COURSES = 'specific_courses';
55 const APPLIES_TO_SPECIFIC_BUNDLES = 'specific_bundles';
56 const APPLIES_TO_SPECIFIC_CATEGORY = 'specific_category';
57
58 /**
59 * Coupon purchase requirement
60 *
61 * @since 3.0.0
62 *
63 * @var string
64 */
65 const REQUIREMENT_NO_MINIMUM = 'no_minimum';
66 const REQUIREMENT_MINIMUM_PURCHASE = 'minimum_purchase';
67 const REQUIREMENT_MINIMUM_QUANTITY = 'minimum_quantity';
68
69 /**
70 * Discount type
71 *
72 * @since 3.0.0
73 *
74 * @var string
75 */
76 const DISCOUNT_TYPE_FLAT = 'flat';
77 const DISCOUNT_TYPE_PERCENTAGE = 'percentage';
78
79 /**
80 * Coupon table name
81 *
82 * @since 3.0.0
83 *
84 * @var string
85 */
86 private $table_name = 'tutor_coupons';
87
88 /**
89 * Coupon usage table name
90 *
91 * @since 3.0.0
92 *
93 * @var string
94 */
95 private $coupon_usage_table = 'tutor_coupon_usages';
96
97 /**
98 * Coupon application table
99 *
100 * @since 3.0.0
101 *
102 * @var string
103 */
104 private $coupon_applies_to_table = 'tutor_coupon_applications';
105
106 /**
107 * Fillable fields
108 *
109 * @since 3.0.0
110 *
111 * @var array
112 */
113 private $fillable_fields = array(
114 'coupon_status',
115 'coupon_type',
116 'coupon_code',
117 'coupon_title',
118 'coupon_description',
119 'discount_type',
120 'discount_amount',
121 'applies_to',
122 'applies_to_items',
123 'total_usage_limit',
124 'per_user_usage_limit',
125 'purchase_requirement',
126 'purchase_requirement_value',
127 'start_date_gmt',
128 'expire_date_gmt',
129 );
130
131 /**
132 * Fillable fields
133 *
134 * @since 3.0.0
135 *
136 * @var array
137 */
138 private $required_fields = array(
139 'coupon_status',
140 'coupon_type',
141 'coupon_title',
142 'discount_type',
143 'discount_amount',
144 'applies_to',
145 'start_date_gmt',
146 );
147
148 /**
149 * Resolve props & dependencies
150 *
151 * @since 3.0.0
152 */
153 public function __construct() {
154 global $wpdb;
155 $this->table_name = $wpdb->prefix . $this->table_name;
156 $this->coupon_usage_table = $wpdb->prefix . $this->coupon_usage_table;
157 $this->coupon_applies_to_table = $wpdb->prefix . $this->coupon_applies_to_table;
158 }
159
160 /**
161 * Get table name with wp prefix
162 *
163 * @since 3.0.0
164 *
165 * @return string
166 */
167 public function get_table_name() {
168 return $this->table_name;
169 }
170
171 /**
172 * Get fillable fields
173 *
174 * @since 3.0.0
175 *
176 * @return string
177 */
178 public function get_fillable_fields() {
179 return $this->fillable_fields;
180 }
181
182 /**
183 * Get required fields
184 *
185 * @since 3.0.0
186 *
187 * @return string
188 */
189 public function get_required_fields() {
190 return $this->required_fields;
191 }
192
193 /**
194 * Get all coupon statuses
195 *
196 * @since 3.0.0
197 *
198 * @return array
199 */
200 public static function get_coupon_status() {
201 return array(
202 self::STATUS_ACTIVE => __( 'Active', 'tutor' ),
203 self::STATUS_INACTIVE => __( 'Inactive', 'tutor' ),
204 self::STATUS_TRASH => __( 'Trash', 'tutor' ),
205 );
206 }
207
208 /**
209 * Get all coupon applies to
210 *
211 * @since 3.0.0
212 *
213 * @return array
214 */
215 public static function get_coupon_applies_to() {
216 return array(
217 self::APPLIES_TO_ALL_COURSES_AND_BUNDLES => __( 'All courses and bundles', 'tutor' ),
218 self::APPLIES_TO_ALL_COURSES => __( 'All courses', 'tutor' ),
219 self::APPLIES_TO_ALL_BUNDLES => __( 'All bundles', 'tutor' ),
220 self::APPLIES_TO_SPECIFIC_COURSES => __( 'Specific courses', 'tutor' ),
221 self::APPLIES_TO_SPECIFIC_BUNDLES => __( 'Specific bundles', 'tutor' ),
222 self::APPLIES_TO_SPECIFIC_CATEGORY => __( 'Specific category', 'tutor' ),
223 );
224 }
225
226 /**
227 * Get all coupon purchase requirements
228 *
229 * @since 3.0.0
230 *
231 * @return array
232 */
233 public static function get_coupon_purchase_requirements() {
234 return array(
235 self::REQUIREMENT_NO_MINIMUM => __( 'no_minimum', 'tutor' ),
236 self::REQUIREMENT_MINIMUM_PURCHASE => __( 'minimum_purchase', 'tutor' ),
237 self::REQUIREMENT_MINIMUM_QUANTITY => __( 'minimum_quantity', 'tutor' ),
238 );
239 }
240
241 /**
242 * Get all coupon types
243 *
244 * @since 3.0.0
245 *
246 * @return array
247 */
248 public static function get_coupon_type() {
249 return array(
250 self::TYPE_CODE => __( 'Code', 'tutor' ),
251 self::TYPE_AUTOMATIC => __( 'Automatic', 'tutor' ),
252 );
253 }
254
255 /**
256 * Get searchable fields
257 *
258 * This method is intendant to use with get order list
259 *
260 * @since 3.0.0
261 *
262 * @return array
263 */
264 private function get_searchable_fields() {
265 return array(
266 'id',
267 'coupon_status',
268 'coupon_code',
269 'coupon_title',
270 );
271 }
272
273 /**
274 * Create coupon using the data argument
275 *
276 * @since 3.0.0
277 *
278 * @param array $data Array as per table column.
279 *
280 * @throws \Exception Database error if occur.
281 *
282 * @return int Coupon id or 0 if failed
283 */
284 public function create_coupon( array $data ) {
285 try {
286 return QueryHelper::insert( $this->table_name, $data );
287 } catch ( \Throwable $th ) {
288 throw new \Exception( $th->getMessage() );
289 }
290 }
291
292 /**
293 * Insert applies to
294 *
295 * @since 3.0.0
296 *
297 * @param string $applies_to Applies to type.
298 * @param array $applies_to_ids Applies to ids.
299 * @param mixed $coupon_code Coupon code.
300 *
301 * @return mixed true|false on insert, void if not insert-able
302 */
303 public function insert_applies_to( string $applies_to, array $applies_to_ids, $coupon_code ) {
304 $specific_applies = array( self::APPLIES_TO_SPECIFIC_BUNDLES, self::APPLIES_TO_SPECIFIC_COURSES, self::APPLIES_TO_SPECIFIC_CATEGORY );
305 if ( in_array( $applies_to, $specific_applies ) ) {
306 $data = array();
307
308 foreach ( $applies_to_ids as $id ) {
309 $data[] = array(
310 'coupon_code' => $coupon_code,
311 'reference_id' => $id,
312 );
313 }
314
315 if ( count( $data ) ) {
316 return QueryHelper::insert_multiple_rows( $this->coupon_applies_to_table, $data );
317 }
318 }
319 }
320
321 /**
322 * Delete applies to
323 *
324 * @since 3.0.0
325 *
326 * @param mixed $coupon_code Coupon code.
327 *
328 * @return bool
329 */
330 public function delete_applies_to( $coupon_code ) {
331 return QueryHelper::delete( $this->coupon_applies_to_table, array( 'coupon_code' => $coupon_code ) );
332 }
333
334 /**
335 * Get coupons list
336 *
337 * @since 3.0.0
338 *
339 * @param array $where where clause conditions.
340 * @param string $search_term search clause conditions.
341 * @param int $limit limit default 10.
342 * @param int $offset default 0.
343 * @param string $order_by column default 'o.id'.
344 * @param string $order list Coupon default 'desc'.
345 *
346 * @return array
347 */
348 public function get_coupons( array $where = array(), $search_term = '', int $limit = 10, int $offset = 0, string $order_by = 'id', string $order = 'desc' ) {
349
350 $search_clause = array();
351 if ( '' !== $search_term ) {
352 foreach ( $this->get_searchable_fields() as $column ) {
353 $search_clause[ $column ] = $search_term;
354 }
355 }
356
357 $response = array(
358 'results' => array(),
359 'total_count' => 0,
360 );
361
362 try {
363 $response = QueryHelper::get_all_with_search( $this->table_name, $where, $search_clause, $order_by, $limit, $offset, $order );
364
365 // Add coupon usage count.
366 foreach ( $response['results'] as $result ) {
367 $result->usage_count = $this->get_coupon_usage_count( $result->coupon_code );
368 }
369
370 return $response;
371 } catch ( \Throwable $th ) {
372 // Log with error, line & file name.
373 error_log( $th->getMessage() . ' in ' . $th->getFile() . ' at line ' . $th->getLine() );
374 return $response;
375 }
376 }
377
378 /**
379 * Update coupon
380 *
381 * @since 3.0.0
382 *
383 * @param int|array $coupon_id Integer or array of ids sql escaped.
384 * @param array $data Data to update, escape data.
385 *
386 * @return bool
387 */
388 public function update_coupon( $coupon_id, array $data ) {
389 $coupon_ids = is_array( $coupon_id ) ? $coupon_id : array( $coupon_id );
390 $coupon_ids = QueryHelper::prepare_in_clause( $coupon_ids );
391 try {
392 QueryHelper::update_where_in(
393 $this->table_name,
394 $data,
395 $coupon_ids
396 );
397 return true;
398 } catch ( \Throwable $th ) {
399 error_log( $th->getMessage() . ' in ' . $th->getFile() . ' at line ' . $th->getLine() );
400 return false;
401 }
402 }
403
404 /**
405 * Update coupon
406 *
407 * @since 3.0.0
408 *
409 * @param int|array $coupon_id Integer or array of ids sql escaped.
410 *
411 * @return bool
412 */
413 public function delete_coupon( $coupon_id ) {
414 $coupon_ids = is_array( $coupon_id ) ? $coupon_id : array( $coupon_id );
415
416 try {
417 QueryHelper::bulk_delete_by_ids(
418 $this->table_name,
419 $coupon_ids
420 );
421 return true;
422 } catch ( \Throwable $th ) {
423 error_log( $th->getMessage() . ' in ' . $th->getFile() . ' at line ' . $th->getLine() );
424 return false;
425 }
426 }
427
428 /**
429 * Get Coupon count
430 *
431 * @since 3.0.0
432 *
433 * @param array $where Where conditions, sql esc data.
434 * @param string $search_term Search terms, sql esc data.
435 *
436 * @return int
437 */
438 public function get_coupon_count( $where = array(), string $search_term = '' ) {
439 $search_clause = array();
440 if ( '' !== $search_term ) {
441 foreach ( $this->get_searchable_fields() as $column ) {
442 $search_clause[ $column ] = $search_term;
443 }
444 }
445
446 return QueryHelper::get_count( $this->table_name, $where, $search_clause, '*' );
447 }
448
449 /**
450 * Get coupon usage count
451 *
452 * @since 3.0.0
453 *
454 * @param mixed $coupon_code Coupon code.
455 *
456 * @return int
457 */
458 public function get_coupon_usage_count( $coupon_code ) {
459 return QueryHelper::get_count(
460 $this->coupon_usage_table,
461 array( 'coupon_code' => $coupon_code ),
462 array(),
463 '*'
464 );
465 }
466
467 /**
468 * Get coupon usage count for a user
469 *
470 * @since 3.0.0
471 *
472 * @param mixed $coupon_code Coupon code.
473 * @param int $user_id User id.
474 *
475 * @return int
476 */
477 public function get_user_usage_count( $coupon_code, $user_id ) {
478 return QueryHelper::get_count(
479 $this->coupon_usage_table,
480 array(
481 'coupon_code' => $coupon_code,
482 'user_id' => $user_id,
483 ),
484 array(),
485 '*'
486 );
487 }
488
489 /**
490 * Retrieve a coupon by its ID.
491 *
492 * This function fetches the coupon data from the database based on the provided coupon ID.
493 * If the coupon is found, it returns the coupon data; otherwise, it returns false.
494 *
495 * @since 3.0.0
496 *
497 * @param int $coupon_id The ID of the coupon to retrieve.
498 *
499 * @return object|false The coupon data as an object if found, or false if not found.
500 */
501 public function get_coupon_by_id( $coupon_id ) {
502 $coupon_data = QueryHelper::get_row(
503 $this->table_name,
504 array( 'id' => $coupon_id ),
505 'id'
506 );
507
508 if ( ! $coupon_data ) {
509 return false;
510 }
511
512 return $this->process_coupon_data( $coupon_data );
513 }
514
515 public function get_coupon_by_code( $coupon_code ) {
516 $coupon_data = QueryHelper::get_row(
517 $this->table_name,
518 array( 'coupon_code' => $coupon_code ),
519 'id'
520 );
521
522 if ( ! $coupon_data ) {
523 return false;
524 }
525
526 return $this->process_coupon_data( $coupon_data );
527 }
528
529 /**
530 * Get the list of the all automatic coupons.
531 *
532 * @since 3.0.0
533 *
534 * @return array
535 */
536 public function get_automatic_coupons() {
537 $coupons = $this->get_coupons(
538 array(
539 'coupon_type' => self::TYPE_AUTOMATIC,
540 'coupon_status' => self::STATUS_ACTIVE,
541 ),
542 '',
543 1000,
544 0
545 );
546
547 if ( empty( $coupons['results'] ) ) {
548 return array();
549 }
550
551 return $coupons['results'];
552 }
553
554 private function process_coupon_data( $coupon_data ) {
555 $coupon_data->id = (int) $coupon_data->id;
556 $coupon_data->usage_limit_status = ! empty( $coupon_data->total_usage_limit ) ? true : false;
557 $coupon_data->total_usage_limit = (int) $coupon_data->total_usage_limit;
558 $coupon_data->is_one_use_per_user = ! empty( $coupon_data->per_user_usage_limit ) ? true : false;
559 $coupon_data->discount_amount = (float) $coupon_data->discount_amount;
560 $coupon_data->created_by = get_userdata( $coupon_data->created_by )->display_name ?? '';
561 $coupon_data->updated_by = get_userdata( $coupon_data->updated_by )->display_name ?? '';
562 $coupon_data->courses = array();
563 $coupon_data->categories = array();
564
565 if ( 'specific_courses' === $coupon_data->applies_to || 'specific_bundles' === $coupon_data->applies_to ) {
566 $coupon_data->courses = $this->get_coupon_courses_by_code( $coupon_data->coupon_code );
567 }
568
569 if ( 'specific_category' === $coupon_data->applies_to ) {
570 $coupon_data->categories = $this->get_coupon_categories_by_code( $coupon_data->coupon_code );
571 }
572
573 return $coupon_data;
574 }
575
576
577
578 /**
579 * Retrieve courses associated with a given coupon code.
580 *
581 * This function fetches courses that have been associated with a specified coupon code
582 * from the WordPress database, using the `tutor_coupon_applications` table and joining
583 * it with the `posts` table to get course details.
584 *
585 * @since 3.0.0
586 *
587 * @param string $coupon_code The coupon code to search for associated courses.
588 *
589 * @global wpdb $wpdb WordPress database abstraction object.
590 *
591 * @return array An array of course objects, each containing:
592 * - id: The ID of the course.
593 * - title: The title of the course.
594 * - type: The post type of the course (e.g., 'course', 'course-bundle').
595 * - price: The price of the course.
596 * - sale_price: The sale price of the course.
597 * - image: The URL of the course's thumbnail image.
598 * - total_courses: (optional) The total number of courses in a bundle, if applicable.
599 */
600 public function get_coupon_courses_by_code( $coupon_code ) {
601 global $wpdb;
602
603 $primary_table = "{$wpdb->prefix}tutor_coupon_applications AS ca";
604 $joining_tables = array(
605 array(
606 'type' => 'LEFT',
607 'table' => "{$wpdb->prefix}posts AS p",
608 'on' => 'p.ID = ca.reference_id',
609 ),
610 );
611
612 $where = array( 'ca.coupon_code' => $coupon_code );
613
614 $select_columns = array( 'ca.reference_id AS id', 'p.post_title AS title', 'p.post_type AS type' );
615
616 $courses_data = QueryHelper::get_joined_data( $primary_table, $joining_tables, $select_columns, $where, array(), 'id', 0, 0 );
617 $courses = $courses_data['results'];
618
619 if ( tutor()->has_pro ) {
620 $bundle_model = new \TutorPro\CourseBundle\Models\BundleModel();
621 }
622
623 if ( ! empty( $courses_data['total_count'] ) ) {
624 foreach ( $courses as &$course ) {
625 if ( tutor()->has_pro && 'course-bundle' === $course->type ) {
626 $course->total_courses = count( $bundle_model->get_bundle_course_ids( $course->id ) );
627 }
628
629 $course_prices = tutor_utils()->get_raw_course_price( $course->id );
630 $course->id = (int) $course->id;
631 $course->price = $course_prices->regular_price;
632 $course->sale_price = $course_prices->sale_price;
633 $course->image = get_the_post_thumbnail_url( $course->id );
634 }
635 }
636
637 unset( $course );
638
639 return ! empty( $courses ) ? $courses : array();
640 }
641
642 /**
643 * Retrieve course categories associated with a given coupon code.
644 *
645 * This function fetches categories that have been associated with a specified coupon code
646 * from the WordPress database, using the `tutor_coupon_applications` table and retrieving
647 * category details from the terms database.
648 *
649 * @since 3.0.0
650 *
651 * @param string $coupon_code The coupon code to search for associated categories.
652 *
653 * @global wpdb $wpdb WordPress database abstraction object.
654 *
655 * @return array An array of category objects, each containing:
656 * - term_id: The ID of the category.
657 * - name: The name of the category.
658 * - slug: The slug of the category.
659 * - term_group: The term group of the category.
660 * - term_taxonomy_id: The taxonomy ID of the category.
661 * - taxonomy: The taxonomy type of the category.
662 * - description: The description of the category.
663 * - parent: The parent ID of the category.
664 * - count: The number of items in the category.
665 */
666 public function get_coupon_categories_by_code( $coupon_code ) {
667 global $wpdb;
668
669 $table = "{$wpdb->prefix}tutor_coupon_applications";
670 $where = array( 'coupon_code' => $coupon_code );
671
672 $categories = QueryHelper::get_all( $table, $where, 'reference_id' );
673 $response = array();
674
675 foreach ( $categories as $category ) {
676 $category_data = get_term_by( 'id', $category->reference_id, 'course-category' );
677
678 if ( $category_data ) {
679 // Fetch the thumbnail_id from the wp_termmeta table.
680 $thumbnail_id = get_term_meta( $category_data->term_id, 'thumbnail_id', true );
681
682 // If the thumbnail ID is retrieved, get the image URL.
683 if ( $thumbnail_id ) {
684 $image = wp_get_attachment_url( $thumbnail_id );
685 } else {
686 $image = ''; // Or set a default image URL if needed.
687 }
688
689 $final_data = new \stdClass();
690 $final_data->id = $category_data->term_id;
691 $final_data->title = $category_data->name;
692 $final_data->number_of_courses = $category_data->count;
693 $final_data->image = $image;
694
695 $response[] = $final_data;
696 }
697 }
698
699 return $response;
700 }
701
702 /**
703 * Get coupon info by coupon code
704 *
705 * @since 3.0.0
706 *
707 * @param array $where Where condition.
708 *
709 * @return mixed
710 */
711 public function get_coupon( array $where ) {
712 return QueryHelper::get_row(
713 $this->table_name,
714 $where,
715 'id'
716 );
717 }
718
719 /**
720 * Get coupon details for checkout.
721 *
722 * @param string $coupon_code coupon code.
723 *
724 * @return object
725 */
726 public function get_coupon_details_for_checkout( $coupon_code = '' ) {
727 $coupon = null;
728 if ( empty( $coupon_code ) ) {
729 $coupon = $this->get_coupon(
730 array(
731 'coupon_type' => self::TYPE_AUTOMATIC,
732 'coupon_status' => self::STATUS_ACTIVE,
733 )
734 );
735 } else {
736 $coupon = $this->get_coupon(
737 array(
738 'coupon_code' => $coupon_code,
739 'coupon_status' => self::STATUS_ACTIVE,
740 )
741 );
742 }
743
744 return $coupon;
745 }
746
747 /**
748 * Deduct coupon discount
749 *
750 * @since 3.0.0
751 *
752 * @param mixed $regular_price Regular price.
753 * @param string $discount_type Discount type.
754 * @param mixed $discount_value Discount value.
755 *
756 * @return float Deducted price
757 */
758 public function deduct_coupon_discount( $regular_price, $discount_type, $discount_value ) {
759 $deducted_price = $regular_price;
760 if ( self::DISCOUNT_TYPE_PERCENTAGE === $discount_type ) {
761 $deducted_price = $regular_price - ( $regular_price * ( $discount_value / 100 ) );
762 } else {
763 $deducted_price = $regular_price - $discount_value;
764 }
765
766 return tutor_get_locale_price( max( 0, $deducted_price ) );
767 }
768
769 /**
770 * Check whether this coupon is valid or not.
771 *
772 * Considering start-expire time & use limit.
773 *
774 * @since 3.0.0
775 *
776 * @param object $coupon Coupon object.
777 *
778 * @return bool
779 */
780 public function is_coupon_valid( object $coupon ): bool {
781 return self::STATUS_ACTIVE === $coupon->coupon_status && $this->has_coupon_validity( $coupon ) && $this->has_user_usage_limit( $coupon, get_current_user_id() );
782 }
783
784 /**
785 * Check whether this coupon is applicable to the given course or not.
786 *
787 * Applicable is getting determined by the coupon applies_to value
788 *
789 * @since 3.0.0
790 *
791 * @param object $coupon Coupon object.
792 * @param int $object_id Course/Bundle id.
793 *
794 * @return bool
795 */
796 public function is_coupon_applicable( object $coupon, int $object_id ): bool {
797 $is_applicable = false;
798
799 $object_id = apply_filters( 'tutor_subscription_course_by_plan', $object_id );
800
801 $course_post_type = tutor()->course_post_type;
802 $bundle_post_type = 'course-bundle';
803 $object_type = get_post_type( $object_id );
804
805 $applies_to = $coupon->applies_to;
806 $applications = $this->get_coupon_applications( $coupon->coupon_code );
807
808 switch ( $applies_to ) {
809 case self::APPLIES_TO_ALL_COURSES_AND_BUNDLES:
810 $is_applicable = true;
811 break;
812
813 case self::APPLIES_TO_ALL_COURSES:
814 case self::APPLIES_TO_SPECIFIC_COURSES:
815 if ( self::APPLIES_TO_ALL_COURSES === $applies_to ) {
816 $is_applicable = $object_type === $course_post_type;
817 } else {
818 $is_applicable = in_array( $object_id, $applications );
819 }
820 break;
821
822 case self::APPLIES_TO_ALL_BUNDLES:
823 case self::APPLIES_TO_SPECIFIC_BUNDLES:
824 if ( self::APPLIES_TO_ALL_BUNDLES === $applies_to ) {
825 $is_applicable = $object_type === $bundle_post_type;
826 } else {
827 $is_applicable = in_array( $object_id, $applications );
828 }
829 break;
830
831 case self::APPLIES_TO_SPECIFIC_CATEGORY:
832 $course_categories = wp_get_post_terms( $object_id, CourseModel::COURSE_CATEGORY );
833 if ( ! is_wp_error( $course_categories ) ) {
834 $term_ids = array_column( $course_categories, 'term_id' );
835 $is_applicable = count( array_intersect( $applications, $term_ids ) );
836 }
837 break;
838 }
839
840 return apply_filters( 'tutor_coupon_is_applicable', $is_applicable, $coupon, $object_id );
841 }
842
843 /**
844 * Check whether meet coupon requirement or not
845 *
846 * @since 3.0.0
847 *
848 * @param int|array $item_id Item id or array of ids. May consist course, bundle or plan.
849 * @param object $coupon Coupon object.
850 * @param string $order_type Order type.
851 *
852 * @return boolean
853 */
854 public function is_coupon_requirement_meet( $item_id, object $coupon, $order_type = OrderModel::TYPE_SINGLE_ORDER ) {
855 $is_meet_requirement = true;
856 $item_ids = is_array( $item_id ) ? $item_id : array( $item_id );
857
858 $total_price = 0;
859 $min_amount = $coupon->purchase_requirement_value;
860 $regular_price_item_count = 0;
861
862 foreach ( $item_ids as $item_id ) {
863 $course_price = tutor_utils()->get_raw_course_price( $item_id );
864 if ( OrderModel::TYPE_SINGLE_ORDER !== $order_type ) {
865 $plan_info = apply_filters( 'tutor_get_plan_info', null, $item_id );
866 if ( $plan_info ) {
867 $course_price->regular_price = $plan_info->regular_price;
868 $course_price->sale_price = $plan_info->in_sale_price ? $plan_info->sale_price : 0;
869 }
870 }
871
872 $total_price += $course_price->sale_price ? $course_price->sale_price : $course_price->regular_price;
873 if ( ! $course_price->sale_price ) {
874 $regular_price_item_count++;
875 }
876 }
877
878 if ( self::REQUIREMENT_MINIMUM_QUANTITY === $coupon->purchase_requirement ) {
879 $min_quantity = $coupon->purchase_requirement_value;
880 $is_meet_requirement = count( $item_ids ) >= $min_quantity;
881 } elseif ( self::REQUIREMENT_MINIMUM_PURCHASE === $coupon->purchase_requirement && $total_price < $min_amount ) {
882 $is_meet_requirement = false;
883 }
884
885 /**
886 * If there is no regular price item in the cart, then it's not meet requirement.
887 *
888 * @since 3.0.0
889 */
890 if ( 0 === $regular_price_item_count ) {
891 $is_meet_requirement = false;
892 }
893
894 return apply_filters( 'tutor_coupon_is_meet_requirement', $is_meet_requirement, $coupon, $item_id );
895 }
896
897 /**
898 * Check coupon time validity
899 *
900 * @since 3.0.0
901 *
902 * @param object $coupon coupon object.
903 *
904 * @return boolean
905 */
906 public function has_coupon_validity( object $coupon ): bool {
907 $now = time();
908 $start_date = strtotime( $coupon->start_date_gmt );
909 $expire_date = $coupon->expire_date_gmt ? strtotime( $coupon->expire_date_gmt ) : 0;
910
911 // Check if the current time is within the start and expiry dates.
912 return ( $now >= $start_date ) && ( $expire_date ? $now <= $expire_date : true );
913 }
914
915 /**
916 * Check coupon usage limit
917 *
918 * @since 3.0.0
919 *
920 * @param object $coupon coupon object.
921 * @param int $user_id user id.
922 *
923 * @return bool true if has usage limit otherwise false
924 */
925 public function has_user_usage_limit( object $coupon, int $user_id ): bool {
926 $has_limit = true;
927
928 $total_usage_limit = (int) $coupon->total_usage_limit;
929 $user_usage_limit = (int) $coupon->per_user_usage_limit;
930
931 if ( $total_usage_limit > 0 ) {
932 $coupon_usage_count = $this->get_coupon_usage_count( $coupon->coupon_code );
933 if ( $coupon_usage_count >= $total_usage_limit ) {
934 $has_limit = false;
935 }
936 }
937
938 if ( $user_usage_limit > 0 ) {
939 $user_usage_count = $this->get_user_usage_count( $coupon->coupon_code, $user_id );
940 if ( $user_usage_count >= $user_usage_limit ) {
941 $has_limit = false;
942 }
943 }
944
945 return apply_filters( 'tutor_coupon_has_user_usage_limit', $has_limit, $coupon, $user_id );
946 }
947
948 /**
949 * Get coupon applications
950 *
951 * @since 3.0.0
952 *
953 * @param mixed $coupon_code Coupon code.
954 *
955 * @return array [1,2,4]
956 */
957 public function get_coupon_applications( $coupon_code ): array {
958 $response = array();
959
960 $result = QueryHelper::get_all(
961 $this->coupon_applies_to_table,
962 array( 'coupon_code' => $coupon_code ),
963 'coupon_code'
964 );
965
966 if ( is_array( $result ) && count( $result ) ) {
967 $response = array_column( $result, 'reference_id' );
968 }
969
970 return $response;
971 }
972
973 /**
974 * Get formatted coupon application items
975 *
976 * @since 3.0.0
977 *
978 * @param object $coupon Coupon object.
979 *
980 * @return array
981 */
982 public function get_formatted_coupon_applications( object $coupon ): array {
983 $applications = $this->get_coupon_applications( $coupon->coupon_code );
984 $response = array();
985
986 foreach ( $applications as $application_id ) {
987 $application = $this->get_application_details( $application_id, $coupon->applies_to );
988
989 if ( $application ) {
990 $response[] = $application;
991 }
992 }
993
994 return $response;
995 }
996
997 /**
998 * Get coupon application details
999 *
1000 * @since 3.0.0
1001 *
1002 * @param int $id Application id.
1003 *
1004 * @return array
1005 */
1006 public function get_application_details( int $id, string $applies_to ): array {
1007 $response = array();
1008 if ( self::APPLIES_TO_SPECIFIC_BUNDLES === $applies_to || self::APPLIES_TO_SPECIFIC_COURSES === $applies_to ) {
1009 $post = get_post( $id );
1010
1011 if ( $post ) {
1012 $response = array(
1013 'id' => $id,
1014 'title' => get_the_title( $id ),
1015 'image' => get_the_post_thumbnail_url( $id ),
1016 'regular_price' => tutor_get_formatted_price( get_post_meta( $id, Course::COURSE_PRICE_META, true ) ),
1017 'sale_price' => tutor_get_formatted_price( get_post_meta( $id, Course::COURSE_SALE_PRICE_META, true ) ),
1018 );
1019 }
1020 } elseif ( term_exists( $id ) ) {
1021 $term = get_term( $id );
1022
1023 if ( $term ) {
1024 $thumb_id = get_term_meta( $id, 'thumbnail_id', true );
1025 $response = array(
1026 'id' => $id,
1027 'title' => $term->name,
1028 'image' => $thumb_id ? wp_get_attachment_thumb_url( $thumb_id ) : '',
1029 'total_courses' => (int) $term->count,
1030 );
1031 }
1032 }
1033
1034 return $response;
1035 }
1036
1037 /**
1038 * Check if applies to is specific
1039 *
1040 * @since 3.0.0
1041 *
1042 * @param string $applies_to Applies to.
1043 *
1044 * @return boolean
1045 */
1046 public function is_specific_applies_to( string $applies_to ) {
1047 return in_array( $applies_to, array( self::APPLIES_TO_SPECIFIC_BUNDLES, self::APPLIES_TO_SPECIFIC_COURSES, self::APPLIES_TO_SPECIFIC_CATEGORY ) );
1048 }
1049
1050 /**
1051 * Store coupon usage by using the provided data
1052 *
1053 * @since 3.0.0
1054 *
1055 * @param array $data Data to store.
1056 *
1057 * @throws \Throwable If database error occur.
1058 *
1059 * @return mixed
1060 */
1061 public function store_coupon_usage( array $data ) {
1062 try {
1063 return QueryHelper::insert( $this->coupon_usage_table, $data );
1064 } catch ( \Throwable $th ) {
1065 throw $th;
1066 }
1067 }
1068
1069 /**
1070 * Delete coupon usage by using the where condition
1071 *
1072 * @since 3.0.0
1073 *
1074 * @param array $where Where condition.
1075 *
1076 * @return mixed
1077 */
1078 public function delete_coupon_usage( array $where ) {
1079 return QueryHelper::delete( $this->coupon_usage_table, $where );
1080 }
1081 }
1082