PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 4.0.0
Tutor LMS – eLearning and online course solution v4.0.0
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 10 months ago PaymentGateways 1 month ago AdminMenu.php 1 day ago BillingController.php 1 day ago CartController.php 1 day ago CheckoutController.php 1 day ago CouponController.php 1 day ago Ecommerce.php 1 day ago EmailController.php 11 months ago HooksHandler.php 1 day ago OptionKeys.php 1 year ago OrderActivitiesController.php 1 year ago OrderController.php 1 day ago PaymentHandler.php 9 months ago Settings.php 1 day ago Tax.php 9 months ago currency.php 5 months ago
CheckoutController.php
1266 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 defined( 'ABSPATH' ) || exit;
14
15 use Tutor\GDPR\Controllers\LegalConsent;
16 use TUTOR\Input;
17 use Tutor\Models\CartModel;
18 use Tutor\Models\OrderModel;
19 use Tutor\Models\CouponModel;
20 use Tutor\Models\CourseModel;
21 use Tutor\Helpers\QueryHelper;
22 use Tutor\Models\BillingModel;
23 use Tutor\Traits\JsonResponse;
24 use Tutor\Helpers\ValidationHelper;
25 use Tutor\Models\EnrollmentModel;
26
27 /**
28 * Checkout Controller class
29 *
30 * @since 3.0.0
31 */
32 class CheckoutController {
33
34 use JsonResponse;
35
36 /**
37 * Page slug for checkout page
38 *
39 * @since 3.0.0
40 *
41 * @var string
42 */
43 const PAGE_SLUG = 'checkout';
44
45 /**
46 * Page slug for checkout page
47 *
48 * @since 3.0.0
49 *
50 * @var string
51 */
52 const PAGE_ID_OPTION_NAME = 'tutor_checkout_page_id';
53
54 /**
55 * Pay now error transient key
56 *
57 * @since 3.0.0
58 *
59 * @var string
60 */
61 const PAY_NOW_ERROR_TRANSIENT_KEY = 'tutor_pay_now_errors_';
62
63 /**
64 * Pay now alert transient key
65 *
66 * @since 3.0.0
67 *
68 * @var string
69 */
70 const PAY_NOW_ALERT_MSG_TRANSIENT_KEY = 'tutor_pay_now_alert_msg_';
71
72 /**
73 * Coupon model instance.
74 *
75 * @since 3.0.0
76 *
77 * @var CouponModel
78 */
79 public $coupon_model;
80
81 /**
82 * Instance of order controller.
83 *
84 * @since 3.5.0
85 *
86 * @var OrderController
87 */
88 public $order_ctrl;
89
90 /**
91 * Constructor.
92 *
93 * Initializes the Checkout class, sets the page title, and optionally registers
94 * hooks for handling AJAX requests related to cart data, bulk actions, cart updates,
95 * and cart deletions.
96 *
97 * @param bool $register_hooks Whether to register hooks for handling requests. Default is true.
98 *
99 * @since 3.0.0
100 *
101 * @return void
102 */
103 public function __construct( $register_hooks = true ) {
104 $this->coupon_model = new CouponModel();
105 $this->order_ctrl = new OrderController( false );
106
107 if ( $register_hooks ) {
108 add_action( 'tutor_action_tutor_pay_now', array( $this, 'pay_now' ) );
109 add_action( 'tutor_action_tutor_pay_incomplete_order', array( $this, 'pay_incomplete_order' ) );
110 add_action( 'template_redirect', array( $this, 'restrict_checkout_page' ) );
111 add_action( 'wp_ajax_tutor_get_checkout_html', array( $this, 'ajax_get_checkout_html' ) );
112 add_action( 'tutor_before_checkout_order_details', array( $this, 'add_warning_alert' ) );
113 }
114 }
115
116 /**
117 * Get cart page url
118 *
119 * @since 3.0.0
120 *
121 * @return string
122 */
123 public static function get_page_url() {
124 return get_post_permalink( self::get_page_id() );
125 }
126
127 /**
128 * Get cart page ID
129 *
130 * @since 3.0.0
131 *
132 * @return string
133 */
134 public static function get_page_id() {
135 return (int) tutor_utils()->get_option( self::PAGE_ID_OPTION_NAME );
136 }
137
138 /**
139 * Create checkout page
140 *
141 * @since 3.0.0
142 *
143 * @return void
144 */
145 public static function create_checkout_page() {
146 $page_id = self::get_page_id();
147 if ( ! $page_id ) {
148 $args = array(
149 'post_title' => ucfirst( self::PAGE_SLUG ),
150 'post_content' => '',
151 'post_type' => 'page',
152 'post_status' => 'publish',
153 );
154
155 $page_id = wp_insert_post( $args );
156 tutor_utils()->update_option( self::PAGE_ID_OPTION_NAME, $page_id );
157 }
158 }
159
160 /**
161 * Get checkout HTML
162 *
163 * @since 3.0.0
164 *
165 * @return void
166 */
167 public function ajax_get_checkout_html() {
168 tutor_utils()->check_nonce();
169
170 ob_start();
171 tutor_load_template( 'ecommerce/checkout-details' );
172 $content = ob_get_clean();
173
174 $this->json_response(
175 __( 'Success', 'tutor' ),
176 $content
177 );
178 }
179
180 /**
181 * Add warning alert to checkout details.
182 *
183 * @since 3.5.0
184 *
185 * @param array $course_list course list.
186 *
187 * @return void
188 */
189 public function add_warning_alert( $course_list ) {
190 /**
191 * Scenario: Guest checkout and buy now option enabled.
192 * Display a warning alert if the user attempts to purchase a course they are already enrolled in.
193 */
194 $course_id = (int) Input::sanitize_request_data( 'course_id', 0 );
195 if ( Settings::is_buy_now_enabled() && $course_id && EnrollmentModel::is_enrolled( $course_id, get_current_user_id() ) ) {
196 add_filter( 'tutor_checkout_enable_pay_now_btn', '__return_false' );
197 ?>
198 <div class="tutor-alert tutor-warning tutor-d-flex tutor-gap-1">
199 <span><?php esc_html_e( 'You\'re already enrolled in this course.', 'tutor' ); ?></span>
200 <a href="<?php echo esc_url( get_the_permalink( $course_id ) ); ?>"><?php esc_html_e( 'Start learning!', 'tutor' ); ?></a>
201 </div>
202 <?php
203 }
204
205 /**
206 * Scenario: user login from the checkout page with courses in the cart.
207 * Display a warning alert if the user tries to purchase a course they are already enrolled in.
208 */
209 if ( ! Settings::is_buy_now_enabled() && is_array( $course_list ) && count( $course_list ) ) {
210 $enrolled_courses = array();
211 foreach ( $course_list as $course ) {
212 if ( EnrollmentModel::is_enrolled( $course->ID, get_current_user_id() ) ) {
213 $enrolled_courses[] = $course;
214 }
215 }
216
217 if ( count( $enrolled_courses ) ) {
218 add_filter( 'tutor_checkout_enable_pay_now_btn', '__return_false' );
219 ?>
220 <div class="tutor-alert tutor-warning">
221 <div>
222 <p class="tutor-mb-8">
223 <?php
224 if ( count( $enrolled_courses ) > 1 ) {
225 esc_html_e( 'You are already enrolled in the following courses. Please remove those from your cart and continue.', 'tutor' );
226 } else {
227 esc_html_e( 'You are already enrolled in the following course. Please remove that from your cart and continue.', 'tutor' );
228 }
229 ?>
230 <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>
231 </p>
232 <ul>
233 <?php foreach ( $enrolled_courses as $course ) : ?>
234 <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>
235 <?php endforeach; ?>
236 </ul>
237 </div>
238 </div>
239 <?php
240 }
241 }
242 }
243
244 /**
245 * Check coupon is applied on checkout item.
246 *
247 * @param array $item checkout item.
248 *
249 * @return boolean applied or not.
250 */
251 private function is_coupon_applied_on_item( array $item ) {
252 return isset( $item['is_coupon_applied'] ) && (bool) $item['is_coupon_applied'];
253 }
254
255 /**
256 * Prepare items
257 *
258 * @since 3.0.0
259 *
260 * @param array $item_ids items.
261 * @param string $order_type order type.
262 * @param object|null $coupon coupon.
263 *
264 * @return array
265 */
266 private function prepare_items( $item_ids, $order_type = OrderModel::TYPE_SINGLE_ORDER, $coupon = null ) {
267 $items = array();
268 $plan_info = null;
269
270 foreach ( $item_ids as $item_id ) {
271
272 if ( OrderModel::TYPE_SINGLE_ORDER === $order_type ) {
273 if ( ! CourseModel::is_course_accessible( $item_id ) ) {
274 continue;
275 }
276 $item_name = get_the_title( $item_id );
277 $course_price = tutor_utils()->get_raw_course_price( $item_id );
278
279 $regular_price = $course_price->regular_price;
280 $sale_price = $course_price->sale_price;
281
282 $item = array(
283 'item_id' => (int) $item_id,
284 'item_name' => $item_name,
285 'regular_price' => $regular_price,
286 'sale_price' => $sale_price ? $sale_price : null,
287 'is_coupon_applied' => false,
288 'coupon_code' => null,
289 'tax_collection' => CourseModel::is_tax_enabled_for_single_purchase( $item_id ),
290 );
291 }
292
293 if ( OrderModel::TYPE_SUBSCRIPTION === $order_type ) {
294 $item = apply_filters( 'tutor_checkout_subscription_item', array(), $item_id, $coupon );
295 }
296
297 $is_coupon_applicable = false;
298 if ( Settings::is_coupon_usage_enabled() && is_object( $coupon ) ) {
299 $is_coupon_applicable = $this->coupon_model->is_coupon_applicable( $coupon, $item_id, $order_type );
300 if ( $is_coupon_applicable ) {
301 $item['is_coupon_applied'] = $is_coupon_applicable;
302 $item['coupon_code'] = $coupon->coupon_code;
303 $item['sale_price'] = null;
304 }
305 }
306
307 $items[] = $item;
308 }
309
310 return array( $items, $plan_info );
311 }
312
313 /**
314 * Calculate discount.
315 *
316 * @since 3.0.0
317 * @since 3.6.0 refactor and inaccurate flat discount distribution.
318 *
319 * @param array $items item array.
320 * @param string $discount_type discount type. like percentage or fixed.
321 * @param float $discount_value value of discount.
322 *
323 * @return array
324 */
325 public function calculate_discount( $items, $discount_type, $discount_value ) {
326 $final = array();
327 $coupon_applied_items = array();
328 $total_regular_price_coupon_applied = 0;
329
330 foreach ( $items as $item ) {
331 if ( $this->is_coupon_applied_on_item( $item ) ) {
332 $coupon_applied_items[] = $item;
333 $total_regular_price_coupon_applied += $item['regular_price'];
334 } else {
335 $item['discount_amount'] = 0;
336 $final[] = $item;
337 }
338 }
339
340 // For flat discount calculation.
341 $cumulative_discount = 0;
342 $coupon_applied_count = count( $coupon_applied_items );
343
344 foreach ( $coupon_applied_items as $index => $item ) {
345 $regular_price = $item['regular_price'];
346
347 if ( 'percentage' === $discount_type ) {
348 // Limit percentage value between 0 and 100.
349 $percentage = max( 0, min( 100, (float) $discount_value ) );
350 $raw_discount = $regular_price * ( $percentage / 100 );
351 $discount = round( $raw_discount, 2 );
352
353 // Prevent discount from exceeding the item price.
354 $discount = min( $discount, $regular_price );
355
356 $discount_price = round( $regular_price - $discount, 2 );
357
358 $item['discount_amount'] = $discount;
359 $item['discount_price'] = $discount_price;
360
361 } elseif ( 'flat' === $discount_type && $total_regular_price_coupon_applied > 0 ) {
362 /**
363 * Apply a proportional fixed discount
364 * based on the total applied coupon item regular price.
365 */
366 $proportion = $regular_price / $total_regular_price_coupon_applied;
367 $discount = $discount_value * $proportion;
368
369 /**
370 * On last item, fix rounding error.
371 *
372 * Example: $100 discount spread over 3 items
373 * could result in $33.33 + $33.33 + $33.33 = $99.99, losing 1 cent.
374 */
375 if ( $index === $coupon_applied_count - 1 ) {
376 $discount = $discount_value - $cumulative_discount;
377 }
378
379 // Prevent discount from exceeding the item price.
380 $discount = min( $discount, $regular_price );
381 $discount_price = $regular_price - $discount;
382
383 $item['discount_amount'] = round( $discount, 2 );
384 $item['discount_price'] = round( $discount_price, 2 );
385 $cumulative_discount += round( $discount, 2 );
386 }
387
388 $final[] = $item;
389 }
390
391 return $final;
392 }
393
394 /**
395 * Prepare checkout item with applying coupon if required.
396 *
397 * @since 3.0.0
398 *
399 * @since 3.3.0 is_coupon_applicable check added
400 *
401 * @param int|array $item_ids Required, course ids or plan id.
402 * @param string $order_type order type.
403 * @param string $coupon_code coupon code.
404 *
405 * @return object
406 */
407 public function prepare_checkout_items( $item_ids, $order_type = OrderModel::TYPE_SINGLE_ORDER, $coupon_code = null ) {
408 $item_ids = is_array( $item_ids ) ? $item_ids : array( $item_ids );
409 $response = array();
410 $user_id = get_current_user_id();
411
412 $coupon_type = empty( $coupon_code ) ? 'automatic' : 'manual';
413 $is_coupon_applied = false;
414 $coupon_title = '';
415
416 $total_price = 0;
417 $subtotal_price = 0;
418 $coupon_discount = 0;
419 $sale_discount = 0;
420
421 $tax_exempt_price = 0;
422 $tax_exempt_amount = 0;
423
424 $coupon = null;
425 $is_coupon_applied = false;
426 $is_meet_min_requirement = false;
427 $selected_coupon = null;
428
429 if ( Settings::is_coupon_usage_enabled() && '-1' !== $coupon_code ) {
430 $selected_coupon = $this->coupon_model->get_coupon_details_for_checkout( $coupon_code );
431 if ( ! $selected_coupon ) {
432 $this->coupon_model->set_apply_coupon_error( $this->coupon_model->get_coupon_failed_error_msg( 'not_found' ) );
433 }
434 }
435
436 $is_valid = is_object( $selected_coupon ) && $this->coupon_model->is_coupon_valid( $selected_coupon );
437 if ( $is_valid ) {
438 $is_meet_min_requirement = $this->coupon_model->is_coupon_requirement_meet( $item_ids, $selected_coupon, $order_type );
439 if ( $is_meet_min_requirement ) {
440 $coupon = $selected_coupon;
441 }
442 }
443
444 list( $items, $plan_info ) = $this->prepare_items( $item_ids, $order_type, $coupon );
445
446 // Iterate with each item and check if coupon is applicable @since 3.3.0.
447 $is_coupon_applicable = false;
448 if ( $coupon ) {
449 foreach ( $items as $item ) {
450 if ( ! $is_coupon_applicable ) {
451 $is_coupon_applicable = $this->coupon_model->is_coupon_applicable( $coupon, $item['item_id'], $order_type );
452 }
453 }
454 if ( $is_coupon_applicable ) {
455 $is_coupon_applied = true;
456 }
457 }
458
459 if ( $is_coupon_applied ) {
460 $items = $this->calculate_discount( $items, $coupon->discount_type, $coupon->discount_amount );
461 $coupon_title = $coupon->coupon_title;
462 }
463
464 $should_calculate_tax = Tax::should_calculate_tax();
465 $tax_included = Tax::is_tax_included_in_price();
466 $tax_rate = Tax::get_user_tax_rate();
467
468 // Keep calculated price for each item.
469 foreach ( $items as $item ) {
470 $discount_amount = isset( $item['discount_amount'] ) ? $item['discount_amount'] : 0;
471 $has_discount_amount = $discount_amount > 0;
472 $item['discount_price'] = $has_discount_amount ? max( 0, $item['discount_price'] ) : null;
473
474 $display_price = isset( $item['sale_price'] ) ? $item['sale_price'] : $item['regular_price'];
475 $display_price = $has_discount_amount ? $item['discount_price'] : $display_price;
476 $item['display_price'] = $display_price;
477
478 $item['tax_amount'] = 0;
479 $item['tax_amount_readable'] = '';
480
481 if ( $should_calculate_tax ) {
482 $tax_amount = Tax::calculate_tax( $display_price, $tax_rate );
483 // translators: %1$s: tax amount %2$s: included text or empty string.
484 $tax_amount_readable = sprintf( __( 'Tax: %1$s%2$s', 'tutor' ), tutor_get_formatted_price( $tax_amount ), $tax_included ? __( ' included', 'tutor' ) : '' );
485
486 $item['tax_amount'] = $tax_amount;
487 $item['tax_amount_readable'] = $tax_amount_readable;
488 }
489
490 $sale_discount_amount = isset( $item['sale_price'] ) ? $item['regular_price'] - $item['sale_price'] : 0;
491 $item['sale_discount_amount'] = $sale_discount_amount;
492
493 $response['items'][] = (object) $item;
494
495 $subtotal_price += $item['regular_price'];
496 $coupon_discount += $discount_amount;
497 $sale_discount += $sale_discount_amount;
498
499 $additional_items = $item['additional_items'] ?? array();
500 foreach ( $additional_items as $additional_item ) {
501 $subtotal_price += $additional_item['regular_price'] ?? 0;
502 }
503
504 if ( isset( $item['tax_collection'] ) && false === $item['tax_collection'] ) {
505 $tax_exempt_price += $display_price;
506 $tax_exempt_price += array_sum( array_column( $additional_items, 'regular_price' ) );
507 }
508 }
509
510 $total_price = $subtotal_price - ( $coupon_discount + $sale_discount );
511 $tax_amount = 0;
512
513 if ( $should_calculate_tax ) {
514 $tax_amount = Tax::calculate_tax( $total_price, $tax_rate );
515 $tax_exempt_amount = Tax::calculate_tax( $tax_exempt_price, $tax_rate );
516 $tax_amount = $tax_amount - $tax_exempt_amount;
517 }
518
519 $total_price_without_tax = $total_price;
520 if ( ! Tax::is_tax_included_in_price() ) {
521 $total_price += $tax_amount;
522 }
523
524 // Total price should not negative.
525 $total_price = max( 0, $total_price );
526
527 $response['plan_info'] = $plan_info;
528
529 $response['total_items'] = tutor_utils()->count( $items );
530 $response['coupon_type'] = $coupon_type;
531 $response['coupon_code'] = $is_coupon_applied ? $coupon->coupon_code : null;
532 $response['coupon_title'] = $coupon_title;
533 $response['is_coupon_applied'] = $is_coupon_applied;
534
535 $response['subtotal_price'] = $subtotal_price;
536 $response['coupon_discount'] = $coupon_discount;
537 $response['sale_discount'] = $sale_discount;
538 $response['tax_rate'] = $tax_rate;
539 $response['total_price_without_tax'] = $total_price_without_tax;
540 $response['tax_exempt_amount'] = $tax_exempt_amount;
541 $response['tax_amount'] = $tax_amount;
542 $response['total_price'] = $total_price;
543 $response['order_type'] = $order_type;
544
545 $response['formatted_total_price_without_tax'] = tutor_get_formatted_price( $total_price_without_tax );
546 $response['formatted_total_price'] = tutor_get_formatted_price( $total_price );
547
548 return (object) $response;
549 }
550
551 /**
552 * Pay now ajax handler
553 * Create pending order, prepare payment data & proceed to payment gateway
554 *
555 * @since 3.0.0
556 *
557 * @return void
558 */
559 public function pay_now() {
560 $errors = array();
561 if ( ! tutor_utils()->is_nonce_verified() ) {
562 array_push( $errors, tutor_utils()->error_message( 'nonce' ) );
563 set_transient( self::PAY_NOW_ALERT_MSG_TRANSIENT_KEY . 'pay_now_nonce_alert', $errors );
564 return;
565 }
566
567 global $wpdb;
568 $order_data = null;
569 $billing_model = new BillingModel();
570 $current_user_id = get_current_user_id();
571
572 $is_guest_checkout_enabled = apply_filters( 'tutor_is_guest_checkout_enabled', false );
573
574 // Prevent invalid request.
575 if ( ! $current_user_id ) {
576 if ( $is_guest_checkout_enabled ) {
577 // Guest user.
578 $current_user_id = wp_rand(); // A random id to uniquely identify.
579 } else {
580 wp_die( esc_html( tutor_utils()->error_message( 'invalid_req' ) ) );
581 }
582 }
583
584 $request = Input::sanitize_array( $_POST ); //phpcs:ignore --sanitized.
585 $order_id = Input::get( 'order_id', 0, Input::TYPE_INT );
586
587 if ( $order_id ) {
588 $order_data = OrderModel::get_valid_incomplete_order( $order_id, get_current_user_id(), true );
589 if ( ! $order_data || OrderModel::TYPE_SINGLE_ORDER !== $order_data->order_type ) {
590 array_push( $errors, __( 'Invalid order', 'tutor' ) );
591 }
592 }
593
594 $billing_fillable_fields = array_intersect_key( $request, array_flip( $billing_model->get_fillable_fields() ) );
595
596 $order_payment_fields = array(
597 'object_ids',
598 'coupon_code',
599 'payment_method',
600 'payment_type',
601 'order_type',
602 );
603
604 $request = array_intersect_key( $request, array_flip( $order_payment_fields ) );
605 // Set required.
606 foreach ( $order_payment_fields as $field ) {
607 if ( ! isset( $request[ $field ] ) ) {
608 $request[ $field ] = '';
609 }
610 }
611
612 // Validate data.
613 $validate = $this->validate_pay_now_req( $request );
614
615 if ( ! $validate->success ) {
616 foreach ( $validate->errors as $error ) {
617 if ( is_array( $error ) ) {
618 foreach ( $error as $err ) {
619 array_push( $errors, $err );
620 }
621 } else {
622 array_push( $errors, $error );
623 }
624 }
625 }
626
627 $validate_consent = LegalConsent::validate_consent( LegalConsent::DISPLAY_ON_CHECKOUT, $_POST );
628 if ( is_wp_error( $validate_consent ) ) {
629 array_push( $errors, $validate_consent->get_error_message() );
630 }
631
632 // Return if validation failed.
633 if ( ! empty( $errors ) ) {
634 set_transient( self::PAY_NOW_ERROR_TRANSIENT_KEY . $current_user_id, $errors );
635 return;
636 }
637
638 $object_ids = array_filter( explode( ',', $request['object_ids'] ), 'is_numeric' );
639 $coupon_code = isset( $request['coupon_code'] ) ? $request['coupon_code'] : '';
640 $payment_method = $request['payment_method'];
641 $payment_type = 'free' === strtolower( $payment_method ) ? 'manual' : $request['payment_type'];
642 $order_type = $request['order_type'];
643
644 if ( empty( $object_ids ) ) {
645 array_push( $errors, __( 'Invalid cart items', 'tutor' ) );
646 } elseif ( OrderModel::TYPE_SINGLE_ORDER === $order_type ) {
647 foreach ( $object_ids as $object_id ) {
648 $can_buy = apply_filters( 'tutor_can_purchase_course', true, $object_id );
649 if ( is_wp_error( $can_buy ) ) {
650 array_push( $errors, $can_buy->get_error_message() );
651 }
652 }
653 } elseif ( OrderModel::TYPE_SUBSCRIPTION === $order_type ) {
654 $item_id = $object_ids[0] ?? 0;
655 if ( $item_id ) {
656 $plan = apply_filters( 'tutor_get_plan_info', null, $item_id );
657 if ( ! $plan ) {
658 array_push( $errors, __( 'Invalid plan', 'tutor' ) );
659 }
660 } else {
661 array_push( $errors, __( 'Invalid plan', 'tutor' ) );
662 }
663 } else {
664 array_push( $errors, __( 'Invalid order type', 'tutor' ) );
665 }
666
667 if ( ! empty( $errors ) ) {
668 set_transient( self::PAY_NOW_ERROR_TRANSIENT_KEY . $current_user_id, $errors );
669 return;
670 }
671
672 $billing_info = $billing_model->get_info( $current_user_id );
673 if ( $billing_info ) {
674 $update_billing = $billing_model->update( $billing_fillable_fields, array( 'user_id' => $current_user_id ) );
675 if ( ! $update_billing ) {
676 array_push( $errors, __( 'Billing information update failed!', 'tutor' ) );
677 }
678 } else {
679 // Save billing info.
680 $billing_fillable_fields['user_id'] = $current_user_id;
681
682 $save = $billing_model->insert( $billing_fillable_fields );
683 if ( ! $save ) {
684 array_push( $errors, __( 'Billing info save failed!', 'tutor' ) );
685 }
686 }
687
688 $checkout_data = $this->prepare_checkout_items( $object_ids, $order_type, $coupon_code );
689
690 if ( ! isset( $checkout_data->items ) || ! tutor_utils()->count( $checkout_data->items ) ) {
691 array_push( $errors, __( 'No items found for purchase', 'tutor' ) );
692 }
693
694 if ( $checkout_data->total_price > 0 && 'free' === $payment_method ) {
695 array_push( $errors, __( 'Select a payment method', 'tutor' ) );
696 }
697
698 if ( ! empty( $errors ) ) {
699 set_transient( self::PAY_NOW_ERROR_TRANSIENT_KEY . $current_user_id, $errors );
700 return;
701 }
702
703 $items = array();
704 foreach ( $checkout_data->items as $item ) {
705 $items[] = array(
706 'item_id' => $item->item_id,
707 'regular_price' => $item->regular_price,
708 'sale_price' => $item->sale_price,
709 'discount_price' => $item->discount_price,
710 'coupon_code' => $item->is_coupon_applied ? $item->coupon_code : null,
711 );
712 }
713
714 $args = apply_filters(
715 'tutor_order_create_args',
716 array(
717 'payment_method' => $payment_method,
718 'coupon_amount' => $checkout_data->coupon_discount,
719 'discount_amount' => $checkout_data->sale_discount,
720 )
721 );
722
723 if ( empty( $errors ) ) {
724 if ( ! is_user_logged_in() ) {
725 $guest_user = apply_filters( 'tutor_guest_user_id', $current_user_id, $order_data, $billing_fillable_fields );
726 if ( is_wp_error( $guest_user ) ) {
727 // Delete the billing info if user registration failed.
728 QueryHelper::delete( "{$wpdb->prefix}tutor_customers", array( 'user_id' => $current_user_id ) );
729
730 add_filter( 'tutor_checkout_user_id', fn () => $current_user_id );
731
732 // translators: wp error message.
733 $error_msg = sprintf( esc_html_x( 'Order placement failed. %s', 'guest checkout', 'tutor' ), $guest_user->get_error_message() );
734 set_transient(
735 self::PAY_NOW_ERROR_TRANSIENT_KEY . $current_user_id,
736 array(
737 'message' => $error_msg,
738 )
739 );
740 return;
741 } else {
742 $current_user_id = $guest_user;
743 }
744 }
745
746 if ( ! empty( $order_data ) ) {
747 $order_data = $this->order_ctrl->update_order(
748 $order_id,
749 $current_user_id,
750 $items,
751 OrderModel::PAYMENT_UNPAID,
752 $order_type,
753 $checkout_data->coupon_code,
754 $args,
755 true
756 );
757 } else {
758 $order_data = $this->order_ctrl->create_order(
759 $current_user_id,
760 $items,
761 OrderModel::PAYMENT_UNPAID,
762 $order_type,
763 $checkout_data->coupon_code,
764 $args,
765 false
766 );
767 }
768
769 if ( ! empty( $order_data ) ) {
770 do_action( 'tutor_after_checkout_consent', $current_user_id, $validate_consent );
771 if ( 'automate' === $payment_type ) {
772 try {
773 $payment_data = self::prepare_payment_data( $order_data );
774 $this->proceed_to_payment( $payment_data, $payment_method, $order_type );
775 } catch ( \Throwable $th ) {
776 tutor_log( $th );
777 tutor_redirect_after_payment( OrderModel::ORDER_PLACEMENT_FAILED, $order_data['id'], $th->getMessage() );
778 }
779 } else {
780 // Set alert message session.
781 $this->set_pay_now_alert_msg( $order_data );
782 tutor_redirect_after_payment( OrderModel::ORDER_PLACEMENT_SUCCESS, $order_data['id'] );
783 }
784 } else {
785 array_push( $errors, __( 'Failed to place order!', 'tutor' ) );
786 set_transient( self::PAY_NOW_ERROR_TRANSIENT_KEY . $current_user_id, $errors );
787 $this->set_pay_now_alert_msg( $order_data );
788 }
789 } else {
790 set_transient( self::PAY_NOW_ERROR_TRANSIENT_KEY . $current_user_id, $errors );
791 $this->set_pay_now_alert_msg( $order_data );
792 }
793 }
794
795 /**
796 * Prepare payment data
797 *
798 * @since 3.0.0
799 *
800 * @param array $order Order object.
801 *
802 * @return mixed
803 */
804 public static function prepare_payment_data( array $order ) {
805 $site_name = get_bloginfo( 'name' );
806 $order_id = $order['id'];
807 $order_user_id = $order['user_id'];
808 $user_data = get_userdata( $order_user_id );
809
810 $items = array();
811 $subtotal_price = $order['subtotal_price'];
812 $total_price = $order['total_price'];
813 $grand_total = $total_price;
814 $order_type = $order['order_type'];
815
816 $currency_code = tutor_utils()->get_option( OptionKeys::CURRENCY_CODE, 'USD' );
817 $currency_symbol = tutor_get_currency_symbol_by_code( $currency_code );
818 $currency_info = tutor_get_currencies_info_by_code( $currency_code );
819
820 $billing_info = ( new BillingModel() )->get_info( $order_user_id );
821
822 $country_info = tutor_get_country_info_by_name( $billing_info->billing_country );
823
824 $country = (object) array(
825 'name' => $country_info['name'],
826 'numeric_code' => $country_info['numeric_code'],
827 'alpha_2' => $country_info['alpha_2'],
828 'alpha_3' => $country_info['alpha_3'],
829 'phone_code' => $country_info['phone_code'],
830 );
831
832 $billing_name = $billing_info ? trim( $billing_info->billing_first_name . ' ' . $billing_info->billing_last_name ) : $user_data->display_name;
833
834 $shipping_and_billing = array(
835 'name' => $billing_name,
836 'address1' => $billing_info->billing_address ?? '',
837 'address2' => $billing_info->billing_address ?? '',
838 'city' => $billing_info->billing_city ?? '',
839 'state' => $billing_info->billing_state ?? '',
840 'region' => '',
841 'postal_code' => $billing_info->billing_zip_code ?? '',
842 'country' => $country,
843 'phone_number' => $billing_info->billing_phone ?? '',
844 'email' => $billing_info->billing_email ?? '',
845 );
846
847 $customer_info = $shipping_and_billing;
848
849 foreach ( $order['items'] as $item ) {
850 $item = (object) $item;
851 $item_id = $item->item_id ?? $item->id;
852
853 if ( OrderModel::TYPE_SINGLE_ORDER === $order_type ) {
854 $items[] = array(
855 'item_id' => $item_id,
856 'item_name' => get_the_title( $item_id ),
857 'regular_price' => $item->sale_price > 0 ? $item->sale_price : $item->regular_price,
858 'quantity' => 1,
859 'discounted_price' => is_null( $item->discount_price ) || '' === $item->discount_price ? null : $item->discount_price,
860 );
861 }
862
863 if ( OrderModel::TYPE_SUBSCRIPTION === $order_type ) {
864 $subscription_items = apply_filters( 'tutor_checkout_subscription_payment_items', array(), $item, $order_id );
865 foreach ( $subscription_items as $subscription_item ) {
866 $items[] = $subscription_item;
867 }
868 }
869 }
870
871 if ( isset( $order['tax_amount'] ) && ! Tax::is_tax_included_in_price() ) {
872 $grand_total += $order['tax_amount'];
873
874 /* translators: %s: tax rate */
875 $tax_item = sprintf( __( 'Tax (%s)', 'tutor' ), $order['tax_rate'] . '%' );
876 $items[] = array(
877 'item_id' => 'tax',
878 'item_name' => $tax_item,
879 'regular_price' => $order['tax_amount'],
880 'quantity' => 1,
881 'discounted_price' => null,
882 );
883 }
884
885 return (object) array(
886 'items' => (object) $items,
887 'subtotal' => floatval( $subtotal_price ),
888 'total_price' => floatval( $total_price ),
889 'order_id' => $order_id,
890 'store_name' => $site_name,
891 'order_description' => 'Tutor Order',
892 'tax' => 0,
893 'currency' => (object) array(
894 'code' => $currency_code,
895 'symbol' => $currency_symbol,
896 'name' => $currency_info['name'] ?? '',
897 'locale' => $currency_info['locale'] ?? '',
898 'numeric_code' => $currency_info['numeric_code'] ?? '',
899 ),
900 'country' => $country,
901 'shipping_charge' => 0,
902 'coupon_discount' => 0,
903 'shipping_address' => (object) $shipping_and_billing,
904 'billing_address' => (object) $shipping_and_billing,
905 'decimal_separator' => tutor_utils()->get_option( OptionKeys::DECIMAL_SEPARATOR, '.' ),
906 'thousand_separator' => tutor_utils()->get_option( OptionKeys::THOUSAND_SEPARATOR, '.' ),
907 'customer' => (object) $customer_info,
908 );
909 }
910
911 /**
912 * Prepare payment data
913 *
914 * @since 3.0.0
915 *
916 * @param int $order_id Order id.
917 *
918 * @throws \Exception Throw exception if order not found.
919 *
920 * @return mixed
921 */
922 public static function prepare_recurring_payment_data( int $order_id ) {
923 $order_data = ( new OrderModel() )->get_order_by_id( $order_id );
924 if ( ! $order_data ) {
925 throw new \Exception( __( 'Order not found!', 'tutor' ) );
926 }
927
928 $amount = $order_data->total_price;
929
930 $order_user_id = $order_data->student->id;
931 $user_data = get_userdata( $order_user_id );
932
933 $currency_code = tutor_utils()->get_option( OptionKeys::CURRENCY_CODE, 'USD' );
934 $currency_symbol = tutor_get_currency_symbol_by_code( $currency_code );
935 $currency_info = tutor_get_currencies_info_by_code( $currency_code );
936
937 $billing_info = ( new BillingModel() )->get_info( $order_user_id );
938
939 $country_info = tutor_get_country_info_by_name( $billing_info->billing_country );
940
941 $country = (object) array(
942 'name' => $country_info['name'],
943 'numeric_code' => $country_info['numeric_code'],
944 'alpha_2' => $country_info['alpha_2'],
945 'alpha_3' => $country_info['alpha_3'],
946 'phone_code' => $country_info['phone_code'],
947 );
948
949 $billing_name = $billing_info ? trim( $billing_info->billing_first_name . ' ' . $billing_info->billing_last_name ) : $user_data->display_name;
950
951 $shipping_and_billing = array(
952 'name' => $billing_name,
953 'address1' => $billing_info->billing_address ?? '',
954 'address2' => $billing_info->billing_address ?? '',
955 'city' => $billing_info->billing_city ?? '',
956 'state' => $billing_info->billing_state ?? '',
957 'region' => '',
958 'postal_code' => $billing_info->billing_zip_code ?? '',
959 'country' => $country,
960 'phone_number' => $billing_info->billing_phone ?? '',
961 'email' => $billing_info->billing_email ?? '',
962 );
963
964 $customer_info = $shipping_and_billing;
965
966 return (object) array(
967 'type' => 'recurring',
968 'previous_payload' => $order_data->payment_payloads,
969 'total_amount' => floatval( $amount ),
970 'sub_total_amount' => floatval( $amount ),
971 'currency' => (object) array(
972 'code' => $currency_code,
973 'symbol' => $currency_symbol,
974 'name' => $currency_info['name'] ?? '',
975 'locale' => $currency_info['locale'] ?? '',
976 'numeric_code' => $currency_info['numeric_code'] ?? '',
977 ),
978 'order_id' => $order_id,
979 'customer' => (object) $customer_info,
980 'shipping_address' => (object) $shipping_and_billing,
981 );
982 }
983
984 /**
985 * Proceed to payment
986 *
987 * @since 3.0.0
988 *
989 * @param mixed $payment_data Payment data for making order.
990 * @param string $payment_method Payment method name.
991 * @param string $order_type Order type.
992 *
993 * @throws \Throwable Throw throwable if error occur.
994 * @throws \Exception Throw exception if payment gateway is invalid.
995 *
996 * @return void
997 */
998 public function proceed_to_payment( $payment_data, $payment_method, $order_type ) {
999 $payment_gateways = apply_filters( 'tutor_gateways_with_class', Ecommerce::payment_gateways_with_ref(), $payment_method );
1000
1001 $payment_gateway_class = isset( $payment_gateways[ $payment_method ] )
1002 ? $payment_gateways[ $payment_method ]['gateway_class']
1003 : null;
1004
1005 if ( $payment_gateway_class ) {
1006 try {
1007
1008 add_filter(
1009 'tutor_ecommerce_webhook_url',
1010 function ( $url ) use ( $payment_method ) {
1011 $url = add_query_arg( array( 'payment_method' => $payment_method ), $url );
1012 return $url;
1013 }
1014 );
1015
1016 add_filter(
1017 'tutor_ecommerce_payment_success_url_args',
1018 function ( $args ) use ( $payment_data ) {
1019 $args['order_id'] = $payment_data->order_id;
1020 return $args;
1021 }
1022 );
1023 add_filter(
1024 'tutor_ecommerce_payment_cancelled_url_args',
1025 function ( $args ) use ( $payment_data ) {
1026 $args['order_id'] = $payment_data->order_id;
1027 return $args;
1028 }
1029 );
1030
1031 $gateway_instance = Ecommerce::get_payment_gateway_object( $payment_gateway_class );
1032 $gateway_instance->setup_payment_and_redirect( $payment_data );
1033 } catch ( \Throwable $th ) {
1034 throw $th;
1035 }
1036 } else {
1037 throw new \Exception( 'Invalid payment gateway class' );
1038 }
1039 }
1040
1041 /**
1042 * Restrict checkout page
1043 *
1044 * @return void
1045 */
1046 public function restrict_checkout_page() {
1047 $page_id = self::get_page_id();
1048 if ( ! $page_id || ! is_page( $page_id ) ) {
1049 return;
1050 }
1051
1052 $course_id = Input::get( 'course_id', 0, Input::TYPE_INT );
1053
1054 if ( $course_id ) {
1055 $can_buy = apply_filters( 'tutor_can_purchase_course', true, $course_id );
1056 $course_url = get_post_permalink( $course_id );
1057 if ( is_wp_error( $can_buy ) ) {
1058 wp_safe_redirect( tutor_utils()->get_nocache_url( $course_url ) );
1059 exit;
1060 }
1061 }
1062
1063 $cart_page_url = CartController::get_page_url();
1064
1065 if ( ! is_user_logged_in() && ! apply_filters( 'tutor_is_guest_checkout_enabled', false ) ) {
1066 wp_safe_redirect( $cart_page_url );
1067 exit;
1068 }
1069
1070 $user_id = tutils()->get_user_id();
1071 $cart_model = new CartModel();
1072 $has_cart_item = $cart_model->has_item_in_cart( $user_id );
1073 $buy_now = Settings::is_buy_now_enabled();
1074 $plan_id = Input::get( 'plan', 0, Input::TYPE_INT );
1075 $order_id = Input::get( 'order_id', 0, Input::TYPE_INT );
1076 $checkout_error = get_transient( self::PAY_NOW_ERROR_TRANSIENT_KEY . $user_id );
1077
1078 if ( ! $has_cart_item && ! $buy_now && ! $plan_id && ! $order_id && ! tutor_utils()->count( $checkout_error ) ) {
1079 wp_safe_redirect( $cart_page_url );
1080 exit;
1081 }
1082 }
1083
1084 /**
1085 * Set alert message on the session based on
1086 * order data
1087 *
1088 * @since 3.0.0
1089 *
1090 * @param mixed $order_data Order data or null. If order
1091 * data is falsy then failed message will be set.
1092 *
1093 * @return void
1094 */
1095 private function set_pay_now_alert_msg( $order_data ) {
1096 $user_id = $order_data ? $order_data['user_id'] : get_current_user_id();
1097 if ( empty( $order_data ) ) {
1098 set_transient(
1099 self::PAY_NOW_ALERT_MSG_TRANSIENT_KEY . $user_id,
1100 array(
1101 'alert' => 'danger',
1102 'message' => __( 'Failed to place order!', 'tutor' ),
1103 ),
1104 );
1105 } else {
1106 set_transient(
1107 self::PAY_NOW_ALERT_MSG_TRANSIENT_KEY . $user_id,
1108 array(
1109 'alert' => 'success',
1110 'message' => __( 'Your order has been placed successfully!', 'tutor' ),
1111 ),
1112 );
1113 }
1114 }
1115
1116 /**
1117 * Pay for the incomplete order
1118 *
1119 * Redirect to the payment gateway to complete the order
1120 * After completing the process it will redirect user to
1121 * order placement page
1122 *
1123 * @since 3.0.0
1124 *
1125 * @return void
1126 */
1127 public function pay_incomplete_order() {
1128
1129 // Authentication check.
1130 if ( ! is_user_logged_in() ) {
1131 tutor_redirect_after_payment( OrderModel::ORDER_PLACEMENT_FAILED, 0, __( 'Please log in first', 'tutor' ) );
1132 }
1133
1134 $order_id = Input::post( 'order_id', 0, Input::TYPE_INT );
1135 $payment_method = Input::post( 'payment_method', '' );
1136 $request = Input::sanitize_array( $_POST ); //phpcs:ignore -- $POST sanitized
1137
1138 $billing_model = new BillingModel();
1139 $billing_fillable_fields = array_intersect_key( $request, array_flip( $billing_model->get_fillable_fields() ) );
1140
1141 if ( ! tutor_utils()->is_nonce_verified() ) {
1142 tutor_utils()->redirect_to( tutor_utils()->tutor_dashboard_url( 'purchase_history' ), tutor_utils()->error_message( 'nonce' ), 'error' );
1143 exit;
1144 }
1145
1146 if ( $order_id ) {
1147 $order_model = new OrderModel();
1148 $order_data = $order_model->get_order_by_id( $order_id );
1149 if ( $order_data && get_current_user_id() === (int) $order_data->user_id ) {
1150 try {
1151
1152 if ( ! empty( $payment_method ) && OrderModel::PAYMENT_METHOD_MANUAL === $order_data->payment_method ) {
1153 $billing_info = $billing_model->get_info( $order_data->user_id );
1154 if ( $billing_info ) {
1155 $update_billing = $billing_model->update( $billing_fillable_fields, array( 'user_id' => $order_data->user_id ) );
1156
1157 if ( ! $update_billing ) {
1158 tutor_redirect_after_payment( OrderModel::ORDER_PLACEMENT_FAILED, $order_data->id, __( 'Billing information update failed!', 'tutor' ) );
1159 }
1160 } else {
1161 // Save billing info.
1162 $billing_fillable_fields['user_id'] = $order_data->user_id;
1163
1164 $save = $billing_model->insert( $billing_fillable_fields );
1165
1166 if ( ! $save ) {
1167 tutor_redirect_after_payment( OrderModel::ORDER_PLACEMENT_FAILED, $order_data->id, __( 'Billing info save failed!', 'tutor' ) );
1168 }
1169 }
1170
1171 $update_order_data = $order_model->get_recalculated_order_tax_data( $order_id );
1172 $update_order_data['payment_method'] = $payment_method;
1173
1174 $updated = $order_model->update_order( $order_data->id, $update_order_data );
1175
1176 if ( $updated ) {
1177 $order_data = $order_model->get_order_by_id( $order_id );
1178 }
1179 }
1180
1181 $payment_data = $this->prepare_payment_data( (array) $order_data, $payment_method ? $payment_method : $order_data->payment_method, $order_data->order_type );
1182 $this->proceed_to_payment( $payment_data, $payment_method ? $payment_method : $order_data->payment_method, $order_data->order_type );
1183 } catch ( \Throwable $th ) {
1184 tutor_log( $th );
1185 tutor_redirect_after_payment( OrderModel::ORDER_PLACEMENT_FAILED, $order_data->id, $th->getMessage() );
1186 }
1187 } else {
1188 $error_msg = __( 'Order not found!', 'tutor' );
1189 tutor_redirect_after_payment( OrderModel::ORDER_PLACEMENT_FAILED, $order_id, $error_msg );
1190 }
1191 } else {
1192 $error_msg = __( 'Invalid order ID!', 'tutor' );
1193 tutor_redirect_after_payment( OrderModel::ORDER_PLACEMENT_FAILED, $order_id, $error_msg );
1194 }
1195 }
1196
1197 /**
1198 * Validate pay now request
1199 *
1200 * @since 3.0.0
1201 *
1202 * @param array $data The data array to validate.
1203 *
1204 * @return object The validation result. It returns validation object.
1205 */
1206 protected function validate_pay_now_req( array $data ) {
1207
1208 $order_types = array(
1209 OrderModel::TYPE_SINGLE_ORDER,
1210 OrderModel::TYPE_SUBSCRIPTION,
1211 OrderModel::TYPE_RENEWAL,
1212 );
1213
1214 $order_types = implode( ',', $order_types );
1215
1216 $validation_rules = array(
1217 'object_ids' => 'required',
1218 'order_type' => "required|match_string:{$order_types}",
1219 'payment_method' => 'required',
1220 );
1221
1222 // Skip validation rules for not available fields in data.
1223 foreach ( $validation_rules as $key => $value ) {
1224 if ( ! array_key_exists( $key, $data ) ) {
1225 unset( $validation_rules[ $key ] );
1226 }
1227 }
1228
1229 return ValidationHelper::validate( $validation_rules, $data );
1230 }
1231
1232 /**
1233 * Retrieve course data for a given set of order items.
1234 *
1235 * @since 3.9.0
1236 *
1237 * @param array $order_items Array of order item objects.
1238 * @return array{
1239 * courses: array{
1240 * total_count: int,
1241 * results: \WP_Post[]
1242 * }
1243 * }
1244 */
1245 public function get_courses_data_by_order_items( $order_items ): array {
1246
1247 $results = array();
1248
1249 foreach ( $order_items as $item ) {
1250
1251 $course = get_post( $item->id );
1252
1253 if ( $course instanceof \WP_Post ) {
1254 $results[] = $course;
1255 }
1256 }
1257
1258 return array(
1259 'courses' => array(
1260 'total_count' => count( $results ),
1261 'results' => $results,
1262 ),
1263 );
1264 }
1265 }
1266