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