PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 3.2.2
Tutor LMS – eLearning and online course solution v3.2.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
1173 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 * 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_get_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_id, $amount, $reason );
536
537 $res_msg = __( 'Order refund successful', 'tutor' );
538
539 try {
540 $this->refund_from_payment_gateway( $order_id, $amount, $reason );
541 $this->json_response( $res_msg );
542 } catch ( \Throwable $th ) {
543 $res_msg = __( 'Order refunded successfully, but pending payment gateway issuance.', 'tutor' );
544 $this->json_response( $res_msg );
545 }
546 } else {
547 $this->json_response(
548 __( 'Failed to make refund', 'tutor' ),
549 null,
550 HttpHelper::STATUS_INTERNAL_SERVER_ERROR
551 );
552 }
553
554 }
555
556 /**
557 * Handle adding a comment to an order.
558 *
559 * This method processes the addition of a comment to an order. It verifies the nonce and user capabilities,
560 * triggers necessary actions before and after the comment addition, validates input data, and
561 * interacts with the OrderActivitiesModel to record the comment metadata. If any validation
562 * fails or the comment addition process encounters an error, it returns an appropriate JSON response.
563 *
564 * @since 3.0.0
565 *
566 * @return void
567 */
568 public function add_comment() {
569 if ( ! tutor_utils()->is_nonce_verified() ) {
570 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
571 }
572
573 if ( ! current_user_can( 'manage_options' ) ) {
574 $this->json_response( tutor_utils()->error_message( HttpHelper::STATUS_UNAUTHORIZED ), null, HttpHelper::STATUS_UNAUTHORIZED );
575 }
576
577 $params = array(
578 'order_id' => Input::post( 'order_id' ),
579 'meta_key' => OrderActivitiesModel::META_KEY_COMMENT,
580 'meta_value' => Input::post( 'comment' ),
581 );
582
583 do_action( 'tutor_before_order_comment', $params );
584
585 // Validate request.
586 $validation = $this->validate( $params );
587 if ( ! $validation->success ) {
588 $this->json_response(
589 tutor_utils()->error_message( HttpHelper::STATUS_BAD_REQUEST ),
590 $validation->errors,
591 HttpHelper::STATUS_BAD_REQUEST
592 );
593 }
594
595 $payload = new \stdClass();
596 $payload->order_id = $params['order_id'];
597 $payload->meta_key = $params['meta_key'];
598 $payload->meta_value = $params['meta_value'];
599
600 $activity_model = new OrderActivitiesModel();
601 $response = $activity_model->add_order_meta( $payload );
602
603 do_action( 'tutor_after_order_comment', $params );
604
605 if ( ! $response ) {
606 $this->json_response(
607 __( 'Failed to make a comment', 'tutor' ),
608 null,
609 HttpHelper::STATUS_INTERNAL_SERVER_ERROR
610 );
611 }
612
613 $this->json_response( __( 'Order comment successful added', 'tutor' ) );
614 }
615
616 /**
617 * Add a discount to an order.
618 *
619 * This function handles the request to add a discount to an order. It verifies the nonce,
620 * checks user permissions, validates the input, and then updates the order with the discount details.
621 *
622 * @since 3.0.0
623 *
624 * @return void This function outputs a JSON response and does not return a value.
625 */
626 public function add_discount() {
627 if ( ! tutor_utils()->is_nonce_verified() ) {
628 $this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
629 }
630
631 if ( ! current_user_can( 'manage_options' ) ) {
632 $this->json_response( tutor_utils()->error_message( HttpHelper::STATUS_UNAUTHORIZED ), null, HttpHelper::STATUS_UNAUTHORIZED );
633 }
634
635 $request = Input::sanitize_array( $_POST ); //phpcs:ignore --already sanitized.
636
637 // Validate request.
638 $validation = $this->validate( $request );
639 if ( ! $validation->success ) {
640 $this->json_response(
641 tutor_utils()->error_message( HttpHelper::STATUS_BAD_REQUEST ),
642 $validation->errors,
643 HttpHelper::STATUS_BAD_REQUEST
644 );
645 }
646
647 $request = (object) $request;
648
649 try {
650 $order = $this->model->get_order_by_id( $request->order_id );
651 $subtotal = $order->subtotal_price;
652
653 $discount_amount = $this->model->calculate_discount_amount( $request->discount_type, $request->discount_amount, $subtotal );
654
655 $deducted_amount = $discount_amount;
656 if ( ! empty( $order->coupon_code ) && $order->coupon_amount > 0 ) {
657 $deducted_amount += $order->coupon_amount;
658 }
659
660 $total_price = $subtotal - $deducted_amount;
661
662 $tax_rate = Tax::get_user_tax_rate( $order->user_id );
663 $tax_amount = Tax::calculate_tax( $total_price, $tax_rate );
664 if ( ! Tax::is_tax_included_in_price() ) {
665 $total_price += $tax_amount;
666 }
667
668 $order_data = array(
669 'discount_amount' => $request->discount_amount,
670 'discount_reason' => $request->discount_reason,
671 'discount_type' => $request->discount_type,
672 'subtotal_price' => $subtotal,
673 'tax_rate' => $tax_rate,
674 'tax_amount' => $tax_amount,
675 'net_payment' => $total_price,
676 'total_price' => $total_price,
677 );
678
679 $update = $this->model->update_order( $request->order_id, $order_data );
680 if ( ! $update ) {
681 $this->json_response(
682 __( 'Failed to add a discount', 'tutor' ),
683 null,
684 HttpHelper::STATUS_INTERNAL_SERVER_ERROR
685 );
686 }
687
688 do_action( 'tutor_after_add_order_discount', $order, $discount_amount );
689
690 $this->json_response( __( 'Order discount successful added', 'tutor' ) );
691 } catch ( \Throwable $th ) {
692 $this->json_response(
693 __( 'Failed to add a discount', 'tutor' ),
694 $th->getMessage(),
695 HttpHelper::STATUS_INTERNAL_SERVER_ERROR
696 );
697 }
698
699 }
700
701 /**
702 * Prepare bulk actions that will show on dropdown options
703 *
704 * @return array
705 * @since 3.0.0
706 */
707 public function prepare_bulk_actions(): array {
708 $actions = array(
709 $this->bulk_action_default(),
710 );
711
712 $active_tab = Input::get( 'data', '' );
713
714 if ( $this->model::ORDER_TRASH !== $active_tab ) {
715 $actions[] = $this->bulk_action_mark_order_trash();
716 }
717
718 if ( ! empty( $active_tab ) ) {
719 switch ( $active_tab ) {
720 case $this->model::ORDER_INCOMPLETE:
721 $actions[] = $this->bulk_action_mark_order_paid();
722 break;
723 case $this->model::ORDER_COMPLETED:
724 $actions[] = $this->bulk_action_mark_order_unpaid();
725 break;
726 case $this->model::ORDER_TRASH:
727 $actions[] = $this->bulk_action_delete();
728 break;
729 default:
730 // code...
731 break;
732 }
733 }
734
735 return apply_filters( 'tutor_order_bulk_actions', $actions );
736 }
737
738 /**
739 * Available tabs that will visible on the right side of page navbar
740 *
741 * @return array
742 *
743 * @since 3.0.0
744 */
745 public function tabs_key_value(): array {
746 $url = apply_filters( 'tutor_data_tab_base_url', get_pagenum_link() );
747
748 $date = Input::get( 'date', '' );
749 $payment_status = Input::get( 'payment-status', '' );
750 $search = Input::get( 'search', '' );
751
752 $where = array(
753 'order_type' => OrderModel::TYPE_SINGLE_ORDER,
754 );
755
756 if ( ! empty( $date ) ) {
757 $where['created_at_gmt'] = tutor_get_formated_date( 'Y-m-d', $date );
758 }
759
760 if ( ! empty( $payment_status ) ) {
761 $where['payment_status'] = $payment_status;
762 }
763
764 $order_status = $this->model->get_order_status();
765
766 $tabs = array();
767
768 $tabs [] = array(
769 'key' => 'all',
770 'title' => __( 'All', 'tutor' ),
771 'value' => $this->model->get_order_count( $where, $search ),
772 'url' => $url . '&data=all',
773 );
774
775 foreach ( $order_status as $key => $value ) {
776 $where['order_status'] = $key;
777
778 $tabs[] = array(
779 'key' => $key,
780 'title' => $value,
781 'value' => $this->model->get_order_count( $where, $search ),
782 'url' => $url . '&data=' . $key,
783 );
784 }
785
786 return apply_filters( 'tutor_order_tabs', $tabs );
787 }
788
789 /**
790 * Count orders by status & filters
791 * Count all | min | published | pending | draft
792 *
793 * @param string $status | required.
794 * @param string $order_id selected order id | optional.
795 * @param string $date selected date | optional.
796 * @param string $search_term search by user name or email | optional.
797 *
798 * @return int
799 *
800 * @since 3.0.0
801 */
802 protected static function count_order( string $status, $order_id = '', $date = '', $search_term = '' ): int {
803 $user_id = get_current_user_id();
804 $status = sanitize_text_field( $status );
805 $order_id = sanitize_text_field( $order_id );
806 $date = sanitize_text_field( $date );
807 $search_term = sanitize_text_field( $search_term );
808
809 $args = array(
810 'post_type' => tutor()->order_post_type,
811 );
812
813 if ( 'all' === $status || 'mine' === $status ) {
814 $args['post_status'] = array( 'publish', 'pending', 'draft', 'private', 'future' );
815 } else {
816 $args['post_status'] = array( $status );
817 }
818
819 // Author query.
820 if ( 'mine' === $status || ! current_user_can( 'administrator' ) ) {
821 $args['author'] = $user_id;
822 }
823
824 $date_filter = sanitize_text_field( $date );
825
826 $year = gmdate( 'Y', strtotime( $date_filter ) );
827 $month = gmdate( 'm', strtotime( $date_filter ) );
828 $day = gmdate( 'd', strtotime( $date_filter ) );
829
830 // Add date query.
831 if ( '' !== $date_filter ) {
832 $args['date_query'] = array(
833 array(
834 'year' => $year,
835 'month' => $month,
836 'day' => $day,
837 ),
838 );
839 }
840
841 if ( '' !== $order_id ) {
842 $args['p'] = $order_id;
843 }
844
845 // Search filter.
846 if ( '' !== $search_term ) {
847 $args['s'] = $search_term;
848 }
849
850 $the_query = new \WP_Query( $args );
851
852 return ! is_null( $the_query ) && isset( $the_query->found_posts ) ? $the_query->found_posts : $the_query;
853 }
854
855 /**
856 * Handle order bulk action
857 *
858 * @since 3.0.0
859 *
860 * @return void send wp_json response
861 */
862 public function bulk_action_handler() {
863
864 tutor_utils()->checking_nonce();
865
866 // Check if user is privileged.
867 if ( ! current_user_can( 'administrator' ) ) {
868 wp_send_json_error( tutor_utils()->error_message() );
869 }
870
871 $request = Input::sanitize_array( $_POST ); //phpcs:ignore -- already sanitized.
872 $bulk_action = $request['bulk-action'];
873
874 $bulk_ids = isset( $request['bulk-ids'] ) ? array_map( 'intval', explode( ',', $request['bulk-ids'] ) ) : array();
875
876 $allowed_bulk_actions = array(
877 $this->model::PAYMENT_PAID,
878 $this->model::PAYMENT_UNPAID,
879 $this->model::ORDER_TRASH,
880 'delete',
881 );
882
883 if ( ! in_array( $bulk_action, $allowed_bulk_actions, true ) ) {
884 wp_send_json_error( __( 'Please select appropriate action', 'tutor' ) );
885 }
886
887 if ( empty( $bulk_ids ) ) {
888 wp_send_json_error( __( 'No items selected for the bulk action.', 'tutor' ) );
889 }
890
891 do_action( 'tutor_before_order_bulk_action', $bulk_action, $bulk_ids );
892
893 $response = false;
894 if ( 'delete' === $bulk_action ) {
895 $response = $this->model->delete_order( $bulk_ids );
896 } else {
897 $data = null;
898
899 switch ( $bulk_action ) {
900 case $this->model::PAYMENT_PAID:
901 $data = array(
902 'payment_status' => $this->model::PAYMENT_PAID,
903 'order_status' => $this->model::ORDER_COMPLETED,
904 );
905 break;
906 case $this->model::PAYMENT_UNPAID:
907 $data = array(
908 'payment_status' => $this->model::PAYMENT_UNPAID,
909 'order_status' => $this->model::ORDER_INCOMPLETE,
910 );
911 break;
912 case $this->model::ORDER_TRASH:
913 $data = array(
914 'order_status' => $this->model::ORDER_TRASH,
915 );
916 break;
917 default:
918 // code...
919 break;
920 }
921
922 if ( ! empty( $data ) ) {
923 $response = $this->model->update_order( $bulk_ids, $data );
924 }
925 }
926
927 if ( $response ) {
928 if ( 'delete' !== $bulk_action ) {
929 foreach ( $bulk_ids as $id ) {
930 do_action( 'tutor_order_payment_status_changed', $id, '', $bulk_action );
931 }
932 }
933 wp_send_json_success( __( 'Order updated successfully.', 'tutor' ) );
934 } else {
935 wp_send_json_error( __( 'Failed to update order.', 'tutor' ) );
936 }
937 }
938
939 /**
940 * Execute bulk delete action
941 *
942 * @param string $bulk_ids ids that need to update.
943 * @return bool
944 * @since 3.0.0
945 */
946 public function bulk_delete_order( $bulk_ids ): bool {
947 $bulk_ids = explode( ',', sanitize_text_field( $bulk_ids ) );
948
949 $response = false;
950 try {
951 $response = QueryHelper::bulk_delete_by_ids( $this->model->get_table_name(), $bulk_ids );
952 } catch ( \Throwable $th ) {
953 error_log( $th->getMessage() . ' Line: ' . $th->getLine() . ' File: ' . $th->getFile() );
954 }
955
956 return $response;
957 }
958
959 /**
960 * Update order status
961 *
962 * @param string $status for updating order status.
963 * @param string $bulk_ids comma separated ids.
964 *
965 * @return bool
966 *
967 * @since 3.0.0
968 */
969 public static function update_order_status( string $status, $bulk_ids ): bool {
970 global $wpdb;
971 $post_table = $wpdb->posts;
972 $status = sanitize_text_field( $status );
973 $bulk_ids = sanitize_text_field( $bulk_ids );
974
975 $ids = array_map( 'intval', explode( ',', $bulk_ids ) );
976 $in_clause = QueryHelper::prepare_in_clause( $ids );
977
978 $update = $wpdb->query(
979 $wpdb->prepare(
980 "UPDATE {$post_table} SET post_status = %s WHERE ID IN ($in_clause)", //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
981 $status
982 )
983 );
984
985 return true;
986 }
987
988 /**
989 * Get orders
990 *
991 * @since 3.0.0
992 *
993 * @param integer $limit List limit.
994 * @param integer $offset List offset.
995 *
996 * @return array
997 */
998 public function get_orders( $limit = 10, $offset = 0 ) {
999
1000 $active_tab = Input::get( 'data', 'all' );
1001
1002 $date = Input::get( 'date', '' );
1003 $search_term = Input::get( 'search', '' );
1004 $payment_status = Input::get( 'payment-status', '' );
1005
1006 $where_clause = array(
1007 'order_type' => OrderModel::TYPE_SINGLE_ORDER,
1008 );
1009
1010 if ( $date ) {
1011 $where_clause['date(o.created_at_gmt)'] = tutor_get_formated_date( '', $date );
1012 }
1013
1014 if ( $payment_status ) {
1015 $where_clause['o.payment_status'] = $payment_status;
1016 }
1017
1018 if ( 'all' !== $active_tab ) {
1019 $where_clause['o.order_status'] = $active_tab;
1020 }
1021
1022 $list_order = Input::get( 'order', 'DESC' );
1023 $list_order_by = 'id';
1024
1025 return $this->model->get_orders( $where_clause, $search_term, $limit, $offset, $list_order_by, $list_order );
1026 }
1027
1028 /**
1029 * Filter discount data if monetization is Tutor
1030 *
1031 * @since 3.0.0
1032 *
1033 * @param int $user_id Current user id.
1034 * @param string $period Period for filter refund data.
1035 * @param string $start_date Filter start date.
1036 * @param string $end_date Filter end date.
1037 * @param int $course_id Course id.
1038 *
1039 * @return array
1040 */
1041 public function get_discount_data( $user_id = 0, $period = '', $start_date = '', $end_date = '', $course_id = 0 ) {
1042 // Sanitize params.
1043 $user_id = is_admin() ? 0 : $user_id;
1044 $period = Input::sanitize( $period );
1045 $start_date = Input::sanitize( $start_date );
1046 $end_date = Input::sanitize( $end_date );
1047 $course_id = (int) $course_id;
1048
1049 return $this->model->get_discounts_by_user( $user_id, $period, $start_date, $end_date, $course_id );
1050 }
1051
1052 /**
1053 * Filter refund data if monetization is Tutor
1054 *
1055 * @since 3.0.0
1056 *
1057 * @param int $user_id Current user id.
1058 * @param string $period Period for filter refund data.
1059 * @param string $start_date Filter start date.
1060 * @param string $end_date Filter end date.
1061 * @param int $course_id Course id.
1062 *
1063 * @return array
1064 */
1065 public function get_refund_data( $user_id = 0, $period = '', $start_date = '', $end_date = '', $course_id = 0 ) {
1066 // Sanitize params.
1067 $user_id = is_admin() ? 0 : $user_id;
1068 $period = Input::sanitize( $period );
1069 $start_date = Input::sanitize( $start_date );
1070 $end_date = Input::sanitize( $end_date );
1071 $course_id = (int) $course_id;
1072
1073 return $this->model->get_refunds_by_user( $user_id, $period, $start_date, $end_date, $course_id );
1074 }
1075
1076 /**
1077 * Validate input data based on predefined rules.
1078 *
1079 * This protected method validates the provided data array against a set of
1080 * predefined validation rules. The rules specify that 'order_id' is required
1081 * and must be numeric. The method will skip validation rules for fields that
1082 * are not present in the data array.
1083 *
1084 * @since 3.0.0
1085 *
1086 * @param array $data The data array to validate.
1087 *
1088 * @return object The validation result. It returns validation object.
1089 */
1090 protected function validate( array $data ) {
1091
1092 $validation_rules = array(
1093 'order_id' => 'required|numeric',
1094 'meta_key' => 'required',
1095 'meta_value' => 'required',
1096 'discount_type' => 'required',
1097 'discount_amount' => 'required',
1098 );
1099
1100 // Skip validation rules for not available fields in data.
1101 foreach ( $validation_rules as $key => $value ) {
1102 if ( ! array_key_exists( $key, $data ) ) {
1103 unset( $validation_rules[ $key ] );
1104 }
1105 }
1106
1107 return ValidationHelper::validate( $validation_rules, $data );
1108 }
1109
1110 /**
1111 * Process refund from payment gateway
1112 *
1113 * @since 3.1.0
1114 *
1115 * @param int $order_id Order id.
1116 * @param string $amount Refund amount.
1117 * @param string $reason Refund reason.
1118 *
1119 * @throws \Throwable If an error occurs during the refund process.
1120 *
1121 * @return void
1122 */
1123 public function refund_from_payment_gateway( $order_id, $amount, $reason ) {
1124 $order = $this->model->get_order_by_id( $order_id );
1125 if ( $order && ! $this->model->is_manual_payment( $order->payment_method ) ) {
1126 $refund_data = $this->prepare_refund_data( $order, $amount, $reason );
1127 try {
1128 $payment_gateway_ref = Ecommerce::payment_gateways_with_ref( $order->payment_method );
1129 if ( $payment_gateway_ref ) {
1130 $gateway_obj = Ecommerce::get_payment_gateway_object( $payment_gateway_ref['gateway_class'] );
1131 $gateway_obj->make_refund( $refund_data );
1132 }
1133 } catch ( \Throwable $th ) {
1134 throw $th;
1135 }
1136 }
1137 }
1138
1139 /**
1140 * Prepare refund data
1141 *
1142 * @since 3.1.0
1143 *
1144 * @param object $order Order object.
1145 * @param string $amount Raw amount.
1146 * @param string $reason Refund reason.
1147 *
1148 * @return object
1149 */
1150 public function prepare_refund_data( $order, $amount, $reason ) {
1151 $currency = tutor_get_currencies_info_by_code( tutor_utils()->get_option( OptionKeys::CURRENCY_CODE ) );
1152
1153 $refund_data = array(
1154 'type' => 'refund',
1155 'amount' => $amount,
1156 'payment_payload' => $order->payment_payloads, // JSON string representing the payment payload.
1157 'order_id' => $order->id,
1158 'reason' => $reason,
1159 'refund_type' => $order->net_payment == $amount ? 'full' : 'partial',
1160 'currency' => (object) array(
1161 'code' => $currency['code'],
1162 'symbol' => $currency['symbol'],
1163 'name' => $currency['name'],
1164 'locale' => $currency['locale'],
1165 'numeric_code' => $currency['numeric_code'],
1166 ),
1167 );
1168
1169 return (object) $refund_data;
1170 }
1171
1172 }
1173