PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 3.6.2
Tutor LMS – eLearning and online course solution v3.6.2
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 / OrderController.php
tutor / ecommerce Last commit date
Cart 1 year ago PaymentGateways 1 year ago AdminMenu.php 1 year ago BillingController.php 1 year ago CartController.php 1 year ago CheckoutController.php 1 year ago CouponController.php 1 year ago Ecommerce.php 1 year ago EmailController.php 1 year ago HooksHandler.php 1 year ago OptionKeys.php 1 year ago OrderActivitiesController.php 1 year ago OrderController.php 1 year ago PaymentHandler.php 1 year ago Settings.php 1 year ago Tax.php 1 year ago currency.php 1 year ago
OrderController.php
1174 lines
1 <?php
2 /**
3 * Manage Order
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\Input;
14 use TUTOR\Earnings;
15 use Tutor\Models\OrderModel;
16 use TUTOR\Backend_Page_Trait;
17 use Tutor\Helpers\HttpHelper;
18 use Tutor\Models\CouponModel;
19 use Tutor\Models\CourseModel;
20 use Tutor\Helpers\QueryHelper;
21 use Tutor\Traits\JsonResponse;
22 use Tutor\Helpers\ValidationHelper;
23 use Tutor\Models\OrderActivitiesModel;
24
25 if ( ! defined( 'ABSPATH' ) ) {
26 exit;
27 }
28 /**
29 * OrderController class
30 *
31 * @since 3.0.0
32 */
33 class OrderController {
34
35 /**
36 * Order page slug
37 *
38 * @since 3.0.0
39 *
40 * @var string
41 */
42 const PAGE_SLUG = 'tutor_orders';
43
44 /**
45 * Order model
46 *
47 * @since 3.0.0
48 *
49 * @var OrderModel
50 */
51 private $model;
52
53 /**
54 * Trait for utilities
55 *
56 * @var $page_title
57 */
58 use Backend_Page_Trait;
59
60 /**
61 * Trait for sending JSON response
62 */
63 use JsonResponse;
64
65 /**
66 * Constructor.
67 *
68 * Initializes the Orders class, sets the page title, and optionally registers
69 * hooks for handling AJAX requests related to order data, bulk actions, order status updates,
70 * and order deletions.
71 *
72 * @param bool $register_hooks Whether to register hooks for handling requests. Default is true.
73 *
74 * @since 3.0.0
75 *
76 * @return void
77 */
78 public function __construct( $register_hooks = true ) {
79 $this->model = new OrderModel();
80
81 if ( $register_hooks ) {
82 /**
83 * Handle AJAX request for getting order related data by order ID.
84 *
85 * @since 3.0.0
86 */
87 add_action( 'wp_ajax_tutor_order_details', array( $this, 'get_order_by_id' ) );
88
89 /**
90 * Handle AJAX request for marking an order as paid by order ID.
91 *
92 * @since 3.0.0
93 */
94 add_action( 'wp_ajax_tutor_order_paid', array( $this, 'order_mark_as_paid' ) );
95
96 /**
97 * Handle AJAX request for canceling an order status.
98 *
99 * @since 3.0.0
100 */
101 add_action( 'wp_ajax_tutor_order_cancel', array( $this, 'order_cancel' ) );
102
103 /**
104 * Handle AJAX request for marking an order's refund action.
105 *
106 * @since 3.0.0
107 */
108 add_action( 'wp_ajax_tutor_order_refund', array( $this, 'make_refund' ) );
109
110 /**
111 * Handle AJAX request for adding an order comment.
112 *
113 * @since 3.0.0
114 */
115 add_action( 'wp_ajax_tutor_order_comment', array( $this, 'add_comment' ) );
116
117 /**
118 * Handle AJAX request for add/update an order's discount action.
119 *
120 * @since 3.0.0
121 */
122 add_action( 'wp_ajax_tutor_order_discount', array( $this, 'add_discount' ) );
123
124 /**
125 * Handle bulk action
126 *
127 * @since 3.0.0
128 */
129 add_action( 'wp_ajax_tutor_order_bulk_action', array( $this, 'bulk_action_handler' ) );
130 }
131 }
132
133 /**
134 * Page title fallback
135 *
136 * @since 3.5.0
137 *
138 * @param string $name Property name.
139 *
140 * @return string
141 */
142 public function __get( $name ) {
143 if ( 'page_title' === $name ) {
144 return esc_html__( 'Orders', 'tutor' );
145 }
146 }
147
148 /**
149 * Get order page url
150 *
151 * @since 3.0.0
152 *
153 * @param boolean $is_admin Whether to get admin or frontend url.
154 *
155 * @return string
156 */
157 public static function get_order_page_url( bool $is_admin = true ) {
158 if ( $is_admin ) {
159 return admin_url( 'admin.php?page=' . self::PAGE_SLUG );
160 } else {
161 return tutor_utils()->get_tutor_dashboard_url() . '/orders';
162 }
163 }
164
165 /**
166 * Create order based on the arguments
167 *
168 * Note: This method assumes nonce & user cap has been validated.
169 *
170 * Note: This method will validate data so it could be
171 * used without validation.
172 *
173 * @since 3.0.0
174 *
175 * @param int $user_id Typically student.
176 * @param array $items Key value pairs based on order_items table.
177 * @param string $payment_status Order payment status.
178 * @param string $order_type Type single_order/subscription.
179 * @param mixed $coupon_code Optional, if not provided automatic coupon.
180 * @param array $args Optional, Args to set data such as fees, tax, etc. Even to modify $order_data.
181 * @param bool $return_id return id.
182 *
183 * @throws \Exception Throw exception if data not valid or
184 * any other exception occur.
185 *
186 * @return mixed order id or order data.
187 */
188 public function create_order( int $user_id, array $items, string $payment_status, string $order_type, $coupon_code = null, array $args = array(), $return_id = true ) {
189 $items = Input::sanitize_array( $items );
190 $payment_status = Input::sanitize( $payment_status );
191 $coupon_code = Input::sanitize( $coupon_code );
192
193 $allowed_item_fields = $this->model->get_order_items_fillable_fields();
194 unset( $allowed_item_fields['order_id'] );
195
196 // Validate order items.
197 if ( ! isset( $items[0] ) ) {
198 $items = array( $items );
199 }
200
201 // Validate payment status.
202 if ( ! in_array( $payment_status, array_keys( $this->model->get_payment_status() ) ) ) {
203 throw new \Exception( __( 'Invalid payment status', 'tutor' ) );
204 }
205
206 $subtotal_price = 0;
207 $total_price = 0;
208
209 if ( $this->model::TYPE_SINGLE_ORDER === $order_type ) {
210 $item_price = $this->model::calculate_order_price( $items );
211 $subtotal_price = $item_price->subtotal;
212 $total_price = $item_price->total;
213 } else {
214 // For subscription and renewal order.
215 $prices = apply_filters( 'tutor_create_order_prices_for_subscription', null, $items, $order_type, $user_id );
216 if ( $prices ) {
217 $subtotal_price = $prices->subtotal_price;
218 $total_price = $prices->total_price;
219 }
220 }
221
222 $order_data = array(
223 'items' => $items,
224 'payment_status' => $payment_status,
225 'order_type' => $order_type,
226 'coupon_code' => $coupon_code,
227 'coupon_amount' => isset( $args['coupon_amount'] ) ? $args['coupon_amount'] : null,
228 'subtotal_price' => $subtotal_price,
229 'total_price' => $total_price,
230 'net_payment' => $total_price,
231 'user_id' => $user_id,
232 'payment_status' => $payment_status,
233 'order_status' => $this->model::PAYMENT_PAID === $payment_status ? $this->model::ORDER_COMPLETED : $this->model::ORDER_INCOMPLETE,
234 'created_at_gmt' => current_time( 'mysql', true ),
235 'created_by' => get_current_user_id(),
236 'updated_at_gmt' => current_time( 'mysql', true ),
237 'updated_by' => get_current_user_id(),
238 );
239
240 if ( isset( $args['discount_amount'] ) && $args['discount_amount'] > 0 ) {
241 $order_data['discount_type'] = 'flat';
242 $order_data['discount_amount'] = floatval( $args['discount_amount'] );
243 $order_data['discount_reason'] = __( 'Sale discount', 'tutor' );
244 }
245
246 $calculate_tax = apply_filters( 'tutor_calculate_order_tax', true, $args );
247
248 if ( $calculate_tax ) {
249 /**
250 * Tax calculation for order.
251 */
252 $tax_rate = Tax::get_user_tax_rate( $user_id );
253 if ( $tax_rate ) {
254 $order_data['tax_type'] = Tax::get_tax_type();
255 $order_data['tax_rate'] = $tax_rate;
256 $order_data['tax_amount'] = Tax::calculate_tax( $order_data['total_price'], $tax_rate );
257
258 if ( ! Tax::is_tax_included_in_price() ) {
259 $total_price += $order_data['tax_amount'];
260 $order_data['total_price'] = $total_price;
261 $order_data['net_payment'] = $total_price;
262 }
263 }
264 }
265 // Update data with arguments.
266 $order_data = apply_filters( 'tutor_before_order_create', array_merge( $order_data, $args ) );
267
268 try {
269 do_action( 'tutor_before_order_create', $order_data );
270 $order_id = $this->model->create_order( $order_data );
271 if ( $order_id ) {
272 $order_data['id'] = $order_id;
273 do_action( 'tutor_order_placed', $order_data );
274 return $return_id ? $order_id : $order_data;
275 }
276 } catch ( \Throwable $th ) {
277 throw new \Exception( $th->getMessage() );
278 }
279 }
280
281 /**
282 * Retrieve order data by order ID and respond with JSON.
283 *
284 * This function retrieves the order ID from the POST request, validates it,
285 * fetches the corresponding order data using the OrderModel class, and returns
286 * a JSON response with the order data or an error message.
287 *
288 * If the order ID is not provided, it responds with a "Bad Request" status.
289 * If the order is not found, it responds with a "Not Found" status.
290 * Otherwise, it responds with the order data and a success message.
291 *
292 * @since 3.0.0
293 *
294 * @return void
295 */
296 public function get_order_by_id() {
297 if ( ! tutor_utils()->is_nonce_verified() ) {
298 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
299 }
300
301 $order_id = Input::post( 'order_id' );
302
303 if ( empty( $order_id ) ) {
304 $this->json_response(
305 __( 'Order ID is required', 'tutor' ),
306 null,
307 HttpHelper::STATUS_BAD_REQUEST
308 );
309 }
310
311 $order_data = $this->model->get_order_by_id( $order_id );
312
313 if ( ! $order_data ) {
314 $this->json_response(
315 __( 'Order not found', 'tutor' ),
316 null,
317 HttpHelper::STATUS_NOT_FOUND
318 );
319 }
320
321 $this->json_response(
322 __( 'Order retrieved successfully', 'tutor' ),
323 $order_data
324 );
325 }
326
327 /**
328 * Mark an order as paid.
329 *
330 * This function verifies a nonce for security, constructs a payload object with
331 * the order ID, note, and payment status, and updates the payment status of the order
332 * to 'paid'. It sends a JSON response indicating the success or failure of the operation.
333 *
334 * @since 3.0.0
335 *
336 * @return void
337 */
338 public function order_mark_as_paid() {
339 tutor_utils()->check_nonce();
340
341 if ( ! current_user_can( 'manage_options' ) ) {
342 $this->json_response( tutor_utils()->error_message( HttpHelper::STATUS_UNAUTHORIZED ), null, HttpHelper::STATUS_UNAUTHORIZED );
343 }
344
345 $params = array(
346 'order_id' => Input::post( 'order_id' ),
347 'note' => Input::post( 'note' ),
348 );
349
350 // Validate request.
351 $validation = $this->validate( $params );
352 if ( ! $validation->success ) {
353 $this->json_response(
354 tutor_utils()->error_message( HttpHelper::STATUS_BAD_REQUEST ),
355 $validation->errors,
356 HttpHelper::STATUS_BAD_REQUEST
357 );
358 }
359
360 $order_id = (int) $params['order_id'];
361
362 $updated = $this->model->mark_as_paid( $order_id, $params['note'] );
363
364 if ( ! $updated ) {
365 $this->json_response(
366 __( 'Failed to update order payment status', 'tutor' ),
367 null,
368 HttpHelper::STATUS_INTERNAL_SERVER_ERROR
369 );
370 }
371
372 $this->json_response( __( 'Order payment status successfully updated', 'tutor' ) );
373 }
374
375 /**
376 * Cancels an order.
377 *
378 * This function cancels an order by updating its status to 'cancelled'. It performs nonce verification
379 * and checks the user's permissions before proceeding. It also validates the input parameters and
380 * triggers actions before and after the order cancellation.
381 *
382 * The function responds with an appropriate JSON message depending on the outcome of the cancellation process.
383 *
384 * @since 3.0.0
385 *
386 * @return void Responds with a JSON message indicating success or failure.
387 */
388 public function order_cancel() {
389 if ( ! tutor_utils()->is_nonce_verified() ) {
390 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
391 }
392
393 if ( ! current_user_can( 'manage_options' ) ) {
394 $this->json_response( tutor_utils()->error_message( HttpHelper::STATUS_UNAUTHORIZED ), null, HttpHelper::STATUS_UNAUTHORIZED );
395 }
396
397 $params = array(
398 'id' => Input::post( 'order_id' ),
399 'order_status' => $this->model::ORDER_CANCELLED,
400 );
401
402 // Validate request.
403 $validation = $this->validate( $params );
404 if ( ! $validation->success ) {
405 $this->json_response(
406 tutor_utils()->error_message( HttpHelper::STATUS_BAD_REQUEST ),
407 $validation->errors,
408 HttpHelper::STATUS_BAD_REQUEST
409 );
410 }
411
412 do_action( 'tutor_before_order_cancel', $params );
413
414 $response = $this->model->update_order( $params['id'], $params );
415 if ( ! $response ) {
416 $this->json_response(
417 __( 'Failed to cancel order status', 'tutor' ),
418 null,
419 HttpHelper::STATUS_INTERNAL_SERVER_ERROR
420 );
421 }
422
423 do_action( 'tutor_order_payment_status_changed', $params['id'], '', $this->model::ORDER_CANCELLED );
424
425 $this->json_response( __( 'Order successfully canceled', 'tutor' ) );
426 }
427
428 /**
429 * Handle order refund process.
430 *
431 * This method processes the refund for an order. It verifies the nonce and user capabilities,
432 * triggers necessary actions before and after the refund process, validates input data, and
433 * interacts with the OrderActivitiesModel to record the refund metadata. If any validation
434 * fails or the refund process encounters an error, it returns an appropriate JSON response.
435 *
436 * @since 3.0.0
437 *
438 * @return void
439 */
440 public function make_refund() {
441 if ( ! tutor_utils()->is_nonce_verified() ) {
442 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
443 }
444
445 if ( ! current_user_can( 'manage_options' ) ) {
446 $this->json_response( tutor_utils()->error_message( HttpHelper::STATUS_UNAUTHORIZED ), null, HttpHelper::STATUS_UNAUTHORIZED );
447 }
448
449 $order_id = Input::post( 'order_id', 0, Input::TYPE_INT );
450 $amount = (float) Input::post( 'amount' );
451 $reason = Input::post( 'reason' );
452 $cancel_enrollment = Input::post( 'is_remove_enrolment', false, Input::TYPE_BOOL );
453
454 if ( $amount <= 0 ) {
455 $this->json_response( __( 'Invalid refund amount provided', 'tutor' ), null, HttpHelper::STATUS_BAD_REQUEST );
456 }
457
458 $meta_value = array(
459 'amount' => $amount,
460 'reason' => $reason,
461 'message' => __( 'Order refunded by ', 'tutor' ) . get_userdata( get_current_user_id() )->display_name,
462 );
463
464 $order_data = $this->model->get_order_by_id( $order_id );
465 $cancel_enrollment = apply_filters( 'tutor_cancel_enrollment_on_refund', $cancel_enrollment, $order_data );
466
467 if ( $amount > (float) $order_data->net_payment ) {
468 $this->json_response(
469 __( 'Refund amount exceeded.', 'tutor' ),
470 null,
471 HttpHelper::STATUS_BAD_REQUEST
472 );
473 }
474
475 $order_status = $order_data->order_status;
476 $payment_status = $order_data->payment_status;
477
478 $meta_key = OrderActivitiesModel::META_KEY_REFUND;
479 if ( $amount < (float) $order_data->net_payment ) {
480 $meta_key = OrderActivitiesModel::META_KEY_PARTIALLY_REFUND;
481 }
482
483 if ( OrderActivitiesModel::META_KEY_PARTIALLY_REFUND === $meta_key ) {
484 $meta_value['message'] = __( 'Partially refunded by ', 'tutor' ) . get_userdata( get_current_user_id() )->display_name;
485 }
486
487 $params = array(
488 'order_id' => $order_id,
489 'meta_key' => $meta_key,
490 'meta_value' => wp_json_encode( $meta_value ),
491 );
492
493 // Validate request.
494 $validation = $this->validate( $params );
495 if ( ! $validation->success ) {
496 $this->json_response(
497 tutor_utils()->error_message( HttpHelper::STATUS_BAD_REQUEST ),
498 $validation->errors,
499 HttpHelper::STATUS_BAD_REQUEST
500 );
501 }
502
503 $payload = new \stdClass();
504 $payload->order_id = $params['order_id'];
505 $payload->meta_key = $params['meta_key'];
506 $payload->meta_value = $params['meta_value'];
507
508 $activity_model = new OrderActivitiesModel();
509 $response = $activity_model->add_order_meta( $payload );
510
511 if ( $response ) {
512 // Update net payment.
513 $refund_amount = $this->model->get_refund_amount( $order_id );
514
515 $net_payment = floatval( $order_data->total_price ) - floatval( $refund_amount );
516 if ( 'refund' === $meta_key ) {
517 $payment_status = $this->model::PAYMENT_REFUNDED;
518 } else {
519 $payment_status = $this->model::PAYMENT_PARTIALLY_REFUNDED;
520 }
521
522 if ( $cancel_enrollment ) {
523 $order_status = $this->model::ORDER_CANCELLED;
524 }
525
526 $update_data = array(
527 'net_payment' => $net_payment,
528 'refund_amount' => $refund_amount,
529 'payment_status' => $payment_status,
530 'order_status' => $order_status,
531 );
532
533 $this->model->update_order( $order_id, $update_data );
534
535 do_action( 'tutor_order_payment_status_changed', $order_data->id, $order_data->payment_status, $payment_status );
536
537 $order_data->payment_status = $update_data['payment_status'];
538 $order_data->order_status = $update_data['order_status'];
539 do_action( 'tutor_after_order_refund', $order_id, $amount, $reason );
540
541 $res_msg = __( 'Order refund successful', 'tutor' );
542
543 try {
544 $this->refund_from_payment_gateway( $order_id, $amount, $reason );
545 $this->json_response( $res_msg );
546 } catch ( \Throwable $th ) {
547 $res_msg = __( 'Order refunded successfully, but pending payment gateway issuance.', 'tutor' );
548 $this->json_response( $res_msg );
549 }
550 } else {
551 $this->json_response(
552 __( 'Failed to make refund', 'tutor' ),
553 null,
554 HttpHelper::STATUS_INTERNAL_SERVER_ERROR
555 );
556 }
557 }
558
559 /**
560 * Handle adding a comment to an order.
561 *
562 * This method processes the addition of a comment to an order. It verifies the nonce and user capabilities,
563 * triggers necessary actions before and after the comment addition, validates input data, and
564 * interacts with the OrderActivitiesModel to record the comment metadata. If any validation
565 * fails or the comment addition process encounters an error, it returns an appropriate JSON response.
566 *
567 * @since 3.0.0
568 *
569 * @return void
570 */
571 public function add_comment() {
572 if ( ! tutor_utils()->is_nonce_verified() ) {
573 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
574 }
575
576 if ( ! current_user_can( 'manage_options' ) ) {
577 $this->json_response( tutor_utils()->error_message( HttpHelper::STATUS_UNAUTHORIZED ), null, HttpHelper::STATUS_UNAUTHORIZED );
578 }
579
580 $params = array(
581 'order_id' => Input::post( 'order_id' ),
582 'meta_key' => OrderActivitiesModel::META_KEY_COMMENT,
583 'meta_value' => Input::post( 'comment' ),
584 );
585
586 do_action( 'tutor_before_order_comment', $params );
587
588 // Validate request.
589 $validation = $this->validate( $params );
590 if ( ! $validation->success ) {
591 $this->json_response(
592 tutor_utils()->error_message( HttpHelper::STATUS_BAD_REQUEST ),
593 $validation->errors,
594 HttpHelper::STATUS_BAD_REQUEST
595 );
596 }
597
598 $payload = new \stdClass();
599 $payload->order_id = $params['order_id'];
600 $payload->meta_key = $params['meta_key'];
601 $payload->meta_value = $params['meta_value'];
602
603 $activity_model = new OrderActivitiesModel();
604 $response = $activity_model->add_order_meta( $payload );
605
606 do_action( 'tutor_after_order_comment', $params );
607
608 if ( ! $response ) {
609 $this->json_response(
610 __( 'Failed to make a comment', 'tutor' ),
611 null,
612 HttpHelper::STATUS_INTERNAL_SERVER_ERROR
613 );
614 }
615
616 $this->json_response( __( 'Order comment successful added', 'tutor' ) );
617 }
618
619 /**
620 * Add a discount to an order.
621 *
622 * This function handles the request to add a discount to an order. It verifies the nonce,
623 * checks user permissions, validates the input, and then updates the order with the discount details.
624 *
625 * @since 3.0.0
626 *
627 * @return void This function outputs a JSON response and does not return a value.
628 */
629 public function add_discount() {
630 if ( ! tutor_utils()->is_nonce_verified() ) {
631 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
632 }
633
634 if ( ! current_user_can( 'manage_options' ) ) {
635 $this->json_response( tutor_utils()->error_message( HttpHelper::STATUS_UNAUTHORIZED ), null, HttpHelper::STATUS_UNAUTHORIZED );
636 }
637
638 $request = Input::sanitize_array( $_POST ); //phpcs:ignore --already sanitized.
639
640 // Validate request.
641 $validation = $this->validate( $request );
642 if ( ! $validation->success ) {
643 $this->json_response(
644 tutor_utils()->error_message( HttpHelper::STATUS_BAD_REQUEST ),
645 $validation->errors,
646 HttpHelper::STATUS_BAD_REQUEST
647 );
648 }
649
650 $request = (object) $request;
651
652 try {
653 $order = $this->model->get_order_by_id( $request->order_id );
654 $subtotal = $order->subtotal_price;
655
656 $discount_amount = $this->model->calculate_discount_amount( $request->discount_type, $request->discount_amount, $subtotal );
657
658 $deducted_amount = $discount_amount;
659 if ( ! empty( $order->coupon_code ) && $order->coupon_amount > 0 ) {
660 $deducted_amount += $order->coupon_amount;
661 }
662
663 $total_price = $subtotal - $deducted_amount;
664
665 $tax_rate = Tax::get_user_tax_rate( $order->user_id );
666 $tax_amount = Tax::calculate_tax( $total_price, $tax_rate );
667 if ( ! Tax::is_tax_included_in_price() ) {
668 $total_price += $tax_amount;
669 }
670
671 $order_data = array(
672 'discount_amount' => $request->discount_amount,
673 'discount_reason' => $request->discount_reason,
674 'discount_type' => $request->discount_type,
675 'subtotal_price' => $subtotal,
676 'tax_rate' => $tax_rate,
677 'tax_amount' => $tax_amount,
678 'net_payment' => $total_price,
679 'total_price' => $total_price,
680 );
681
682 $update = $this->model->update_order( $request->order_id, $order_data );
683 if ( ! $update ) {
684 $this->json_response(
685 __( 'Failed to add a discount', 'tutor' ),
686 null,
687 HttpHelper::STATUS_INTERNAL_SERVER_ERROR
688 );
689 }
690
691 do_action( 'tutor_after_add_order_discount', $order, $discount_amount );
692
693 $this->json_response( __( 'Order discount successful added', 'tutor' ) );
694 } catch ( \Throwable $th ) {
695 $this->json_response(
696 __( 'Failed to add a discount', 'tutor' ),
697 $th->getMessage(),
698 HttpHelper::STATUS_INTERNAL_SERVER_ERROR
699 );
700 }
701 }
702
703 /**
704 * Prepare bulk actions that will show on dropdown options
705 *
706 * @return array
707 * @since 3.0.0
708 */
709 public function prepare_bulk_actions(): array {
710 $actions = array(
711 $this->bulk_action_default(),
712 );
713
714 $active_tab = Input::get( 'data', '' );
715
716 if ( $this->model::ORDER_TRASH !== $active_tab ) {
717 $actions[] = $this->bulk_action_mark_order_trash();
718 }
719
720 if ( ! empty( $active_tab ) ) {
721 switch ( $active_tab ) {
722 case $this->model::ORDER_INCOMPLETE:
723 $actions[] = $this->bulk_action_mark_order_paid();
724 break;
725 case $this->model::ORDER_COMPLETED:
726 $actions[] = $this->bulk_action_mark_order_unpaid();
727 break;
728 case $this->model::ORDER_TRASH:
729 $actions[] = $this->bulk_action_delete();
730 break;
731 default:
732 // code...
733 break;
734 }
735 }
736
737 return apply_filters( 'tutor_order_bulk_actions', $actions );
738 }
739
740 /**
741 * Available tabs that will visible on the right side of page navbar
742 *
743 * @return array
744 *
745 * @since 3.0.0
746 */
747 public function tabs_key_value(): array {
748 $url = apply_filters( 'tutor_data_tab_base_url', get_pagenum_link() );
749
750 $date = Input::get( 'date', '' );
751 $payment_status = Input::get( 'payment-status', '' );
752 $search = Input::get( 'search', '' );
753
754 $where = array(
755 'order_type' => OrderModel::TYPE_SINGLE_ORDER,
756 );
757
758 if ( ! empty( $date ) ) {
759 $where['created_at_gmt'] = tutor_get_formated_date( 'Y-m-d', $date );
760 }
761
762 if ( ! empty( $payment_status ) ) {
763 $where['payment_status'] = $payment_status;
764 }
765
766 $order_status = $this->model->get_order_status();
767
768 $tabs = array();
769
770 $tabs [] = array(
771 'key' => 'all',
772 'title' => __( 'All', 'tutor' ),
773 'value' => $this->model->get_order_count( $where, $search ),
774 'url' => $url . '&data=all',
775 );
776
777 foreach ( $order_status as $key => $value ) {
778 $where['order_status'] = $key;
779
780 $tabs[] = array(
781 'key' => $key,
782 'title' => $value,
783 'value' => $this->model->get_order_count( $where, $search ),
784 'url' => $url . '&data=' . $key,
785 );
786 }
787
788 return apply_filters( 'tutor_order_tabs', $tabs );
789 }
790
791 /**
792 * Count orders by status & filters
793 * Count all | min | published | pending | draft
794 *
795 * @param string $status | required.
796 * @param string $order_id selected order id | optional.
797 * @param string $date selected date | optional.
798 * @param string $search_term search by user name or email | optional.
799 *
800 * @return int
801 *
802 * @since 3.0.0
803 */
804 protected static function count_order( string $status, $order_id = '', $date = '', $search_term = '' ): int {
805 $user_id = get_current_user_id();
806 $status = sanitize_text_field( $status );
807 $order_id = sanitize_text_field( $order_id );
808 $date = sanitize_text_field( $date );
809 $search_term = sanitize_text_field( $search_term );
810
811 $args = array(
812 'post_type' => tutor()->order_post_type,
813 );
814
815 if ( 'all' === $status || 'mine' === $status ) {
816 $args['post_status'] = array( 'publish', 'pending', 'draft', 'private', 'future' );
817 } else {
818 $args['post_status'] = array( $status );
819 }
820
821 // Author query.
822 if ( 'mine' === $status || ! current_user_can( 'administrator' ) ) {
823 $args['author'] = $user_id;
824 }
825
826 $date_filter = sanitize_text_field( $date );
827
828 $year = gmdate( 'Y', strtotime( $date_filter ) );
829 $month = gmdate( 'm', strtotime( $date_filter ) );
830 $day = gmdate( 'd', strtotime( $date_filter ) );
831
832 // Add date query.
833 if ( '' !== $date_filter ) {
834 $args['date_query'] = array(
835 array(
836 'year' => $year,
837 'month' => $month,
838 'day' => $day,
839 ),
840 );
841 }
842
843 if ( '' !== $order_id ) {
844 $args['p'] = $order_id;
845 }
846
847 // Search filter.
848 if ( '' !== $search_term ) {
849 $args['s'] = $search_term;
850 }
851
852 $the_query = new \WP_Query( $args );
853
854 return ! is_null( $the_query ) && isset( $the_query->found_posts ) ? $the_query->found_posts : $the_query;
855 }
856
857 /**
858 * Handle order bulk action
859 *
860 * @since 3.0.0
861 *
862 * @return void send wp_json response
863 */
864 public function bulk_action_handler() {
865
866 tutor_utils()->checking_nonce();
867
868 // Check if user is privileged.
869 if ( ! current_user_can( 'administrator' ) ) {
870 wp_send_json_error( tutor_utils()->error_message() );
871 }
872
873 $request = Input::sanitize_array( $_POST ); //phpcs:ignore -- already sanitized.
874 $bulk_action = $request['bulk-action'];
875
876 $bulk_ids = isset( $request['bulk-ids'] ) ? array_map( 'intval', explode( ',', $request['bulk-ids'] ) ) : array();
877
878 $allowed_bulk_actions = array(
879 $this->model::PAYMENT_PAID,
880 $this->model::PAYMENT_UNPAID,
881 $this->model::ORDER_TRASH,
882 'delete',
883 );
884
885 if ( ! in_array( $bulk_action, $allowed_bulk_actions, true ) ) {
886 wp_send_json_error( __( 'Please select appropriate action', 'tutor' ) );
887 }
888
889 if ( empty( $bulk_ids ) ) {
890 wp_send_json_error( __( 'No items selected for the bulk action.', 'tutor' ) );
891 }
892
893 do_action( 'tutor_before_order_bulk_action', $bulk_action, $bulk_ids );
894
895 $response = false;
896 if ( 'delete' === $bulk_action ) {
897 $response = $this->model->delete_order( $bulk_ids );
898 } else {
899 $data = null;
900
901 switch ( $bulk_action ) {
902 case $this->model::PAYMENT_PAID:
903 $data = array(
904 'payment_status' => $this->model::PAYMENT_PAID,
905 'order_status' => $this->model::ORDER_COMPLETED,
906 );
907 break;
908 case $this->model::PAYMENT_UNPAID:
909 $data = array(
910 'payment_status' => $this->model::PAYMENT_UNPAID,
911 'order_status' => $this->model::ORDER_INCOMPLETE,
912 );
913 break;
914 case $this->model::ORDER_TRASH:
915 $data = array(
916 'order_status' => $this->model::ORDER_TRASH,
917 );
918 break;
919 default:
920 // code...
921 break;
922 }
923
924 if ( ! empty( $data ) ) {
925 $response = $this->model->update_order( $bulk_ids, $data );
926 }
927 }
928
929 if ( $response ) {
930 if ( 'delete' !== $bulk_action ) {
931 foreach ( $bulk_ids as $id ) {
932 do_action( 'tutor_order_payment_status_changed', $id, '', $bulk_action );
933 }
934 }
935 wp_send_json_success( __( 'Order updated successfully.', 'tutor' ) );
936 } else {
937 wp_send_json_error( __( 'Failed to update order.', 'tutor' ) );
938 }
939 }
940
941 /**
942 * Execute bulk delete action
943 *
944 * @param string $bulk_ids ids that need to update.
945 * @return bool
946 * @since 3.0.0
947 */
948 public function bulk_delete_order( $bulk_ids ): bool {
949 $bulk_ids = explode( ',', sanitize_text_field( $bulk_ids ) );
950
951 $response = false;
952 try {
953 $response = QueryHelper::bulk_delete_by_ids( $this->model->get_table_name(), $bulk_ids );
954 } catch ( \Throwable $th ) {
955 error_log( $th->getMessage() . ' Line: ' . $th->getLine() . ' File: ' . $th->getFile() );
956 }
957
958 return $response;
959 }
960
961 /**
962 * Update order status
963 *
964 * @param string $status for updating order status.
965 * @param string $bulk_ids comma separated ids.
966 *
967 * @return bool
968 *
969 * @since 3.0.0
970 */
971 public static function update_order_status( string $status, $bulk_ids ): bool {
972 global $wpdb;
973 $post_table = $wpdb->posts;
974 $status = sanitize_text_field( $status );
975 $bulk_ids = sanitize_text_field( $bulk_ids );
976
977 $ids = array_map( 'intval', explode( ',', $bulk_ids ) );
978 $in_clause = QueryHelper::prepare_in_clause( $ids );
979
980 $update = $wpdb->query(
981 $wpdb->prepare(
982 "UPDATE {$post_table} SET post_status = %s WHERE ID IN ($in_clause)", //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
983 $status
984 )
985 );
986
987 return true;
988 }
989
990 /**
991 * Get orders
992 *
993 * @since 3.0.0
994 *
995 * @param integer $limit List limit.
996 * @param integer $offset List offset.
997 *
998 * @return array
999 */
1000 public function get_orders( $limit = 10, $offset = 0 ) {
1001
1002 $active_tab = Input::get( 'data', 'all' );
1003
1004 $date = Input::get( 'date', '' );
1005 $search_term = Input::get( 'search', '' );
1006 $payment_status = Input::get( 'payment-status', '' );
1007
1008 $where_clause = array(
1009 'order_type' => OrderModel::TYPE_SINGLE_ORDER,
1010 );
1011
1012 if ( $date ) {
1013 $where_clause['date(o.created_at_gmt)'] = tutor_get_formated_date( '', $date );
1014 }
1015
1016 if ( $payment_status ) {
1017 $where_clause['o.payment_status'] = $payment_status;
1018 }
1019
1020 if ( 'all' !== $active_tab ) {
1021 $where_clause['o.order_status'] = $active_tab;
1022 }
1023
1024 $list_order = Input::get( 'order', 'DESC' );
1025 $list_order_by = 'id';
1026
1027 return $this->model->get_orders( $where_clause, $search_term, $limit, $offset, $list_order_by, $list_order );
1028 }
1029
1030 /**
1031 * Filter discount data if monetization is Tutor
1032 *
1033 * @since 3.0.0
1034 *
1035 * @param int $user_id Current user id.
1036 * @param string $period Period for filter refund data.
1037 * @param string $start_date Filter start date.
1038 * @param string $end_date Filter end date.
1039 * @param int $course_id Course id.
1040 *
1041 * @return array
1042 */
1043 public function get_discount_data( $user_id = 0, $period = '', $start_date = '', $end_date = '', $course_id = 0 ) {
1044 // Sanitize params.
1045 $user_id = is_admin() ? 0 : $user_id;
1046 $period = Input::sanitize( $period );
1047 $start_date = Input::sanitize( $start_date );
1048 $end_date = Input::sanitize( $end_date );
1049 $course_id = (int) $course_id;
1050
1051 return $this->model->get_discounts_by_user( $user_id, $period, $start_date, $end_date, $course_id );
1052 }
1053
1054 /**
1055 * Filter refund data if monetization is Tutor
1056 *
1057 * @since 3.0.0
1058 *
1059 * @param int $user_id Current user id.
1060 * @param string $period Period for filter refund data.
1061 * @param string $start_date Filter start date.
1062 * @param string $end_date Filter end date.
1063 * @param int $course_id Course id.
1064 *
1065 * @return array
1066 */
1067 public function get_refund_data( $user_id = 0, $period = '', $start_date = '', $end_date = '', $course_id = 0 ) {
1068 // Sanitize params.
1069 $user_id = is_admin() ? 0 : $user_id;
1070 $period = Input::sanitize( $period );
1071 $start_date = Input::sanitize( $start_date );
1072 $end_date = Input::sanitize( $end_date );
1073 $course_id = (int) $course_id;
1074
1075 return $this->model->get_refunds_by_user( $user_id, $period, $start_date, $end_date, $course_id );
1076 }
1077
1078 /**
1079 * Validate input data based on predefined rules.
1080 *
1081 * This protected method validates the provided data array against a set of
1082 * predefined validation rules. The rules specify that 'order_id' is required
1083 * and must be numeric. The method will skip validation rules for fields that
1084 * are not present in the data array.
1085 *
1086 * @since 3.0.0
1087 *
1088 * @param array $data The data array to validate.
1089 *
1090 * @return object The validation result. It returns validation object.
1091 */
1092 protected function validate( array $data ) {
1093
1094 $validation_rules = array(
1095 'order_id' => 'required|numeric',
1096 'meta_key' => 'required',
1097 'meta_value' => 'required',
1098 'discount_type' => 'required',
1099 'discount_amount' => 'required',
1100 );
1101
1102 // Skip validation rules for not available fields in data.
1103 foreach ( $validation_rules as $key => $value ) {
1104 if ( ! array_key_exists( $key, $data ) ) {
1105 unset( $validation_rules[ $key ] );
1106 }
1107 }
1108
1109 return ValidationHelper::validate( $validation_rules, $data );
1110 }
1111
1112 /**
1113 * Process refund from payment gateway
1114 *
1115 * @since 3.1.0
1116 *
1117 * @param int $order_id Order id.
1118 * @param string $amount Refund amount.
1119 * @param string $reason Refund reason.
1120 *
1121 * @throws \Throwable If an error occurs during the refund process.
1122 *
1123 * @return void
1124 */
1125 public function refund_from_payment_gateway( $order_id, $amount, $reason ) {
1126 $order = $this->model->get_order_by_id( $order_id );
1127 if ( $order && ! $this->model->is_manual_payment( $order->payment_method ) ) {
1128 $refund_data = $this->prepare_refund_data( $order, $amount, $reason );
1129 try {
1130 $payment_gateway_ref = Ecommerce::payment_gateways_with_ref( $order->payment_method );
1131 if ( $payment_gateway_ref ) {
1132 $gateway_obj = Ecommerce::get_payment_gateway_object( $payment_gateway_ref['gateway_class'] );
1133 $gateway_obj->make_refund( $refund_data );
1134 }
1135 } catch ( \Throwable $th ) {
1136 throw $th;
1137 }
1138 }
1139 }
1140
1141 /**
1142 * Prepare refund data
1143 *
1144 * @since 3.1.0
1145 *
1146 * @param object $order Order object.
1147 * @param string $amount Raw amount.
1148 * @param string $reason Refund reason.
1149 *
1150 * @return object
1151 */
1152 public function prepare_refund_data( $order, $amount, $reason ) {
1153 $currency = tutor_get_currencies_info_by_code( tutor_utils()->get_option( OptionKeys::CURRENCY_CODE ) );
1154
1155 $refund_data = array(
1156 'type' => 'refund',
1157 'amount' => $amount,
1158 'payment_payload' => $order->payment_payloads, // JSON string representing the payment payload.
1159 'order_id' => $order->id,
1160 'reason' => $reason,
1161 'refund_type' => $order->net_payment == $amount ? 'full' : 'partial',
1162 'currency' => (object) array(
1163 'code' => $currency['code'],
1164 'symbol' => $currency['symbol'],
1165 'name' => $currency['name'],
1166 'locale' => $currency['locale'],
1167 'numeric_code' => $currency['numeric_code'],
1168 ),
1169 );
1170
1171 return (object) $refund_data;
1172 }
1173 }
1174