PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 3.9.0
Tutor LMS – eLearning and online course solution v3.9.0
3.9.14 3.9.13 3.9.12 3.9.11 trunk 1.0.0 1.0.0-alpha 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.2.0 1.2.1 1.2.11 1.2.12 1.2.13 1.2.20 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 1.7.6 1.7.7 1.7.8 1.7.9 1.8.0 1.8.1 1.8.10 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7 1.8.8 1.8.9 1.9.0 1.9.1 1.9.10 1.9.11 1.9.12 1.9.13 1.9.14 1.9.15 1.9.16 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.1 2.0.10 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1.0 2.1.1 2.1.10 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.3.0 2.4.0 2.5.0 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 3.0.0 3.0.1 3.0.2 3.1.0 3.2.0 3.2.1 3.2.2 3.2.3 3.3.0 3.3.1 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.7.0 3.7.1 3.7.2 3.7.3 3.7.4 3.8.0 3.8.1 3.8.2 3.8.3 3.9.0 3.9.1 3.9.10 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9
tutor / ecommerce / CouponController.php
tutor / ecommerce Last commit date
Cart 10 months ago PaymentGateways 8 months ago AdminMenu.php 9 months ago BillingController.php 1 year ago CartController.php 1 year ago CheckoutController.php 8 months ago CouponController.php 11 months ago Ecommerce.php 1 year ago EmailController.php 11 months ago HooksHandler.php 9 months ago OptionKeys.php 1 year ago OrderActivitiesController.php 1 year ago OrderController.php 9 months ago PaymentHandler.php 9 months ago Settings.php 9 months ago Tax.php 9 months ago currency.php 1 year ago
CouponController.php
939 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 // Set updated by.
291 $data['updated_by'] = get_current_user_id();
292
293 try {
294 $update = $this->model->update_coupon( $coupon_id, $data );
295 if ( $update ) {
296 $coupon_data = $this->model->get_coupon( array( 'id' => $coupon_id ) );
297 $this->model->delete_applies_to( $coupon_data->coupon_code );
298
299 if ( $this->has_applies_to_items( $data ) ) {
300 $this->model->insert_applies_to( $data['applies_to'], $data['applies_to_items'], $coupon_data->coupon_code );
301 }
302
303 $this->json_response( __( 'Coupon updated successfully!', 'tutor' ) );
304 } else {
305 $this->json_response(
306 __( 'Failed to update!', 'tutor' ),
307 null,
308 HttpHelper::STATUS_INTERNAL_SERVER_ERROR
309 );
310 }
311 } catch ( \Throwable $th ) {
312 $this->json_response(
313 tutor_utils()->error_message( 'server_error' ),
314 $th->getMessage(),
315 HttpHelper::STATUS_INTERNAL_SERVER_ERROR
316 );
317 }
318 }
319
320 /**
321 * Get list of coupon applies to on which coupon
322 * will be applicable
323 *
324 * @since 3.0.0
325 *
326 * @return void send wp_json response
327 */
328 public function ajax_coupon_applies_to_list() {
329 tutor_utils()->check_nonce();
330 tutor_utils()->check_current_user_capability();
331
332 $applies_to = Input::post( 'applies_to' );
333 $limit = Input::post( 'limit', 10, Input::TYPE_INT );
334 $offset = Input::post( 'offset', 0, Input::TYPE_INT );
335 $search_term = '';
336
337 $filter = json_decode( wp_unslash( $_POST['filter'] ) ); //phpcs:ignore --sanitized already
338 if ( ! empty( $filter ) && property_exists( $filter, 'search' ) ) {
339 $search_term = Input::sanitize( $filter->search );
340 }
341
342 if ( $this->model->is_specific_applies_to( $applies_to ) ) {
343 try {
344 $list = $this->get_application_list( $applies_to, $limit, $offset, $search_term );
345 if ( $list ) {
346 $this->json_response(
347 __( 'Coupon application list retrieved successfully!', 'tutor' ),
348 $list
349 );
350 } else {
351 $this->json_response(
352 tutor_utils()->error_message( 'not_found' ),
353 null,
354 HttpHelper::STATUS_NOT_FOUND
355 );
356 }
357 } catch ( \Throwable $th ) {
358 $this->json_response(
359 tutor_utils()->error_message( 'server_error' ),
360 $th->getMessage(),
361 HttpHelper::STATUS_INTERNAL_SERVER_ERROR
362 );
363 }
364 } else {
365 $this->json_response(
366 tutor_utils()->error_message( 'invalid_req' ),
367 null,
368 HttpHelper::STATUS_UNPROCESSABLE_ENTITY
369 );
370 }
371 }
372
373 /**
374 * Prepare bulk actions that will show on dropdown options
375 *
376 * @return array
377 * @since 3.0.0
378 */
379 public function prepare_bulk_actions(): array {
380 $actions = array(
381 $this->bulk_action_default(),
382 $this->bulk_action_active(),
383 $this->bulk_action_inactive(),
384 );
385
386 $active_tab = Input::get( 'data', '' );
387
388 if ( 'trash' === $active_tab ) {
389 array_push( $actions, $this->bulk_action_delete() );
390 } else {
391 array_push( $actions, $this->bulk_action_trash() );
392 }
393
394 return apply_filters( 'tutor_coupon_bulk_actions', $actions );
395 }
396
397 /**
398 * Get coupon page url
399 *
400 * @since 3.0.0
401 *
402 * @param boolean $is_admin Whether to get admin or frontend url.
403 *
404 * @return string
405 */
406 public static function get_coupon_page_url( bool $is_admin = true ) {
407 if ( $is_admin ) {
408 return admin_url( 'admin.php?page=' . self::PAGE_SLUG );
409 } else {
410 return tutor_utils()->get_tutor_dashboard_url() . '/coupons';
411 }
412 }
413
414 /**
415 * Available tabs that will visible on the right side of page navbar
416 *
417 * @return array
418 *
419 * @since 3.0.0
420 */
421 public function tabs_key_value(): array {
422 $url = apply_filters( 'tutor_data_tab_base_url', get_pagenum_link() );
423
424 $date = Input::get( 'date', '' );
425 $coupon_status = Input::get( 'coupon-status', '' );
426 $applies_to = Input::get( 'applies_to', '' );
427 $search = Input::get( 'search', '' );
428
429 $where = array();
430
431 if ( ! empty( $date ) ) {
432 $where['date(created_at_gmt)'] = tutor_get_formated_date( 'Y-m-d', $date );
433 }
434
435 if ( ! empty( $coupon_status ) ) {
436 $where['coupon_status'] = $coupon_status;
437 }
438
439 if ( ! empty( $applies_to ) ) {
440 $where['applies_to'] = $applies_to;
441 }
442
443 if ( ! tutor_utils()->is_addon_enabled( 'subscription' ) && empty( $applies_to ) ) {
444 $where['applies_to'] = $this->model->get_course_bundle_applies_to( true );
445 }
446
447 $coupon_status = $this->model->get_coupon_status();
448
449 $tabs = array();
450
451 $tabs [] = array(
452 'key' => '',
453 'title' => __( 'All', 'tutor' ),
454 'value' => $this->model->get_coupon_count( $where, $search ),
455 'url' => $url . '&data=all',
456 );
457
458 $gm_date = DateTimeHelper::now()->to_date_time_string();
459
460 foreach ( $coupon_status as $key => $value ) {
461 if ( CouponModel::STATUS_EXPIRED === $key ) {
462 $where['coupon_status'] = CouponModel::STATUS_ACTIVE;
463 $raw_query = '( start_date_gmt > %s OR expire_date_gmt < %s )';
464 $where[ $raw_query ] = array(
465 'RAW',
466 array( $gm_date, $gm_date ),
467 );
468 } else {
469
470 $where['coupon_status'] = $key;
471 $where['start_date_gmt'] = array(
472 '<=',
473 $gm_date,
474 );
475
476 $where[ "IFNULL( expire_date_gmt, '{$gm_date}' )" ] = array(
477 '>=',
478 $gm_date,
479 );
480
481 }
482
483 $tabs[] = array(
484 'key' => $key,
485 'title' => $value,
486 'value' => $this->model->get_coupon_count( $where, $search ),
487 'url' => $url . '&data=' . $key,
488 );
489
490 if ( isset( $where['start_date_gmt'] ) ) {
491 unset( $where['start_date_gmt'] );
492 }
493
494 if ( isset( $where[ "IFNULL( expire_date_gmt, '{$gm_date}' )" ] ) ) {
495 unset( $where[ "IFNULL( expire_date_gmt, '{$gm_date}' )" ] );
496 }
497 }
498
499 return apply_filters( 'tutor_coupon_tabs', $tabs );
500 }
501
502 /**
503 * Get coupons
504 *
505 * @since 3.0.0
506 *
507 * @param integer $limit List limit.
508 * @param integer $offset List offset.
509 *
510 * @return array
511 */
512 public function get_coupons( $limit = 10, $offset = 0 ) {
513
514 $active_tab = Input::get( 'data', 'all' );
515
516 $date = Input::get( 'date', '' );
517 $search_term = Input::get( 'search', '' );
518 $coupon_status = Input::get( 'coupon-status' );
519 $applies_to = Input::get( 'applies_to' );
520
521 $where_clause = array();
522
523 if ( $date ) {
524 $where_clause['date(created_at_gmt)'] = tutor_get_formated_date( '', $date );
525 }
526
527 if ( $coupon_status ) {
528 $where_clause['coupon_status'] = $coupon_status;
529 }
530
531 $gm_date = DateTimeHelper::now()->to_date_time_string();
532
533 if ( 'all' !== $active_tab && in_array( $active_tab, array_keys( $this->model->get_coupon_status() ), true ) ) {
534 if ( CouponModel::STATUS_EXPIRED === $active_tab ) {
535 $where_clause['coupon_status'] = CouponModel::STATUS_ACTIVE;
536 $raw_query = '( start_date_gmt > %s OR expire_date_gmt < %s )';
537 $where_clause[ $raw_query ] = array(
538 'RAW',
539 array( $gm_date, $gm_date ),
540 );
541 } else {
542 $where_clause['coupon_status'] = $active_tab;
543 $where_clause['start_date_gmt'] = array(
544 '<=',
545 $gm_date,
546 );
547
548 $where_clause[ "IFNULL( expire_date_gmt, '{$gm_date}' )" ] = array(
549 '>=',
550 $gm_date,
551 );
552 }
553 }
554
555 if ( $applies_to ) {
556 $where_clause['applies_to'] = $applies_to;
557 }
558
559 $list_order = Input::get( 'order', 'DESC' );
560 $list_order_by = 'id';
561
562 if ( ! tutor_utils()->is_addon_enabled( 'subscription' ) && ! $applies_to ) {
563 $where_clause['applies_to'] = $this->model->get_course_bundle_applies_to( true );
564 }
565
566 return $this->model->get_coupons( $where_clause, $search_term, $limit, $offset, $list_order_by, $list_order );
567 }
568
569 /**
570 * Handle bulk action AJAX request.
571 *
572 * Bulk actions: active, inactive, trash, delete
573 *
574 * @since 3.0.0
575 *
576 * @return void send wp_json response
577 */
578 public function bulk_action_handler() {
579 tutor_utils()->checking_nonce();
580
581 if ( ! current_user_can( 'manage_options' ) ) {
582 tutor_utils()->error_message();
583 }
584
585 // Get and sanitize input data.
586 $request = Input::sanitize_array( $_POST ); //phpcs:ignore --sanitized already
587 $bulk_action = $request['bulk-action'];
588
589 $bulk_ids = isset( $request['bulk-ids'] ) ? array_map( 'intval', explode( ',', $request['bulk-ids'] ) ) : array();
590
591 if ( empty( $bulk_ids ) ) {
592 wp_send_json_error( __( 'No items selected for the bulk action.', 'tutor' ) );
593 }
594
595 $allowed_bulk_actions = array_keys( $this->model->get_coupon_status() );
596 array_push( $allowed_bulk_actions, 'delete' );
597
598 if ( ! in_array( $bulk_action, $allowed_bulk_actions, true ) ) {
599 wp_send_json_error( __( 'Invalid bulk action.', 'tutor' ) );
600 }
601
602 do_action( 'tutor_before_coupon_bulk_action', $bulk_action, $bulk_ids );
603
604 $response = false;
605 if ( 'delete' === $bulk_action ) {
606 $response = $this->model->delete_coupon( $bulk_ids );
607 } else {
608 $data = array(
609 'coupon_status' => $bulk_action,
610 );
611 $response = $this->model->update_coupon( $bulk_ids, $data );
612 }
613
614 do_action( 'tutor_after_coupon_bulk_action', $bulk_action, $bulk_ids );
615
616 if ( $response ) {
617 wp_send_json_success( __( 'Coupon updated successfully.', 'tutor' ) );
618 } else {
619 wp_send_json_error( __( 'Failed to update coupon.', 'tutor' ) );
620 }
621 }
622
623 /**
624 * Handle coupon permanent delete
625 *
626 * @since 3.0.0
627 *
628 * @return void send wp_json response
629 */
630 public function coupon_permanent_delete() {
631 tutor_utils()->checking_nonce();
632
633 if ( ! current_user_can( 'manage_options' ) ) {
634 tutor_utils()->error_message();
635 }
636
637 // Get and sanitize input data.
638 $id = Input::post( 'id', 0, Input::TYPE_INT );
639 if ( ! $id ) {
640 wp_send_json_error( __( 'Invalid coupon ID', 'tutor' ) );
641 }
642
643 do_action( 'tutor_before_coupon_permanent_delete', $id );
644
645 $response = $this->model->delete_coupon( $id );
646 if ( $response ) {
647 do_action( 'tutor_after_coupon_permanent_delete', $id );
648
649 wp_send_json_success( __( 'Coupon delete successfully.', 'tutor' ) );
650 } else {
651 wp_send_json_error( __( 'Failed to delete coupon.', 'tutor' ) );
652 }
653 }
654
655 /**
656 * Ajax handler to retrieve coupon details.
657 *
658 * @since 3.0.0
659 *
660 * @return void Sends a JSON response with the coupon data or an error message.
661 */
662 public function ajax_coupon_details() {
663 if ( ! tutor_utils()->is_nonce_verified() ) {
664 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
665 }
666
667 $coupon_id = Input::post( 'id' );
668
669 if ( empty( $coupon_id ) ) {
670 $this->json_response(
671 __( 'Coupon code is required', 'tutor' ),
672 null,
673 HttpHelper::STATUS_BAD_REQUEST
674 );
675 }
676
677 $coupon_data = $this->model->get_coupon( array( 'id' => $coupon_id ) );
678
679 if ( ! $coupon_data ) {
680 $this->json_response(
681 __( 'Coupon not found', 'tutor' ),
682 null,
683 HttpHelper::STATUS_NOT_FOUND
684 );
685 }
686
687 $applications = $this->model->get_formatted_coupon_applications( $coupon_data );
688
689 // Set applies to items.
690 $coupon_data->applies_to_items = apply_filters( 'tutor_coupon_details_applies_to_items_response', $applications, $coupon_data );
691
692 // Set coupon usage.
693 $coupon_data->coupon_usage = $this->model->get_coupon_usage_count( $coupon_data->coupon_code );
694
695 // Set created & updated by.
696 $coupon_data->coupon_created_by = tutor_utils()->display_name( $coupon_data->created_by );
697 $coupon_data->coupon_update_by = tutor_utils()->display_name( $coupon_data->updated_by );
698
699 $coupon_data->start_date_readable = empty( $coupon_data->start_date_gmt ) ? '' : DateTimeHelper::get_gmt_to_user_timezone_date( $coupon_data->start_date_gmt );
700 $coupon_data->expire_date_readable = empty( $coupon_data->expire_date_gmt ) ? '' : DateTimeHelper::get_gmt_to_user_timezone_date( $coupon_data->expire_date_gmt );
701 $coupon_data->created_at_readable = DateTimeHelper::get_gmt_to_user_timezone_date( $coupon_data->created_at_gmt );
702 $coupon_data->updated_at_readable = empty( $coupon_data->updated_at_gmt ) ? '' : DateTimeHelper::get_gmt_to_user_timezone_date( $coupon_data->updated_at_gmt );
703
704 $this->json_response(
705 __( 'Coupon retrieved successfully', 'tutor' ),
706 $coupon_data
707 );
708 }
709
710 /**
711 * Get application if applies to a specific category or bundle.
712 *
713 * @since 3.0.0
714 *
715 * @param string $applies_to Applies to.
716 * @param int $limit Number of items to fetch.
717 * @param int $offset Offset for fetching items.
718 * @param int $search_term Search term.
719 *
720 * @return array
721 */
722 public function get_application_list( string $applies_to, int $limit = 10, int $offset = 0, $search_term = '' ) {
723
724 $response = array(
725 'total_items' => 0,
726 'results' => array(),
727 );
728
729 if ( $this->model::APPLIES_TO_SPECIFIC_COURSES === $applies_to ) {
730 $args = array(
731 'post_type' => tutor()->course_post_type,
732 'posts_per_page' => $limit,
733 'offset' => $offset,
734 );
735
736 // Add search.
737 if ( $search_term ) {
738 $args['s'] = $search_term;
739 }
740
741 $courses = ( new CourseModel() )->get_paid_courses( $args );
742
743 $response['total_items'] = is_a( $courses, 'WP_Query' ) ? $courses->found_posts : 0;
744
745 if ( is_a( $courses, 'WP_Query' ) && $courses->have_posts() ) {
746 $courses = $courses->get_posts();
747 foreach ( $courses as $course ) {
748 $response['results'][] = Course::get_mini_info( $course );
749 }
750 }
751 } elseif ( $this->model::APPLIES_TO_SPECIFIC_BUNDLES === $applies_to && tutor_utils()->is_addon_enabled( 'tutor-pro/addons/course-bundle/course-bundle.php' ) ) {
752 $args = array(
753 'post_type' => 'course-bundle',
754 'posts_per_page' => $limit,
755 'offset' => $offset,
756 );
757
758 // Add search.
759 if ( $search_term ) {
760 $args['s'] = $search_term;
761 }
762
763 $bundles = ( new CourseModel() )->get_paid_courses( $args );
764
765 $response['total_items'] = is_a( $bundles, 'WP_Query' ) ? $bundles->found_posts : 0;
766
767 if ( is_a( $bundles, 'WP_Query' ) && $bundles->have_posts() ) {
768 $bundles = $bundles->get_posts();
769 foreach ( $bundles as $bundle ) {
770 $response['results'][] = Course::get_mini_info( $bundle );
771 }
772 }
773 } elseif ( $this->model::APPLIES_TO_SPECIFIC_CATEGORY === $applies_to ) {
774 $args = array(
775 'number' => $limit,
776 'offset' => $offset,
777 'hide_empty' => true,
778 );
779
780 $total_arg = array(
781 'fields' => 'ids',
782 'taxonomy' => CourseModel::COURSE_CATEGORY,
783 'hide_empty' => true,
784 );
785
786 // Add search.
787 if ( $search_term ) {
788 $args['search'] = $search_term;
789 $total_arg['search'] = $search_term;
790 }
791
792 $terms = tutor_utils()->get_course_categories( 0, $args );
793 $total = get_terms( $total_arg );
794
795 $response['total_items'] = is_array( $total ) ? count( $total ) : 0;
796
797 if ( ! is_wp_error( $terms ) ) {
798 foreach ( $terms as $term ) {
799 $thumb_id = get_term_meta( $term->term_id, 'thumbnail_id', true );
800
801 $response['results'][] = array(
802 'id' => $term->term_id,
803 'title' => $term->name,
804 'image' => $thumb_id ? wp_get_attachment_thumb_url( $thumb_id ) : tutor()->url . 'assets/images/placeholder.svg',
805 'total_courses' => (int) $term->count,
806 );
807 }
808 }
809 }
810
811 return $response;
812 }
813
814 /**
815 * Ajax handler for applying coupon
816 *
817 * @since 3.0.0
818 *
819 * @return void send wp_json response
820 */
821 public function ajax_apply_coupon() {
822 tutor_utils()->check_nonce();
823
824 if ( ! Settings::is_coupon_usage_enabled() ) {
825 $this->json_response(
826 __( 'Coupon usage is disabled', 'tutor' ),
827 null,
828 HttpHelper::STATUS_BAD_REQUEST
829 );
830 }
831
832 $object_ids = Input::post( 'object_ids' ); // Course/bundle ids.
833 $object_ids = array_filter( explode( ',', $object_ids ), 'is_numeric' );
834
835 if ( empty( $object_ids ) ) {
836 $this->json_response(
837 tutor_utils()->error_message( 'invalid_req' ),
838 null,
839 HttpHelper::STATUS_BAD_REQUEST
840 );
841 }
842
843 try {
844 $coupon_code = Input::post( 'coupon_code' );
845 $plan = Input::post( 'plan', 0, Input::TYPE_INT );
846 $order_type = $plan ? OrderModel::TYPE_SUBSCRIPTION : OrderModel::TYPE_SINGLE_ORDER;
847
848 $checkout_data = $this->checkout_ctrl->prepare_checkout_items( $object_ids, $order_type, $coupon_code );
849
850 if ( $checkout_data->is_coupon_applied ) {
851 $this->json_response(
852 __( 'Coupon applied successfully', 'tutor' ),
853 $checkout_data
854 );
855 } else {
856 global $tutor_coupon_apply_err_msg;
857 $this->json_response(
858 $tutor_coupon_apply_err_msg,
859 null,
860 HttpHelper::STATUS_BAD_REQUEST
861 );
862 }
863 } catch ( \Throwable $th ) {
864 $this->json_response(
865 $th->getMessage(),
866 null,
867 HttpHelper::STATUS_INTERNAL_SERVER_ERROR
868 );
869 }
870 }
871
872 /**
873 * Manage coupon usage
874 *
875 * Store usage upon order completion
876 *
877 * @since 3.0.0
878 *
879 * @param int $order_id Order id.
880 *
881 * @return void
882 */
883 public function store_coupon_usage( $order_id ) {
884 $order_model = ( new OrderModel() );
885
886 $order = $order_model->get_order_by_id( $order_id );
887 if ( $order ) {
888 if ( $order->coupon_amount > 0 && $order_model::ORDER_COMPLETED === $order->order_status ) {
889 // Store coupon usage.
890 $data = array(
891 'coupon_code' => $order->coupon_code,
892 'user_id' => $order->user_id,
893 );
894
895 try {
896 $this->model->store_coupon_usage( $data );
897 } catch ( \Throwable $th ) {
898 tutor_log( $th );
899 }
900 }
901 }
902 }
903
904 /**
905 * Validate input data based on predefined rules.
906 *
907 * @since 3.0.0
908 *
909 * @param array $data The data array to validate.
910 *
911 * @return object The validation result. It returns validation object.
912 */
913 protected function validate( array $data ) {
914
915 $validation_rules = array(
916 'coupon_id' => 'numeric',
917 'coupon_status' => 'required',
918 'coupon_type' => 'required',
919 'coupon_code' => 'required',
920 'coupon_title' => 'required',
921 'discount_type' => 'required',
922 'discount_amount' => 'required',
923 'applies_to' => 'required',
924 'total_usage_limit' => 'numeric',
925 'per_user_usage_limit' => 'numeric',
926 'start_date_gmt' => 'required|date_format:Y-m-d H:i:s',
927 );
928
929 // Skip validation rules for not available fields in data.
930 foreach ( $validation_rules as $key => $value ) {
931 if ( ! array_key_exists( $key, $data ) ) {
932 unset( $validation_rules[ $key ] );
933 }
934 }
935
936 return ValidationHelper::validate( $validation_rules, $data );
937 }
938 }
939