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 / ecommerce / CouponController.php
tutor / ecommerce Last commit date
Cart 10 months ago PaymentGateways 4 weeks ago AdminMenu.php 1 day ago BillingController.php 1 day ago CartController.php 1 day ago CheckoutController.php 1 day ago CouponController.php 1 day ago Ecommerce.php 1 day ago EmailController.php 11 months ago HooksHandler.php 1 day ago OptionKeys.php 1 year ago OrderActivitiesController.php 1 year ago OrderController.php 1 day ago PaymentHandler.php 9 months ago Settings.php 1 day ago Tax.php 9 months ago currency.php 5 months ago
CouponController.php
1002 lines
1 <?php
2 /**
3 * Manage Coupon
4 *
5 * @package Tutor\Ecommerce
6 * @author Themeum <support@themeum.com>
7 * @link https://themeum.com
8 * @since 3.0.0
9 */
10
11 namespace Tutor\Ecommerce;
12
13 use TUTOR\Backend_Page_Trait;
14 use TUTOR\BaseController;
15 use TUTOR\Course;
16 use Tutor\Helpers\DateTimeHelper;
17 use Tutor\Helpers\HttpHelper;
18 use Tutor\Helpers\ValidationHelper;
19 use TUTOR\Input;
20 use Tutor\Models\CouponModel;
21 use Tutor\Models\CourseModel;
22 use Tutor\Models\OrderModel;
23 use Tutor\Traits\JsonResponse;
24 use TutorPro\CourseBundle\Models\BundleModel;
25
26 if ( ! defined( 'ABSPATH' ) ) {
27 exit;
28 }
29 /**
30 * CouponController class
31 *
32 * @since 3.0.0
33 */
34 class CouponController extends BaseController {
35
36 /**
37 * Page slug
38 *
39 * @var string
40 */
41 const PAGE_SLUG = 'tutor_coupons';
42
43 /**
44 * Coupon model
45 *
46 * @since 3.0.0
47 *
48 * @var CouponModel
49 */
50 public $model;
51
52 /**
53 * Checkout controller instance.
54 *
55 * @since 3.6.0
56 *
57 * @var CheckoutController
58 */
59 private $checkout_ctrl;
60
61 /**
62 * Trait for utilities
63 *
64 * @var $page_title
65 */
66 use Backend_Page_Trait;
67
68 /**
69 * Trait for sending JSON response
70 */
71 use JsonResponse;
72
73 /**
74 * Bulk Action
75 *
76 * @var $bulk_action
77 */
78 public $bulk_action = true;
79
80 /**
81 * Constructor.
82 *
83 * Initializes the Coupons class, sets the page title, and optionally registers
84 * hooks for handling AJAX requests related to coupon data, bulk actions, coupon status updates,
85 * and coupon deletions.
86 *
87 * @param bool $register_hooks Whether to register hooks for handling requests. Default is true.
88 *
89 * @since 3.0.0
90 *
91 * @return void
92 */
93 public function __construct( $register_hooks = true ) {
94 $this->model = new CouponModel();
95 $this->checkout_ctrl = new CheckoutController( false );
96
97 if ( $register_hooks ) {
98 // Register hooks here.
99 add_action( 'wp_ajax_tutor_coupon_bulk_action', array( $this, 'bulk_action_handler' ) );
100 add_action( 'wp_ajax_tutor_coupon_permanent_delete', array( $this, 'coupon_permanent_delete' ) );
101 /**
102 * Handle AJAX request for getting coupon related data by coupon ID.
103 *
104 * @since 3.0.0
105 */
106 add_action( 'wp_ajax_tutor_coupon_details', array( $this, 'ajax_coupon_details' ) );
107 /**
108 * Handle AJAX request for getting courses for coupon.
109 *
110 * @since 3.0.0
111 */
112 add_action( 'wp_ajax_tutor_get_coupon_applies_to', array( $this, 'get_coupon_applies_to' ) );
113
114 add_action( 'wp_ajax_tutor_coupon_create', array( $this, 'ajax_create_coupon' ) );
115 add_action( 'wp_ajax_tutor_coupon_update', array( $this, 'ajax_update_coupon' ) );
116 add_action( 'wp_ajax_tutor_coupon_applies_to_list', array( $this, 'ajax_coupon_applies_to_list' ) );
117 add_action( 'wp_ajax_tutor_apply_coupon', array( $this, 'ajax_apply_coupon' ) );
118 }
119 }
120
121 /**
122 * Page title fallback
123 *
124 * @since 3.5.0
125 *
126 * @param string $name Property name.
127 *
128 * @return string
129 */
130 public function __get( $name ) {
131 if ( 'page_title' === $name ) {
132 return esc_html__( 'Coupons', 'tutor' );
133 }
134 }
135
136
137 /**
138 * Get coupon model object
139 *
140 * @since 3.0.0
141 *
142 * @return CouponModel
143 */
144 public function get_model() {
145 return $this->model;
146 }
147
148 /**
149 * Check if applies to items exist and are valid
150 *
151 * @since 3.5.0
152 *
153 * @param array $data The data to validate.
154 *
155 * @return bool Whether applies to items exist and are valid
156 */
157 private function has_applies_to_items( $data ) {
158 return isset( $data['applies_to_items'] ) && is_array( $data['applies_to_items'] ) && count( $data['applies_to_items'] );
159 }
160
161 /**
162 * Validate applies to item
163 *
164 * @since 3.5.0
165 *
166 * @param array $data The data to validate.
167 *
168 * @return void send wp_json response when validation fails
169 */
170 public function validate_applies_to_item( $data ) {
171 $is_specific_applies_to = $this->model->is_specific_applies_to( $data['applies_to'] );
172
173 if ( $is_specific_applies_to && ! $this->has_applies_to_items( $data ) ) {
174 $this->json_response(
175 __( 'Add items first', 'tutor' ),
176 null,
177 HttpHelper::STATUS_UNPROCESSABLE_ENTITY
178 );
179 }
180 }
181
182 /**
183 * Handle ajax request for creating coupon
184 *
185 * @since 3.0.0
186 *
187 * @return void send wp_json response
188 */
189 public function ajax_create_coupon() {
190 tutor_utils()->check_nonce();
191 tutor_utils()->check_current_user_capability();
192
193 $data = $this->get_allowed_fields( Input::sanitize_array( $_POST ), true );//phpcs:ignore --sanitized already
194
195 if ( $this->model::TYPE_AUTOMATIC === $data['coupon_type'] ) {
196 $data['coupon_code'] = time();
197 }
198
199 $validation = $this->validate( $data );
200 if ( ! $validation->success ) {
201 $this->json_response(
202 tutor_utils()->error_message( 'validation_error' ),
203 $validation->errors,
204 HttpHelper::STATUS_UNPROCESSABLE_ENTITY
205 );
206 }
207
208 $this->validate_applies_to_item( $data );
209
210 if ( $this->model->get_coupon( array( 'coupon_code' => $data['coupon_code'] ) ) ) {
211 $this->json_response(
212 __( 'Coupon code already exists!', 'tutor' ),
213 null,
214 HttpHelper::STATUS_UNPROCESSABLE_ENTITY
215 );
216 }
217
218 // Convert start & expire date time into gmt.
219 $data['start_date_gmt'] = $data['start_date_gmt'];
220 $data['created_by'] = get_current_user_id();
221 $data['created_at_gmt'] = current_time( 'mysql', true );
222 $data['updated_at_gmt'] = current_time( 'mysql', true );
223 $applies_to_items = isset( $data['applies_to_items'] ) ? $data['applies_to_items'] : array();
224 unset( $data['applies_to_items'] );
225
226 // Set expire date if isset.
227 if ( isset( $data['expire_date_gmt'] ) ) {
228 $data['expire_date_gmt'] = $data['expire_date_gmt'];
229 }
230
231 try {
232 $coupon_id = $this->model->create_coupon( $data );
233 if ( $coupon_id ) {
234 if ( is_array( $applies_to_items ) && count( $applies_to_items ) ) {
235 $this->model->insert_applies_to( $data['applies_to'], $applies_to_items, $data['coupon_code'] );
236 }
237
238 $this->json_response( __( 'Coupon created successfully!', 'tutor' ) );
239 } else {
240 $this->json_response(
241 __( 'Failed to create!', 'tutor' ),
242 null,
243 HttpHelper::STATUS_INTERNAL_SERVER_ERROR
244 );
245 }
246 } catch ( \Throwable $th ) {
247 $this->json_response(
248 tutor_utils()->error_message( 'server_error' ),
249 $th->getMessage(),
250 HttpHelper::STATUS_INTERNAL_SERVER_ERROR
251 );
252 }
253 }
254
255 /**
256 * Handle ajax request for updating coupon
257 *
258 * @since 3.0.0
259 *
260 * @return void send wp_json response
261 */
262 public function ajax_update_coupon() {
263 tutor_utils()->check_nonce();
264 tutor_utils()->check_current_user_capability();
265
266 $data = $this->get_allowed_fields( Input::sanitize_array( $_POST ), false );//phpcs:ignore --sanitized already
267
268 $coupon_id = Input::post( 'id', null, Input::TYPE_INT );
269 $data['coupon_id'] = $coupon_id;
270 $data['updated_at_gmt'] = current_time( 'mysql', true );
271
272 $validation = $this->validate( $data );
273 if ( ! $validation->success ) {
274 $this->json_response(
275 tutor_utils()->error_message( 'validation_error' ),
276 $validation->errors,
277 HttpHelper::STATUS_UNPROCESSABLE_ENTITY
278 );
279 }
280
281 $this->validate_applies_to_item( $data );
282
283 unset( $data['coupon_id'] );
284 unset( $data['coupon_type'] );
285
286 if ( ! isset( $data['expire_date_gmt'] ) ) {
287 $data['expire_date_gmt'] = null;
288 }
289
290 /**
291 * Validate that the expiration date is not earlier than the start date.
292 *
293 * @since 4.0.0
294 */
295 if ( $data['expire_date_gmt'] && strtotime( $data['expire_date_gmt'] ) < strtotime( $data['start_date_gmt'] ) ) {
296 $this->json_response(
297 __( 'End date cannot be prior to the start date.', 'tutor' ),
298 null,
299 HttpHelper::STATUS_UNPROCESSABLE_ENTITY
300 );
301 }
302
303 // Set updated by.
304 $data['updated_by'] = get_current_user_id();
305
306 try {
307 $update = $this->model->update_coupon( $coupon_id, $data );
308 if ( $update ) {
309 $coupon_data = $this->model->get_coupon( array( 'id' => $coupon_id ) );
310 $this->model->delete_applies_to( $coupon_data->coupon_code );
311
312 if ( $this->has_applies_to_items( $data ) ) {
313 $this->model->insert_applies_to( $data['applies_to'], $data['applies_to_items'], $coupon_data->coupon_code );
314 }
315
316 $this->json_response( __( 'Coupon updated successfully!', 'tutor' ) );
317 } else {
318 $this->json_response(
319 __( 'Failed to update!', 'tutor' ),
320 null,
321 HttpHelper::STATUS_INTERNAL_SERVER_ERROR
322 );
323 }
324 } catch ( \Throwable $th ) {
325 $this->json_response(
326 tutor_utils()->error_message( 'server_error' ),
327 $th->getMessage(),
328 HttpHelper::STATUS_INTERNAL_SERVER_ERROR
329 );
330 }
331 }
332
333 /**
334 * Get list of coupon applies to on which coupon
335 * will be applicable
336 *
337 * @since 3.0.0
338 *
339 * @return void send wp_json response
340 */
341 public function ajax_coupon_applies_to_list() {
342 tutor_utils()->check_nonce();
343 tutor_utils()->check_current_user_capability();
344
345 $applies_to = Input::post( 'applies_to' );
346 $limit = Input::post( 'limit', 10, Input::TYPE_INT );
347 $offset = Input::post( 'offset', 0, Input::TYPE_INT );
348 $search_term = '';
349
350 $filter = json_decode( wp_unslash( $_POST['filter'] ) ); //phpcs:ignore --sanitized already
351 if ( ! empty( $filter ) && property_exists( $filter, 'search' ) ) {
352 $search_term = Input::sanitize( $filter->search );
353 }
354
355 if ( $this->model->is_specific_applies_to( $applies_to ) ) {
356 try {
357 $list = $this->get_application_list( $applies_to, $limit, $offset, $search_term );
358 if ( $list ) {
359 $this->json_response(
360 __( 'Coupon application list retrieved successfully!', 'tutor' ),
361 $list
362 );
363 } else {
364 $this->json_response(
365 tutor_utils()->error_message( 'not_found' ),
366 null,
367 HttpHelper::STATUS_NOT_FOUND
368 );
369 }
370 } catch ( \Throwable $th ) {
371 $this->json_response(
372 tutor_utils()->error_message( 'server_error' ),
373 $th->getMessage(),
374 HttpHelper::STATUS_INTERNAL_SERVER_ERROR
375 );
376 }
377 } else {
378 $this->json_response(
379 tutor_utils()->error_message( 'invalid_req' ),
380 null,
381 HttpHelper::STATUS_UNPROCESSABLE_ENTITY
382 );
383 }
384 }
385
386 /**
387 * Prepare bulk actions that will show on dropdown options
388 *
389 * @return array
390 * @since 3.0.0
391 */
392 public function prepare_bulk_actions(): array {
393 $actions = array(
394 $this->bulk_action_default(),
395 $this->bulk_action_active(),
396 $this->bulk_action_inactive(),
397 );
398
399 $active_tab = Input::get( 'data', '' );
400
401 if ( 'trash' === $active_tab ) {
402 array_push( $actions, $this->bulk_action_delete() );
403 } else {
404 array_push( $actions, $this->bulk_action_trash() );
405 }
406
407 return apply_filters( 'tutor_coupon_bulk_actions', $actions );
408 }
409
410 /**
411 * Get coupon page url
412 *
413 * @since 3.0.0
414 *
415 * @param boolean $is_admin Whether to get admin or frontend url.
416 *
417 * @return string
418 */
419 public static function get_coupon_page_url( bool $is_admin = true ) {
420 if ( $is_admin ) {
421 return admin_url( 'admin.php?page=' . self::PAGE_SLUG );
422 } else {
423 return tutor_utils()->get_tutor_dashboard_url() . '/coupons';
424 }
425 }
426
427 /**
428 * Available tabs that will visible on the right side of page navbar
429 *
430 * @return array
431 *
432 * @since 3.0.0
433 */
434 public function tabs_key_value(): array {
435 $url = apply_filters( 'tutor_data_tab_base_url', get_pagenum_link() );
436
437 $date = Input::get( 'date', '' );
438 $coupon_status = Input::get( 'coupon-status', '' );
439 $applies_to = Input::get( 'applies_to', '' );
440 $search = Input::get( 'search', '' );
441 $where = array();
442 $status_filter = array();
443
444 if ( ! empty( $date ) ) {
445 $where['date(created_at_gmt)'] = tutor_get_formated_date( 'Y-m-d', $date );
446 }
447
448 if ( ! empty( $coupon_status ) ) {
449 $where['coupon_status'] = $coupon_status;
450 }
451
452 if ( ! empty( $applies_to ) ) {
453 $where['applies_to'] = $applies_to;
454 }
455
456 if ( ! tutor_utils()->is_addon_enabled( 'subscription' ) && empty( $applies_to ) ) {
457 $where['applies_to'] = $this->model->get_course_bundle_applies_to( true );
458 }
459
460 $coupon_status = $this->model->get_coupon_status();
461
462 $tabs = array();
463
464 $tabs [] = array(
465 'key' => '',
466 'title' => __( 'All', 'tutor' ),
467 'value' => $this->model->get_coupon_count( $where, $search ),
468 'url' => $url . '&data=all',
469 );
470
471 $gm_date = DateTimeHelper::now()->to_date_time_string();
472
473 foreach ( $coupon_status as $key => $value ) {
474 $status_filter = $where;
475
476 /**
477 * Map expired and scheduled filters to active coupon status for querying.
478 */
479 $status_filter['coupon_status'] = in_array(
480 $key,
481 array( CouponModel::STATUS_EXPIRED, CouponModel::STATUS_SCHEDULED ),
482 true
483 ) ? CouponModel::STATUS_ACTIVE : $key;
484
485 switch ( $key ) {
486 case CouponModel::STATUS_EXPIRED:
487 $raw_query = '( start_date_gmt < %s AND expire_date_gmt < %s )';
488 $status_filter[ $raw_query ] = array(
489 'RAW',
490 array( $gm_date, $gm_date ),
491 );
492 break;
493
494 case CouponModel::STATUS_SCHEDULED:
495 $status_filter['start_date_gmt'] = array(
496 '>',
497 $gm_date,
498 );
499 break;
500
501 case CouponModel::STATUS_ACTIVE:
502 $status_filter['start_date_gmt'] = array(
503 '<=',
504 $gm_date,
505 );
506
507 $status_filter[ "IFNULL( expire_date_gmt, '{$gm_date}' )" ] = array(
508 '>=',
509 $gm_date,
510 );
511 break;
512 }
513
514 $tabs[] = array(
515 'key' => $key,
516 'title' => $value,
517 'value' => $this->model->get_coupon_count( $status_filter, $search ),
518 'url' => $url . '&data=' . $key,
519 );
520 }
521
522 return apply_filters( 'tutor_coupon_tabs', $tabs );
523 }
524
525 /**
526 * Get coupons
527 *
528 * @since 3.0.0
529 *
530 * @param integer $limit List limit.
531 * @param integer $offset List offset.
532 *
533 * @return array
534 */
535 public function get_coupons( $limit = 10, $offset = 0 ) {
536
537 $active_tab = Input::get( 'data', 'all' );
538
539 $date = Input::get( 'date', '' );
540 $search_term = Input::get( 'search', '' );
541 $coupon_status = Input::get( 'coupon-status' );
542 $applies_to = Input::get( 'applies_to' );
543
544 $where_clause = array();
545
546 if ( $date ) {
547 $where_clause['date(created_at_gmt)'] = tutor_get_formated_date( '', $date );
548 }
549
550 if ( $coupon_status ) {
551 $where_clause['coupon_status'] = $coupon_status;
552 }
553
554 $gm_date = DateTimeHelper::now()->to_date_time_string();
555
556 $available_statuses = array_keys( $this->model->get_coupon_status() );
557
558 if ( 'all' !== $active_tab && in_array( $active_tab, $available_statuses, true ) ) {
559
560 /**
561 * Map expired and scheduled filters to active coupon status for querying.
562 */
563 $where_clause['coupon_status'] = in_array(
564 $active_tab,
565 array( CouponModel::STATUS_EXPIRED, CouponModel::STATUS_SCHEDULED ),
566 true
567 ) ? CouponModel::STATUS_ACTIVE : $active_tab;
568
569 switch ( $active_tab ) {
570 case CouponModel::STATUS_EXPIRED:
571 $raw_query = '( start_date_gmt < %s AND expire_date_gmt < %s )';
572 $where_clause[ $raw_query ] = array(
573 'RAW',
574 array( $gm_date, $gm_date ),
575 );
576 break;
577
578 case CouponModel::STATUS_SCHEDULED:
579 $where_clause['start_date_gmt'] = array(
580 '>',
581 $gm_date,
582 );
583 break;
584
585 case CouponModel::STATUS_ACTIVE:
586 $where_clause['start_date_gmt'] = array(
587 '<=',
588 $gm_date,
589 );
590
591 $where_clause[ "IFNULL( expire_date_gmt, '{$gm_date}' )" ] = array(
592 '>=',
593 $gm_date,
594 );
595 break;
596 }
597 }
598
599 if ( $applies_to ) {
600 $where_clause['applies_to'] = $applies_to;
601 }
602
603 $list_order = Input::get( 'order', 'DESC' );
604 $list_order_by = 'id';
605
606 if ( ! tutor_utils()->is_addon_enabled( 'subscription' ) && ! $applies_to ) {
607 $where_clause['applies_to'] = $this->model->get_course_bundle_applies_to( true );
608 }
609
610 return $this->model->get_coupons( $where_clause, $search_term, $limit, $offset, $list_order_by, $list_order );
611 }
612
613 /**
614 * Handle bulk action AJAX request.
615 *
616 * Bulk actions: active, inactive, trash, delete
617 *
618 * @since 3.0.0
619 *
620 * @return void send wp_json response
621 */
622 public function bulk_action_handler() {
623 tutor_utils()->checking_nonce();
624 tutor_utils()->check_current_user_capability();
625
626 // Get and sanitize input data.
627 $request = Input::sanitize_array( $_POST ); //phpcs:ignore --sanitized already
628 $bulk_action = $request['bulk-action'];
629
630 $bulk_ids = isset( $request['bulk-ids'] ) ? array_map( 'intval', explode( ',', $request['bulk-ids'] ) ) : array();
631
632 if ( empty( $bulk_ids ) ) {
633 wp_send_json_error( __( 'No items selected for the bulk action.', 'tutor' ) );
634 }
635
636 $allowed_bulk_actions = array_keys( $this->model->get_coupon_status() );
637 array_push( $allowed_bulk_actions, 'delete' );
638
639 if ( ! in_array( $bulk_action, $allowed_bulk_actions, true ) ) {
640 wp_send_json_error( __( 'Invalid bulk action.', 'tutor' ) );
641 }
642
643 do_action( 'tutor_before_coupon_bulk_action', $bulk_action, $bulk_ids );
644
645 $response = false;
646 if ( 'delete' === $bulk_action ) {
647 $response = $this->model->delete_coupon( $bulk_ids );
648 } else {
649 $data = array(
650 'coupon_status' => $bulk_action,
651 );
652 $response = $this->model->update_coupon( $bulk_ids, $data );
653 }
654
655 do_action( 'tutor_after_coupon_bulk_action', $bulk_action, $bulk_ids );
656
657 if ( $response ) {
658 wp_send_json_success( __( 'Coupon updated successfully.', 'tutor' ) );
659 } else {
660 wp_send_json_error( __( 'Failed to update coupon.', 'tutor' ) );
661 }
662 }
663
664 /**
665 * Handle coupon permanent delete
666 *
667 * @since 3.0.0
668 *
669 * @return void send wp_json response
670 */
671 public function coupon_permanent_delete() {
672 tutor_utils()->checking_nonce();
673
674 tutor_utils()->check_current_user_capability();
675
676 // Get and sanitize input data.
677 $id = Input::post( 'id', 0, Input::TYPE_INT );
678 if ( ! $id ) {
679 wp_send_json_error( __( 'Invalid coupon ID', 'tutor' ) );
680 }
681
682 do_action( 'tutor_before_coupon_permanent_delete', $id );
683
684 $response = $this->model->delete_coupon( $id );
685 if ( $response ) {
686 do_action( 'tutor_after_coupon_permanent_delete', $id );
687
688 wp_send_json_success( __( 'Coupon delete successfully.', 'tutor' ) );
689 } else {
690 wp_send_json_error( __( 'Failed to delete coupon.', 'tutor' ) );
691 }
692 }
693
694 /**
695 * Ajax handler to retrieve coupon details.
696 *
697 * @since 3.0.0
698 *
699 * @return void Sends a JSON response with the coupon data or an error message.
700 */
701 public function ajax_coupon_details() {
702 if ( ! tutor_utils()->is_nonce_verified() ) {
703 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
704 }
705
706 tutor_utils()->check_current_user_capability();
707
708 $coupon_id = Input::post( 'id' );
709
710 if ( empty( $coupon_id ) ) {
711 $this->json_response(
712 __( 'Coupon code is required', 'tutor' ),
713 null,
714 HttpHelper::STATUS_BAD_REQUEST
715 );
716 }
717
718 $coupon_data = $this->model->get_coupon( array( 'id' => $coupon_id ) );
719
720 if ( ! $coupon_data ) {
721 $this->json_response(
722 __( 'Coupon not found', 'tutor' ),
723 null,
724 HttpHelper::STATUS_NOT_FOUND
725 );
726 }
727
728 $applications = $this->model->get_formatted_coupon_applications( $coupon_data );
729
730 // Set applies to items.
731 $coupon_data->applies_to_items = apply_filters( 'tutor_coupon_details_applies_to_items_response', $applications, $coupon_data );
732
733 // Set coupon usage.
734 $coupon_data->coupon_usage = $this->model->get_coupon_usage_count( $coupon_data->coupon_code );
735
736 // Set created & updated by.
737 $coupon_data->coupon_created_by = tutor_utils()->display_name( $coupon_data->created_by );
738 $coupon_data->coupon_update_by = tutor_utils()->display_name( $coupon_data->updated_by );
739
740 $coupon_data->start_date_readable = empty( $coupon_data->start_date_gmt ) ? '' : DateTimeHelper::get_gmt_to_user_timezone_date( $coupon_data->start_date_gmt );
741 $coupon_data->expire_date_readable = empty( $coupon_data->expire_date_gmt ) ? '' : DateTimeHelper::get_gmt_to_user_timezone_date( $coupon_data->expire_date_gmt );
742 $coupon_data->created_at_readable = DateTimeHelper::get_gmt_to_user_timezone_date( $coupon_data->created_at_gmt );
743 $coupon_data->updated_at_readable = empty( $coupon_data->updated_at_gmt ) ? '' : DateTimeHelper::get_gmt_to_user_timezone_date( $coupon_data->updated_at_gmt );
744
745 $this->json_response(
746 __( 'Coupon retrieved successfully', 'tutor' ),
747 $coupon_data
748 );
749 }
750
751 /**
752 * Get application if applies to a specific category or bundle.
753 *
754 * @since 3.0.0
755 *
756 * @param string $applies_to Applies to.
757 * @param int $limit Number of items to fetch.
758 * @param int $offset Offset for fetching items.
759 * @param int $search_term Search term.
760 *
761 * @return array
762 */
763 public function get_application_list( string $applies_to, int $limit = 10, int $offset = 0, $search_term = '' ) {
764
765 $response = array(
766 'total_items' => 0,
767 'results' => array(),
768 );
769
770 if ( $this->model::APPLIES_TO_SPECIFIC_COURSES === $applies_to ) {
771 $args = array(
772 'post_type' => tutor()->course_post_type,
773 'posts_per_page' => $limit,
774 'offset' => $offset,
775 );
776
777 // Add search.
778 if ( $search_term ) {
779 $args['s'] = $search_term;
780 }
781
782 $courses = ( new CourseModel() )->get_paid_courses( $args );
783
784 $response['total_items'] = is_a( $courses, 'WP_Query' ) ? $courses->found_posts : 0;
785
786 if ( is_a( $courses, 'WP_Query' ) && $courses->have_posts() ) {
787 $courses = $courses->get_posts();
788 foreach ( $courses as $course ) {
789 $response['results'][] = Course::get_mini_info( $course );
790 }
791 }
792 } elseif ( $this->model::APPLIES_TO_SPECIFIC_BUNDLES === $applies_to && tutor_utils()->is_addon_enabled( 'tutor-pro/addons/course-bundle/course-bundle.php' ) ) {
793 $args = array(
794 'post_type' => 'course-bundle',
795 'posts_per_page' => $limit,
796 'offset' => $offset,
797 );
798
799 // Add search.
800 if ( $search_term ) {
801 $args['s'] = $search_term;
802 }
803
804 $bundles = ( new CourseModel() )->get_paid_courses( $args );
805
806 $response['total_items'] = is_a( $bundles, 'WP_Query' ) ? $bundles->found_posts : 0;
807
808 if ( is_a( $bundles, 'WP_Query' ) && $bundles->have_posts() ) {
809 $bundles = $bundles->get_posts();
810 foreach ( $bundles as $bundle ) {
811 $response['results'][] = Course::get_mini_info( $bundle );
812 }
813 }
814 } elseif ( $this->model::APPLIES_TO_SPECIFIC_CATEGORY === $applies_to ) {
815 $args = array(
816 'number' => $limit,
817 'offset' => $offset,
818 'hide_empty' => true,
819 );
820
821 $total_arg = array(
822 'fields' => 'ids',
823 'taxonomy' => CourseModel::COURSE_CATEGORY,
824 'hide_empty' => true,
825 );
826
827 // Add search.
828 if ( $search_term ) {
829 $args['search'] = $search_term;
830 $total_arg['search'] = $search_term;
831 }
832
833 $terms = tutor_utils()->get_course_categories( 0, $args );
834 $total = get_terms( $total_arg );
835
836 $response['total_items'] = is_array( $total ) ? count( $total ) : 0;
837
838 if ( ! is_wp_error( $terms ) ) {
839 foreach ( $terms as $term ) {
840 $thumb_id = get_term_meta( $term->term_id, 'thumbnail_id', true );
841
842 $response['results'][] = array(
843 'id' => $term->term_id,
844 'title' => tutor_decode_unicode_sequences( $term->name ),
845 'image' => $thumb_id ? wp_get_attachment_thumb_url( $thumb_id ) : tutor()->url . 'assets/images/placeholder.svg',
846 'total_courses' => (int) $term->count,
847 );
848 }
849 }
850 }
851
852 return $response;
853 }
854
855 /**
856 * Ajax handler for applying coupon
857 *
858 * @since 3.0.0
859 *
860 * @return void send wp_json response
861 */
862 public function ajax_apply_coupon() {
863 tutor_utils()->check_nonce();
864
865 if ( ! Settings::is_coupon_usage_enabled() ) {
866 $this->json_response(
867 __( 'Coupon usage is disabled', 'tutor' ),
868 null,
869 HttpHelper::STATUS_BAD_REQUEST
870 );
871 }
872
873 $object_ids = Input::post( 'object_ids' ); // Course/bundle ids.
874 $object_ids = array_filter( explode( ',', $object_ids ), 'is_numeric' );
875
876 if ( empty( $object_ids ) ) {
877 $this->json_response(
878 tutor_utils()->error_message( 'invalid_req' ),
879 null,
880 HttpHelper::STATUS_BAD_REQUEST
881 );
882 }
883
884 try {
885 $coupon_code = Input::post( 'coupon_code' );
886 $plan = Input::post( 'plan', 0, Input::TYPE_INT );
887 $order_type = $plan ? OrderModel::TYPE_SUBSCRIPTION : OrderModel::TYPE_SINGLE_ORDER;
888
889 $checkout_data = $this->checkout_ctrl->prepare_checkout_items( $object_ids, $order_type, $coupon_code );
890
891 if ( $checkout_data->is_coupon_applied ) {
892 $this->json_response(
893 __( 'Coupon applied successfully', 'tutor' ),
894 $checkout_data
895 );
896 } else {
897 global $tutor_coupon_apply_err_msg;
898 $this->json_response(
899 $tutor_coupon_apply_err_msg,
900 null,
901 HttpHelper::STATUS_BAD_REQUEST
902 );
903 }
904 } catch ( \Throwable $th ) {
905 $this->json_response(
906 $th->getMessage(),
907 null,
908 HttpHelper::STATUS_INTERNAL_SERVER_ERROR
909 );
910 }
911 }
912
913 /**
914 * Manage coupon usage
915 *
916 * Store usage upon order completion
917 *
918 * @since 3.0.0
919 *
920 * @param int $order_id Order id.
921 *
922 * @return void
923 */
924 public function store_coupon_usage( $order_id ) {
925 $order_model = ( new OrderModel() );
926
927 $order = $order_model->get_order_by_id( $order_id );
928 if ( $order ) {
929 if ( $order->coupon_amount > 0 && $order_model::ORDER_COMPLETED === $order->order_status ) {
930 // Store coupon usage.
931 $data = array(
932 'coupon_code' => $order->coupon_code,
933 'user_id' => $order->user_id,
934 );
935
936 try {
937 $this->model->store_coupon_usage( $data );
938 } catch ( \Throwable $th ) {
939 tutor_log( $th );
940 }
941 }
942 }
943 }
944
945 /**
946 * Validate input data based on predefined rules.
947 *
948 * @since 3.0.0
949 *
950 * @param array $data The data array to validate.
951 *
952 * @return object The validation result. It returns validation object.
953 */
954 protected function validate( array $data ) {
955
956 $validation_rules = array(
957 'coupon_id' => 'numeric',
958 'coupon_status' => 'required',
959 'coupon_type' => 'required',
960 'coupon_code' => 'required',
961 'coupon_title' => 'required',
962 'discount_type' => 'required',
963 'discount_amount' => 'required',
964 'applies_to' => 'required',
965 'total_usage_limit' => 'numeric',
966 'per_user_usage_limit' => 'numeric',
967 'start_date_gmt' => 'required|date_format:Y-m-d H:i:s',
968 );
969
970 // Skip validation rules for not available fields in data.
971 foreach ( $validation_rules as $key => $value ) {
972 if ( ! array_key_exists( $key, $data ) ) {
973 unset( $validation_rules[ $key ] );
974 }
975 }
976
977 return ValidationHelper::validate( $validation_rules, $data );
978 }
979
980 /**
981 * Get the display status for a coupon.
982 *
983 * @since 4.0.0
984 *
985 * @param object $coupon Coupon object.
986 *
987 * @return string Coupon display status.
988 */
989 public function get_coupon_display_status( $coupon ) {
990
991 if ( CouponModel::STATUS_ACTIVE === $coupon->coupon_status && $this->model->is_scheduled( $coupon ) ) {
992 return CouponModel::STATUS_SCHEDULED;
993 }
994
995 if ( CouponModel::STATUS_ACTIVE === $coupon->coupon_status && ! $this->model->has_coupon_validity( $coupon ) ) {
996 return CouponModel::STATUS_EXPIRED;
997 }
998
999 return $coupon->coupon_status;
1000 }
1001 }
1002