PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 3.0.1
Tutor LMS – eLearning and online course solution v3.0.1
3.9.14 3.9.13 3.9.12 3.9.11 trunk 1.0.0 1.0.0-alpha 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.2.0 1.2.1 1.2.11 1.2.12 1.2.13 1.2.20 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 1.7.6 1.7.7 1.7.8 1.7.9 1.8.0 1.8.1 1.8.10 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7 1.8.8 1.8.9 1.9.0 1.9.1 1.9.10 1.9.11 1.9.12 1.9.13 1.9.14 1.9.15 1.9.16 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.1 2.0.10 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1.0 2.1.1 2.1.10 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.3.0 2.4.0 2.5.0 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 3.0.0 3.0.1 3.0.2 3.1.0 3.2.0 3.2.1 3.2.2 3.2.3 3.3.0 3.3.1 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.7.0 3.7.1 3.7.2 3.7.3 3.7.4 3.8.0 3.8.1 3.8.2 3.8.3 3.9.0 3.9.1 3.9.10 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9
tutor / models / OrderModel.php
tutor / models Last commit date
BillingModel.php 1 year ago CartModel.php 1 year ago CouponModel.php 1 year ago CourseModel.php 1 year ago LessonModel.php 2 years ago OrderActivitiesModel.php 1 year ago OrderMetaModel.php 1 year ago OrderModel.php 1 year ago QuizModel.php 1 year ago UserModel.php 1 year ago WithdrawModel.php 1 year ago
OrderModel.php
1665 lines
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\Ecommerce\Ecommerce;
15 use Tutor\Ecommerce\OrderActivitiesController;
16 use Tutor\Helpers\DateTimeHelper;
17 use Tutor\Helpers\QueryHelper;
18
19 /**
20 * OrderModel Class
21 *
22 * @since 3.0.0
23 */
24 class OrderModel {
25
26 /**
27 * Order status
28 *
29 * @since 3.0.0
30 *
31 * @var string
32 */
33 const ORDER_INCOMPLETE = 'incomplete';
34 const ORDER_COMPLETED = 'completed';
35 const ORDER_CANCELLED = 'cancelled';
36 const ORDER_TRASH = 'trash';
37
38 /**
39 * Payment status
40 *
41 * @since 3.0.0
42 *
43 * @var string
44 */
45 const PAYMENT_PAID = 'paid';
46 const PAYMENT_FAILED = 'failed';
47 const PAYMENT_UNPAID = 'unpaid';
48 const PAYMENT_REFUNDED = 'refunded';
49 const PAYMENT_PARTIALLY_REFUNDED = 'partially-refunded';
50
51 /**
52 * Order Meta keys for history & refunds
53 *
54 * @since 3.0.0
55 *
56 * @var string
57 */
58 const META_KEY_HISTORY = 'history';
59 const META_KEY_REFUND = 'refund';
60
61 /**
62 * Tax type constants
63 *
64 * @since 3.0.0
65 *
66 * @var string
67 */
68 const TAX_TYPE_EXCLUSIVE = 'exclusive';
69 const TAX_TYPE_INCLUSIVE = 'inclusive';
70
71
72 /**
73 * Order type
74 *
75 * @since 3.0.0
76 *
77 * @var string
78 */
79 const TYPE_SINGLE_ORDER = 'single_order';
80 const TYPE_SUBSCRIPTION = 'subscription';
81 const TYPE_RENEWAL = 'renewal';
82
83 /**
84 * Transient constants
85 *
86 * @since 3.0.0
87 */
88 const TRANSIENT_ORDER_BADGE_COUNT = 'tutor_order_badge_count';
89
90 /**
91 * Order placement success
92 *
93 * @since 3.0.0
94 */
95 const ORDER_PLACEMENT_SUCCESS = 'success';
96
97 /**
98 * Order placement failed
99 *
100 * @since 3.0.0
101 */
102 const ORDER_PLACEMENT_FAILED = 'failed';
103
104 /**
105 * Order table name
106 *
107 * @since 3.0.0
108 *
109 * @var string
110 */
111 private $table_name = 'tutor_orders';
112
113 /**
114 * Order item table name
115 *
116 * @since 3.0.0
117 *
118 * @var string
119 */
120 private $order_item_table = 'tutor_order_items';
121
122 /**
123 * Order item fillable fields
124 *
125 * @since 3.0.0
126 *
127 * @var array
128 */
129 private $order_items_fillable_fields = array(
130 'order_id',
131 'item_id',
132 'regular_price',
133 'sale_price',
134 'discount_price',
135 'coupon_code',
136 );
137
138 /**
139 * Resolve props & dependencies
140 *
141 * @since 3.0.0
142 */
143 public function __construct() {
144 global $wpdb;
145 $this->table_name = $wpdb->prefix . $this->table_name;
146 $this->order_item_table = $wpdb->prefix . $this->order_item_table;
147 }
148
149 /**
150 * Get table name with wp prefix
151 *
152 * @since 3.0.0
153 *
154 * @return string
155 */
156 public function get_table_name() {
157 return $this->table_name;
158 }
159
160
161 /**
162 * Get all order statuses
163 *
164 * @since 3.0.0
165 *
166 * @return array
167 */
168 public static function get_order_status() {
169 return array(
170 self::ORDER_INCOMPLETE => __( 'Incomplete', 'tutor' ),
171 self::ORDER_COMPLETED => __( 'Completed', 'tutor' ),
172 self::ORDER_CANCELLED => __( 'Cancelled', 'tutor' ),
173 self::ORDER_TRASH => __( 'Trash', 'tutor' ),
174 );
175 }
176
177 /**
178 * Get all payment statuses
179 *
180 * @since 3.0.0
181 *
182 * @return array
183 */
184 public static function get_payment_status() {
185 return array(
186 self::PAYMENT_PAID => __( 'Paid', 'tutor' ),
187 self::PAYMENT_UNPAID => __( 'Unpaid', 'tutor' ),
188 self::PAYMENT_FAILED => __( 'Failed', 'tutor' ),
189 self::PAYMENT_REFUNDED => __( 'Refunded', 'tutor' ),
190 self::PAYMENT_PARTIALLY_REFUNDED => __( 'Partially Refunded', 'tutor' ),
191 );
192 }
193
194 /**
195 * Get order items fillable fields
196 *
197 * @since 3.0.0
198 *
199 * @return array
200 */
201 public function get_order_items_fillable_fields() {
202 return $this->order_items_fillable_fields;
203 }
204
205 /**
206 * Get searchable fields
207 *
208 * This method is intendant to use with get order list
209 *
210 * @since 3.0.0
211 *
212 * @return array
213 */
214 private function get_searchable_fields() {
215 return array(
216 'o.id',
217 'o.transaction_id',
218 'o.coupon_code',
219 'o.payment_method',
220 'o.order_status',
221 'o.payment_status',
222 'u.display_name',
223 'u.user_login',
224 'u.user_email',
225 );
226 }
227
228 /**
229 * Create order
230 *
231 * Note: validate data before using this method
232 *
233 * This method will also insert items if
234 * item is set.
235 *
236 * Ex: data['order_items] = [
237 * user_id => 1,
238 * course_id => 1,
239 * regular_price => 100,
240 * sale_price => 90
241 * ]
242 *
243 * @since 3.0.0
244 *
245 * @param array $data Order data based on db table.
246 *
247 * @throws \Exception Database error if occur.
248 *
249 * @return int Order id on success
250 */
251 public function create_order( array $data ) {
252 $order_items = $data['items'] ?? null;
253 unset( $data['items'] );
254
255 global $wpdb;
256
257 // Start transaction.
258 $wpdb->query( 'START TRANSACTION' );
259
260 try {
261 $order_id = QueryHelper::insert( $this->table_name, $data );
262 if ( $order_id ) {
263 if ( $order_items ) {
264 $insert = $this->insert_order_items( $order_id, $order_items );
265 if ( $insert ) {
266 $wpdb->query( 'COMMIT' );
267 return $order_id;
268 } else {
269 $wpdb->query( 'ROLLBACK' );
270 throw new \Exception( __( 'Failed to insert order items', 'tutor' ) );
271 }
272 } else {
273 $wpdb->query( 'COMMIT' );
274 return $order_id;
275 }
276 }
277 } catch ( \Throwable $th ) {
278 throw new \Exception( $th->getMessage() );
279 }
280 }
281
282 /**
283 * Insert order items
284 *
285 * Note: validate data before using this method
286 *
287 * @since 3.0.0
288 *
289 * @param int $order_id Order ID.
290 * @param array $items Order items.
291 *
292 * @throws Exception Database error if occur.
293 *
294 * @return bool
295 */
296 public function insert_order_items( int $order_id, array $items ): bool {
297 // Check if item is multi dimensional.
298 if ( ! isset( $items[0] ) ) {
299 $items = array( $items );
300 }
301
302 // Set order id on each item.
303 foreach ( $items as $key => $item ) {
304 $items[ $key ]['order_id'] = $order_id;
305 }
306
307 try {
308 $insert = QueryHelper::insert_multiple_rows(
309 $this->order_item_table,
310 $items
311 );
312 return $insert ? true : false;
313 } catch ( \Throwable $th ) {
314 throw new Exception( $th->getMessage() );
315 }
316 }
317
318 /**
319 * Retrieve order details by order ID.
320 *
321 * This function fetches order information from the database based on the given
322 * order ID. It queries the 'tutor_orders' table for the order data, retrieves
323 * the corresponding user information and metadata, and constructs a detailed
324 * student object with placeholder values for billing address and phone.
325 *
326 * The function then assigns this student object to the order data, removes
327 * the user ID from the order data, and returns the modified order data.
328 *
329 * @since 3.0.0
330 *
331 * @global wpdb $wpdb WordPress database abstraction object.
332 *
333 * @param int $order_id The ID of the order to retrieve.
334 *
335 * @return object|false The order data with the student's information included, or false if no order is found.
336 */
337 public function get_order_by_id( $order_id ) {
338 $order_data = QueryHelper::get_row(
339 $this->table_name,
340 array( 'id' => $order_id ),
341 'id'
342 );
343
344 if ( ! $order_data ) {
345 return false;
346 }
347
348 $user_info = get_userdata( $order_data->user_id );
349 if ( ! is_a( $user_info, 'WP_User' ) ) {
350 return false;
351 }
352
353 $student = new \stdClass();
354 $student->id = (int) $user_info->ID;
355 $student->name = $user_info->data->display_name;
356 $student->email = $user_info->data->user_email;
357 $student->phone = get_user_meta( $order_data->user_id, 'phone_number', true );
358 $student->billing_address = $this->get_tutor_customer_data( $order_data->user_id );
359 $student->image = get_avatar_url( $order_data->user_id );
360
361 $order_data->student = $student;
362 $order_data->items = $this->get_order_items_by_id( $order_id );
363
364 $order_data->subtotal_price = (float) $order_data->subtotal_price;
365 $order_data->total_price = (float) $order_data->total_price;
366 $order_data->net_payment = (float) $order_data->net_payment;
367 $order_data->discount_amount = (float) $order_data->discount_amount;
368 $order_data->coupon_amount = (float) $order_data->coupon_amount;
369 $order_data->tax_rate = (float) $order_data->tax_rate;
370 $order_data->tax_amount = (float) $order_data->tax_amount;
371
372 $order_data->payment_method_readable = Ecommerce::get_payment_method_label( $order_data->payment_method );
373 $order_data->created_at_readable = DateTimeHelper::get_gmt_to_user_timezone_date( $order_data->created_at_gmt );
374 $order_data->updated_at_readable = empty( $order_data->updated_at_gmt ) ? '' : DateTimeHelper::get_gmt_to_user_timezone_date( $order_data->updated_at_gmt );
375
376 $order_data->created_by = get_userdata( $order_data->created_by )->display_name ?? '';
377 $order_data->updated_by = get_userdata( $order_data->updated_by )->display_name ?? '';
378
379 $order_activities_model = new OrderActivitiesModel();
380 $order_data->activities = $order_activities_model->get_order_activities( $order_id );
381 $order_data->refunds = $this->get_order_refunds( $order_id );
382
383 unset( $student->billing_address->id );
384 unset( $student->billing_address->user_id );
385
386 return apply_filters( 'tutor_order_details', $order_data );
387 }
388
389 /**
390 * Mark order Unpaid to Paid.
391 *
392 * @since 3.0.0
393 *
394 * @param int $order_id order id.
395 * @param string $note note.
396 * @param bool $trigger_hooks trigger hooks or not.
397 *
398 * @return bool
399 */
400 public function mark_as_paid( $order_id, $note = '', $trigger_hooks = true ) {
401 if ( $trigger_hooks ) {
402 do_action( 'tutor_before_order_mark_as_paid', $order_id );
403 }
404
405 $data = array(
406 'payment_status' => self::PAYMENT_PAID,
407 'order_status' => self::ORDER_COMPLETED,
408 'note' => $note,
409 );
410
411 $response = $this->update_order( $order_id, $data );
412 if ( ! $response ) {
413 return false;
414 }
415
416 if ( $trigger_hooks ) {
417 do_action( 'tutor_order_payment_status_changed', $order_id, self::PAYMENT_UNPAID, self::PAYMENT_PAID );
418
419 $order = $this->get_order_by_id( $order_id );
420 $discount_amount = $this->calculate_discount_amount( $order->discount_type, $order->discount_amount, $order->subtotal_price );
421 do_action( 'tutor_after_order_mark_as_paid', $order, $discount_amount );
422 }
423
424 return true;
425 }
426
427
428 /**
429 * Retrieve order items by order ID.
430 *
431 * This function fetches order item details from the database based on the given
432 * order ID. It queries the 'tutor_order_items' table and joins it with the 'posts'
433 * table to get the course titles associated with each order item.
434 *
435 * The function then returns the retrieved order items, or an empty array if no
436 * items are found.
437 *
438 * @since 3.0.0
439 *
440 * @global wpdb $wpdb WordPress database abstraction object.
441 *
442 * @param int $order_id The ID of the order to retrieve items for.
443 *
444 * @return array The order items, each containing details and course titles, or an empty array if no items are found.
445 */
446 public function get_order_items_by_id( $order_id ) {
447 global $wpdb;
448
449 $primary_table = "{$wpdb->prefix}tutor_order_items AS oi";
450 $joining_tables = array(
451 array(
452 'type' => 'LEFT',
453 'table' => "{$wpdb->prefix}posts AS p",
454 'on' => 'p.ID = oi.item_id',
455 ),
456 );
457
458 $where = array( 'order_id' => $order_id );
459
460 $select_columns = array( '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' );
461
462 $courses_data = QueryHelper::get_joined_data( $primary_table, $joining_tables, $select_columns, $where, array(), 'id', 0, 0 );
463 $courses = $courses_data['results'];
464
465 if ( tutor()->has_pro ) {
466 $bundle_model = new \TutorPro\CourseBundle\Models\BundleModel();
467 }
468
469 if ( ! empty( $courses_data['total_count'] ) ) {
470 foreach ( $courses as &$course ) {
471 if ( tutor()->has_pro && 'course-bundle' === $course->type ) {
472 $course->total_courses = count( $bundle_model->get_bundle_course_ids( $course->id ) );
473 }
474
475 $course->id = (int) $course->id;
476 $course->regular_price = (float) $course->regular_price;
477 $course->sale_price = (float) $course->sale_price;
478 $course->image = get_the_post_thumbnail_url( $course->id );
479 }
480 }
481
482 unset( $course );
483
484 return ! empty( $courses ) ? $courses : array();
485 }
486
487 /**
488 * Retrieve tutor customer data by user ID.
489 *
490 * This function fetches customer data from the 'tutor_customers' table based on
491 * the given user ID. It utilizes a helper function from the QueryHelper class
492 * to perform the database query.
493 *
494 * The function returns the customer data as an object.
495 *
496 * @global wpdb $wpdb WordPress database abstraction object.
497 *
498 * @param int $user_id The ID of the user to retrieve customer data for.
499 *
500 * @since 3.0.0
501 *
502 * @return object|null The customer data retrieved from the database.
503 */
504 public function get_tutor_customer_data( $user_id ) {
505 global $wpdb;
506
507 // Retrieve customer data for the given user ID from the 'tutor_customers' table.
508 $customer_data = QueryHelper::get_row( "{$wpdb->prefix}tutor_customers", array( 'user_id' => $user_id ), 'id' );
509
510 if ( empty( $customer_data ) ) {
511 return null;
512 }
513
514 $return_data = (object) array(
515 'id' => $customer_data->id,
516 'user_id' => $customer_data->user_id,
517 'name' => $customer_data->billing_first_name . ' ' . $customer_data->billing_last_name,
518 'email' => $customer_data->billing_email,
519 'phone' => $customer_data->billing_phone,
520 'address' => $customer_data->billing_address,
521 'city' => $customer_data->billing_city,
522 'state' => $customer_data->billing_state,
523 'country' => $customer_data->billing_country,
524 'zip_code' => $customer_data->billing_zip_code,
525 );
526
527 return $return_data;
528 }
529
530 /**
531 * Retrieve order refunds by order ID.
532 *
533 * This function fetches all order refunds from the 'tutor_ordermeta' table
534 * based on the given order ID and the 'refund' meta key. It uses a helper
535 * function from the QueryHelper class to perform the database query.
536 *
537 * If no order refunds are found, the function returns an empty array.
538 * Otherwise, it decodes the JSON-encoded meta values and returns them as an array.
539 *
540 * @global wpdb $wpdb WordPress database abstraction object.
541 *
542 * @param int $order_id The ID of the order to retrieve refunds for.
543 *
544 * @since 3.0.0
545 *
546 * @return array An array of order refunds, each decoded from its JSON representation.
547 */
548 public function get_order_refunds( $order_id ) {
549 global $wpdb;
550
551 $meta_keys = array(
552 OrderActivitiesModel::META_KEY_REFUND,
553 OrderActivitiesModel::META_KEY_PARTIALLY_REFUND,
554 );
555
556 // Retrieve order refunds for the given order ID from the 'tutor_ordermeta' table.
557 $order_refunds = QueryHelper::get_all(
558 "{$wpdb->prefix}tutor_ordermeta",
559 array(
560 'order_id' => $order_id,
561 'meta_key' => $meta_keys,
562 ),
563 'created_at_gmt',
564 1000,
565 'ASC'
566 );
567
568 if ( empty( $order_refunds ) ) {
569 return array();
570 }
571
572 $response = array();
573
574 foreach ( $order_refunds as $refund ) {
575 $parsed_meta_value = json_decode( $refund->meta_value );
576 $values = new \stdClass();
577 $values->id = (int) $refund->id;
578
579 foreach ( $parsed_meta_value as $key => $value ) {
580 $values->$key = $value;
581 }
582
583 $values->date = $refund->created_at_gmt;
584
585 $response[] = $values;
586 }
587
588 // Custom comparison function for sorting by date.
589 usort(
590 $response,
591 function ( $a, $b ) {
592 $date_a = strtotime( $a->date );
593 $date_b = strtotime( $b->date );
594
595 return $date_b - $date_a;
596 }
597 );
598
599 return $response;
600 }
601
602 /**
603 * Update an order
604 *
605 * @since 3.0.0
606 *
607 * @param int|array $order_id Integer or array of ids sql escaped.
608 * @param array $data Data to update, escape data.
609 *
610 * @return bool
611 */
612 public function update_order( $order_id, array $data ) {
613 $order_id = is_array( $order_id ) ? $order_id : array( $order_id );
614 $order_id = QueryHelper::prepare_in_clause( $order_id );
615 try {
616 QueryHelper::update_where_in(
617 $this->table_name,
618 $data,
619 $order_id
620 );
621 return true;
622 } catch ( \Throwable $th ) {
623 error_log( $th->getMessage() . ' in ' . $th->getFile() . ' at line ' . $th->getLine() );
624 return false;
625 }
626 }
627
628 /**
629 * Get enrollment ids by order id.
630 *
631 * @since 3.0.0
632 *
633 * @param int $order_id order id.
634 *
635 * @return array
636 */
637 public function get_enrollment_ids( $order_id ) {
638 global $wpdb;
639 $enrollment_ids = array();
640
641 $enrollments = $wpdb->get_results(
642 $wpdb->prepare(
643 "SELECT * FROM {$wpdb->postmeta}
644 WHERE meta_key=%s
645 AND meta_value LIKE %d",
646 '_tutor_enrolled_by_order_id',
647 $order_id
648 )
649 );
650
651 if ( $enrollments ) {
652 $enrollment_ids = array_column( $enrollments, 'post_id' );
653 }
654
655 return $enrollment_ids;
656 }
657
658 /**
659 * Delete an order by order ID.
660 *
661 * This function deletes an order from the 'tutor_orders' table based on the given
662 * order ID. It uses the QueryHelper class to perform the database delete operation.
663 *
664 * @since 3.0.0
665 *
666 * @param int|array $order_id The ID of the order to delete.
667 *
668 * @return bool
669 */
670 public function delete_order( $order_id ) {
671 global $wpdb;
672 $order_ids = is_array( $order_id ) ? $order_id : array( intval( $order_id ) );
673
674 try {
675 $wpdb->query( 'START TRANSACTION' );
676
677 foreach ( $order_ids as $order_id ) {
678 // Delete enrollments if exist.
679 $enrollment_ids = $this->get_enrollment_ids( $order_id );
680 if ( $enrollment_ids ) {
681 QueryHelper::bulk_delete_by_ids( $wpdb->posts, $enrollment_ids );
682 // After enrollment delete, delete the course progress.
683 foreach ( $enrollment_ids as $enrollment_id ) {
684 $course_id = get_post_field( 'post_parent', $enrollment_id );
685 $student_id = get_post_field( 'post_author', $enrollment_id );
686
687 if ( $course_id && $student_id ) {
688 tutor_utils()->delete_course_progress( $course_id, $student_id );
689 }
690 }
691 }
692
693 // Delete earnings.
694 QueryHelper::delete(
695 $wpdb->prefix . 'tutor_earnings',
696 array(
697 'order_id' => $order_id,
698 'process_by' => 'Tutor',
699 )
700 );
701
702 // Now delete order.
703 QueryHelper::delete( $this->table_name, array( 'id' => $order_id ) );
704 }
705
706 $wpdb->query( 'COMMIT' );
707 return true;
708
709 } catch ( \Throwable $th ) {
710 $wpdb->query( 'ROLLBACK' );
711 return false;
712 }
713 }
714
715 /**
716 * Get orders list
717 *
718 * @since 3.0.0
719 *
720 * @param array $where where clause conditions.
721 * @param string $search_term search clause conditions.
722 * @param int $limit limit default 10.
723 * @param int $offset default 0.
724 * @param string $order_by column default 'o.id'.
725 * @param string $order list order default 'desc'.
726 *
727 * @return array
728 */
729 public function get_orders( array $where = array(), $search_term = '', int $limit = 10, int $offset = 0, string $order_by = 'o.id', string $order = 'desc' ) {
730
731 global $wpdb;
732
733 $primary_table = "{$this->table_name} o";
734 $joining_tables = array(
735 array(
736 'type' => 'LEFT',
737 'table' => "{$wpdb->users} u",
738 'on' => 'o.user_id = u.ID',
739 ),
740 );
741
742 $select_columns = array( 'o.*', 'u.user_login' );
743
744 $search_clause = array();
745 if ( '' !== $search_term ) {
746 foreach ( $this->get_searchable_fields() as $column ) {
747 $search_clause[ $column ] = $search_term;
748 }
749 }
750
751 $response = array(
752 'results' => array(),
753 'total_count' => 0,
754 );
755
756 try {
757 return QueryHelper::get_joined_data( $primary_table, $joining_tables, $select_columns, $where, $search_clause, $order_by, $limit, $offset, $order );
758 } catch ( \Throwable $th ) {
759 // Log with error, line & file name.
760 error_log( $th->getMessage() . ' in ' . $th->getFile() . ' at line ' . $th->getLine() );
761 return $response;
762 }
763 }
764
765 /**
766 * Get order count
767 *
768 * @since 3.0.0
769 *
770 * @param array $where Where conditions, sql esc data.
771 * @param string $search_term Search terms, sql esc data.
772 *
773 * @return int
774 */
775 public function get_order_count( $where = array(), string $search_term = '' ) {
776 global $wpdb;
777
778 $search_clause = array();
779 if ( '' !== $search_term ) {
780 foreach ( $this->get_searchable_fields() as $column ) {
781 $search_clause[ $column ] = $search_term;
782 }
783 }
784
785 $join_table = array(
786 array(
787 'type' => 'INNER',
788 'table' => "{$wpdb->users} u",
789 'on' => 'o.user_id = u.ID',
790 ),
791 );
792 $primary_table = "{$this->table_name} o";
793 return QueryHelper::get_joined_count( $primary_table, $join_table, $where, $search_clause );
794 }
795
796 /**
797 * Get order of a user
798 *
799 * @since 3.0.0
800 *
801 * @param string $time_period $time_period Sorting time period,
802 * supported time periods are: today, monthly & yearly.
803 * @param string $start_date $start_date For date range sorting.
804 * @param string $end_date $end_date For date range sorting.
805 * @param int $user_id User id for fetching order list.
806 * @param int $limit Limit to fetch record.
807 * @param int $offset Offset to fetch record.
808 *
809 * @throws \Exception Throw exception if database error occur.
810 *
811 * @return array
812 */
813 public function get_user_orders( $time_period = null, $start_date = null, $end_date = null, int $user_id = 0, $limit = 10, int $offset = 0 ) {
814 $user_id = $user_id ? $user_id : get_current_user_id();
815
816 $response = array(
817 'results' => array(),
818 'total_count' => 0,
819 );
820
821 global $wpdb;
822
823 $time_period_clause = '';
824 $date_range_clause = '';
825
826 if ( $start_date && $end_date ) {
827 $date_range_clause = $wpdb->prepare( 'AND DATE(created_at_gmt) BETWEEN %s AND %s', $start_date, $end_date );
828 } else {
829 if ( $time_period ) {
830 if ( 'today' === $time_period ) {
831 $time_period_clause = 'AND DATE(o.created_at_gmt) = CURDATE()';
832 } elseif ( 'monthly' === $time_period ) {
833 $time_period_clause = 'AND MONTH(o.created_at_gmt) = MONTH(CURDATE()) ';
834 } else {
835 $time_period_clause = 'AND YEAR(o.created_at_gmt) = YEAR(CURDATE()) ';
836 }
837 }
838 }
839
840 $query = $wpdb->prepare(
841 "SELECT
842 SQL_CALC_FOUND_ROWS
843 o.*
844 FROM $this->table_name AS o
845 WHERE o.user_id = %d
846 {$time_period_clause}
847 {$date_range_clause}
848 ORDER BY o.id DESC
849 LIMIT %d OFFSET %d
850 ",
851 $user_id,
852 $limit,
853 $offset
854 );
855
856 $results = $wpdb->get_results( $query );
857
858 if ( $wpdb->last_error ) {
859 throw new \Exception( $wpdb->last_error );
860 } else {
861 $response['results'] = $results;
862 $response['total_count'] = is_array( $results ) && count( $results ) ? (int) $wpdb->get_var( 'SELECT FOUND_ROWS()' ) : 0;
863 }
864
865 return $response;
866 }
867
868 /**
869 * Get total discounts by user_id (instructor), optionally can set period ( today | monthly| yearly )
870 *
871 * Optionally can set start date & end date to get enrollment list from date range
872 *
873 * If period or date range not pass then it will return all time enrollment list
874 *
875 * @since 3.0.0
876 *
877 * @param int $user_id User id, if user not have admin access
878 * then only this user's refund amount will fetched.
879 * @param string $period Time period.
880 * @param string $start_date Start date.
881 * @param string $end_date End date.
882 * @param int $course_id Course id.
883 *
884 * @return array
885 */
886 public function get_discounts_by_user( int $user_id, string $period = '', $start_date = '', string $end_date = '', int $course_id = 0 ): array {
887 $response = array(
888 'discounts' => array(),
889 'total_discounts' => 0,
890 );
891
892 global $wpdb;
893
894 $user_clause = '';
895 $date_range_clause = '';
896 $period_clause = '';
897 $course_clause = '';
898 $group_clause = ' GROUP BY DATE(date_format) ';
899 $discount_clause = 'o.coupon_amount as total';
900
901 if ( $start_date && $end_date ) {
902 $date_range_clause = $wpdb->prepare(
903 'AND o.created_at_gmt BETWEEN %s AND %s',
904 $start_date,
905 $end_date
906 );
907 } else {
908 $period_clause = QueryHelper::get_period_clause( 'o.created_at_gmt', $period );
909 }
910
911 if ( 'today' !== $period ) {
912 $group_clause = ' GROUP BY MONTH(date_format) ';
913 }
914
915 if ( $course_id ) {
916 $course_clause = $wpdb->prepare( 'AND i.item_id = %d', $course_id );
917 $discount_clause = 'i.regular_price - i.discount_price AS total';
918 }
919
920 $item_table = $wpdb->prefix . 'tutor_order_items';
921
922 if ( $course_id ) {
923 if ( $user_id ) {
924 $user_clause = $wpdb->prepare( 'AND c.post_author = %d', $user_id );
925 }
926
927 $discounts = $wpdb->get_results(
928 $wpdb->prepare(
929 "SELECT
930 i.item_id AS course_id,
931 SUM(
932 COALESCE(o.coupon_amount, 0) +
933 COALESCE(
934 IF(
935 o.discount_type = 'percentage',
936 COALESCE(o.subtotal_price * (o.discount_amount / 100), 0),
937 COALESCE(o.discount_amount, 0)
938 ),
939 0
940 )
941 ) AS total,
942 o.created_at_gmt AS date_format
943 FROM
944 {$this->table_name} o
945 JOIN
946 {$item_table} i ON o.id = i.order_id
947 JOIN
948 {$wpdb->posts} c
949 ON c.ID = i.item_id
950 AND c.post_type = %s
951 WHERE
952 1 = 1
953 AND i.item_id = %d
954 {$user_clause}
955 {$period_clause}
956 {$date_range_clause}
957 {$group_clause}
958 ",
959 tutor()->course_post_type,
960 $course_id
961 )
962 );
963 } else {
964 if ( $user_id ) {
965 $user_clause = $wpdb->prepare( "AND %d = (SELECT user_id FROM {$wpdb->tutor_earnings} WHERE order_status = 'completed' LIMIT 1) ", $user_id );
966 }
967
968 $discounts = $wpdb->get_results(
969 $wpdb->prepare(
970 "SELECT
971 SUM(
972 COALESCE(o.coupon_amount, 0) +
973 COALESCE(
974 IF(
975 o.discount_type = 'percentage',
976 COALESCE(o.subtotal_price * (o.discount_amount / 100), 0),
977 COALESCE(o.discount_amount, 0)
978 ),
979 0
980 )
981 ) AS total,
982 o.created_at_gmt AS date_format
983 FROM {$this->table_name} AS o
984 WHERE 1 = %d
985 AND o.order_status = 'completed'
986 {$user_clause}
987 {$period_clause}
988 {$date_range_clause}
989 {$course_clause}
990 {$group_clause}
991 HAVING total > 0
992 ",
993 1
994 )
995 );
996 }
997
998 $total_discount = 0;
999 $discount_items = array();
1000
1001 $response = array(
1002 'discounts' => array(),
1003 'total_discounts' => 0,
1004 );
1005
1006 if ( $discounts ) {
1007 foreach ( $discounts as $discount ) {
1008 $total_discount += $discount->total;
1009 $discount_items[] = $discount;
1010
1011 // Split each discount.
1012 list( $admin_discount, $instructor_discount ) = array_values( tutor_split_amounts( $discount->total ) );
1013
1014 $discount->total = is_admin() ? $admin_discount : $instructor_discount;
1015 }
1016
1017 list( $admin_total, $instructor_total ) = array_values( tutor_split_amounts( $total_discount ) );
1018
1019 $response['discounts'] = $discount_items;
1020 $response['total_discounts'] = is_admin() ? $admin_total : $instructor_total;
1021 }
1022
1023 return $response;
1024 }
1025
1026 /**
1027 * Get total refunds by user_id (instructor), optionally can set period ( today | monthly| yearly )
1028 *
1029 * Optionally can set start date & end date to get enrollment list from date range
1030 *
1031 * If period or date range not pass then it will return all time enrollment list
1032 *
1033 * @since 3.0.0
1034 *
1035 * @param int $user_id User id, if user not have admin access
1036 * then only this user's refund amount will fetched.
1037 * @param string $period Time period.
1038 * @param string $start_date Start date.
1039 * @param string $end_date End date.
1040 * @param int $course_id Course id.
1041 *
1042 * @return array
1043 */
1044 public function get_refunds_by_user( int $user_id, string $period = '', $start_date = '', string $end_date = '', int $course_id = 0 ): array {
1045 $response = array(
1046 'refunds' => array(),
1047 'total_refunds' => 0,
1048 );
1049
1050 global $wpdb;
1051
1052 $user_clause = '';
1053 $date_range_clause = '';
1054 $period_clause = '';
1055 $course_clause = '';
1056 $commission_clause = '';
1057 $group_clause = ' GROUP BY DATE(o.created_at_gmt) ';
1058
1059 if ( $start_date && $end_date ) {
1060 $date_range_clause = $wpdb->prepare(
1061 'AND o.created_at_gmt BETWEEN %s AND %s',
1062 $start_date,
1063 $end_date
1064 );
1065 $group_clause = ' GROUP BY DATE(o.created_at_gmt) ';
1066
1067 } else {
1068 $period_clause = QueryHelper::get_period_clause( 'o.created_at_gmt', $period );
1069 }
1070
1071 if ( 'today' !== $period ) {
1072 $group_clause = ' GROUP BY MONTH(o.created_at_gmt) ';
1073 }
1074
1075 if ( $course_id ) {
1076 if ( $user_id ) {
1077 $user_clause = $wpdb->prepare( 'AND c.post_author = %d', $user_id );
1078 }
1079 } else {
1080 if ( $user_id ) {
1081 $user_clause = $wpdb->prepare( 'AND c.post_author = %d', $user_id );
1082 }
1083 }
1084
1085 // Refund query logic remains the same.
1086 $item_table = $wpdb->prefix . 'tutor_order_items';
1087
1088 if ( $course_id ) {
1089 $refunds = $wpdb->get_results(
1090 $wpdb->prepare(
1091 "SELECT
1092 i.item_id AS course_id,
1093 ROUND(
1094 SUM(
1095 o.refund_amount *
1096 (
1097 CASE
1098 WHEN i.discount_price THEN i.discount_price
1099 WHEN i.sale_price > 0 THEN i.sale_price
1100 ELSE i.regular_price
1101 END / o.total_price
1102 )
1103 ), 2
1104 ) AS total
1105 FROM
1106 {$this->table_name} o
1107 JOIN
1108 {$item_table} i ON o.id = i.order_id
1109 JOIN
1110 {$wpdb->posts} c
1111 ON c.ID = i.item_id
1112 AND c.post_type = %s
1113 WHERE
1114 o.refund_amount > 0
1115 AND i.item_id = %d
1116 {$user_clause}
1117 {$period_clause}
1118 {$date_range_clause}
1119 {$group_clause},
1120 i.item_id
1121 ",
1122 tutor()->course_post_type,
1123 $course_id
1124 )
1125 );
1126 } else {
1127 $earning_table = $wpdb->tutor_earnings;
1128 if ( $user_id ) {
1129 $user_clause = "AND {$user_id} = (SELECT user_id FROM {$earning_table} LIMIT 1)";
1130 }
1131
1132 $refunds = $wpdb->get_results(
1133 $wpdb->prepare(
1134 "SELECT
1135 COALESCE(SUM(o.refund_amount), 0) AS total,
1136 created_at_gmt AS date_format
1137 FROM {$this->table_name} AS o
1138 -- LEFT JOIN {$item_table} AS i ON i.order_id = o.id
1139 -- LEFT JOIN {$wpdb->posts} AS c ON c.id = i.item_id
1140 WHERE 1 = %d
1141 AND o.refund_amount > %d
1142 {$user_clause}
1143 {$period_clause}
1144 {$date_range_clause}
1145 {$group_clause},
1146 o.id",
1147 1,
1148 0
1149 )
1150 );
1151 }
1152
1153 $total_refund = 0;
1154
1155 foreach ( $refunds as $refund ) {
1156 $total_refund += $refund->total;
1157
1158 // Update total amount from list.
1159 $split_refund = (object) tutor_split_amounts( $refund->total );
1160 $refund->total = is_admin() ? $split_refund->admin : $split_refund->instructor;
1161 }
1162
1163 $split_total_refund = (object) tutor_split_amounts( $total_refund );
1164
1165 $response = array(
1166 'refunds' => $refunds,
1167 'total_refunds' => is_admin() ? $split_total_refund->admin : $split_total_refund->instructor,
1168 );
1169
1170 return $response;
1171 }
1172
1173 /**
1174 * Update the payment status of an order.
1175 *
1176 * This function updates the payment status and note of an order in the database.
1177 * It uses the QueryHelper class to perform the update operation.
1178 *
1179 * @since 3.0.0
1180 *
1181 * @param object $data An object containing the payment status, note, and order ID.
1182 * - 'payment_status' (string): The new payment status.
1183 * - 'note' (string): A note regarding the payment status update.
1184 * - 'order_id' (int): The ID of the order to update.
1185 *
1186 * @return bool True on successful update, false on failure.
1187 */
1188 public function payment_status_update( object $data ) {
1189 $response = QueryHelper::update(
1190 $this->table_name,
1191 array(
1192 'payment_status' => $data->payment_status,
1193 'note' => $data->note,
1194 ),
1195 array( 'id' => $data->order_id )
1196 );
1197
1198 if ( $response ) {
1199 $activity_controller = new OrderActivitiesController();
1200 $activity_controller->store_order_activity_for_marked_as_paid( $data->order_id );
1201 }
1202
1203 return $response;
1204 }
1205
1206 /**
1207 * Add a discount to an order.
1208 *
1209 * This function updates the order in the database with the provided discount details.
1210 * It updates the discount type, discount amount, and discount reason for the given order ID.
1211 *
1212 * @since 3.0.0
1213 *
1214 * @param object $data An object containing the discount details:
1215 * - $data->order_id (int) The ID of the order.
1216 * - $data->discount_type (string) The type of the discount.
1217 * - $data->discount_amount(float) The amount of the discount.
1218 * - $data->discount_reason(string) The reason for the discount.
1219 *
1220 * @return bool True on successful update, false on failure.
1221 */
1222 public function add_order_discount( object $data ) {
1223 $response = QueryHelper::update(
1224 $this->table_name,
1225 array(
1226 'discount_type' => $data->discount_type,
1227 'discount_amount' => $data->discount_amount,
1228 'discount_reason' => $data->discount_reason,
1229 ),
1230 array( 'id' => $data->order_id )
1231 );
1232
1233 return $response;
1234 }
1235
1236 /**
1237 * Updates the status of an order and logs the activity.
1238 *
1239 * This function updates the status of an order in the database and, if successful, logs the activity
1240 * with a message indicating the status change. The message includes the current user's display name,
1241 * if available.
1242 *
1243 * The possible order statuses include:
1244 * - ORDER_CANCELLED
1245 * - ORDER_COMPLETED
1246 * - ORDER_INCOMPLETE
1247 * - ORDER_TRASH
1248 *
1249 * If the update is successful, an order activity log entry is created with the current date, time,
1250 * and status change message.
1251 *
1252 * @since 3.0.0
1253 *
1254 * @param object $data An object containing:
1255 * - int $order_id The ID of the order to update.
1256 * - string $order_status The new status of the order.
1257 * - string $cancel_reason The reason for the order cancellation (optional).
1258 *
1259 * @return bool True on successful update, false on failure.
1260 */
1261 public function order_status_update( object $data ) {
1262 $response = QueryHelper::update(
1263 $this->table_name,
1264 array(
1265 'order_status' => $data->order_status,
1266 ),
1267 array( 'id' => $data->order_id )
1268 );
1269
1270 if ( $response ) {
1271 $user_name = '';
1272 $current_user = wp_get_current_user();
1273
1274 if ( $current_user->exists() ) {
1275 $user_name = $current_user->display_name;
1276 }
1277
1278 $message = '';
1279
1280 if ( self::ORDER_CANCELLED === $data->order_status ) {
1281 $message = empty( $user_name ) ? __( 'Order marked as cancelled', 'tutor' ) : sprintf( __( 'Order marked as cancelled by %s', 'tutor' ), $user_name );
1282 } elseif ( self::ORDER_COMPLETED === $data->order_status ) {
1283 $message = empty( $user_name ) ? __( 'Order marked as completed', 'tutor' ) : sprintf( __( 'Order marked as completed by %s', 'tutor' ), $user_name );
1284 } elseif ( self::ORDER_INCOMPLETE === $data->order_status ) {
1285 $message = empty( $user_name ) ? __( 'Order marked as incomplete', 'tutor' ) : sprintf( __( 'Order marked as incomplete by %s', 'tutor' ), $user_name );
1286 } elseif ( self::ORDER_TRASH === $data->order_status ) {
1287 $message = empty( $user_name ) ? __( 'Order marked as trash', 'tutor' ) : sprintf( __( 'Order marked as trash by %s', 'tutor' ), $user_name );
1288 }
1289
1290 // insert cancel reason in tutor_ordermeta table.
1291 if ( self::ORDER_CANCELLED === $data->order_status && ! empty( $data->cancel_reason ) ) {
1292 $meta_payload = new \stdClass();
1293 $meta_payload->order_id = $data->order_id;
1294 $meta_payload->meta_key = OrderActivitiesModel::META_KEY_CANCEL_REASON;
1295 $meta_payload->meta_value = $data->cancel_reason;
1296
1297 $order_activities_model = new OrderActivitiesModel();
1298 $order_activities_model->add_order_meta( $meta_payload );
1299 }
1300
1301 if ( $message ) {
1302 $value = wp_json_encode(
1303 array(
1304 'message' => $message,
1305 )
1306 );
1307 OrderActivitiesController::store_order_activity( $data->order_id, OrderActivitiesModel::META_KEY_HISTORY, $value );
1308 }
1309 }
1310
1311 return $response;
1312 }
1313
1314 /**
1315 * Calculate discount amount.
1316 *
1317 * @since 3.0.0
1318 *
1319 * @param string $discount_type The type of discount ('percent' or 'flat').
1320 * @param float $discount_amount The amount of discount to apply.
1321 * @param float $sub_total The subtotal amount before applying the discount.
1322 *
1323 * @return float discount amount.
1324 */
1325 public function calculate_discount_amount( $discount_type, $discount_amount, $sub_total ) {
1326 if ( 'percentage' === $discount_type ) {
1327 $discounted_price = (float) $sub_total * ( ( (float) $discount_amount / 100 ) );
1328 } else {
1329 $discounted_price = (float) $discount_amount;
1330 }
1331 return $discounted_price;
1332 }
1333
1334 /**
1335 * Retrieves the total refund amount for a given order.
1336 *
1337 * This method fetches all refund records for the specified order ID from the database,
1338 * calculates the total refund amount, and returns it. The refund records are retrieved
1339 * from the `tutor_ordermeta` table where the `meta_key` matches the refund meta keys.
1340 *
1341 * @since 3.0.0
1342 *
1343 * @param int $order_id The ID of the order for which the refund amount is to be calculated.
1344 *
1345 * @return float The total refund amount for the order.
1346 */
1347 public function get_refund_amount( $order_id ) {
1348 global $wpdb;
1349
1350 $table = $wpdb->prefix . 'tutor_ordermeta';
1351 $meta_keys = array( OrderActivitiesModel::META_KEY_REFUND, OrderActivitiesModel::META_KEY_PARTIALLY_REFUND );
1352
1353 $where = array(
1354 'meta_key' => $meta_keys,
1355 'order_id' => $order_id,
1356 );
1357 $refund_records = QueryHelper::get_all( $table, $where, 'created_at_gmt' );
1358
1359 $refund_amount = 0;
1360
1361 foreach ( $refund_records as $refund ) {
1362 $refund_data = json_decode( $refund->meta_value );
1363
1364 if ( ! empty( $refund_data->amount ) ) {
1365 $refund_amount += (float) $refund_data->amount;
1366 }
1367 }
1368
1369 return $refund_amount;
1370 }
1371
1372 /**
1373 * Get order status based on the payment status
1374 *
1375 * @since 3.0.0
1376 *
1377 * @param string $payment_status Order payment status.
1378 *
1379 * @return string
1380 */
1381 public function get_order_status_by_payment_status( $payment_status ) {
1382 $status = '';
1383
1384 switch ( $payment_status ) {
1385 case self::PAYMENT_PAID:
1386 $status = self::ORDER_COMPLETED;
1387 break;
1388 case self::PAYMENT_UNPAID:
1389 $status = self::ORDER_INCOMPLETE;
1390 break;
1391 case self::PAYMENT_PARTIALLY_REFUNDED:
1392 $status = self::ORDER_COMPLETED;
1393 break;
1394 case self::PAYMENT_REFUNDED:
1395 $status = self::ORDER_CANCELLED;
1396 break;
1397 case self::PAYMENT_FAILED:
1398 $status = self::ORDER_CANCELLED;
1399 break;
1400 case self::ORDER_TRASH:
1401 $status = self::ORDER_TRASH;
1402 break;
1403 case 'delete':
1404 $status = self::ORDER_CANCELLED;
1405 break;
1406 case self::ORDER_CANCELLED:
1407 $status = self::ORDER_CANCELLED;
1408 break;
1409 }
1410
1411 return $status;
1412 }
1413
1414 /**
1415 * Calculate order price
1416 *
1417 * @since 3.0.0
1418 *
1419 * @param array $items Order items, multi or single dimensional arr.
1420 *
1421 * @return object {subtotal => 10, total => 10}
1422 */
1423 public static function calculate_order_price( array $items ) {
1424 $subtotal = 0;
1425 $total = 0;
1426
1427 if ( isset( $items[0] ) ) {
1428 foreach ( $items as $item ) {
1429 $regular_price = tutor_get_locale_price( $item['regular_price'] );
1430 $sale_price = is_null( $item['sale_price'] ) || '' === $item['sale_price'] ? null : tutor_get_locale_price( $item['sale_price'] );
1431 $discount_price = is_null( $item['discount_price'] ) || '' === $item['discount_price'] ? null : tutor_get_locale_price( $item['discount_price'] );
1432
1433 // Subtotal is the original price (regular price).
1434 $item_subtotal = $regular_price;
1435 $item_total = $regular_price;
1436
1437 // Determine the total based on sale price and discount.
1438 if ( ! is_null( $sale_price ) && $sale_price < $regular_price ) {
1439 $item_subtotal = $sale_price;
1440 $item_total = $sale_price;
1441 } else {
1442 // If there's a discount, apply it to the total price.
1443 if ( ! is_null( $discount_price ) && $discount_price >= 0 ) {
1444 $item_total = max( 0, $discount_price ); // Ensure total doesn't go below 0.
1445 }
1446 }
1447
1448 // $subtotal += $item_subtotal;
1449 $subtotal += $regular_price;
1450 $total += $item_total;
1451 }
1452 } else {
1453 // for single dimensional array.
1454 $regular_price = tutor_get_locale_price( $items['regular_price'] );
1455 $sale_price = is_null( $items['sale_price'] ) || '' === $items['sale_price'] ? null : tutor_get_locale_price( $items['sale_price'] );
1456 $discount_price = is_null( $items['discount_price'] ) || '' === $items['discount_price'] ? null : tutor_get_locale_price( $items['discount_price'] );
1457
1458 // Subtotal is the original price (regular price).
1459 $item_subtotal = $regular_price;
1460 $item_total = $regular_price;
1461
1462 // Determine the total based on sale price and discount.
1463 if ( ! is_null( $sale_price ) && $sale_price < $regular_price ) {
1464 $item_subtotal = $sale_price;
1465 $item_total = $sale_price;
1466 } else {
1467 // If there's a discount, apply it to the total price.
1468 if ( ! is_null( $discount_price ) && $discount_price >= 0 ) {
1469 $item_total = max( 0, $discount_price ); // Ensure total doesn't go below 0.
1470 }
1471 }
1472
1473 // $subtotal = $item_subtotal;
1474 $subtotal = $regular_price;
1475 $total = $item_total;
1476 }
1477
1478 return (object) array(
1479 'subtotal' => tutor_get_locale_price( $subtotal ),
1480 'total' => tutor_get_locale_price( $total ),
1481 );
1482 }
1483
1484 /**
1485 * Check has exclusive type tax.
1486 *
1487 * @since 3.0.0
1488 *
1489 * @param object $order order object.
1490 *
1491 * @return boolean
1492 */
1493 public static function has_exclusive_tax( $order ) {
1494 return self::TAX_TYPE_EXCLUSIVE === $order->tax_type && $order->tax_rate > 0 && $order->tax_amount > 0;
1495 }
1496
1497 /**
1498 * Check has inclusive type tax.
1499 *
1500 * @since 3.0.0
1501 *
1502 * @param object $order order object.
1503 *
1504 * @return boolean
1505 */
1506 public static function has_inclusive_tax( $order ) {
1507 return self::TAX_TYPE_INCLUSIVE === $order->tax_type && $order->tax_rate > 0 && $order->tax_amount > 0;
1508 }
1509
1510 /**
1511 * Get an item
1512 *
1513 * @since 3.0.0
1514 *
1515 * @param integer $item_id Item id.
1516 *
1517 * @return mixed
1518 */
1519 public function get_item( int $item_id ) {
1520 return QueryHelper::get_row(
1521 $this->order_item_table,
1522 array(
1523 'item_id' => $item_id,
1524 ),
1525 'id'
1526 );
1527 }
1528
1529 /**
1530 * Get sellable price
1531 *
1532 * @since 3.0.0
1533 *
1534 * @param mixed $regular_price Regular price.
1535 * @param mixed $sale_price Sale price.
1536 * @param mixed $discount_price Discount price.
1537 *
1538 * @return float item sellable price
1539 */
1540 public static function get_item_sellable_price( $regular_price, $sale_price = null, $discount_price = null ) {
1541 // Ensure prices are numeric and properly formatted.
1542 $sellable_price = (
1543 ! empty( $sale_price )
1544 ? $sale_price
1545 : (
1546 ( ! is_null( $discount_price ) && '' !== $discount_price ) && $discount_price >= 0
1547 ? $discount_price
1548 : $regular_price
1549 )
1550 );
1551
1552 return $sellable_price;
1553 }
1554
1555 /**
1556 * Get item sold price
1557 *
1558 * @since 3.0.0
1559 *
1560 * @param mixed $item_id Item id.
1561 * @param bool $format Item id.
1562 *
1563 * @return mixed item sellable price
1564 */
1565 public static function get_item_sold_price( $item_id, $format = true ) {
1566 $item = ( new self() )->get_item( $item_id );
1567
1568 if ( $item ) {
1569 $sold_price = self::get_item_sellable_price( $item->regular_price, $item->sale_price, $item->discount_price );
1570
1571 return $format ? tutor_get_formatted_price( $sold_price ) : $sold_price;
1572 }
1573
1574 return 0;
1575 }
1576
1577 /**
1578 * Should show pay btn to the user
1579 *
1580 * @since 3.0.0
1581 *
1582 * @param object $order Order object.
1583 *
1584 * @return boolean
1585 */
1586 public static function should_show_pay_btn( object $order ) {
1587 $order_items = ( new self() )->get_order_items_by_id( $order->id );
1588 $is_enrolled_any_course = false;
1589 $is_incomplete_payment = ! empty( $order->payment_method ) && self::ORDER_INCOMPLETE === $order->order_status;
1590 $is_manual_payment = $order->payment_method ? self::is_manual_payment( $order->payment_method ) : true;
1591
1592 if ( $is_incomplete_payment && ! $is_manual_payment && $order_items ) {
1593 if ( self::TYPE_SINGLE_ORDER === $order->order_type ) {
1594 foreach ( $order_items as $item ) {
1595 $course_id = $item->id;
1596 if ( $course_id ) {
1597 $is_enrolled = tutor_utils()->is_enrolled( $course_id );
1598 if ( $is_enrolled ) {
1599 $is_enrolled_any_course = true;
1600 break;
1601 }
1602 }
1603 }
1604 } else {
1605 if ( tutor_utils()->count( $order_items ) ) {
1606 $course_id = apply_filters( 'tutor_subscription_course_by_plan', $order_items[0]->id );
1607 if ( tutor_utils()->is_enrolled( $course_id ) ) {
1608 $is_enrolled_any_course = true;
1609 }
1610 }
1611 }
1612 }
1613
1614 return apply_filters( 'tutor_should_show_pay_btn', $is_incomplete_payment && ! $is_manual_payment && ! $is_enrolled_any_course );
1615 }
1616
1617 /**
1618 * Check is manual payment
1619 *
1620 * @since 3.0.0
1621 *
1622 * @param string $method_name Payment method name.
1623 *
1624 * @return boolean
1625 */
1626 public static function is_manual_payment( $method_name ) {
1627 $payment_methods = tutor_get_manual_payment_gateways();
1628
1629 $is_manual_payment = false;
1630 foreach ( $payment_methods as $payment_method ) {
1631 $is_manual_payment = $payment_method->name === $method_name;
1632 }
1633
1634 return $is_manual_payment;
1635 }
1636
1637 /**
1638 * Render pay button
1639 *
1640 * @since 3.0.1
1641 *
1642 * @param int|object $order Order id or object.
1643 *
1644 * @return void
1645 */
1646 public static function render_pay_button( $order ) {
1647 if ( is_numeric( $order ) ) {
1648 $order = ( new self() )->get_order_by_id( $order );
1649 }
1650
1651 if ( self::should_show_pay_btn( $order ) ) {
1652 ?>
1653 <form method="post">
1654 <?php tutor_nonce_field(); ?>
1655 <input type="hidden" name="tutor_action" value="tutor_pay_incomplete_order">
1656 <input type="hidden" name="order_id" value="<?php echo esc_attr( $order->id ); ?>">
1657 <button type="submit" class="tutor-btn tutor-btn-sm tutor-btn-outline-primary">
1658 <?php esc_html_e( 'Pay', 'tutor' ); ?>
1659 </button>
1660 </form>
1661 <?php
1662 }
1663 }
1664 }
1665