PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 3.9.4
Tutor LMS – eLearning and online course solution v3.9.4
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 10 months ago BillingController.php 1 year ago CartController.php 1 year ago CheckoutController.php 7 months ago CouponController.php 6 months ago Ecommerce.php 1 year ago EmailController.php 11 months ago HooksHandler.php 7 months ago OptionKeys.php 1 year ago OrderActivitiesController.php 1 year ago OrderController.php 6 months ago PaymentHandler.php 9 months ago Settings.php 9 months ago Tax.php 9 months ago currency.php 1 year ago
CouponController.php
934 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 tutor_utils()->check_current_user_capability();
581
582 // Get and sanitize input data.
583 $request = Input::sanitize_array( $_POST ); //phpcs:ignore --sanitized already
584 $bulk_action = $request['bulk-action'];
585
586 $bulk_ids = isset( $request['bulk-ids'] ) ? array_map( 'intval', explode( ',', $request['bulk-ids'] ) ) : array();
587
588 if ( empty( $bulk_ids ) ) {
589 wp_send_json_error( __( 'No items selected for the bulk action.', 'tutor' ) );
590 }
591
592 $allowed_bulk_actions = array_keys( $this->model->get_coupon_status() );
593 array_push( $allowed_bulk_actions, 'delete' );
594
595 if ( ! in_array( $bulk_action, $allowed_bulk_actions, true ) ) {
596 wp_send_json_error( __( 'Invalid bulk action.', 'tutor' ) );
597 }
598
599 do_action( 'tutor_before_coupon_bulk_action', $bulk_action, $bulk_ids );
600
601 $response = false;
602 if ( 'delete' === $bulk_action ) {
603 $response = $this->model->delete_coupon( $bulk_ids );
604 } else {
605 $data = array(
606 'coupon_status' => $bulk_action,
607 );
608 $response = $this->model->update_coupon( $bulk_ids, $data );
609 }
610
611 do_action( 'tutor_after_coupon_bulk_action', $bulk_action, $bulk_ids );
612
613 if ( $response ) {
614 wp_send_json_success( __( 'Coupon updated successfully.', 'tutor' ) );
615 } else {
616 wp_send_json_error( __( 'Failed to update coupon.', 'tutor' ) );
617 }
618 }
619
620 /**
621 * Handle coupon permanent delete
622 *
623 * @since 3.0.0
624 *
625 * @return void send wp_json response
626 */
627 public function coupon_permanent_delete() {
628 tutor_utils()->checking_nonce();
629
630 tutor_utils()->check_current_user_capability();
631
632 // Get and sanitize input data.
633 $id = Input::post( 'id', 0, Input::TYPE_INT );
634 if ( ! $id ) {
635 wp_send_json_error( __( 'Invalid coupon ID', 'tutor' ) );
636 }
637
638 do_action( 'tutor_before_coupon_permanent_delete', $id );
639
640 $response = $this->model->delete_coupon( $id );
641 if ( $response ) {
642 do_action( 'tutor_after_coupon_permanent_delete', $id );
643
644 wp_send_json_success( __( 'Coupon delete successfully.', 'tutor' ) );
645 } else {
646 wp_send_json_error( __( 'Failed to delete coupon.', 'tutor' ) );
647 }
648 }
649
650 /**
651 * Ajax handler to retrieve coupon details.
652 *
653 * @since 3.0.0
654 *
655 * @return void Sends a JSON response with the coupon data or an error message.
656 */
657 public function ajax_coupon_details() {
658 if ( ! tutor_utils()->is_nonce_verified() ) {
659 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
660 }
661
662 $coupon_id = Input::post( 'id' );
663
664 if ( empty( $coupon_id ) ) {
665 $this->json_response(
666 __( 'Coupon code is required', 'tutor' ),
667 null,
668 HttpHelper::STATUS_BAD_REQUEST
669 );
670 }
671
672 $coupon_data = $this->model->get_coupon( array( 'id' => $coupon_id ) );
673
674 if ( ! $coupon_data ) {
675 $this->json_response(
676 __( 'Coupon not found', 'tutor' ),
677 null,
678 HttpHelper::STATUS_NOT_FOUND
679 );
680 }
681
682 $applications = $this->model->get_formatted_coupon_applications( $coupon_data );
683
684 // Set applies to items.
685 $coupon_data->applies_to_items = apply_filters( 'tutor_coupon_details_applies_to_items_response', $applications, $coupon_data );
686
687 // Set coupon usage.
688 $coupon_data->coupon_usage = $this->model->get_coupon_usage_count( $coupon_data->coupon_code );
689
690 // Set created & updated by.
691 $coupon_data->coupon_created_by = tutor_utils()->display_name( $coupon_data->created_by );
692 $coupon_data->coupon_update_by = tutor_utils()->display_name( $coupon_data->updated_by );
693
694 $coupon_data->start_date_readable = empty( $coupon_data->start_date_gmt ) ? '' : DateTimeHelper::get_gmt_to_user_timezone_date( $coupon_data->start_date_gmt );
695 $coupon_data->expire_date_readable = empty( $coupon_data->expire_date_gmt ) ? '' : DateTimeHelper::get_gmt_to_user_timezone_date( $coupon_data->expire_date_gmt );
696 $coupon_data->created_at_readable = DateTimeHelper::get_gmt_to_user_timezone_date( $coupon_data->created_at_gmt );
697 $coupon_data->updated_at_readable = empty( $coupon_data->updated_at_gmt ) ? '' : DateTimeHelper::get_gmt_to_user_timezone_date( $coupon_data->updated_at_gmt );
698
699 $this->json_response(
700 __( 'Coupon retrieved successfully', 'tutor' ),
701 $coupon_data
702 );
703 }
704
705 /**
706 * Get application if applies to a specific category or bundle.
707 *
708 * @since 3.0.0
709 *
710 * @param string $applies_to Applies to.
711 * @param int $limit Number of items to fetch.
712 * @param int $offset Offset for fetching items.
713 * @param int $search_term Search term.
714 *
715 * @return array
716 */
717 public function get_application_list( string $applies_to, int $limit = 10, int $offset = 0, $search_term = '' ) {
718
719 $response = array(
720 'total_items' => 0,
721 'results' => array(),
722 );
723
724 if ( $this->model::APPLIES_TO_SPECIFIC_COURSES === $applies_to ) {
725 $args = array(
726 'post_type' => tutor()->course_post_type,
727 'posts_per_page' => $limit,
728 'offset' => $offset,
729 );
730
731 // Add search.
732 if ( $search_term ) {
733 $args['s'] = $search_term;
734 }
735
736 $courses = ( new CourseModel() )->get_paid_courses( $args );
737
738 $response['total_items'] = is_a( $courses, 'WP_Query' ) ? $courses->found_posts : 0;
739
740 if ( is_a( $courses, 'WP_Query' ) && $courses->have_posts() ) {
741 $courses = $courses->get_posts();
742 foreach ( $courses as $course ) {
743 $response['results'][] = Course::get_mini_info( $course );
744 }
745 }
746 } elseif ( $this->model::APPLIES_TO_SPECIFIC_BUNDLES === $applies_to && tutor_utils()->is_addon_enabled( 'tutor-pro/addons/course-bundle/course-bundle.php' ) ) {
747 $args = array(
748 'post_type' => 'course-bundle',
749 'posts_per_page' => $limit,
750 'offset' => $offset,
751 );
752
753 // Add search.
754 if ( $search_term ) {
755 $args['s'] = $search_term;
756 }
757
758 $bundles = ( new CourseModel() )->get_paid_courses( $args );
759
760 $response['total_items'] = is_a( $bundles, 'WP_Query' ) ? $bundles->found_posts : 0;
761
762 if ( is_a( $bundles, 'WP_Query' ) && $bundles->have_posts() ) {
763 $bundles = $bundles->get_posts();
764 foreach ( $bundles as $bundle ) {
765 $response['results'][] = Course::get_mini_info( $bundle );
766 }
767 }
768 } elseif ( $this->model::APPLIES_TO_SPECIFIC_CATEGORY === $applies_to ) {
769 $args = array(
770 'number' => $limit,
771 'offset' => $offset,
772 'hide_empty' => true,
773 );
774
775 $total_arg = array(
776 'fields' => 'ids',
777 'taxonomy' => CourseModel::COURSE_CATEGORY,
778 'hide_empty' => true,
779 );
780
781 // Add search.
782 if ( $search_term ) {
783 $args['search'] = $search_term;
784 $total_arg['search'] = $search_term;
785 }
786
787 $terms = tutor_utils()->get_course_categories( 0, $args );
788 $total = get_terms( $total_arg );
789
790 $response['total_items'] = is_array( $total ) ? count( $total ) : 0;
791
792 if ( ! is_wp_error( $terms ) ) {
793 foreach ( $terms as $term ) {
794 $thumb_id = get_term_meta( $term->term_id, 'thumbnail_id', true );
795
796 $response['results'][] = array(
797 'id' => $term->term_id,
798 'title' => $term->name,
799 'image' => $thumb_id ? wp_get_attachment_thumb_url( $thumb_id ) : tutor()->url . 'assets/images/placeholder.svg',
800 'total_courses' => (int) $term->count,
801 );
802 }
803 }
804 }
805
806 return $response;
807 }
808
809 /**
810 * Ajax handler for applying coupon
811 *
812 * @since 3.0.0
813 *
814 * @return void send wp_json response
815 */
816 public function ajax_apply_coupon() {
817 tutor_utils()->check_nonce();
818
819 if ( ! Settings::is_coupon_usage_enabled() ) {
820 $this->json_response(
821 __( 'Coupon usage is disabled', 'tutor' ),
822 null,
823 HttpHelper::STATUS_BAD_REQUEST
824 );
825 }
826
827 $object_ids = Input::post( 'object_ids' ); // Course/bundle ids.
828 $object_ids = array_filter( explode( ',', $object_ids ), 'is_numeric' );
829
830 if ( empty( $object_ids ) ) {
831 $this->json_response(
832 tutor_utils()->error_message( 'invalid_req' ),
833 null,
834 HttpHelper::STATUS_BAD_REQUEST
835 );
836 }
837
838 try {
839 $coupon_code = Input::post( 'coupon_code' );
840 $plan = Input::post( 'plan', 0, Input::TYPE_INT );
841 $order_type = $plan ? OrderModel::TYPE_SUBSCRIPTION : OrderModel::TYPE_SINGLE_ORDER;
842
843 $checkout_data = $this->checkout_ctrl->prepare_checkout_items( $object_ids, $order_type, $coupon_code );
844
845 if ( $checkout_data->is_coupon_applied ) {
846 $this->json_response(
847 __( 'Coupon applied successfully', 'tutor' ),
848 $checkout_data
849 );
850 } else {
851 global $tutor_coupon_apply_err_msg;
852 $this->json_response(
853 $tutor_coupon_apply_err_msg,
854 null,
855 HttpHelper::STATUS_BAD_REQUEST
856 );
857 }
858 } catch ( \Throwable $th ) {
859 $this->json_response(
860 $th->getMessage(),
861 null,
862 HttpHelper::STATUS_INTERNAL_SERVER_ERROR
863 );
864 }
865 }
866
867 /**
868 * Manage coupon usage
869 *
870 * Store usage upon order completion
871 *
872 * @since 3.0.0
873 *
874 * @param int $order_id Order id.
875 *
876 * @return void
877 */
878 public function store_coupon_usage( $order_id ) {
879 $order_model = ( new OrderModel() );
880
881 $order = $order_model->get_order_by_id( $order_id );
882 if ( $order ) {
883 if ( $order->coupon_amount > 0 && $order_model::ORDER_COMPLETED === $order->order_status ) {
884 // Store coupon usage.
885 $data = array(
886 'coupon_code' => $order->coupon_code,
887 'user_id' => $order->user_id,
888 );
889
890 try {
891 $this->model->store_coupon_usage( $data );
892 } catch ( \Throwable $th ) {
893 tutor_log( $th );
894 }
895 }
896 }
897 }
898
899 /**
900 * Validate input data based on predefined rules.
901 *
902 * @since 3.0.0
903 *
904 * @param array $data The data array to validate.
905 *
906 * @return object The validation result. It returns validation object.
907 */
908 protected function validate( array $data ) {
909
910 $validation_rules = array(
911 'coupon_id' => 'numeric',
912 'coupon_status' => 'required',
913 'coupon_type' => 'required',
914 'coupon_code' => 'required',
915 'coupon_title' => 'required',
916 'discount_type' => 'required',
917 'discount_amount' => 'required',
918 'applies_to' => 'required',
919 'total_usage_limit' => 'numeric',
920 'per_user_usage_limit' => 'numeric',
921 'start_date_gmt' => 'required|date_format:Y-m-d H:i:s',
922 );
923
924 // Skip validation rules for not available fields in data.
925 foreach ( $validation_rules as $key => $value ) {
926 if ( ! array_key_exists( $key, $data ) ) {
927 unset( $validation_rules[ $key ] );
928 }
929 }
930
931 return ValidationHelper::validate( $validation_rules, $data );
932 }
933 }
934