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