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 / ecommerce / CouponController.php
tutor / ecommerce Last commit date
Cart 10 months ago PaymentGateways 3 weeks ago AdminMenu.php 9 months ago BillingController.php 1 year ago CartController.php 1 year ago CheckoutController.php 2 months ago CouponController.php 5 months ago Ecommerce.php 1 year ago EmailController.php 11 months ago HooksHandler.php 2 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 5 months ago
CouponController.php
936 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 tutor_utils()->check_current_user_capability();
663
664 $coupon_id = Input::post( 'id' );
665
666 if ( empty( $coupon_id ) ) {
667 $this->json_response(
668 __( 'Coupon code is required', 'tutor' ),
669 null,
670 HttpHelper::STATUS_BAD_REQUEST
671 );
672 }
673
674 $coupon_data = $this->model->get_coupon( array( 'id' => $coupon_id ) );
675
676 if ( ! $coupon_data ) {
677 $this->json_response(
678 __( 'Coupon not found', 'tutor' ),
679 null,
680 HttpHelper::STATUS_NOT_FOUND
681 );
682 }
683
684 $applications = $this->model->get_formatted_coupon_applications( $coupon_data );
685
686 // Set applies to items.
687 $coupon_data->applies_to_items = apply_filters( 'tutor_coupon_details_applies_to_items_response', $applications, $coupon_data );
688
689 // Set coupon usage.
690 $coupon_data->coupon_usage = $this->model->get_coupon_usage_count( $coupon_data->coupon_code );
691
692 // Set created & updated by.
693 $coupon_data->coupon_created_by = tutor_utils()->display_name( $coupon_data->created_by );
694 $coupon_data->coupon_update_by = tutor_utils()->display_name( $coupon_data->updated_by );
695
696 $coupon_data->start_date_readable = empty( $coupon_data->start_date_gmt ) ? '' : DateTimeHelper::get_gmt_to_user_timezone_date( $coupon_data->start_date_gmt );
697 $coupon_data->expire_date_readable = empty( $coupon_data->expire_date_gmt ) ? '' : DateTimeHelper::get_gmt_to_user_timezone_date( $coupon_data->expire_date_gmt );
698 $coupon_data->created_at_readable = DateTimeHelper::get_gmt_to_user_timezone_date( $coupon_data->created_at_gmt );
699 $coupon_data->updated_at_readable = empty( $coupon_data->updated_at_gmt ) ? '' : DateTimeHelper::get_gmt_to_user_timezone_date( $coupon_data->updated_at_gmt );
700
701 $this->json_response(
702 __( 'Coupon retrieved successfully', 'tutor' ),
703 $coupon_data
704 );
705 }
706
707 /**
708 * Get application if applies to a specific category or bundle.
709 *
710 * @since 3.0.0
711 *
712 * @param string $applies_to Applies to.
713 * @param int $limit Number of items to fetch.
714 * @param int $offset Offset for fetching items.
715 * @param int $search_term Search term.
716 *
717 * @return array
718 */
719 public function get_application_list( string $applies_to, int $limit = 10, int $offset = 0, $search_term = '' ) {
720
721 $response = array(
722 'total_items' => 0,
723 'results' => array(),
724 );
725
726 if ( $this->model::APPLIES_TO_SPECIFIC_COURSES === $applies_to ) {
727 $args = array(
728 'post_type' => tutor()->course_post_type,
729 'posts_per_page' => $limit,
730 'offset' => $offset,
731 );
732
733 // Add search.
734 if ( $search_term ) {
735 $args['s'] = $search_term;
736 }
737
738 $courses = ( new CourseModel() )->get_paid_courses( $args );
739
740 $response['total_items'] = is_a( $courses, 'WP_Query' ) ? $courses->found_posts : 0;
741
742 if ( is_a( $courses, 'WP_Query' ) && $courses->have_posts() ) {
743 $courses = $courses->get_posts();
744 foreach ( $courses as $course ) {
745 $response['results'][] = Course::get_mini_info( $course );
746 }
747 }
748 } elseif ( $this->model::APPLIES_TO_SPECIFIC_BUNDLES === $applies_to && tutor_utils()->is_addon_enabled( 'tutor-pro/addons/course-bundle/course-bundle.php' ) ) {
749 $args = array(
750 'post_type' => 'course-bundle',
751 'posts_per_page' => $limit,
752 'offset' => $offset,
753 );
754
755 // Add search.
756 if ( $search_term ) {
757 $args['s'] = $search_term;
758 }
759
760 $bundles = ( new CourseModel() )->get_paid_courses( $args );
761
762 $response['total_items'] = is_a( $bundles, 'WP_Query' ) ? $bundles->found_posts : 0;
763
764 if ( is_a( $bundles, 'WP_Query' ) && $bundles->have_posts() ) {
765 $bundles = $bundles->get_posts();
766 foreach ( $bundles as $bundle ) {
767 $response['results'][] = Course::get_mini_info( $bundle );
768 }
769 }
770 } elseif ( $this->model::APPLIES_TO_SPECIFIC_CATEGORY === $applies_to ) {
771 $args = array(
772 'number' => $limit,
773 'offset' => $offset,
774 'hide_empty' => true,
775 );
776
777 $total_arg = array(
778 'fields' => 'ids',
779 'taxonomy' => CourseModel::COURSE_CATEGORY,
780 'hide_empty' => true,
781 );
782
783 // Add search.
784 if ( $search_term ) {
785 $args['search'] = $search_term;
786 $total_arg['search'] = $search_term;
787 }
788
789 $terms = tutor_utils()->get_course_categories( 0, $args );
790 $total = get_terms( $total_arg );
791
792 $response['total_items'] = is_array( $total ) ? count( $total ) : 0;
793
794 if ( ! is_wp_error( $terms ) ) {
795 foreach ( $terms as $term ) {
796 $thumb_id = get_term_meta( $term->term_id, 'thumbnail_id', true );
797
798 $response['results'][] = array(
799 'id' => $term->term_id,
800 'title' => $term->name,
801 'image' => $thumb_id ? wp_get_attachment_thumb_url( $thumb_id ) : tutor()->url . 'assets/images/placeholder.svg',
802 'total_courses' => (int) $term->count,
803 );
804 }
805 }
806 }
807
808 return $response;
809 }
810
811 /**
812 * Ajax handler for applying coupon
813 *
814 * @since 3.0.0
815 *
816 * @return void send wp_json response
817 */
818 public function ajax_apply_coupon() {
819 tutor_utils()->check_nonce();
820
821 if ( ! Settings::is_coupon_usage_enabled() ) {
822 $this->json_response(
823 __( 'Coupon usage is disabled', 'tutor' ),
824 null,
825 HttpHelper::STATUS_BAD_REQUEST
826 );
827 }
828
829 $object_ids = Input::post( 'object_ids' ); // Course/bundle ids.
830 $object_ids = array_filter( explode( ',', $object_ids ), 'is_numeric' );
831
832 if ( empty( $object_ids ) ) {
833 $this->json_response(
834 tutor_utils()->error_message( 'invalid_req' ),
835 null,
836 HttpHelper::STATUS_BAD_REQUEST
837 );
838 }
839
840 try {
841 $coupon_code = Input::post( 'coupon_code' );
842 $plan = Input::post( 'plan', 0, Input::TYPE_INT );
843 $order_type = $plan ? OrderModel::TYPE_SUBSCRIPTION : OrderModel::TYPE_SINGLE_ORDER;
844
845 $checkout_data = $this->checkout_ctrl->prepare_checkout_items( $object_ids, $order_type, $coupon_code );
846
847 if ( $checkout_data->is_coupon_applied ) {
848 $this->json_response(
849 __( 'Coupon applied successfully', 'tutor' ),
850 $checkout_data
851 );
852 } else {
853 global $tutor_coupon_apply_err_msg;
854 $this->json_response(
855 $tutor_coupon_apply_err_msg,
856 null,
857 HttpHelper::STATUS_BAD_REQUEST
858 );
859 }
860 } catch ( \Throwable $th ) {
861 $this->json_response(
862 $th->getMessage(),
863 null,
864 HttpHelper::STATUS_INTERNAL_SERVER_ERROR
865 );
866 }
867 }
868
869 /**
870 * Manage coupon usage
871 *
872 * Store usage upon order completion
873 *
874 * @since 3.0.0
875 *
876 * @param int $order_id Order id.
877 *
878 * @return void
879 */
880 public function store_coupon_usage( $order_id ) {
881 $order_model = ( new OrderModel() );
882
883 $order = $order_model->get_order_by_id( $order_id );
884 if ( $order ) {
885 if ( $order->coupon_amount > 0 && $order_model::ORDER_COMPLETED === $order->order_status ) {
886 // Store coupon usage.
887 $data = array(
888 'coupon_code' => $order->coupon_code,
889 'user_id' => $order->user_id,
890 );
891
892 try {
893 $this->model->store_coupon_usage( $data );
894 } catch ( \Throwable $th ) {
895 tutor_log( $th );
896 }
897 }
898 }
899 }
900
901 /**
902 * Validate input data based on predefined rules.
903 *
904 * @since 3.0.0
905 *
906 * @param array $data The data array to validate.
907 *
908 * @return object The validation result. It returns validation object.
909 */
910 protected function validate( array $data ) {
911
912 $validation_rules = array(
913 'coupon_id' => 'numeric',
914 'coupon_status' => 'required',
915 'coupon_type' => 'required',
916 'coupon_code' => 'required',
917 'coupon_title' => 'required',
918 'discount_type' => 'required',
919 'discount_amount' => 'required',
920 'applies_to' => 'required',
921 'total_usage_limit' => 'numeric',
922 'per_user_usage_limit' => 'numeric',
923 'start_date_gmt' => 'required|date_format:Y-m-d H:i:s',
924 );
925
926 // Skip validation rules for not available fields in data.
927 foreach ( $validation_rules as $key => $value ) {
928 if ( ! array_key_exists( $key, $data ) ) {
929 unset( $validation_rules[ $key ] );
930 }
931 }
932
933 return ValidationHelper::validate( $validation_rules, $data );
934 }
935 }
936