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