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