Diff: STRATO-apps/wordpress_03/app/wp-content/plugins/tutor/models/OrderModel.php

Keine Baseline-Datei – Diff nur gegen leer.
Zur Liste
1 -
1 + <?php
2 + /**
3 + * Order Model
4 + *
5 + * @package Tutor\Models
6 + * @author Themeum <support@themeum.com>
7 + * @link https://themeum.com
8 + * @since 3.0.0
9 + */
10 +
11 + namespace Tutor\Models;
12 +
13 + use Exception;
14 + use TUTOR\Earnings;
15 + use Tutor\Ecommerce\Tax;
16 + use Tutor\Ecommerce\Ecommerce;
17 + use Tutor\Helpers\QueryHelper;
18 + use Tutor\Helpers\DateTimeHelper;
19 + use Tutor\Ecommerce\BillingController;
20 + use Tutor\Ecommerce\CheckoutController;
21 + use Tutor\Ecommerce\OrderActivitiesController;
22 +
23 + /**
24 + * OrderModel Class
25 + *
26 + * @since 3.0.0
27 + */
28 + class OrderModel {
29 +
30 + /**
31 + * Order status
32 + *
33 + * @since 3.0.0
34 + *
35 + * @var string
36 + */
37 + const ORDER_INCOMPLETE = 'incomplete';
38 + const ORDER_COMPLETED = 'completed';
39 + const ORDER_CANCELLED = 'cancelled';
40 + const ORDER_TRASH = 'trash';
41 +
42 + /**
43 + * Payment status
44 + *
45 + * @since 3.0.0
46 + *
47 + * @var string
48 + */
49 + const PAYMENT_PAID = 'paid';
50 + const PAYMENT_FAILED = 'failed';
51 + const PAYMENT_UNPAID = 'unpaid';
52 + const PAYMENT_REFUNDED = 'refunded';
53 + const PAYMENT_PARTIALLY_REFUNDED = 'partially-refunded';
54 +
55 + /**
56 + * Payment methods
57 + *
58 + * @since 3.5.0
59 + *
60 + * @var string
61 + */
62 + const PAYMENT_METHOD_MANUAL = 'manual';
63 + const PAYMENT_METHOD_FREE = 'free';
64 +
65 + /**
66 + * Order Meta keys for history & refunds
67 + *
68 + * @since 3.0.0
69 + *
70 + * @var string
71 + */
72 + const META_KEY_HISTORY = 'history';
73 + const META_KEY_REFUND = 'refund';
74 + const META_KEY_ORDER_ID = 'tutor_order_id_';
75 + const META_KEY_BILLING_ADDRESS = 'billing_address';
76 +
77 + /**
78 + * Order meta for subscription order.
79 + *
80 + * @since 3.4.0
81 + *
82 + * @var string
83 + */
84 + const META_ENROLLMENT_FEE = 'plan_enrollment_fee';
85 + const META_TRIAL_FEE = 'plan_trial_fee';
86 + const META_PLAN_INFO = 'plan_info';
87 + const META_IS_PLAN_TRIAL_ORDER = 'is_plan_trial_order';
88 + const META_IS_RESUBSCRIPTION_ORDER = 'is_resubscription_order';
89 +
90 + /**
91 + * Tax type constants
92 + *
93 + * @since 3.0.0
94 + *
95 + * @var string
96 + */
97 + const TAX_TYPE_EXCLUSIVE = 'exclusive';
98 + const TAX_TYPE_INCLUSIVE = 'inclusive';
99 +
100 +
101 + /**
102 + * Order type
103 + *
104 + * @since 3.0.0
105 + *
106 + * @var string
107 + */
108 + const TYPE_SINGLE_ORDER = 'single_order';
109 + const TYPE_SUBSCRIPTION = 'subscription';
110 + const TYPE_RENEWAL = 'renewal';
111 +
112 +
113 + /**
114 + * Transient constants
115 + *
116 + * @since 3.0.0
117 + */
118 + const TRANSIENT_ORDER_BADGE_COUNT = 'tutor_order_badge_count';
119 +
120 + /**
121 + * Order placement success
122 + *
123 + * @since 3.0.0
124 + */
125 + const ORDER_PLACEMENT_SUCCESS = 'success';
126 +
127 + /**
128 + * Order placement failed
129 + *
130 + * @since 3.0.0
131 + */
132 + const ORDER_PLACEMENT_FAILED = 'failed';
133 +
134 + /**
135 + * Order table name
136 + *
137 + * @since 3.0.0
138 + *
139 + * @var string
140 + */
141 + private $table_name = 'tutor_orders';
142 +
143 + /**
144 + * Order item table name
145 + *
146 + * @since 3.0.0
147 + *
148 + * @var string
149 + */
150 + private $order_item_table = 'tutor_order_items';
151 +
152 + /**
153 + * Order item fillable fields
154 + *
155 + * @since 3.0.0
156 + *
157 + * @var array
158 + */
159 + private $order_items_fillable_fields = array(
160 + 'order_id',
161 + 'item_id',
162 + 'regular_price',
163 + 'sale_price',
164 + 'discount_price',
165 + 'coupon_code',
166 + );
167 +
168 + /**
169 + * Resolve props & dependencies
170 + *
171 + * @since 3.0.0
172 + */
173 + public function __construct() {
174 + global $wpdb;
175 + $this->table_name = $wpdb->prefix . $this->table_name;
176 + $this->order_item_table = $wpdb->prefix . $this->order_item_table;
177 + }
178 +
179 + /**
180 + * Get table name with wp prefix
181 + *
182 + * @since 3.0.0
183 + *
184 + * @return string
185 + */
186 + public function get_table_name() {
187 + return $this->table_name;
188 + }
189 +
190 + /**
191 + * Get a order record.
192 + *
193 + * @since 3.6.0
194 + *
195 + * @param array $where where clause.
196 + *
197 + * @return mixed
198 + */
199 + public function get_row( $where = array() ) {
200 + return QueryHelper::get_row( $this->table_name, $where, 'id' );
201 + }
202 +
203 + /**
204 + * Get recalculated order tax data.
205 + *
206 + * @since 3.4.0
207 + * @since 3.8.0 tax re-calculation based on pre_tax_price column value.
208 + *
209 + * @param int|object $order the order id or object.
210 + *
211 + * @return array
212 + */
213 + public function get_recalculated_order_tax_data( $order ) {
214 + $tax_type = Tax::get_tax_type();
215 + $tax_rate = Tax::get_user_tax_rate( $order->user_id );
216 + $order = self::get_order( $order );
217 + $order_data = array(
218 + 'tax_type' => null,
219 + 'tax_rate' => null,
220 + 'tax_amount' => null,
221 + );
222 +
223 + if ( ! Tax::should_calculate_tax()
224 + || ! $tax_rate
225 + || ! $order
226 + || ! $order->pre_tax_price ) {
227 + return $order_data;
228 + }
229 +
230 + $order_data['tax_type'] = $tax_type;
231 + $order_data['tax_rate'] = $tax_rate;
232 +
233 + if ( ! Tax::is_tax_included_in_price() ) {
234 + // For exclusive tax type.
235 + $tax_amount = Tax::calculate_tax( $order->pre_tax_price, $tax_rate );
236 + $total_price = $order->pre_tax_price + $tax_amount;
237 +
238 + $order_data['tax_amount'] = $tax_amount;
239 + $order_data['total_price'] = $total_price;
240 + $order_data['net_payment'] = $total_price;
241 + } else {
242 + // For inclusive tax type.
243 + $tax_amount = Tax::calculate_tax( $order->total_price, $tax_rate );
244 +
245 + $order_data['tax_amount'] = $tax_amount;
246 + $order_data['pre_tax_price'] = $order->total_price - $tax_amount;
247 + }
248 +
249 + return $order_data;
250 + }
251 +
252 + /**
253 + * Get order item display price.
254 + *
255 + * @since 3.5.0
256 + *
257 + * @param object $item order item object.
258 + *
259 + * @return string
260 + */
261 + public function get_order_item_display_price( $item ) {
262 + $display_price = is_numeric( $item->sale_price )
263 + ? $item->sale_price
264 + : ( is_numeric( $item->discount_price ) ? $item->discount_price : $item->regular_price );
265 + return $display_price;
266 + }
267 +
268 + /**
269 + * Check order item has sale price or discount price
270 + *
271 + * @since 3.5.0
272 + *
273 + * @param object $item order item object.
274 + *
275 + * @return boolean
276 + */
277 + public function has_order_item_sale_price( $item ) {
278 + return is_numeric( $item->sale_price ) || is_numeric( $item->discount_price );
279 + }
280 +
281 + /**
282 + * Get all order statuses
283 + *
284 + * @since 3.0.0
285 + *
286 + * @return array
287 + */
288 + public static function get_order_status() {
289 + return array(
290 + self::ORDER_INCOMPLETE => __( 'Incomplete', 'tutor' ),
291 + self::ORDER_COMPLETED => __( 'Completed', 'tutor' ),
292 + self::ORDER_CANCELLED => __( 'Cancelled', 'tutor' ),
293 + self::ORDER_TRASH => __( 'Trash', 'tutor' ),
294 + );
295 + }
296 +
297 + /**
298 + * Get all order types
299 + *
300 + * @since 3.7.0
301 + *
302 + * @return array
303 + */
304 + public static function get_order_type_list() {
305 + return array(
306 + self::TYPE_SINGLE_ORDER => __( 'Single Order', 'tutor' ),
307 + self::TYPE_SUBSCRIPTION => __( 'Subscription', 'tutor' ),
308 + self::TYPE_RENEWAL => __( 'Renewal', 'tutor' ),
309 + );
310 + }
311 +
312 + /**
313 + * Get all payment statuses
314 + *
315 + * @since 3.0.0
316 + *
317 + * @return array
318 + */
319 + public static function get_payment_status() {
320 + return array(
321 + self::PAYMENT_PAID => __( 'Paid', 'tutor' ),
322 + self::PAYMENT_UNPAID => __( 'Unpaid', 'tutor' ),
323 + self::PAYMENT_FAILED => __( 'Failed', 'tutor' ),
324 + self::PAYMENT_REFUNDED => __( 'Refunded', 'tutor' ),
325 + self::PAYMENT_PARTIALLY_REFUNDED => __( 'Partially Refunded', 'tutor' ),
326 + );
327 + }
328 +
329 + /**
330 + * Get order items fillable fields
331 + *
332 + * @since 3.0.0
333 + *
334 + * @return array
335 + */
336 + public function get_order_items_fillable_fields() {
337 + return $this->order_items_fillable_fields;
338 + }
339 +
340 + /**
341 + * Get searchable fields
342 + *
343 + * This method is intendant to use with get order list
344 + *
345 + * @since 3.0.0
346 + *
347 + * @return array
348 + */
349 + private function get_searchable_fields() {
350 + return array(
351 + 'o.id',
352 + 'o.transaction_id',
353 + 'o.coupon_code',
354 + 'o.payment_method',
355 + 'o.order_status',
356 + 'o.payment_status',
357 + 'u.display_name',
358 + 'u.user_login',
359 + 'u.user_email',
360 + );
361 + }
362 +
363 + /**
364 + * Create order
365 + *
366 + * Note: validate data before using this method
367 + *
368 + * This method will also insert items if
369 + * item is set.
370 + *
371 + * Ex: data['order_items] = [
372 + * user_id => 1,
373 + * course_id => 1,
374 + * regular_price => 100,
375 + * sale_price => 90
376 + * ]
377 + *
378 + * @since 3.0.0
379 + *
380 + * @param array $data Order data based on db table.
381 + *
382 + * @throws \Exception Database error if occur.
383 + *
384 + * @return int Order id on success
385 + */
386 + public function create_order( array $data ) {
387 + $order_items = $data['items'] ?? null;
388 + unset( $data['items'] );
389 +
390 + global $wpdb;
391 +
392 + // Start transaction.
393 + $wpdb->query( 'START TRANSACTION' );
394 +
395 + try {
396 + $order_id = QueryHelper::insert( $this->table_name, $data );
397 + if ( $order_id ) {
398 + if ( $order_items ) {
399 + $insert = $this->insert_order_items( $order_id, $order_items );
400 + if ( $insert ) {
401 + $wpdb->query( 'COMMIT' );
402 + return $order_id;
403 + } else {
404 + $wpdb->query( 'ROLLBACK' );
405 + throw new \Exception( __( 'Failed to insert order items', 'tutor' ) );
406 + }
407 + } else {
408 + $wpdb->query( 'COMMIT' );
409 + return $order_id;
410 + }
411 + }
412 + } catch ( \Throwable $th ) {
413 + throw new \Exception( $th->getMessage() );
414 + }
415 + }
416 +
417 + /**
418 + * Insert order items
419 + *
420 + * Note: validate data before using this method
421 + *
422 + * @since 3.0.0
423 + *
424 + * @param int $order_id Order ID.
425 + * @param array $items Order items.
426 + *
427 + * @throws Exception Database error if occur.
428 + *
429 + * @return bool
430 + */
431 + public function insert_order_items( int $order_id, array $items ): bool {
432 + // Check if item is multi dimensional.
433 + if ( ! isset( $items[0] ) ) {
434 + $items = array( $items );
435 + }
436 +
437 + // Set order id on each item.
438 + foreach ( $items as $item ) {
439 + $item['order_id'] = $order_id;
440 + $meta_data = $item['meta_data'] ?? null;
441 + try {
442 + unset( $item['meta_data'] );
443 + $insert = QueryHelper::insert(
444 + $this->order_item_table,
445 + $item,
446 + );
447 + if ( $insert ) {
448 + if ( ! empty( $meta_data ) ) {
449 + foreach ( $meta_data as $meta ) {
450 + ( new OrderItemMetaModel() )->add_meta( $insert, $meta['meta_key'], maybe_serialize( $meta['meta_value'] ) );
451 + }
452 + }
453 + }
454 + } catch ( \Throwable $th ) {
455 + return false;
456 + }
457 + }
458 +
459 + return true;
460 + }
461 +
462 + /**
463 + * Retrieve order details by order ID.
464 + *
465 + * This function fetches order information from the database based on the given
466 + * order ID. It queries the 'tutor_orders' table for the order data, retrieves
467 + * the corresponding user information and metadata, and constructs a detailed
468 + * student object with placeholder values for billing address and phone.
469 + *
470 + * The function then assigns this student object to the order data, removes
471 + * the user ID from the order data, and returns the modified order data.
472 + *
473 + * @since 3.0.0
474 + *
475 + * @global wpdb $wpdb WordPress database abstraction object.
476 + *
477 + * @param int $order_id The ID of the order to retrieve.
478 + *
479 + * @return object|false The order data with the student's information included, or false if no order is found.
480 + */
481 + public function get_order_by_id( $order_id ) {
482 + $order_data = QueryHelper::get_row(
483 + $this->table_name,
484 + array( 'id' => $order_id ),
485 + 'id'
486 + );
487 +
488 + if ( ! $order_data ) {
489 + return false;
490 + }
491 +
492 + $user_info = get_userdata( $order_data->user_id );
493 +
494 + $student = new \stdClass();
495 + $student->id = (int) $user_info->ID ?? 0;
496 + $student->name = $user_info->data->display_name ?? '';
497 + $student->email = $user_info->data->user_email ?? '';
498 + $student->phone = get_user_meta( $order_data->user_id, 'phone_number', true );
499 + $student->billing_address = $this->get_order_billing_address( $order_id, $order_data->user_id );
500 + $student->image = get_avatar_url( $order_data->user_id );
501 +
502 + $order_data->student = $student;
503 + $order_data->items = $this->get_order_items_by_id( $order_id );
504 +
505 + $order_data->subtotal_price = (float) $order_data->subtotal_price;
506 + $order_data->total_price = (float) $order_data->total_price;
507 + $order_data->net_payment = (float) $order_data->net_payment;
508 + $order_data->discount_amount = (float) $order_data->discount_amount;
509 + $order_data->coupon_amount = (float) $order_data->coupon_amount;
510 + $order_data->tax_rate = (float) $order_data->tax_rate;
511 + $order_data->tax_amount = (float) $order_data->tax_amount;
512 +
513 + $order_data->payment_method_readable = Ecommerce::get_payment_method_label( $order_data->payment_method );
514 + $order_data->created_at_readable = DateTimeHelper::get_gmt_to_user_timezone_date( $order_data->created_at_gmt );
515 + $order_data->updated_at_readable = empty( $order_data->updated_at_gmt ) ? '' : DateTimeHelper::get_gmt_to_user_timezone_date( $order_data->updated_at_gmt );
516 +
517 + $order_data->created_by = get_userdata( $order_data->created_by )->display_name ?? '';
518 + $order_data->updated_by = get_userdata( $order_data->updated_by )->display_name ?? '';
519 +
520 + $order_activities_model = new OrderActivitiesModel();
521 + $order_data->activities = $order_activities_model->get_order_activities( $order_id );
522 + $order_data->refunds = $this->get_order_refunds( $order_id );
523 +
524 + unset( $student->billing_address->id );
525 + unset( $student->billing_address->user_id );
526 +
527 + return apply_filters( 'tutor_order_details', $order_data );
528 + }
529 +
530 + /**
531 + * Get order data
532 + *
533 + * @since 3.1.0
534 + *
535 + * @param int|object $order order id or object.
536 + *
537 + * @return object
538 + */
539 + public static function get_order( $order ) {
540 + if ( is_numeric( $order ) ) {
541 + $order = ( new self() )->get_order_by_id( $order );
542 + }
543 +
544 + return $order;
545 + }
546 +
547 + /**
548 + * Check order is subscription order
549 + *
550 + * @since 3.1.0
551 + *
552 + * @param int|object $order order id or object.
553 + *
554 + * @return boolean
555 + */
556 + public static function is_subscription_order( $order ) {
557 + $order = self::get_order( $order );
558 + return $order && self::TYPE_SUBSCRIPTION === $order->order_type;
559 + }
560 +
561 + /**
562 + * Check order is single order
563 + *
564 + * @since 3.2.0
565 + *
566 + * @param int|object $order order id or object.
567 + *
568 + * @return boolean
569 + */
570 + public static function is_single_order( $order ) {
571 + $order = self::get_order( $order );
572 + return $order && self::TYPE_SINGLE_ORDER === $order->order_type;
573 + }
574 +
575 + /**
576 + * Mark order Unpaid to Paid.
577 + *
578 + * @since 3.0.0
579 + *
580 + * @param int $order_id order id.
581 + * @param string $note note.
582 + * @param bool $trigger_hooks trigger hooks or not.
583 + *
584 + * @return bool
585 + */
586 + public function mark_as_paid( $order_id, $note = '', $trigger_hooks = true ) {
587 + if ( $trigger_hooks ) {
588 + do_action( 'tutor_before_order_mark_as_paid', $order_id );
589 + }
590 +
591 + $data = array(
592 + 'payment_status' => self::PAYMENT_PAID,
593 + 'order_status' => self::ORDER_COMPLETED,
594 + 'note' => $note,
595 + );
596 +
597 + $response = $this->update_order( $order_id, $data );
598 + if ( ! $response ) {
599 + return false;
600 + }
601 +
602 + if ( $trigger_hooks ) {
603 + do_action( 'tutor_order_payment_status_changed', $order_id, self::PAYMENT_UNPAID, self::PAYMENT_PAID );
604 +
605 + $order = $this->get_order_by_id( $order_id );
606 + $discount_amount = $this->calculate_discount_amount( $order->discount_type, $order->discount_amount, $order->subtotal_price );
607 + do_action( 'tutor_after_order_mark_as_paid', $order, $discount_amount );
608 + }
609 +
610 + return true;
611 + }
612 +
613 +
614 + /**
615 + * Retrieve order items by order ID.
616 + *
617 + * This function fetches order item details from the database based on the given
618 + * order ID. It queries the 'tutor_order_items' table and joins it with the 'posts'
619 + * table to get the course titles associated with each order item.
620 + *
621 + * The function then returns the retrieved order items, or an empty array if no
622 + * items are found.
623 + *
624 + * @since 3.0.0
625 + *
626 + * @global wpdb $wpdb WordPress database abstraction object.
627 + *
628 + * @param int $order_id The ID of the order to retrieve items for.
629 + *
630 + * @return array The order items, each containing details and course titles, or an empty array if no items are found.
631 + */
632 + public function get_order_items_by_id( $order_id ) {
633 + global $wpdb;
634 +
635 + $primary_table = "{$wpdb->prefix}tutor_order_items AS oi";
636 + $joining_tables = array(
637 + array(
638 + 'type' => 'LEFT',
639 + 'table' => "{$wpdb->prefix}posts AS p",
640 + 'on' => 'p.ID = oi.item_id',
641 + ),
642 + );
643 +
644 + $where = array( 'order_id' => $order_id );
645 +
646 + $select_columns = array( 'oi.id AS primary_id', 'oi.item_id AS id', 'oi.regular_price', 'oi.sale_price', 'oi.discount_price', 'oi.coupon_code', 'p.post_title AS title', 'p.post_type AS type' );
647 +
648 + $courses_data = QueryHelper::get_joined_data( $primary_table, $joining_tables, $select_columns, $where, array(), 'id', 0, 0 );
649 + $courses = $courses_data['results'];
650 +
651 + if ( tutor()->has_pro ) {
652 + $bundle_model = new \TutorPro\CourseBundle\Models\BundleModel();
653 + }
654 +
655 + if ( ! empty( $courses_data['total_count'] ) ) {
656 + foreach ( $courses as &$course ) {
657 + if ( tutor()->has_pro && 'course-bundle' === $course->type ) {
658 + $course->total_courses = count( $bundle_model->get_bundle_course_ids( $course->id ) );
659 + }
660 +
661 + $course->id = (int) $course->id;
662 + $course->regular_price = (float) $course->regular_price;
663 + $course->image = get_the_post_thumbnail_url( $course->id );
664 +
665 + // Add meta items.
666 + $order_item_meta = new OrderItemMetaModel();
667 + $course->item_meta_list = apply_filters( 'tutor_order_item_meta', $order_item_meta->get_meta( $course->primary_id, null, false ) );
668 + }
669 + }
670 +
671 + unset( $course );
672 +
673 + return ! empty( $courses ) ? $courses : array();
674 + }
675 +
676 + /**
677 + * Get order billing address with fallback customer billing address record support.
678 + * It'll return order billing address if found, otherwise it'll return customer billing address record.
679 + *
680 + * @since 3.5.0
681 + *
682 + * @param int $order_id order id.
683 + * @param int $user_id order id.
684 + *
685 + * @return object
686 + */
687 + public static function get_order_billing_address( $order_id, $user_id ) {
688 + $billing_address = OrderMetaModel::get_meta_value( $order_id, self::META_KEY_BILLING_ADDRESS, true );
689 +
690 + /**
691 + * Fallback data from customer billing record.
692 + */
693 + if ( false === $billing_address ) {
694 + $billing_address = ( new BillingController( false ) )->get_billing_info( $user_id );
695 + } else {
696 + $billing_address = json_decode( $billing_address );
697 + }
698 +
699 + $data = (object) array(
700 + 'first_name' => $billing_address->billing_first_name ?? '',
701 + 'last_name' => $billing_address->billing_last_name ?? '',
702 + 'full_name' => trim( ( $billing_address->billing_first_name ?? '' ) . ' ' . ( $billing_address->billing_last_name ?? '' ) ),
703 + 'email' => $billing_address->billing_email ?? '',
704 + 'phone' => $billing_address->billing_phone ?? '',
705 + 'address' => $billing_address->billing_address ?? '',
706 + 'country' => $billing_address->billing_country ?? '',
707 + 'state' => $billing_address->billing_state ?? '',
708 + 'city' => $billing_address->billing_city ?? '',
709 + 'zip_code' => $billing_address->billing_zip_code ?? '',
710 + );
711 +
712 + return $data;
713 + }
714 +
715 + /**
716 + * Retrieve order refunds by order ID.
717 + *
718 + * This function fetches all order refunds from the 'tutor_ordermeta' table
719 + * based on the given order ID and the 'refund' meta key. It uses a helper
720 + * function from the QueryHelper class to perform the database query.
721 + *
722 + * If no order refunds are found, the function returns an empty array.
723 + * Otherwise, it decodes the JSON-encoded meta values and returns them as an array.
724 + *
725 + * @global wpdb $wpdb WordPress database abstraction object.
726 + *
727 + * @param int $order_id The ID of the order to retrieve refunds for.
728 + *
729 + * @since 3.0.0
730 + *
731 + * @return array An array of order refunds, each decoded from its JSON representation.
732 + */
733 + public function get_order_refunds( $order_id ) {
734 + global $wpdb;
735 +
736 + $meta_keys = array(
737 + OrderActivitiesModel::META_KEY_REFUND,
738 + OrderActivitiesModel::META_KEY_PARTIALLY_REFUND,
739 + );
740 +
741 + // Retrieve order refunds for the given order ID from the 'tutor_ordermeta' table.
742 + $order_refunds = QueryHelper::get_all(
743 + "{$wpdb->prefix}tutor_ordermeta",
744 + array(
745 + 'order_id' => $order_id,
746 + 'meta_key' => $meta_keys,
747 + ),
748 + 'created_at_gmt',
749 + 1000,
750 + 'ASC'
751 + );
752 +
753 + if ( empty( $order_refunds ) ) {
754 + return array();
755 + }
756 +
757 + $response = array();
758 +
759 + foreach ( $order_refunds as $refund ) {
760 + $parsed_meta_value = json_decode( $refund->meta_value );
761 + $values = new \stdClass();
762 + $values->id = (int) $refund->id;
763 +
764 + foreach ( $parsed_meta_value as $key => $value ) {
765 + $values->$key = $value;
766 + }
767 +
768 + $values->date = $refund->created_at_gmt;
769 +
770 + $response[] = $values;
771 + }
772 +
773 + // Custom comparison function for sorting by date.
774 + usort(
775 + $response,
776 + function ( $a, $b ) {
777 + $date_a = strtotime( $a->date );
778 + $date_b = strtotime( $b->date );
779 +
780 + return $date_b - $date_a;
781 + }
782 + );
783 +
784 + return $response;
785 + }
786 +
787 + /**
788 + * Update an order
789 + *
790 + * @since 3.0.0
791 + *
792 + * @param int|array $order_id Integer or array of ids sql escaped.
793 + * @param array $data Data to update, escape data.
794 + * @param array $order_items order items (optional).
795 + *
796 + * @return bool
797 + */
798 + public function update_order( $order_id, array $data, array $order_items = array() ) {
799 + $order_id = is_array( $order_id ) ? $order_id : array( $order_id );
800 + $order_id = QueryHelper::prepare_in_clause( $order_id );
801 +
802 + try {
803 + QueryHelper::update_where_in(
804 + $this->table_name,
805 + $data,
806 + $order_id
807 + );
808 +
809 + if ( ! empty( $order_items ) ) {
810 + $this->update_order_items( $order_id, $order_items );
811 + }
812 + return true;
813 + } catch ( \Throwable $th ) {
814 + error_log( $th->getMessage() . ' in ' . $th->getFile() . ' at line ' . $th->getLine() );
815 + return false;
816 + }
817 + }
818 +
819 + /**
820 + * Get enrollment ids by order id.
821 + *
822 + * @since 3.0.0
823 + *
824 + * @param int $order_id order id.
825 + *
826 + * @return array
827 + */
828 + public function get_enrollment_ids( $order_id ) {
829 + global $wpdb;
830 + $enrollment_ids = array();
831 +
832 + $enrollments = $wpdb->get_results(
833 + $wpdb->prepare(
834 + "SELECT * FROM {$wpdb->postmeta}
835 + WHERE meta_key=%s
836 + AND meta_value LIKE %d",
837 + '_tutor_enrolled_by_order_id',
838 + $order_id
839 + )
840 + );
841 +
842 + if ( $enrollments ) {
843 + $enrollment_ids = array_column( $enrollments, 'post_id' );
844 + }
845 +
846 + return $enrollment_ids;
847 + }
848 +
849 + /**
850 + * Delete an order by order ID.
851 + *
852 + * This function deletes an order from the 'tutor_orders' table based on the given
853 + * order ID. It uses the QueryHelper class to perform the database delete operation.
854 + *
855 + * @since 3.0.0
856 + *
857 + * @param int|array $order_id The ID of the order to delete.
858 + *
859 + * @return bool
860 + */
861 + public function delete_order( $order_id ) {
862 + global $wpdb;
863 + $order_ids = is_array( $order_id ) ? $order_id : array( intval( $order_id ) );
864 +
865 + try {
866 + $wpdb->query( 'START TRANSACTION' );
867 +
868 + foreach ( $order_ids as $order_id ) {
869 + // Delete enrollments if exist.
870 + $enrollment_ids = $this->get_enrollment_ids( $order_id );
871 + if ( $enrollment_ids ) {
872 + QueryHelper::bulk_delete_by_ids( $wpdb->posts, $enrollment_ids );
873 + // After enrollment delete, delete the course progress.
874 + foreach ( $enrollment_ids as $enrollment_id ) {
875 + $course_id = get_post_field( 'post_parent', $enrollment_id );
876 + $student_id = get_post_field( 'post_author', $enrollment_id );
877 +
878 + if ( $course_id && $student_id ) {
879 + tutor_utils()->delete_course_progress( $course_id, $student_id );
880 + }
881 + }
882 + }
883 +
884 + // Delete earnings.
885 + QueryHelper::delete(
886 + $wpdb->prefix . 'tutor_earnings',
887 + array(
888 + 'order_id' => $order_id,
889 + 'process_by' => Earnings::PROCESS_BY_TUTOR,
890 + )
891 + );
892 +
893 + // Now delete order.
894 + QueryHelper::delete( $this->table_name, array( 'id' => $order_id ) );
895 + }
896 +
897 + $wpdb->query( 'COMMIT' );
898 + return true;
899 +
900 + } catch ( \Throwable $th ) {
901 + $wpdb->query( 'ROLLBACK' );
902 + return false;
903 + }
904 + }
905 +
906 + /**
907 + * Get orders list
908 + *
909 + * @since 3.0.0
910 + *
911 + * @param array $where where clause conditions.
912 + * @param string $search_term search clause conditions.
913 + * @param int $limit limit default 10.
914 + * @param int $offset default 0.
915 + * @param string $order_by column default 'o.id'.
916 + * @param string $order list order default 'desc'.
917 + *
918 + * @return array
919 + */
920 + public function get_orders( array $where = array(), $search_term = '', int $limit = 10, int $offset = 0, string $order_by = 'o.id', string $order = 'desc' ) {
921 +
922 + global $wpdb;
923 +
924 + $primary_table = "{$this->table_name} o";
925 + $joining_tables = array(
926 + array(
927 + 'type' => 'LEFT',
928 + 'table' => "{$wpdb->users} u",
929 + 'on' => 'o.user_id = u.ID',
930 + ),
931 + );
932 +
933 + $select_columns = array( 'o.*', 'u.user_login' );
934 +
935 + $search_clause = array();
936 + if ( '' !== $search_term ) {
937 + foreach ( $this->get_searchable_fields() as $column ) {
938 + $search_clause[ $column ] = $search_term;
939 + }
940 + }
941 +
942 + $response = array(
943 + 'results' => array(),
944 + 'total_count' => 0,
945 + );
946 +
947 + try {
948 + return QueryHelper::get_joined_data( $primary_table, $joining_tables, $select_columns, $where, $search_clause, $order_by, $limit, $offset, $order );
949 + } catch ( \Throwable $th ) {
950 + // Log with error, line & file name.
951 + error_log( $th->getMessage() . ' in ' . $th->getFile() . ' at line ' . $th->getLine() );
952 + return $response;
953 + }
954 + }
955 +
956 + /**
957 + * Get order count
958 + *
959 + * @since 3.0.0
960 + *
961 + * @param array $where Where conditions, sql esc data.
962 + * @param string $search_term Search terms, sql esc data.
963 + *
964 + * @return int
965 + */
966 + public function get_order_count( $where = array(), string $search_term = '' ) {
967 + global $wpdb;
968 +
969 + $search_clause = array();
970 + if ( '' !== $search_term ) {
971 + foreach ( $this->get_searchable_fields() as $column ) {
972 + $search_clause[ $column ] = $search_term;
973 + }
974 + }
975 +
976 + $join_table = array(
977 + array(
978 + 'type' => 'INNER',
979 + 'table' => "{$wpdb->users} u",
980 + 'on' => 'o.user_id = u.ID',
981 + ),
982 + );
983 + $primary_table = "{$this->table_name} o";
984 + return QueryHelper::get_joined_count( $primary_table, $join_table, $where, $search_clause );
985 + }
986 +
987 + /**
988 + * Get order of a user
989 + *
990 + * @since 3.0.0
991 + *
992 + * @param string $time_period $time_period Sorting time period,
993 + * supported time periods are: today, monthly & yearly.
994 + * @param string $start_date $start_date For date range sorting.
995 + * @param string $end_date $end_date For date range sorting.
996 + * @param int $user_id User id for fetching order list.
997 + * @param int $limit Limit to fetch record.
998 + * @param int $offset Offset to fetch record.
999 + *
1000 + * @throws \Exception Throw exception if database error occur.
1001 + *
1002 + * @return array
1003 + */
1004 + public function get_user_orders( $time_period = null, $start_date = null, $end_date = null, int $user_id = 0, $limit = 10, int $offset = 0 ) {
1005 + $user_id = $user_id ? $user_id : get_current_user_id();
1006 +
1007 + $response = array(
1008 + 'results' => array(),
1009 + 'total_count' => 0,
1010 + );
1011 +
1012 + global $wpdb;
1013 +
1014 + $time_period_clause = '';
1015 + $date_range_clause = '';
1016 +
1017 + if ( $start_date && $end_date ) {
1018 + $date_range_clause = $wpdb->prepare( 'AND DATE(created_at_gmt) BETWEEN %s AND %s', $start_date, $end_date );
1019 + } elseif ( $time_period ) {
1020 + if ( 'today' === $time_period ) {
1021 + $time_period_clause = 'AND DATE(o.created_at_gmt) = CURDATE()';
1022 + } elseif ( 'monthly' === $time_period ) {
1023 + $time_period_clause = 'AND MONTH(o.created_at_gmt) = MONTH(CURDATE()) ';
1024 + } else {
1025 + $time_period_clause = 'AND YEAR(o.created_at_gmt) = YEAR(CURDATE()) ';
1026 + }
1027 + }
1028 +
1029 + //phpcs:disable
1030 + $query = $wpdb->prepare(
1031 + "SELECT
1032 + SQL_CALC_FOUND_ROWS
1033 + o.*
1034 + FROM $this->table_name AS o
1035 + WHERE o.user_id = %d
1036 + {$time_period_clause}
1037 + {$date_range_clause}
1038 + ORDER BY o.id DESC
1039 + LIMIT %d OFFSET %d
1040 + ",
1041 + $user_id,
1042 + $limit,
1043 + $offset
1044 + );
1045 +
1046 + $results = $wpdb->get_results( $query );
1047 + //phpcs:enable
1048 +
1049 + if ( $wpdb->last_error ) {
1050 + throw new \Exception( $wpdb->last_error );
1051 + } else {
1052 + $response['results'] = $results;
1053 + $response['total_count'] = is_array( $results ) && count( $results ) ? (int) $wpdb->get_var( 'SELECT FOUND_ROWS()' ) : 0;
1054 + }
1055 +
1056 + return $response;
1057 + }
1058 +
1059 + /**
1060 + * Get total discounts by user_id (instructor), optionally can set period ( today | monthly| yearly )
1061 + *
1062 + * Optionally can set start date & end date to get enrollment list from date range
1063 + *
1064 + * If period or date range not pass then it will return all time enrollment list
1065 + *
1066 + * @since 3.0.0
1067 + *
1068 + * @param int $user_id User id, if user not have admin access
1069 + * then only this user's refund amount will fetched.
1070 + * @param string $period Time period.
1071 + * @param string $start_date Start date.
1072 + * @param string $end_date End date.
1073 + * @param int $course_id Course id.
1074 + *
1075 + * @return array
1076 + */
1077 + public function get_discounts_by_user( int $user_id, string $period = '', $start_date = '', string $end_date = '', int $course_id = 0 ): array {
1078 + $response = array(
1079 + 'discounts' => array(),
1080 + 'total_discounts' => 0,
1081 + );
1082 +
1083 + global $wpdb;
1084 +
1085 + $user_clause = '';
1086 + $date_range_clause = '';
1087 + $period_clause = '';
1088 + $course_clause = '';
1089 + $group_clause = ' GROUP BY DATE(date_format) ';
1090 + $discount_clause = 'o.coupon_amount as total';
1091 +
1092 + if ( $start_date && $end_date ) {
1093 + $date_range_clause = $wpdb->prepare(
1094 + 'AND o.created_at_gmt BETWEEN %s AND %s',
1095 + $start_date,
1096 + $end_date
1097 + );
1098 + } else {
1099 + $period_clause = QueryHelper::get_period_clause( 'o.created_at_gmt', $period );
1100 + }
1101 +
1102 + if ( 'today' !== $period ) {
1103 + $group_clause = ' GROUP BY MONTH(date_format) ';
1104 + }
1105 +
1106 + if ( $course_id ) {
1107 + $course_clause = $wpdb->prepare( 'AND i.item_id = %d', $course_id );
1108 + $discount_clause = 'i.regular_price - i.discount_price AS total';
1109 + }
1110 +
1111 + $item_table = $wpdb->prefix . 'tutor_order_items';
1112 +
1113 + if ( $course_id ) {
1114 + if ( $user_id ) {
1115 + $user_clause = $wpdb->prepare( 'AND c.post_author = %d', $user_id );
1116 + }
1117 +
1118 + //phpcs:disable
1119 + $discounts = $wpdb->get_results(
1120 + $wpdb->prepare(
1121 + "SELECT
1122 + i.item_id AS course_id,
1123 + SUM(
1124 + COALESCE(o.coupon_amount, 0) +
1125 + COALESCE(
1126 + IF(
1127 + o.discount_type = 'percentage',
1128 + COALESCE(o.subtotal_price * (o.discount_amount / 100), 0),
1129 + COALESCE(o.discount_amount, 0)
1130 + ),
1131 + 0
1132 + )
1133 + ) AS total,
1134 + o.created_at_gmt AS date_format
1135 + FROM
1136 + {$this->table_name} o
1137 + JOIN
1138 + {$item_table} i ON o.id = i.order_id
1139 + JOIN
1140 + {$wpdb->posts} c
1141 + ON c.ID = i.item_id
1142 + AND c.post_type = %s
1143 + WHERE
1144 + 1 = 1
1145 + AND i.item_id = %d
1146 + {$user_clause}
1147 + {$period_clause}
1148 + {$date_range_clause}
1149 + {$group_clause}
1150 + ",
1151 + tutor()->course_post_type,
1152 + $course_id
1153 + )
1154 + );
1155 + //phpcs:enable
1156 + } else {
1157 + if ( $user_id ) {
1158 + $user_clause = $wpdb->prepare( "AND %d = (SELECT user_id FROM {$wpdb->tutor_earnings} WHERE order_status = 'completed' LIMIT 1) ", $user_id );
1159 + }
1160 +
1161 + //phpcs:disable
1162 + $discounts = $wpdb->get_results(
1163 + $wpdb->prepare(
1164 + "SELECT
1165 + SUM(
1166 + COALESCE(o.coupon_amount, 0) +
1167 + COALESCE(
1168 + IF(
1169 + o.discount_type = 'percentage',
1170 + COALESCE(o.subtotal_price * (o.discount_amount / 100), 0),
1171 + COALESCE(o.discount_amount, 0)
1172 + ),
1173 + 0
1174 + )
1175 + ) AS total,
1176 + o.created_at_gmt AS date_format
1177 + FROM {$this->table_name} AS o
1178 + WHERE 1 = %d
1179 + AND o.order_status = 'completed'
1180 + {$user_clause}
1181 + {$period_clause}
1182 + {$date_range_clause}
1183 + {$course_clause}
1184 + {$group_clause}
1185 + HAVING total > 0
1186 + ",
1187 + 1
1188 + )
1189 + );
1190 + //phpcs:enable
1191 + }
1192 +
1193 + $total_discount = 0;
1194 + $discount_items = array();
1195 +
1196 + $response = array(
1197 + 'discounts' => array(),
1198 + 'total_discounts' => 0,
1199 + );
1200 +
1201 + if ( $discounts ) {
1202 + foreach ( $discounts as $discount ) {
1203 + $total_discount += $discount->total;
1204 + $discount_items[] = $discount;
1205 +
1206 + // Split each discount.
1207 + list( $admin_discount, $instructor_discount ) = array_values( tutor_split_amounts( $discount->total ) );
1208 +
1209 + $discount->total = is_admin() ? $admin_discount : $instructor_discount;
1210 + }
1211 +
1212 + list( $admin_total, $instructor_total ) = array_values( tutor_split_amounts( $total_discount ) );
1213 +
1214 + $response['discounts'] = $discount_items;
1215 + $response['total_discounts'] = is_admin() ? $admin_total : $instructor_total;
1216 + }
1217 +
1218 + return $response;
1219 + }
1220 +
1221 + /**
1222 + * Get total refunds by user_id (instructor), optionally can set period ( today | monthly| yearly )
1223 + *
1224 + * Optionally can set start date & end date to get enrollment list from date range
1225 + *
1226 + * If period or date range not pass then it will return all time enrollment list
1227 + *
1228 + * @since 3.0.0
1229 + *
1230 + * @param int $user_id User id, if user not have admin access
1231 + * then only this user's refund amount will fetched.
1232 + * @param string $period Time period.
1233 + * @param string $start_date Start date.
1234 + * @param string $end_date End date.
1235 + * @param int $course_id Course id.
1236 + *
1237 + * @return array
1238 + */
1239 + public function get_refunds_by_user( int $user_id, string $period = '', $start_date = '', string $end_date = '', int $course_id = 0 ): array {
1240 + $response = array(
1241 + 'refunds' => array(),
1242 + 'total_refunds' => 0,
1243 + );
1244 +
1245 + global $wpdb;
1246 +
1247 + $user_clause = '';
1248 + $date_range_clause = '';
1249 + $period_clause = '';
1250 + $course_clause = '';
1251 + $commission_clause = '';
1252 + $group_clause = ' GROUP BY DATE(o.created_at_gmt) ';
1253 +
1254 + if ( $start_date && $end_date ) {
1255 + $date_range_clause = $wpdb->prepare(
1256 + 'AND o.created_at_gmt BETWEEN %s AND %s',
1257 + $start_date,
1258 + $end_date
1259 + );
1260 + $group_clause = ' GROUP BY DATE(o.created_at_gmt) ';
1261 +
1262 + } else {
1263 + $period_clause = QueryHelper::get_period_clause( 'o.created_at_gmt', $period );
1264 + }
1265 +
1266 + if ( 'today' !== $period ) {
1267 + $group_clause = ' GROUP BY MONTH(o.created_at_gmt) ';
1268 + }
1269 +
1270 + if ( $course_id ) {
1271 + if ( $user_id ) {
1272 + $user_clause = $wpdb->prepare( 'AND c.post_author = %d', $user_id );
1273 + }
1274 + } elseif ( $user_id ) {
1275 + $user_clause = $wpdb->prepare( 'AND c.post_author = %d', $user_id );
1276 + }
1277 +
1278 + // Refund query logic remains the same.
1279 + $item_table = $wpdb->prefix . 'tutor_order_items';
1280 +
1281 + if ( $course_id ) {
1282 + //phpcs:disable
1283 + $refunds = $wpdb->get_results(
1284 + $wpdb->prepare(
1285 + "SELECT
1286 + i.item_id AS course_id,
1287 + ROUND(
1288 + SUM(
1289 + o.refund_amount *
1290 + (
1291 + CASE
1292 + WHEN i.discount_price THEN i.discount_price
1293 + WHEN i.sale_price > 0 THEN i.sale_price
1294 + ELSE i.regular_price
1295 + END / o.total_price
1296 + )
1297 + ), 2
1298 + ) AS total
1299 + FROM
1300 + {$this->table_name} o
1301 + JOIN
1302 + {$item_table} i ON o.id = i.order_id
1303 + JOIN
1304 + {$wpdb->posts} c
1305 + ON c.ID = i.item_id
1306 + AND c.post_type = %s
1307 + WHERE
1308 + o.refund_amount > 0
1309 + AND i.item_id = %d
1310 + {$user_clause}
1311 + {$period_clause}
1312 + {$date_range_clause}
1313 + {$group_clause},
1314 + i.item_id
1315 + ",
1316 + tutor()->course_post_type,
1317 + $course_id
1318 + )
1319 + );
1320 + //phpcs:enable
1321 + } else {
1322 + $earning_table = $wpdb->tutor_earnings;
1323 + if ( $user_id ) {
1324 + $user_clause = "AND {$user_id} = (SELECT user_id FROM {$earning_table} LIMIT 1)";
1325 + }
1326 +
1327 + //phpcs:disable
1328 + $refunds = $wpdb->get_results(
1329 + $wpdb->prepare(
1330 + "SELECT
1331 + COALESCE(SUM(o.refund_amount), 0) AS total,
1332 + created_at_gmt AS date_format
1333 + FROM {$this->table_name} AS o
1334 + -- LEFT JOIN {$item_table} AS i ON i.order_id = o.id
1335 + -- LEFT JOIN {$wpdb->posts} AS c ON c.id = i.item_id
1336 + WHERE 1 = %d
1337 + AND o.refund_amount > %d
1338 + {$user_clause}
1339 + {$period_clause}
1340 + {$date_range_clause}
1341 + {$group_clause},
1342 + o.id",
1343 + 1,
1344 + 0
1345 + )
1346 + );
1347 + //phpcs:enable
1348 + }
1349 +
1350 + $total_refund = 0;
1351 +
1352 + foreach ( $refunds as $refund ) {
1353 + $total_refund += $refund->total;
1354 +
1355 + // Update total amount from list.
1356 + $split_refund = (object) tutor_split_amounts( $refund->total );
1357 + $refund->total = is_admin() ? $split_refund->admin : $split_refund->instructor;
1358 + }
1359 +
1360 + $split_total_refund = (object) tutor_split_amounts( $total_refund );
1361 +
1362 + $response = array(
1363 + 'refunds' => $refunds,
1364 + 'total_refunds' => is_admin() ? $split_total_refund->admin : $split_total_refund->instructor,
1365 + );
1366 +
1367 + return $response;
1368 + }
1369 +
1370 + /**
1371 + * Update the payment status of an order.
1372 + *
1373 + * This function updates the payment status and note of an order in the database.
1374 + * It uses the QueryHelper class to perform the update operation.
1375 + *
1376 + * @since 3.0.0
1377 + *
1378 + * @param object $data An object containing the payment status, note, and order ID.
1379 + * - 'payment_status' (string): The new payment status.
1380 + * - 'note' (string): A note regarding the payment status update.
1381 + * - 'order_id' (int): The ID of the order to update.
1382 + *
1383 + * @return bool True on successful update, false on failure.
1384 + */
1385 + public function payment_status_update( object $data ) {
1386 + $response = QueryHelper::update(
1387 + $this->table_name,
1388 + array(
1389 + 'payment_status' => $data->payment_status,
1390 + 'note' => $data->note,
1391 + ),
1392 + array( 'id' => $data->order_id )
1393 + );
1394 +
1395 + if ( $response ) {
1396 + $activity_controller = new OrderActivitiesController();
1397 + $activity_controller->store_order_activity_for_marked_as_paid( $data->order_id );
1398 + }
1399 +
1400 + return $response;
1401 + }
1402 +
1403 + /**
1404 + * Add a discount to an order.
1405 + *
1406 + * This function updates the order in the database with the provided discount details.
1407 + * It updates the discount type, discount amount, and discount reason for the given order ID.
1408 + *
1409 + * @since 3.0.0
1410 + *
1411 + * @param object $data An object containing the discount details:
1412 + * - $data->order_id (int) The ID of the order.
1413 + * - $data->discount_type (string) The type of the discount.
1414 + * - $data->discount_amount(float) The amount of the discount.
1415 + * - $data->discount_reason(string) The reason for the discount.
1416 + *
1417 + * @return bool True on successful update, false on failure.
1418 + */
1419 + public function add_order_discount( object $data ) {
1420 + $response = QueryHelper::update(
1421 + $this->table_name,
1422 + array(
1423 + 'discount_type' => $data->discount_type,
1424 + 'discount_amount' => $data->discount_amount,
1425 + 'discount_reason' => $data->discount_reason,
1426 + ),
1427 + array( 'id' => $data->order_id )
1428 + );
1429 +
1430 + return $response;
1431 + }
1432 +
1433 + /**
1434 + * Updates the status of an order and logs the activity.
1435 + *
1436 + * This function updates the status of an order in the database and, if successful, logs the activity
1437 + * with a message indicating the status change. The message includes the current user's display name,
1438 + * if available.
1439 + *
1440 + * The possible order statuses include:
1441 + * - ORDER_CANCELLED
1442 + * - ORDER_COMPLETED
1443 + * - ORDER_INCOMPLETE
1444 + * - ORDER_TRASH
1445 + *
1446 + * If the update is successful, an order activity log entry is created with the current date, time,
1447 + * and status change message.
1448 + *
1449 + * @since 3.0.0
1450 + *
1451 + * @param object $data An object containing:
1452 + * - int $order_id The ID of the order to update.
1453 + * - string $order_status The new status of the order.
1454 + * - string $cancel_reason The reason for the order cancellation (optional).
1455 + *
1456 + * @return bool True on successful update, false on failure.
1457 + */
1458 + public function order_status_update( object $data ) {
1459 + $response = QueryHelper::update(
1460 + $this->table_name,
1461 + array(
1462 + 'order_status' => $data->order_status,
1463 + ),
1464 + array( 'id' => $data->order_id )
1465 + );
1466 +
1467 + if ( $response ) {
1468 + $user_name = '';
1469 + $current_user = wp_get_current_user();
1470 +
1471 + if ( $current_user->exists() ) {
1472 + $user_name = $current_user->display_name;
1473 + }
1474 +
1475 + $message = '';
1476 +
1477 + if ( self::ORDER_CANCELLED === $data->order_status ) {
1478 + /* translators: %s: username */
1479 + $message = empty( $user_name ) ? __( 'Order marked as cancelled', 'tutor' ) : sprintf( __( 'Order marked as cancelled by %s', 'tutor' ), $user_name );
1480 + } elseif ( self::ORDER_COMPLETED === $data->order_status ) {
1481 + /* translators: %s: username */
1482 + $message = empty( $user_name ) ? __( 'Order marked as completed', 'tutor' ) : sprintf( __( 'Order marked as completed by %s', 'tutor' ), $user_name );
1483 + } elseif ( self::ORDER_INCOMPLETE === $data->order_status ) {
1484 + /* translators: %s: username */
1485 + $message = empty( $user_name ) ? __( 'Order marked as incomplete', 'tutor' ) : sprintf( __( 'Order marked as incomplete by %s', 'tutor' ), $user_name );
1486 + } elseif ( self::ORDER_TRASH === $data->order_status ) {
1487 + /* translators: %s: username */
1488 + $message = empty( $user_name ) ? __( 'Order marked as trash', 'tutor' ) : sprintf( __( 'Order marked as trash by %s', 'tutor' ), $user_name );
1489 + }
1490 +
1491 + // insert cancel reason in tutor_ordermeta table.
1492 + if ( self::ORDER_CANCELLED === $data->order_status && ! empty( $data->cancel_reason ) ) {
1493 + $meta_payload = new \stdClass();
1494 + $meta_payload->order_id = $data->order_id;
1495 + $meta_payload->meta_key = OrderActivitiesModel::META_KEY_CANCEL_REASON;
1496 + $meta_payload->meta_value = $data->cancel_reason;
1497 +
1498 + $order_activities_model = new OrderActivitiesModel();
1499 + $order_activities_model->add_order_meta( $meta_payload );
1500 + }
1501 +
1502 + if ( $message ) {
1503 + $value = wp_json_encode(
1504 + array(
1505 + 'message' => $message,
1506 + )
1507 + );
1508 + OrderActivitiesController::store_order_activity( $data->order_id, OrderActivitiesModel::META_KEY_HISTORY, $value );
1509 + }
1510 + }
1511 +
1512 + return $response;
1513 + }
1514 +
1515 + /**
1516 + * Calculate discount amount.
1517 + *
1518 + * @since 3.0.0
1519 + *
1520 + * @param string $discount_type The type of discount ('percent' or 'flat').
1521 + * @param float $discount_amount The amount of discount to apply.
1522 + * @param float $sub_total The subtotal amount before applying the discount.
1523 + *
1524 + * @return float discount amount.
1525 + */
1526 + public function calculate_discount_amount( $discount_type, $discount_amount, $sub_total ) {
1527 + if ( 'percentage' === $discount_type ) {
1528 + $discounted_price = (float) $sub_total * ( ( (float) $discount_amount / 100 ) );
1529 + } else {
1530 + $discounted_price = (float) $discount_amount;
1531 + }
1532 + return $discounted_price;
1533 + }
1534 +
1535 + /**
1536 + * Retrieves the total refund amount for a given order.
1537 + *
1538 + * This method fetches all refund records for the specified order ID from the database,
1539 + * calculates the total refund amount, and returns it. The refund records are retrieved
1540 + * from the `tutor_ordermeta` table where the `meta_key` matches the refund meta keys.
1541 + *
1542 + * @since 3.0.0
1543 + *
1544 + * @param int $order_id The ID of the order for which the refund amount is to be calculated.
1545 + *
1546 + * @return float The total refund amount for the order.
1547 + */
1548 + public function get_refund_amount( $order_id ) {
1549 + global $wpdb;
1550 +
1551 + $table = $wpdb->prefix . 'tutor_ordermeta';
1552 + $meta_keys = array( OrderActivitiesModel::META_KEY_REFUND, OrderActivitiesModel::META_KEY_PARTIALLY_REFUND );
1553 +
1554 + $where = array(
1555 + 'meta_key' => $meta_keys,
1556 + 'order_id' => $order_id,
1557 + );
1558 + $refund_records = QueryHelper::get_all( $table, $where, 'created_at_gmt' );
1559 +
1560 + $refund_amount = 0;
1561 +
1562 + foreach ( $refund_records as $refund ) {
1563 + $refund_data = json_decode( $refund->meta_value );
1564 +
1565 + if ( ! empty( $refund_data->amount ) ) {
1566 + $refund_amount += (float) $refund_data->amount;
1567 + }
1568 + }
1569 +
1570 + return $refund_amount;
1571 + }
1572 +
1573 + /**
1574 + * Get order status based on the payment status
1575 + *
1576 + * @since 3.0.0
1577 + *
1578 + * @param string $payment_status Order payment status.
1579 + *
1580 + * @return string
1581 + */
1582 + public function get_order_status_by_payment_status( $payment_status ) {
1583 + $status = '';
1584 +
1585 + switch ( $payment_status ) {
1586 + case self::PAYMENT_PAID:
1587 + $status = self::ORDER_COMPLETED;
1588 + break;
1589 + case self::PAYMENT_UNPAID:
1590 + $status = self::ORDER_INCOMPLETE;
1591 + break;
1592 + case self::PAYMENT_PARTIALLY_REFUNDED:
1593 + $status = self::ORDER_COMPLETED;
1594 + break;
1595 + case self::PAYMENT_REFUNDED:
1596 + $status = self::ORDER_CANCELLED;
1597 + break;
1598 + case self::PAYMENT_FAILED:
1599 + $status = self::ORDER_CANCELLED;
1600 + break;
1601 + case self::ORDER_TRASH:
1602 + $status = self::ORDER_TRASH;
1603 + break;
1604 + case 'delete':
1605 + $status = self::ORDER_CANCELLED;
1606 + break;
1607 + case self::ORDER_CANCELLED:
1608 + $status = self::ORDER_CANCELLED;
1609 + break;
1610 + }
1611 +
1612 + return $status;
1613 + }
1614 +
1615 + /**
1616 + * Calculate order price
1617 + *
1618 + * @since 3.0.0
1619 + *
1620 + * @param array $items Order items, multi or single dimensional arr.
1621 + *
1622 + * @return object {subtotal => 10, total => 10}
1623 + */
1624 + public static function calculate_order_price( array $items ) {
1625 + $subtotal = 0;
1626 + $total = 0;
1627 +
1628 + if ( isset( $items[0] ) ) {
1629 + foreach ( $items as $item ) {
1630 + $regular_price = tutor_get_locale_price( $item['regular_price'] );
1631 + $sale_price = is_null( $item['sale_price'] ) || '' === $item['sale_price'] ? null : tutor_get_locale_price( $item['sale_price'] );
1632 + $discount_price = is_null( $item['discount_price'] ) || '' === $item['discount_price'] ? null : tutor_get_locale_price( $item['discount_price'] );
1633 +
1634 + // Subtotal is the original price (regular price).
1635 + $item_subtotal = $regular_price;
1636 + $item_total = $regular_price;
1637 +
1638 + // Determine the total based on sale price and discount.
1639 + if ( ! is_null( $sale_price ) && $sale_price < $regular_price ) {
1640 + $item_subtotal = $sale_price;
1641 + $item_total = $sale_price;
1642 + } else {
1643 + // If there's a discount, apply it to the total price.
1644 + if ( ! is_null( $discount_price ) && $discount_price >= 0 ) {
1645 + $item_total = max( 0, $discount_price ); // Ensure total doesn't go below 0.
1646 + }
1647 + }
1648 +
1649 + // $subtotal += $item_subtotal;
1650 + $subtotal += $regular_price;
1651 + $total += $item_total;
1652 + }
1653 + } else {
1654 + // for single dimensional array.
1655 + $regular_price = tutor_get_locale_price( $items['regular_price'] );
1656 + $sale_price = is_null( $items['sale_price'] ) || '' === $items['sale_price'] ? null : tutor_get_locale_price( $items['sale_price'] );
1657 + $discount_price = is_null( $items['discount_price'] ) || '' === $items['discount_price'] ? null : tutor_get_locale_price( $items['discount_price'] );
1658 +
1659 + // Subtotal is the original price (regular price).
1660 + $item_subtotal = $regular_price;
1661 + $item_total = $regular_price;
1662 +
1663 + // Determine the total based on sale price and discount.
1664 + if ( ! is_null( $sale_price ) && $sale_price < $regular_price ) {
1665 + $item_subtotal = $sale_price;
1666 + $item_total = $sale_price;
1667 + } else {
1668 + // If there's a discount, apply it to the total price.
1669 + if ( ! is_null( $discount_price ) && $discount_price >= 0 ) {
1670 + $item_total = max( 0, $discount_price ); // Ensure total doesn't go below 0.
1671 + }
1672 + }
1673 +
1674 + // $subtotal = $item_subtotal;
1675 + $subtotal = $regular_price;
1676 + $total = $item_total;
1677 + }
1678 +
1679 + return (object) array(
1680 + 'subtotal' => tutor_get_locale_price( $subtotal ),
1681 + 'total' => tutor_get_locale_price( $total ),
1682 + );
1683 + }
1684 +
1685 + /**
1686 + * Check has exclusive type tax.
1687 + *
1688 + * @since 3.0.0
1689 + *
1690 + * @param object $order order object.
1691 + *
1692 + * @return boolean
1693 + */
1694 + public static function has_exclusive_tax( $order ) {
1695 + return self::TAX_TYPE_EXCLUSIVE === $order->tax_type && $order->tax_rate > 0 && $order->tax_amount > 0;
1696 + }
1697 +
1698 + /**
1699 + * Check has inclusive type tax.
1700 + *
1701 + * @since 3.0.0
1702 + *
1703 + * @param object $order order object.
1704 + *
1705 + * @return boolean
1706 + */
1707 + public static function has_inclusive_tax( $order ) {
1708 + return self::TAX_TYPE_INCLUSIVE === $order->tax_type && $order->tax_rate > 0 && $order->tax_amount > 0;
1709 + }
1710 +
1711 + /**
1712 + * Get an item
1713 + *
1714 + * @since 3.0.0
1715 + *
1716 + * @param integer $item_id Item id.
1717 + *
1718 + * @return mixed
1719 + */
1720 + public function get_item( int $item_id ) {
1721 + return QueryHelper::get_row(
1722 + $this->order_item_table,
1723 + array(
1724 + 'item_id' => $item_id,
1725 + ),
1726 + 'id'
1727 + );
1728 + }
1729 +
1730 + /**
1731 + * Get sellable price
1732 + *
1733 + * @since 3.0.0
1734 + *
1735 + * @param mixed $regular_price Regular price.
1736 + * @param mixed $sale_price Sale price.
1737 + * @param mixed $discount_price Discount price.
1738 + *
1739 + * @return float item sellable price
1740 + */
1741 + public static function get_item_sellable_price( $regular_price, $sale_price = null, $discount_price = null ) {
1742 + // Ensure prices are numeric and properly formatted.
1743 + $sellable_price = (
1744 + ! empty( $sale_price )
1745 + ? $sale_price
1746 + : (
1747 + ( ! is_null( $discount_price ) && '' !== $discount_price ) && $discount_price >= 0
1748 + ? $discount_price
1749 + : $regular_price
1750 + )
1751 + );
1752 +
1753 + return $sellable_price;
1754 + }
1755 +
1756 + /**
1757 + * Get item sold price
1758 + *
1759 + * @since 3.0.0
1760 + *
1761 + * @param mixed $item_id Item id.
1762 + * @param bool $format Item id.
1763 + *
1764 + * @return mixed item sellable price
1765 + */
1766 + public static function get_item_sold_price( $item_id, $format = true ) {
1767 + $item = ( new self() )->get_item( $item_id );
1768 +
1769 + if ( $item ) {
1770 + $sold_price = self::get_item_sellable_price( $item->regular_price, $item->sale_price, $item->discount_price );
1771 +
1772 + return $format ? tutor_get_formatted_price( $sold_price ) : $sold_price;
1773 + }
1774 +
1775 + return 0;
1776 + }
1777 +
1778 + /**
1779 + * Should show pay btn to the user
1780 + *
1781 + * @since 3.0.0
1782 + *
1783 + * @param object $order Order object.
1784 + *
1785 + * @return boolean
1786 + */
1787 + public static function should_show_pay_btn( object $order ) {
1788 + $order_items = ( new self() )->get_order_items_by_id( $order->id );
1789 + $is_enrolled_any_course = false;
1790 + $is_incomplete_payment = self::PAYMENT_UNPAID === $order->payment_status && self::ORDER_INCOMPLETE === $order->order_status;
1791 + $is_manual_payment = $order->payment_method ? self::is_manual_payment( $order->payment_method ) : false;
1792 +
1793 + if ( $is_incomplete_payment && ! $is_manual_payment && $order_items ) {
1794 + if ( self::TYPE_SINGLE_ORDER === $order->order_type ) {
1795 + foreach ( $order_items as $item ) {
1796 + $course_id = $item->id;
1797 + if ( $course_id ) {
1798 + $is_enrolled = tutor_utils()->is_enrolled( $course_id );
1799 + if ( $is_enrolled ) {
1800 + $is_enrolled_any_course = true;
1801 + break;
1802 + }
1803 + }
1804 + }
1805 + } elseif ( tutor_utils()->count( $order_items ) ) {
1806 + $course_id = apply_filters( 'tutor_subscription_course_by_plan', $order_items[0]->id );
1807 + if ( tutor_utils()->is_enrolled( $course_id ) ) {
1808 + $is_enrolled_any_course = true;
1809 + }
1810 + }
1811 + }
1812 +
1813 + return apply_filters( 'tutor_should_show_pay_btn', $is_incomplete_payment && ! $is_manual_payment && ! $is_enrolled_any_course );
1814 + }
1815 +
1816 + /**
1817 + * Check is manual payment
1818 + *
1819 + * @since 3.0.0
1820 + *
1821 + * @param string $method_name Payment method name.
1822 + *
1823 + * @return boolean
1824 + */
1825 + public static function is_manual_payment( $method_name ) {
1826 + $payment_methods = tutor_get_manual_payment_gateways();
1827 +
1828 + $is_manual_payment = false;
1829 + foreach ( $payment_methods as $payment_method ) {
1830 + $is_manual_payment = $payment_method->name === $method_name;
1831 + }
1832 +
1833 + return $is_manual_payment;
1834 + }
1835 +
1836 + /**
1837 + * Render pay button
1838 + *
1839 + * @since 3.0.1
1840 + *
1841 + * @param int|object $order Order id or object.
1842 + *
1843 + * @return void
1844 + */
1845 + public static function render_pay_button( $order ) {
1846 +
1847 + $self = new self();
1848 +
1849 + if ( is_numeric( $order ) ) {
1850 + $order = $self->get_order_by_id( $order );
1851 + }
1852 +
1853 + $show_pay_button = self::should_show_pay_btn( $order );
1854 +
1855 + if ( ! self::should_active_pay_button( $order, $show_pay_button ) && $show_pay_button ) : ?>
1856 +
1857 + <div class="tooltip-wrap tooltip-icon">
1858 + <span class="tooltip-txt tooltip-left">
1859 + <?php esc_html_e( 'Payment Is Pending Due To Gateway Processing.', 'tutor' ); ?>
1860 + </span>
1861 + </div>
1862 + <?php
1863 + elseif ( $show_pay_button ) :
1864 + ob_start();
1865 + $self->pay_now_link( $order->id );
1866 + echo apply_filters( 'tutor_after_pay_button', ob_get_clean(), $order );//phpcs:ignore --sanitized output.
1867 + endif;
1868 + }
1869 +
1870 + /**
1871 + * Checks if the Repay Order-Time expired based on stored expiry time.
1872 + *
1873 + * @since 3.3.0
1874 + *
1875 + * @param object $order The order object containing order details.
1876 + * @param bool $show_pay_button Whether the pay button should be shown.
1877 + *
1878 + * @return bool Returns true if the order is expired or expiry time is not set, otherwise false.
1879 + */
1880 + private static function should_active_pay_button( $order, $show_pay_button ) {
1881 +
1882 + $current_time = time();
1883 + $meta_key = self::META_KEY_ORDER_ID . $order->id;
1884 + $user_id = get_current_user_id();
1885 + $expiry_time = get_user_meta( $user_id, $meta_key, true );
1886 +
1887 + if ( $expiry_time ) {
1888 +
1889 + // If the time is expired or the order is paid then delete the meta key.
1890 + if ( $expiry_time < $current_time || ! $show_pay_button ) {
1891 + delete_user_meta( $user_id, $meta_key );
1892 + return true;
1893 + }
1894 +
1895 + return false;
1896 + }
1897 +
1898 + return true;
1899 + }
1900 +
1901 + /**
1902 + * Retrieves statements for a specific user.
1903 + *
1904 + * @since 3.5.0
1905 + *
1906 + * @param string $post_type_in_clause SQL clause to filter the course post types.
1907 + * @param string $course_query SQL query string to further filter the courses .
1908 + * @param string $date_query SQL query string to filter by date range.
1909 + * @param int $user_id The user ID for which the statements are being retrieved.
1910 + * @param int $offset The offset for pagination.
1911 + * @param int $limit The number of rows to return.
1912 + *
1913 + * @return array
1914 + */
1915 + public function get_statements( $post_type_in_clause, $course_query, $date_query, $user_id, $offset, $limit ): array {
1916 + global $wpdb;
1917 +
1918 + //phpcs:disable
1919 + $statements = $wpdb->get_results(
1920 + $wpdb->prepare(
1921 + "SELECT
1922 + IF (
1923 + orders.total_price,
1924 + orders.total_price,
1925 + statements.course_price_total
1926 + ) AS order_total_price,
1927 + orders.tax_amount AS order_tax_amount,
1928 + orders.tax_type AS order_tax_type,
1929 + statements.*,
1930 + course.post_title AS course_title
1931 + FROM {$wpdb->prefix}tutor_earnings AS statements
1932 + LEFT JOIN {$wpdb->prefix}tutor_orders AS orders
1933 + ON statements.order_id = orders.id
1934 + INNER JOIN {$wpdb->posts} AS course ON course.ID = statements.course_id
1935 + AND course.post_type IN ({$post_type_in_clause})
1936 + WHERE statements.user_id = %d
1937 + {$course_query}
1938 + {$date_query}
1939 + ORDER BY statements.created_at DESC
1940 + LIMIT %d, %d
1941 + ",
1942 + $user_id,
1943 + $offset,
1944 + $limit
1945 + )
1946 + );
1947 +
1948 + $total_statements = $wpdb->get_var(
1949 + $wpdb->prepare(
1950 + "SELECT COUNT(*)
1951 + FROM {$wpdb->prefix}tutor_earnings AS statements
1952 + INNER JOIN {$wpdb->posts} AS course ON course.ID = statements.course_id
1953 + AND course.post_type IN ({$post_type_in_clause})
1954 + WHERE statements.user_id = %d
1955 + {$course_query}
1956 + {$date_query}
1957 + ",
1958 + $user_id
1959 + )
1960 + );
1961 + //phpcs:enable
1962 +
1963 + return array(
1964 + 'statements' => $statements,
1965 + 'total_statements' => $total_statements,
1966 + );
1967 + }
1968 +
1969 + /**
1970 + * Get order details for given course IDs.
1971 + *
1972 + * @since 3.8.1
1973 + *
1974 + * @param int[] $course_ids Array of course IDs to fetch order details.
1975 + *
1976 + * @return array Returns an array of order details with each element containing:
1977 + * - order data (all columns from tutor_orders table)
1978 + * - order_items data (al columns except id)
1979 + * Returns an empty array if no results found or on error.
1980 + */
1981 + public function get_order_details( array $course_ids ) {
1982 + global $wpdb;
1983 +
1984 + $result = array();
1985 +
1986 + $select_columns = array(
1987 + 'orders.*,
1988 + order_items.id AS order_items_id,
1989 + order_items.order_id,
1990 + order_items.item_id,
1991 + order_items.regular_price,
1992 + order_items.sale_price,
1993 + order_items.discount_price,
1994 + order_items.coupon_code AS item_coupon_code',
1995 + );
1996 + $primary_table = "{$wpdb->tutor_order_items} AS order_items";
1997 + $joining_tables = array(
1998 + array(
1999 + 'type' => 'LEFT',
2000 + 'table' => "{$wpdb->tutor_orders} AS orders",
2001 + 'on' => 'orders.id = order_items.order_id',
2002 + ),
2003 + );
2004 + $where = array( 'order_items.item_id' => array( 'IN', $course_ids ) );
2005 +
2006 + $result = QueryHelper::get_joined_data( $primary_table, $joining_tables, $select_columns, $where, array(), '', -1 );
2007 +
2008 + return $result['results'];
2009 + }
2010 +
2011 + /**
2012 + * Retrieve order meta for a specific order.
2013 + *
2014 + * @since 3.8.1
2015 + *
2016 + * @param int $order_id The ID of the order for which the metadata is to be retrieved.
2017 + *
2018 + * @return array An array of order meta. Returns an empty array if no meta is found.
2019 + */
2020 + public function get_order_meta_by_order_id( $order_id ) {
2021 +
2022 + return QueryHelper::get_all( 'tutor_ordermeta', array( 'order_id' => $order_id ), 'order_id' );
2023 + }
2024 +
2025 + /**
2026 + * Retrieve earnings for a specific order and course.
2027 + *
2028 + * @since 3.8.1
2029 + *
2030 + * @param int $order_id The ID of the order for which the earnings are being retrieved.
2031 + * @param int $course_id The ID of the course for which the earnings are being retrieved.
2032 + *
2033 + * @return array An array of earnings data. Returns an empty array if no data is found.
2034 + */
2035 + public function get_earnings_by_order_and_course( $order_id, $course_id ) {
2036 +
2037 + $where = array( 'order_id' => $order_id );
2038 +
2039 + if ( ! empty( $course_id ) ) {
2040 + $where['course_id'] = $course_id;
2041 + }
2042 +
2043 + return QueryHelper::get_all( 'tutor_earnings', $where, 'order_id' );
2044 + }
2045 +
2046 + /**
2047 + * Retrieve an existing order if it is incomplete, unpaid, and belongs to the given user.
2048 + *
2049 + * @since 3.9.2
2050 + *
2051 + * @param int $order_id The ID of the order.
2052 + * @param int $user_id The ID of the current user.
2053 + * @param bool $return_order_data Optional. Whether to return the order data instead of a boolean.
2054 + *
2055 + * @return bool|object Returns:
2056 + * - The order object if valid and $return_order_data is true.
2057 + * - True if valid and $return_order_data is false.
2058 + * - false if the order is invalid or not found.
2059 + */
2060 + public static function get_valid_incomplete_order( int $order_id, int $user_id, $return_order_data = false ) {
2061 +
2062 + if ( empty( $order_id ) || empty( $user_id ) ) {
2063 + return false;
2064 + }
2065 +
2066 + $order_data = ( new self() )->get_order_by_id( $order_id );
2067 +
2068 + if ( empty( $order_data ) ) {
2069 + return false;
2070 + }
2071 +
2072 + $is_valid = self::ORDER_INCOMPLETE === $order_data->order_status
2073 + && self::PAYMENT_UNPAID === $order_data->payment_status
2074 + && $user_id === $order_data->student->id
2075 + && self::should_show_pay_btn( $order_data );
2076 +
2077 + if ( $is_valid && $return_order_data ) {
2078 + return $order_data;
2079 + }
2080 +
2081 + return $is_valid;
2082 + }
2083 +
2084 + /**
2085 + * Update all order items for a given order.
2086 + *
2087 + * @since 3.9.2
2088 + *
2089 + * @param int $order_id The ID of the order whose items are being updated.
2090 + * @param array $order_items An array of order item data arrays or objects to update.
2091 + *
2092 + * @return bool True on success, false if any update fails.
2093 + */
2094 + public function update_order_items( int $order_id, array $order_items ) {
2095 +
2096 + foreach ( $order_items as $item ) {
2097 + try {
2098 + QueryHelper::update_where_in(
2099 + $this->order_item_table,
2100 + $item,
2101 + $order_id
2102 + );
2103 +
2104 + } catch ( \Throwable $th ) {
2105 + tutor_log( "Failed to update order item for order ID {$order_id}: " . $th->getMessage() );
2106 + return false;
2107 + }
2108 + }
2109 +
2110 + return true;
2111 + }
2112 +
2113 + /**
2114 + * Generate the payment link button HTML for a given order.
2115 + *
2116 + * @since 3.9.2
2117 + *
2118 + * @param int $order_id The unique ID of the order.
2119 + * @param bool $display Optional. Whether to echo the link (true) or return it (false). Default true.
2120 + *
2121 + * @return string|void
2122 + */
2123 + public static function pay_now_link( $order_id, $display = true ) {
2124 +
2125 + $checkout_url = add_query_arg( array( 'order_id' => $order_id ), CheckoutController::get_page_url() );
2126 +
2127 + $link =
2128 + sprintf(
2129 + '<a href="%s" class="tutor-btn tutor-btn-sm tutor-btn-outline-primary">
2130 + %s
2131 + </a>',
2132 + esc_url( $checkout_url ),
2133 + esc_html__( 'Pay', 'tutor' )
2134 + );
2135 +
2136 + if ( $display ) {
2137 + echo wp_kses(
2138 + $link,
2139 + array(
2140 + 'a' => array(
2141 + 'href' => true,
2142 + 'class' => true,
2143 + ),
2144 + )
2145 + );
2146 + } else {
2147 + return $link;
2148 + }
2149 + }
2150 + }
2151 +