PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 3.5.0
Tutor LMS – eLearning and online course solution v3.5.0
4.0.1 4.0.0 3.9.15 3.9.14 3.9.13 3.9.12 3.9.11 trunk 1.0.0 1.0.0-alpha 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.2.0 1.2.1 1.2.11 1.2.12 1.2.13 1.2.20 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 1.7.6 1.7.7 1.7.8 1.7.9 1.8.0 1.8.1 1.8.10 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7 1.8.8 1.8.9 1.9.0 1.9.1 1.9.10 1.9.11 1.9.12 1.9.13 1.9.14 1.9.15 1.9.16 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.1 2.0.10 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1.0 2.1.1 2.1.10 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.3.0 2.4.0 2.5.0 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 3.0.0 3.0.1 3.0.2 3.1.0 3.2.0 3.2.1 3.2.2 3.2.3 3.3.0 3.3.1 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.7.0 3.7.1 3.7.2 3.7.3 3.7.4 3.8.0 3.8.1 3.8.2 3.8.3 3.9.0 3.9.1 3.9.10 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9
tutor / ecommerce / CheckoutController.php
tutor / ecommerce Last commit date
Cart 1 year ago 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
CheckoutController.php
1045 lines
1 <?php
2 /**
3 * Manage Checkout
4 *
5 * @package Tutor\Ecommerce
6 * @author Themeum
7 * @link https://themeum.com
8 * @since 3.0.0
9 */
10
11 namespace Tutor\Ecommerce;
12
13 use Tutor\Helpers\QueryHelper;
14 use Tutor\Helpers\ValidationHelper;
15 use TUTOR\Input;
16 use Tutor\Models\BillingModel;
17 use Tutor\Traits\JsonResponse;
18 use Tutor\Models\CartModel;
19 use Tutor\Models\CouponModel;
20 use Tutor\Models\OrderMetaModel;
21 use Tutor\Models\OrderModel;
22 use TutorPro\Ecommerce\GuestCheckout\GuestCheckout;
23
24 if ( ! defined( 'ABSPATH' ) ) {
25 exit;
26 }
27
28 /**
29 * Checkout Controller class
30 *
31 * @since 3.0.0
32 */
33 class CheckoutController {
34
35 use JsonResponse;
36
37 /**
38 * Page slug for checkout page
39 *
40 * @since 3.0.0
41 *
42 * @var string
43 */
44 const PAGE_SLUG = 'checkout';
45
46 /**
47 * Page slug for checkout page
48 *
49 * @since 3.0.0
50 *
51 * @var string
52 */
53 const PAGE_ID_OPTION_NAME = 'tutor_checkout_page_id';
54
55 /**
56 * Pay now error transient key
57 *
58 * @since 3.0.0
59 *
60 * @var string
61 */
62 const PAY_NOW_ERROR_TRANSIENT_KEY = 'tutor_pay_now_errors_';
63
64 /**
65 * Pay now alert transient key
66 *
67 * @since 3.0.0
68 *
69 * @var string
70 */
71 const PAY_NOW_ALERT_MSG_TRANSIENT_KEY = 'tutor_pay_now_alert_msg_';
72
73 /**
74 * Coupon model instance.
75 *
76 * @since 3.0.0
77 *
78 * @var CouponModel
79 */
80 public $coupon_model;
81
82 /**
83 * Instance of order controller.
84 *
85 * @since 3.5.0
86 *
87 * @var OrderController
88 */
89 public $order_ctrl;
90
91 /**
92 * Constructor.
93 *
94 * Initializes the Checkout class, sets the page title, and optionally registers
95 * hooks for handling AJAX requests related to cart data, bulk actions, cart updates,
96 * and cart deletions.
97 *
98 * @param bool $register_hooks Whether to register hooks for handling requests. Default is true.
99 *
100 * @since 3.0.0
101 *
102 * @return void
103 */
104 public function __construct( $register_hooks = true ) {
105 $this->coupon_model = new CouponModel();
106 $this->order_ctrl = new OrderController( false );
107
108 if ( $register_hooks ) {
109 add_action( 'tutor_action_tutor_pay_now', array( $this, 'pay_now' ) );
110 add_action( 'tutor_action_tutor_pay_incomplete_order', array( $this, 'pay_incomplete_order' ) );
111 add_action( 'template_redirect', array( $this, 'restrict_checkout_page' ) );
112 add_action( 'wp_ajax_tutor_get_checkout_html', array( $this, 'ajax_get_checkout_html' ) );
113 add_action( 'tutor_before_checkout_order_details', array( $this, 'add_warning_alert' ) );
114 }
115 }
116
117 /**
118 * Get cart page url
119 *
120 * @since 3.0.0
121 *
122 * @return string
123 */
124 public static function get_page_url() {
125 return get_post_permalink( self::get_page_id() );
126 }
127
128 /**
129 * Get cart page ID
130 *
131 * @since 3.0.0
132 *
133 * @return string
134 */
135 public static function get_page_id() {
136 return (int) tutor_utils()->get_option( self::PAGE_ID_OPTION_NAME );
137 }
138
139 /**
140 * Create checkout page
141 *
142 * @since 3.0.0
143 *
144 * @return void
145 */
146 public static function create_checkout_page() {
147 $page_id = self::get_page_id();
148 if ( ! $page_id ) {
149 $args = array(
150 'post_title' => ucfirst( self::PAGE_SLUG ),
151 'post_content' => '',
152 'post_type' => 'page',
153 'post_status' => 'publish',
154 );
155
156 $page_id = wp_insert_post( $args );
157 tutor_utils()->update_option( self::PAGE_ID_OPTION_NAME, $page_id );
158 }
159 }
160
161 /**
162 * Get checkout HTML
163 *
164 * @since 3.0.0
165 *
166 * @return void
167 */
168 public function ajax_get_checkout_html() {
169 tutor_utils()->check_nonce();
170
171 ob_start();
172 tutor_load_template( 'ecommerce/checkout-details' );
173 $content = ob_get_clean();
174
175 $this->json_response(
176 __( 'Success', 'tutor' ),
177 $content
178 );
179 }
180
181 /**
182 * Add warning alert to checkout details.
183 *
184 * @since 3.5.0
185 *
186 * @param array $course_list course list.
187 *
188 * @return void
189 */
190 public function add_warning_alert( $course_list ) {
191 /**
192 * Scenario: Guest checkout and buy now option enabled.
193 * Display a warning alert if the user attempts to purchase a course they are already enrolled in.
194 */
195 $course_id = (int) Input::sanitize_request_data( 'course_id', 0 );
196 if ( Settings::is_buy_now_enabled() && $course_id && tutor_utils()->is_enrolled( $course_id, get_current_user_id() ) ) {
197 add_filter( 'tutor_checkout_enable_pay_now_btn', '__return_false' );
198 ?>
199 <div class="tutor-alert tutor-warning tutor-d-flex tutor-gap-1">
200 <span><?php esc_html_e( 'You\'re already enrolled in this course.', 'tutor' ); ?></span>
201 <a href="<?php echo esc_url( get_the_permalink( $course_id ) ); ?>"><?php esc_html_e( 'Start learning!', 'tutor' ); ?></a>
202 </div>
203 <?php
204 }
205
206 /**
207 * Scenario: user login from the checkout page with courses in the cart.
208 * Display a warning alert if the user tries to purchase a course they are already enrolled in.
209 */
210 if ( ! Settings::is_buy_now_enabled() && is_array( $course_list ) && count( $course_list ) ) {
211 $enrolled_courses = array();
212 foreach ( $course_list as $course ) {
213 if ( tutor_utils()->is_enrolled( $course->ID, get_current_user_id() ) ) {
214 $enrolled_courses[] = $course;
215 }
216 }
217
218 if ( count( $enrolled_courses ) ) {
219 add_filter( 'tutor_checkout_enable_pay_now_btn', '__return_false' );
220 ?>
221 <div class="tutor-alert tutor-warning">
222 <div>
223 <p class="tutor-mb-8">
224 <?php
225 if ( count( $enrolled_courses ) > 1 ) {
226 esc_html_e( 'You are already enrolled in the following courses. Please remove those from your cart and continue.', 'tutor' );
227 } else {
228 esc_html_e( 'You are already enrolled in the following course. Please remove that from your cart and continue.', 'tutor' );
229 }
230 ?>
231 <a class="tutor-text-decoration-none tutor-color-primary" href="<?php echo esc_url( CartController::get_page_url() ); ?>"><?php esc_html_e( 'View Cart', 'tutor' ); ?></a>
232 </p>
233 <ul>
234 <?php foreach ( $enrolled_courses as $course ) : ?>
235 <li><a class="tutor-text-decoration-none tutor-color-primary" href="<?php echo esc_url( get_the_permalink( $course->ID ) ); ?>"><?php echo esc_html( $course->post_title ); ?></a></li>
236 <?php endforeach; ?>
237 </ul>
238 </div>
239 </div>
240 <?php
241 }
242 }
243 }
244
245 /**
246 * Prepare items
247 *
248 * @since 3.0.0
249 *
250 * @param array $item_ids items.
251 * @param string $order_type order type.
252 * @param object|null $coupon coupon.
253 *
254 * @return array
255 */
256 private function prepare_items( $item_ids, $order_type = OrderModel::TYPE_SINGLE_ORDER, $coupon = null ) {
257 $items = array();
258 $plan_info = null;
259
260 foreach ( $item_ids as $item_id ) {
261
262 if ( OrderModel::TYPE_SINGLE_ORDER === $order_type ) {
263 $item_name = get_the_title( $item_id );
264 $course_price = tutor_utils()->get_raw_course_price( $item_id );
265
266 $regular_price = $course_price->regular_price;
267 $sale_price = $course_price->sale_price;
268
269 $item = array(
270 'item_id' => (int) $item_id,
271 'item_name' => $item_name,
272 'regular_price' => $regular_price,
273 'sale_price' => $sale_price ? $sale_price : null,
274 'is_coupon_applied' => false,
275 'coupon_code' => null,
276 );
277 }
278
279 if ( OrderModel::TYPE_SUBSCRIPTION === $order_type ) {
280 $item = apply_filters( 'tutor_checkout_subscription_item', array(), $item_id, $coupon );
281 }
282
283 $is_coupon_applicable = false;
284 if ( Settings::is_coupon_usage_enabled() && is_object( $coupon ) ) {
285 $is_coupon_applicable = ! $item['sale_price'] && $this->coupon_model->is_coupon_applicable( $coupon, $item_id, $order_type );
286 if ( $is_coupon_applicable ) {
287 $item['is_coupon_applied'] = $is_coupon_applicable;
288 $item['coupon_code'] = $coupon->coupon_code;
289 }
290 }
291
292 $items[] = $item;
293 }
294
295 return array( $items, $plan_info );
296 }
297
298 /**
299 * Calculate discount.
300 *
301 * @since 3.0.0
302 *
303 * @param array $items item array.
304 * @param string $discount_type discount type. like percentage or fixed.
305 * @param float $discount_value value of discount.
306 *
307 * @return array
308 */
309 public function calculate_discount( $items, $discount_type, $discount_value ) {
310 // Filter products without a sale price.
311 $items_without_sale = array_filter( $items, fn( $item ) => empty( $item['sale_price'] ) );
312
313 // Calculate the total regular price of products without a sale price.
314 $total_regular_price = array_sum( array_column( $items_without_sale, 'regular_price' ) );
315
316 $final = array();
317 foreach ( $items as $item ) {
318 // If product already has a sale price, no discount is applied.
319 if ( ! empty( $item['sale_price'] ) || ( isset( $item['is_coupon_applied'] ) && ! $item['is_coupon_applied'] ) ) {
320 $item['discount_amount'] = 0;
321 $final[] = $item;
322 } else {
323 if ( 'percentage' === $discount_type ) {
324 // Apply percentage discount.
325 $discount = $item['regular_price'] * ( $discount_value / 100 );
326 $discount_price = $item['regular_price'] - $discount;
327
328 $item['discount_amount'] = round( $discount, 2 );
329 $item['discount_price'] = $discount_price;
330
331 } elseif ( 'flat' === $discount_type && $total_regular_price > 0 ) {
332 // Apply a proportional fixed discount based on the regular price.
333 $proportion = $item['regular_price'] / $total_regular_price;
334 $discount = $discount_value * $proportion;
335 $discount_price = $item['regular_price'] - $discount;
336
337 $item['discount_amount'] = round( $discount, 2 );
338 $item['discount_price'] = round( $discount_price, 2 );
339 }
340 $final[] = $item;
341 }
342 }
343
344 return $final;
345 }
346
347 /**
348 * Prepare checkout item with applying coupon if required.
349 *
350 * @since 3.0.0
351 *
352 * @since 3.3.0 is_coupon_applicable check added
353 *
354 * @param int|array $item_ids Required, course ids or plan id.
355 * @param string $order_type order type.
356 * @param string $coupon_code coupon code.
357 *
358 * @return object
359 */
360 public function prepare_checkout_items( $item_ids, $order_type = OrderModel::TYPE_SINGLE_ORDER, $coupon_code = null ) {
361 $item_ids = is_array( $item_ids ) ? $item_ids : array( $item_ids );
362 $response = array();
363 $user_id = get_current_user_id();
364
365 $coupon_type = empty( $coupon_code ) ? 'automatic' : 'manual';
366 $is_coupon_applied = false;
367 $coupon_title = '';
368
369 $total_price = 0;
370 $subtotal_price = 0;
371 $coupon_discount = 0;
372 $sale_discount = 0;
373
374 $coupon = null;
375 $is_coupon_applied = false;
376 $is_meet_min_requirement = false;
377 $selected_coupon = null;
378
379 if ( Settings::is_coupon_usage_enabled() ) {
380 $selected_coupon = $this->coupon_model->get_coupon_details_for_checkout( $coupon_code );
381 }
382
383 $is_valid = is_object( $selected_coupon ) && $this->coupon_model->is_coupon_valid( $selected_coupon );
384 if ( $is_valid ) {
385 $is_meet_min_requirement = $this->coupon_model->is_coupon_requirement_meet( $item_ids, $selected_coupon, $order_type );
386 if ( $is_meet_min_requirement ) {
387 $coupon = $selected_coupon;
388 }
389 }
390
391 list( $items, $plan_info ) = $this->prepare_items( $item_ids, $order_type, $coupon );
392
393 // Iterate with each item and check if coupon is applicable @since 3.3.0.
394 $is_coupon_applicable = false;
395 if ( $coupon ) {
396 foreach ( $items as $item ) {
397 if ( ! $is_coupon_applicable ) {
398 $is_coupon_applicable = $this->coupon_model->is_coupon_applicable( $coupon, $item['item_id'], $order_type );
399 }
400 }
401 if ( $is_coupon_applicable ) {
402 $is_coupon_applied = true;
403 }
404 }
405
406 if ( $is_coupon_applied ) {
407 $items = $this->calculate_discount( $items, $coupon->discount_type, $coupon->discount_amount );
408 $coupon_title = $coupon->coupon_title;
409 }
410
411 // Keep calculated price for each item.
412 foreach ( $items as $item ) {
413 $discount_amount = isset( $item['discount_amount'] ) ? $item['discount_amount'] : 0;
414 $has_discount_amount = $discount_amount > 0;
415 $item['discount_price'] = $has_discount_amount ? max( 0, $item['discount_price'] ) : null;
416
417 $display_price = isset( $item['sale_price'] ) ? $item['sale_price'] : $item['regular_price'];
418 $display_price = $has_discount_amount ? $item['discount_price'] : $display_price;
419 $item['display_price'] = $display_price;
420
421 $sale_discount_amount = isset( $item['sale_price'] ) ? $item['regular_price'] - $item['sale_price'] : 0;
422 $item['sale_discount_amount'] = $sale_discount_amount;
423
424 $response['items'][] = (object) $item;
425
426 $subtotal_price += $item['regular_price'];
427 $coupon_discount += $discount_amount;
428 $sale_discount += $sale_discount_amount;
429
430 $additional_items = $item['additional_items'] ?? array();
431 foreach ( $additional_items as $additional_item ) {
432 $subtotal_price += $additional_item['regular_price'] ?? 0;
433 }
434 }
435
436 $total_price = $subtotal_price - ( $coupon_discount + $sale_discount );
437 $tax_rate = Tax::get_user_tax_rate();
438 $tax_amount = Tax::calculate_tax( $total_price, $tax_rate );
439
440 $total_price_without_tax = $total_price;
441 if ( ! Tax::is_tax_included_in_price() ) {
442 $total_price += $tax_amount;
443 }
444
445 // Total price should not negative.
446 $total_price = max( 0, $total_price );
447
448 $response['plan_info'] = $plan_info;
449
450 $response['coupon_type'] = $coupon_type;
451 $response['coupon_code'] = $is_coupon_applied ? $coupon->coupon_code : null;
452 $response['coupon_title'] = $coupon_title;
453 $response['is_coupon_applied'] = $is_coupon_applied;
454
455 $response['subtotal_price'] = $subtotal_price;
456 $response['coupon_discount'] = $coupon_discount;
457 $response['sale_discount'] = $sale_discount;
458 $response['tax_rate'] = $tax_rate;
459 $response['total_price_without_tax'] = $total_price_without_tax;
460 $response['tax_amount'] = $tax_amount;
461 $response['total_price'] = $total_price;
462 $response['order_type'] = $order_type;
463
464 return (object) $response;
465 }
466
467 /**
468 * Pay now ajax handler
469 * Create pending order, prepare payment data & proceed to payment gateway
470 *
471 * @since 3.0.0
472 *
473 * @return void
474 */
475 public function pay_now() {
476 tutor_utils()->check_nonce();
477 global $wpdb;
478
479 $errors = array();
480 $order_data = null;
481
482 $billing_model = new BillingModel();
483 $current_user_id = is_user_logged_in() ? get_current_user_id() : wp_rand();
484 $request = Input::sanitize_array( $_POST ); //phpcs:ignore --sanitized.
485
486 $billing_fillable_fields = array_intersect_key( $request, array_flip( $billing_model->get_fillable_fields() ) );
487
488 $order_payment_fields = array(
489 'object_ids',
490 'coupon_code',
491 'payment_method',
492 'payment_type',
493 'order_type',
494 );
495
496 $request = array_intersect_key( $request, array_flip( $order_payment_fields ) );
497 // Set required.
498 foreach ( $order_payment_fields as $field ) {
499 if ( ! isset( $request[ $field ] ) ) {
500 $request[ $field ] = '';
501 }
502 }
503
504 // Validate data.
505 $validate = $this->validate_pay_now_req( $request );
506
507 if ( ! $validate->success ) {
508 foreach ( $validate->errors as $error ) {
509 if ( is_array( $error ) ) {
510 foreach ( $error as $err ) {
511 array_push( $errors, $err );
512 }
513 } else {
514 array_push( $errors, $error );
515 }
516 }
517 }
518
519 // Return if validation failed.
520 if ( ! empty( $errors ) ) {
521 set_transient( self::PAY_NOW_ERROR_TRANSIENT_KEY . $current_user_id, $errors );
522 return;
523 }
524
525 $object_ids = array_filter( explode( ',', $request['object_ids'] ), 'is_numeric' );
526 $coupon_code = isset( $request['coupon_code'] ) ? $request['coupon_code'] : '';
527 $payment_method = $request['payment_method'];
528 $payment_type = $request['payment_type'];
529 $order_type = $request['order_type'];
530
531 if ( empty( $object_ids ) ) {
532 array_push( $errors, __( 'Invalid cart items', 'tutor' ) );
533 }
534
535 $billing_info = $billing_model->get_info( $current_user_id );
536 if ( $billing_info ) {
537 $update_billing = $billing_model->update( $billing_fillable_fields, array( 'user_id' => $current_user_id ) );
538 if ( ! $update_billing ) {
539 array_push( $errors, __( 'Billing information update failed!', 'tutor' ) );
540 }
541 } else {
542 // Save billing info.
543 $billing_fillable_fields['user_id'] = $current_user_id;
544
545 $save = $billing_model->insert( $billing_fillable_fields );
546 if ( ! $save ) {
547 array_push( $errors, __( 'Billing info save failed!', 'tutor' ) );
548 }
549 }
550
551 $checkout_data = $this->prepare_checkout_items( $object_ids, $order_type, $coupon_code );
552 $items = array();
553 foreach ( $checkout_data->items as $item ) {
554 $items[] = array(
555 'item_id' => $item->item_id,
556 'regular_price' => $item->regular_price,
557 'sale_price' => $item->sale_price,
558 'discount_price' => $item->discount_price,
559 'coupon_code' => $item->is_coupon_applied ? $item->coupon_code : null,
560 );
561 }
562
563 $args = apply_filters(
564 'tutor_order_create_args',
565 array(
566 'payment_method' => $payment_method,
567 'coupon_amount' => $checkout_data->coupon_discount,
568 'discount_amount' => $checkout_data->sale_discount,
569 )
570 );
571
572 if ( empty( $errors ) ) {
573 if ( ! is_user_logged_in() ) {
574 $guest_user = apply_filters( 'tutor_guest_user_id', $current_user_id, $order_data, $billing_fillable_fields );
575 if ( is_wp_error( $guest_user ) ) {
576 // Delete the billing info if user registration failed.
577 QueryHelper::delete( "{$wpdb->prefix}tutor_customers", array( 'user_id' => $current_user_id ) );
578
579 add_filter( 'tutor_checkout_user_id', fn () => $current_user_id );
580
581 // translators: wp error message.
582 $error_msg = sprintf( esc_html_x( 'Order placement failed. %s', 'guest checkout', 'tutor' ), $guest_user->get_error_message() );
583 set_transient(
584 self::PAY_NOW_ERROR_TRANSIENT_KEY . $current_user_id,
585 array(
586 'message' => $error_msg,
587 )
588 );
589 return;
590 } else {
591 $current_user_id = $guest_user;
592 }
593 }
594
595 $order_data = $this->order_ctrl->create_order(
596 $current_user_id,
597 $items,
598 OrderModel::PAYMENT_UNPAID,
599 $order_type,
600 $checkout_data->coupon_code,
601 $args,
602 false
603 );
604
605 if ( ! empty( $order_data ) ) {
606 if ( 'automate' === $payment_type ) {
607 try {
608 $payment_data = self::prepare_payment_data( $order_data );
609 $this->proceed_to_payment( $payment_data, $payment_method, $order_type );
610 } catch ( \Throwable $th ) {
611 tutor_log( $th );
612 tutor_redirect_after_payment( OrderModel::ORDER_PLACEMENT_FAILED, $order_data['id'], $th->getMessage() );
613 }
614 } else {
615 // Set alert message session.
616 $this->set_pay_now_alert_msg( $order_data );
617 tutor_redirect_after_payment( OrderModel::ORDER_PLACEMENT_SUCCESS, $order_data['id'] );
618 }
619 } else {
620 array_push( $errors, __( 'Failed to place order!', 'tutor' ) );
621 set_transient( self::PAY_NOW_ERROR_TRANSIENT_KEY . $current_user_id, $errors );
622 $this->set_pay_now_alert_msg( $order_data );
623 }
624 } else {
625 set_transient( self::PAY_NOW_ERROR_TRANSIENT_KEY . $current_user_id, $errors );
626 $this->set_pay_now_alert_msg( $order_data );
627 }
628 }
629
630 /**
631 * Prepare payment data
632 *
633 * @since 3.0.0
634 *
635 * @param array $order Order object.
636 *
637 * @return mixed
638 */
639 public static function prepare_payment_data( array $order ) {
640 $site_name = get_bloginfo( 'name' );
641 $order_id = $order['id'];
642 $order_user_id = $order['user_id'];
643 $user_data = get_userdata( $order_user_id );
644
645 $items = array();
646 $subtotal_price = $order['subtotal_price'];
647 $total_price = $order['total_price'];
648 $grand_total = $total_price;
649 $order_type = $order['order_type'];
650
651 $currency_code = tutor_utils()->get_option( OptionKeys::CURRENCY_CODE, 'USD' );
652 $currency_symbol = tutor_get_currency_symbol_by_code( $currency_code );
653 $currency_info = tutor_get_currencies_info_by_code( $currency_code );
654
655 $billing_info = ( new BillingModel() )->get_info( $order_user_id );
656
657 $country_info = tutor_get_country_info_by_name( $billing_info->billing_country );
658
659 $country = (object) array(
660 'name' => $country_info['name'],
661 'numeric_code' => $country_info['numeric_code'],
662 'alpha_2' => $country_info['alpha_2'],
663 'alpha_3' => $country_info['alpha_3'],
664 'phone_code' => $country_info['phone_code'],
665 );
666
667 $billing_name = $billing_info ? trim( $billing_info->billing_first_name . ' ' . $billing_info->billing_last_name ) : $user_data->display_name;
668
669 $shipping_and_billing = array(
670 'name' => $billing_name,
671 'address1' => $billing_info->billing_address ?? '',
672 'address2' => $billing_info->billing_address ?? '',
673 'city' => $billing_info->billing_city ?? '',
674 'state' => $billing_info->billing_state ?? '',
675 'region' => '',
676 'postal_code' => $billing_info->billing_zip_code ?? '',
677 'country' => $country,
678 'phone_number' => $billing_info->billing_phone ?? '',
679 'email' => $billing_info->billing_email ?? '',
680 );
681
682 $customer_info = $shipping_and_billing;
683
684 foreach ( $order['items'] as $item ) {
685 $item = (object) $item;
686 $item_id = $item->item_id ?? $item->id;
687
688 if ( OrderModel::TYPE_SINGLE_ORDER === $order_type ) {
689 $items[] = array(
690 'item_id' => $item_id,
691 'item_name' => get_the_title( $item_id ),
692 'regular_price' => $item->sale_price > 0 ? $item->sale_price : $item->regular_price,
693 'quantity' => 1,
694 'discounted_price' => is_null( $item->discount_price ) || '' === $item->discount_price ? null : $item->discount_price,
695 );
696 }
697
698 if ( OrderModel::TYPE_SUBSCRIPTION === $order_type ) {
699 $subscription_items = apply_filters( 'tutor_checkout_subscription_payment_items', array(), $item, $order_id );
700 foreach ( $subscription_items as $subscription_item ) {
701 $items[] = $subscription_item;
702 }
703 }
704 }
705
706 if ( isset( $order['tax_amount'] ) && ! Tax::is_tax_included_in_price() ) {
707 $grand_total += $order['tax_amount'];
708
709 /* translators: %s: tax rate */
710 $tax_item = sprintf( __( 'Tax (%s)', 'tutor' ), $order['tax_rate'] . '%' );
711 $items[] = array(
712 'item_id' => 'tax',
713 'item_name' => $tax_item,
714 'regular_price' => $order['tax_amount'],
715 'quantity' => 1,
716 'discounted_price' => null,
717 );
718 }
719
720 return (object) array(
721 'items' => (object) $items,
722 'subtotal' => floatval( $subtotal_price ),
723 'total_price' => floatval( $total_price ),
724 'order_id' => $order_id,
725 'store_name' => $site_name,
726 'order_description' => 'Tutor Order',
727 'tax' => 0,
728 'currency' => (object) array(
729 'code' => $currency_code,
730 'symbol' => $currency_symbol,
731 'name' => $currency_info['name'] ?? '',
732 'locale' => $currency_info['locale'] ?? '',
733 'numeric_code' => $currency_info['numeric_code'] ?? '',
734 ),
735 'country' => $country,
736 'shipping_charge' => 0,
737 'coupon_discount' => 0,
738 'shipping_address' => (object) $shipping_and_billing,
739 'billing_address' => (object) $shipping_and_billing,
740 'decimal_separator' => tutor_utils()->get_option( OptionKeys::DECIMAL_SEPARATOR, '.' ),
741 'thousand_separator' => tutor_utils()->get_option( OptionKeys::THOUSAND_SEPARATOR, '.' ),
742 'customer' => (object) $customer_info,
743 );
744 }
745
746 /**
747 * Prepare payment data
748 *
749 * @since 3.0.0
750 *
751 * @param int $order_id Order id.
752 *
753 * @throws \Exception Throw exception if order not found.
754 *
755 * @return mixed
756 */
757 public static function prepare_recurring_payment_data( int $order_id ) {
758 $order_data = ( new OrderModel() )->get_order_by_id( $order_id );
759 if ( ! $order_data ) {
760 throw new \Exception( __( 'Order not found!', 'tutor' ) );
761 }
762
763 $amount = $order_data->total_price;
764
765 $order_user_id = $order_data->student->id;
766 $user_data = get_userdata( $order_user_id );
767
768 $currency_code = tutor_utils()->get_option( OptionKeys::CURRENCY_CODE, 'USD' );
769 $currency_symbol = tutor_get_currency_symbol_by_code( $currency_code );
770 $currency_info = tutor_get_currencies_info_by_code( $currency_code );
771
772 $billing_info = ( new BillingModel() )->get_info( $order_user_id );
773
774 $country_info = tutor_get_country_info_by_name( $billing_info->billing_country );
775
776 $country = (object) array(
777 'name' => $country_info['name'],
778 'numeric_code' => $country_info['numeric_code'],
779 'alpha_2' => $country_info['alpha_2'],
780 'alpha_3' => $country_info['alpha_3'],
781 'phone_code' => $country_info['phone_code'],
782 );
783
784 $billing_name = $billing_info ? trim( $billing_info->billing_first_name . ' ' . $billing_info->billing_last_name ) : $user_data->display_name;
785
786 $shipping_and_billing = array(
787 'name' => $billing_name,
788 'address1' => $billing_info->billing_address ?? '',
789 'address2' => $billing_info->billing_address ?? '',
790 'city' => $billing_info->billing_city ?? '',
791 'state' => $billing_info->billing_state ?? '',
792 'region' => '',
793 'postal_code' => $billing_info->billing_zip_code ?? '',
794 'country' => $country,
795 'phone_number' => $billing_info->billing_phone ?? '',
796 'email' => $billing_info->billing_email ?? '',
797 );
798
799 $customer_info = $shipping_and_billing;
800
801 return (object) array(
802 'type' => 'recurring',
803 'previous_payload' => $order_data->payment_payloads,
804 'total_amount' => floatval( $amount ),
805 'sub_total_amount' => floatval( $amount ),
806 'currency' => (object) array(
807 'code' => $currency_code,
808 'symbol' => $currency_symbol,
809 'name' => $currency_info['name'] ?? '',
810 'locale' => $currency_info['locale'] ?? '',
811 'numeric_code' => $currency_info['numeric_code'] ?? '',
812 ),
813 'order_id' => $order_id,
814 'customer' => (object) $customer_info,
815 'shipping_address' => (object) $shipping_and_billing,
816 );
817 }
818
819 /**
820 * Proceed to payment
821 *
822 * @since 3.0.0
823 *
824 * @param mixed $payment_data Payment data for making order.
825 * @param string $payment_method Payment method name.
826 * @param string $order_type Order type.
827 *
828 * @throws \Throwable Throw throwable if error occur.
829 * @throws \Exception Throw exception if payment gateway is invalid.
830 *
831 * @return void
832 */
833 public function proceed_to_payment( $payment_data, $payment_method, $order_type ) {
834 $payment_gateways = apply_filters( 'tutor_gateways_with_class', Ecommerce::payment_gateways_with_ref(), $payment_method );
835
836 $payment_gateway_class = isset( $payment_gateways[ $payment_method ] )
837 ? $payment_gateways[ $payment_method ]['gateway_class']
838 : null;
839
840 if ( $payment_gateway_class ) {
841 try {
842
843 add_filter(
844 'tutor_ecommerce_webhook_url',
845 function ( $url ) use ( $payment_method ) {
846 $url = add_query_arg( array( 'payment_method' => $payment_method ), $url );
847 return $url;
848 }
849 );
850
851 add_filter(
852 'tutor_ecommerce_payment_success_url_args',
853 function ( $args ) use ( $payment_data ) {
854 $args['order_id'] = $payment_data->order_id;
855 return $args;
856 }
857 );
858 add_filter(
859 'tutor_ecommerce_payment_cancelled_url_args',
860 function ( $args ) use ( $payment_data ) {
861 $args['order_id'] = $payment_data->order_id;
862 return $args;
863 }
864 );
865
866 $gateway_instance = Ecommerce::get_payment_gateway_object( $payment_gateway_class );
867 $gateway_instance->setup_payment_and_redirect( $payment_data );
868 } catch ( \Throwable $th ) {
869 throw $th;
870 }
871 } else {
872 throw new \Exception( 'Invalid payment gateway class' );
873 }
874 }
875
876 /**
877 * Restrict checkout page
878 *
879 * @return void
880 */
881 public function restrict_checkout_page() {
882 if ( ! is_page( self::get_page_id() ) ) {
883 return;
884 }
885
886 $cart_page_url = CartController::get_page_url();
887
888 if ( ! is_user_logged_in() && ! GuestCheckout::is_enable() ) {
889 wp_safe_redirect( $cart_page_url );
890 exit;
891 }
892
893 $user_id = tutils()->get_user_id();
894 $cart_model = new CartModel();
895 $has_cart_item = $cart_model->has_item_in_cart( $user_id );
896 $buy_now = Settings::is_buy_now_enabled();
897 $plan_id = Input::get( 'plan', 0, Input::TYPE_INT );
898
899 if ( ! $has_cart_item && ! $buy_now && ! $plan_id ) {
900 wp_safe_redirect( $cart_page_url );
901 exit;
902 }
903 }
904
905 /**
906 * Set alert message on the session based on
907 * order data
908 *
909 * @since 3.0.0
910 *
911 * @param mixed $order_data Order data or null. If order
912 * data is falsy then failed message will be set.
913 *
914 * @return void
915 */
916 private function set_pay_now_alert_msg( $order_data ) {
917 $user_id = $order_data ? $order_data['user_id'] : get_current_user_id();
918 if ( empty( $order_data ) ) {
919 set_transient(
920 self::PAY_NOW_ALERT_MSG_TRANSIENT_KEY . $user_id,
921 array(
922 'alert' => 'danger',
923 'message' => __( 'Failed to place order!', 'tutor' ),
924 ),
925 );
926 } else {
927 set_transient(
928 self::PAY_NOW_ALERT_MSG_TRANSIENT_KEY . $user_id,
929 array(
930 'alert' => 'success',
931 'message' => __( 'Your order has been placed successfully!', 'tutor' ),
932 ),
933 );
934 }
935 }
936
937 /**
938 * Pay for the incomplete order
939 *
940 * Redirect to the payment gateway to complete the order
941 * After completing the process it will redirect user to
942 * order placement page
943 *
944 * @since 3.0.0
945 *
946 * @return void
947 */
948 public function pay_incomplete_order() {
949 $order_id = Input::post( 'order_id', 0, Input::TYPE_INT );
950 $payment_method = Input::post( 'payment_method', '' );
951 $request = Input::sanitize_array( $_POST ); //phpcs:ignore -- $POST sanitized
952
953 $billing_model = new BillingModel();
954 $billing_fillable_fields = array_intersect_key( $request, array_flip( $billing_model->get_fillable_fields() ) );
955
956 if ( ! tutor_utils()->is_nonce_verified() ) {
957 tutor_utils()->redirect_to( tutor_utils()->tutor_dashboard_url( 'purchase_history' ), tutor_utils()->error_message( 'nonce' ), 'error' );
958 exit;
959 }
960 if ( $order_id ) {
961 $order_model = new OrderModel();
962 $order_data = $order_model->get_order_by_id( $order_id );
963 if ( $order_data ) {
964 try {
965 if ( ! empty( $payment_method ) && OrderModel::PAYMENT_METHOD_MANUAL === $order_data->payment_method ) {
966 $billing_info = $billing_model->get_info( $order_data->user_id );
967 if ( $billing_info ) {
968 $update_billing = $billing_model->update( $billing_fillable_fields, array( 'user_id' => $order_data->user_id ) );
969
970 if ( ! $update_billing ) {
971 tutor_redirect_after_payment( OrderModel::ORDER_PLACEMENT_FAILED, $order_data->id, __( 'Billing information update failed!', 'tutor' ) );
972 }
973 } else {
974 // Save billing info.
975 $billing_fillable_fields['user_id'] = $order_data->user_id;
976
977 $save = $billing_model->insert( $billing_fillable_fields );
978
979 if ( ! $save ) {
980 tutor_redirect_after_payment( OrderModel::ORDER_PLACEMENT_FAILED, $order_data->id, __( 'Billing info save failed!', 'tutor' ) );
981 }
982 }
983
984 $update_order_data = $order_model->get_recalculated_order_tax_data( $order_id );
985 $update_order_data['payment_method'] = $payment_method;
986
987 $updated = $order_model->update_order( $order_data->id, $update_order_data );
988
989 if ( $updated ) {
990 $order_data = $order_model->get_order_by_id( $order_id );
991 }
992 }
993
994 $payment_data = $this->prepare_payment_data( (array) $order_data, $payment_method ? $payment_method : $order_data->payment_method, $order_data->order_type );
995 $this->proceed_to_payment( $payment_data, $payment_method ? $payment_method : $order_data->payment_method, $order_data->order_type );
996 } catch ( \Throwable $th ) {
997 tutor_log( $th );
998 tutor_redirect_after_payment( OrderModel::ORDER_PLACEMENT_FAILED, $order_data->id, $th->getMessage() );
999 }
1000 } else {
1001 $error_msg = __( 'Order not found!', 'tutor' );
1002 tutor_redirect_after_payment( OrderModel::ORDER_PLACEMENT_FAILED, $order_id, $error_msg );
1003 }
1004 } else {
1005 $error_msg = __( 'Invalid order ID!', 'tutor' );
1006 tutor_redirect_after_payment( OrderModel::ORDER_PLACEMENT_FAILED, $order_id, $error_msg );
1007 }
1008 }
1009
1010 /**
1011 * Validate pay now request
1012 *
1013 * @since 3.0.0
1014 *
1015 * @param array $data The data array to validate.
1016 *
1017 * @return object The validation result. It returns validation object.
1018 */
1019 protected function validate_pay_now_req( array $data ) {
1020
1021 $order_types = array(
1022 OrderModel::TYPE_SINGLE_ORDER,
1023 OrderModel::TYPE_SUBSCRIPTION,
1024 OrderModel::TYPE_RENEWAL,
1025 );
1026
1027 $order_types = implode( ',', $order_types );
1028
1029 $validation_rules = array(
1030 'object_ids' => 'required',
1031 'order_type' => "required|match_string:{$order_types}",
1032 'payment_method' => 'required',
1033 );
1034
1035 // Skip validation rules for not available fields in data.
1036 foreach ( $validation_rules as $key => $value ) {
1037 if ( ! array_key_exists( $key, $data ) ) {
1038 unset( $validation_rules[ $key ] );
1039 }
1040 }
1041
1042 return ValidationHelper::validate( $validation_rules, $data );
1043 }
1044 }
1045