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