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