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