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