Diff: STRATO-apps/wordpress_03/app/wp-content/plugins/tutor/ecommerce/CouponController.php

Keine Baseline-Datei – Diff nur gegen leer.
Zur Liste
1 -
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 +