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