PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 3.1.0
Tutor LMS – eLearning and online course solution v3.1.0
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 1 year 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
1696 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 * Get order data
391 *
392 * @since 3.1.0
393 *
394 * @param int|object $order order id or object.
395 *
396 * @return object
397 */
398 public static function get_order( $order ) {
399 if ( is_numeric( $order ) ) {
400 $order = ( new self() )->get_order_by_id( $order );
401 }
402
403 return $order;
404 }
405
406 /**
407 * Check order is subscription order
408 *
409 * @since 3.1.0
410 *
411 * @param int|object $order order id or object.
412 *
413 * @return boolean
414 */
415 public static function is_subscription_order( $order ) {
416 $order = self::get_order( $order );
417 return $order && self::TYPE_SUBSCRIPTION === $order->order_type;
418 }
419
420 /**
421 * Mark order Unpaid to Paid.
422 *
423 * @since 3.0.0
424 *
425 * @param int $order_id order id.
426 * @param string $note note.
427 * @param bool $trigger_hooks trigger hooks or not.
428 *
429 * @return bool
430 */
431 public function mark_as_paid( $order_id, $note = '', $trigger_hooks = true ) {
432 if ( $trigger_hooks ) {
433 do_action( 'tutor_before_order_mark_as_paid', $order_id );
434 }
435
436 $data = array(
437 'payment_status' => self::PAYMENT_PAID,
438 'order_status' => self::ORDER_COMPLETED,
439 'note' => $note,
440 );
441
442 $response = $this->update_order( $order_id, $data );
443 if ( ! $response ) {
444 return false;
445 }
446
447 if ( $trigger_hooks ) {
448 do_action( 'tutor_order_payment_status_changed', $order_id, self::PAYMENT_UNPAID, self::PAYMENT_PAID );
449
450 $order = $this->get_order_by_id( $order_id );
451 $discount_amount = $this->calculate_discount_amount( $order->discount_type, $order->discount_amount, $order->subtotal_price );
452 do_action( 'tutor_after_order_mark_as_paid', $order, $discount_amount );
453 }
454
455 return true;
456 }
457
458
459 /**
460 * Retrieve order items by order ID.
461 *
462 * This function fetches order item details from the database based on the given
463 * order ID. It queries the 'tutor_order_items' table and joins it with the 'posts'
464 * table to get the course titles associated with each order item.
465 *
466 * The function then returns the retrieved order items, or an empty array if no
467 * items are found.
468 *
469 * @since 3.0.0
470 *
471 * @global wpdb $wpdb WordPress database abstraction object.
472 *
473 * @param int $order_id The ID of the order to retrieve items for.
474 *
475 * @return array The order items, each containing details and course titles, or an empty array if no items are found.
476 */
477 public function get_order_items_by_id( $order_id ) {
478 global $wpdb;
479
480 $primary_table = "{$wpdb->prefix}tutor_order_items AS oi";
481 $joining_tables = array(
482 array(
483 'type' => 'LEFT',
484 'table' => "{$wpdb->prefix}posts AS p",
485 'on' => 'p.ID = oi.item_id',
486 ),
487 );
488
489 $where = array( 'order_id' => $order_id );
490
491 $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' );
492
493 $courses_data = QueryHelper::get_joined_data( $primary_table, $joining_tables, $select_columns, $where, array(), 'id', 0, 0 );
494 $courses = $courses_data['results'];
495
496 if ( tutor()->has_pro ) {
497 $bundle_model = new \TutorPro\CourseBundle\Models\BundleModel();
498 }
499
500 if ( ! empty( $courses_data['total_count'] ) ) {
501 foreach ( $courses as &$course ) {
502 if ( tutor()->has_pro && 'course-bundle' === $course->type ) {
503 $course->total_courses = count( $bundle_model->get_bundle_course_ids( $course->id ) );
504 }
505
506 $course->id = (int) $course->id;
507 $course->regular_price = (float) $course->regular_price;
508 $course->sale_price = (float) $course->sale_price;
509 $course->image = get_the_post_thumbnail_url( $course->id );
510 }
511 }
512
513 unset( $course );
514
515 return ! empty( $courses ) ? $courses : array();
516 }
517
518 /**
519 * Retrieve tutor customer data by user ID.
520 *
521 * This function fetches customer data from the 'tutor_customers' table based on
522 * the given user ID. It utilizes a helper function from the QueryHelper class
523 * to perform the database query.
524 *
525 * The function returns the customer data as an object.
526 *
527 * @global wpdb $wpdb WordPress database abstraction object.
528 *
529 * @param int $user_id The ID of the user to retrieve customer data for.
530 *
531 * @since 3.0.0
532 *
533 * @return object|null The customer data retrieved from the database.
534 */
535 public function get_tutor_customer_data( $user_id ) {
536 global $wpdb;
537
538 // Retrieve customer data for the given user ID from the 'tutor_customers' table.
539 $customer_data = QueryHelper::get_row( "{$wpdb->prefix}tutor_customers", array( 'user_id' => $user_id ), 'id' );
540
541 if ( empty( $customer_data ) ) {
542 return null;
543 }
544
545 $return_data = (object) array(
546 'id' => $customer_data->id,
547 'user_id' => $customer_data->user_id,
548 'name' => $customer_data->billing_first_name . ' ' . $customer_data->billing_last_name,
549 'email' => $customer_data->billing_email,
550 'phone' => $customer_data->billing_phone,
551 'address' => $customer_data->billing_address,
552 'city' => $customer_data->billing_city,
553 'state' => $customer_data->billing_state,
554 'country' => $customer_data->billing_country,
555 'zip_code' => $customer_data->billing_zip_code,
556 );
557
558 return $return_data;
559 }
560
561 /**
562 * Retrieve order refunds by order ID.
563 *
564 * This function fetches all order refunds from the 'tutor_ordermeta' table
565 * based on the given order ID and the 'refund' meta key. It uses a helper
566 * function from the QueryHelper class to perform the database query.
567 *
568 * If no order refunds are found, the function returns an empty array.
569 * Otherwise, it decodes the JSON-encoded meta values and returns them as an array.
570 *
571 * @global wpdb $wpdb WordPress database abstraction object.
572 *
573 * @param int $order_id The ID of the order to retrieve refunds for.
574 *
575 * @since 3.0.0
576 *
577 * @return array An array of order refunds, each decoded from its JSON representation.
578 */
579 public function get_order_refunds( $order_id ) {
580 global $wpdb;
581
582 $meta_keys = array(
583 OrderActivitiesModel::META_KEY_REFUND,
584 OrderActivitiesModel::META_KEY_PARTIALLY_REFUND,
585 );
586
587 // Retrieve order refunds for the given order ID from the 'tutor_ordermeta' table.
588 $order_refunds = QueryHelper::get_all(
589 "{$wpdb->prefix}tutor_ordermeta",
590 array(
591 'order_id' => $order_id,
592 'meta_key' => $meta_keys,
593 ),
594 'created_at_gmt',
595 1000,
596 'ASC'
597 );
598
599 if ( empty( $order_refunds ) ) {
600 return array();
601 }
602
603 $response = array();
604
605 foreach ( $order_refunds as $refund ) {
606 $parsed_meta_value = json_decode( $refund->meta_value );
607 $values = new \stdClass();
608 $values->id = (int) $refund->id;
609
610 foreach ( $parsed_meta_value as $key => $value ) {
611 $values->$key = $value;
612 }
613
614 $values->date = $refund->created_at_gmt;
615
616 $response[] = $values;
617 }
618
619 // Custom comparison function for sorting by date.
620 usort(
621 $response,
622 function ( $a, $b ) {
623 $date_a = strtotime( $a->date );
624 $date_b = strtotime( $b->date );
625
626 return $date_b - $date_a;
627 }
628 );
629
630 return $response;
631 }
632
633 /**
634 * Update an order
635 *
636 * @since 3.0.0
637 *
638 * @param int|array $order_id Integer or array of ids sql escaped.
639 * @param array $data Data to update, escape data.
640 *
641 * @return bool
642 */
643 public function update_order( $order_id, array $data ) {
644 $order_id = is_array( $order_id ) ? $order_id : array( $order_id );
645 $order_id = QueryHelper::prepare_in_clause( $order_id );
646 try {
647 QueryHelper::update_where_in(
648 $this->table_name,
649 $data,
650 $order_id
651 );
652 return true;
653 } catch ( \Throwable $th ) {
654 error_log( $th->getMessage() . ' in ' . $th->getFile() . ' at line ' . $th->getLine() );
655 return false;
656 }
657 }
658
659 /**
660 * Get enrollment ids by order id.
661 *
662 * @since 3.0.0
663 *
664 * @param int $order_id order id.
665 *
666 * @return array
667 */
668 public function get_enrollment_ids( $order_id ) {
669 global $wpdb;
670 $enrollment_ids = array();
671
672 $enrollments = $wpdb->get_results(
673 $wpdb->prepare(
674 "SELECT * FROM {$wpdb->postmeta}
675 WHERE meta_key=%s
676 AND meta_value LIKE %d",
677 '_tutor_enrolled_by_order_id',
678 $order_id
679 )
680 );
681
682 if ( $enrollments ) {
683 $enrollment_ids = array_column( $enrollments, 'post_id' );
684 }
685
686 return $enrollment_ids;
687 }
688
689 /**
690 * Delete an order by order ID.
691 *
692 * This function deletes an order from the 'tutor_orders' table based on the given
693 * order ID. It uses the QueryHelper class to perform the database delete operation.
694 *
695 * @since 3.0.0
696 *
697 * @param int|array $order_id The ID of the order to delete.
698 *
699 * @return bool
700 */
701 public function delete_order( $order_id ) {
702 global $wpdb;
703 $order_ids = is_array( $order_id ) ? $order_id : array( intval( $order_id ) );
704
705 try {
706 $wpdb->query( 'START TRANSACTION' );
707
708 foreach ( $order_ids as $order_id ) {
709 // Delete enrollments if exist.
710 $enrollment_ids = $this->get_enrollment_ids( $order_id );
711 if ( $enrollment_ids ) {
712 QueryHelper::bulk_delete_by_ids( $wpdb->posts, $enrollment_ids );
713 // After enrollment delete, delete the course progress.
714 foreach ( $enrollment_ids as $enrollment_id ) {
715 $course_id = get_post_field( 'post_parent', $enrollment_id );
716 $student_id = get_post_field( 'post_author', $enrollment_id );
717
718 if ( $course_id && $student_id ) {
719 tutor_utils()->delete_course_progress( $course_id, $student_id );
720 }
721 }
722 }
723
724 // Delete earnings.
725 QueryHelper::delete(
726 $wpdb->prefix . 'tutor_earnings',
727 array(
728 'order_id' => $order_id,
729 'process_by' => 'Tutor',
730 )
731 );
732
733 // Now delete order.
734 QueryHelper::delete( $this->table_name, array( 'id' => $order_id ) );
735 }
736
737 $wpdb->query( 'COMMIT' );
738 return true;
739
740 } catch ( \Throwable $th ) {
741 $wpdb->query( 'ROLLBACK' );
742 return false;
743 }
744 }
745
746 /**
747 * Get orders list
748 *
749 * @since 3.0.0
750 *
751 * @param array $where where clause conditions.
752 * @param string $search_term search clause conditions.
753 * @param int $limit limit default 10.
754 * @param int $offset default 0.
755 * @param string $order_by column default 'o.id'.
756 * @param string $order list order default 'desc'.
757 *
758 * @return array
759 */
760 public function get_orders( array $where = array(), $search_term = '', int $limit = 10, int $offset = 0, string $order_by = 'o.id', string $order = 'desc' ) {
761
762 global $wpdb;
763
764 $primary_table = "{$this->table_name} o";
765 $joining_tables = array(
766 array(
767 'type' => 'LEFT',
768 'table' => "{$wpdb->users} u",
769 'on' => 'o.user_id = u.ID',
770 ),
771 );
772
773 $select_columns = array( 'o.*', 'u.user_login' );
774
775 $search_clause = array();
776 if ( '' !== $search_term ) {
777 foreach ( $this->get_searchable_fields() as $column ) {
778 $search_clause[ $column ] = $search_term;
779 }
780 }
781
782 $response = array(
783 'results' => array(),
784 'total_count' => 0,
785 );
786
787 try {
788 return QueryHelper::get_joined_data( $primary_table, $joining_tables, $select_columns, $where, $search_clause, $order_by, $limit, $offset, $order );
789 } catch ( \Throwable $th ) {
790 // Log with error, line & file name.
791 error_log( $th->getMessage() . ' in ' . $th->getFile() . ' at line ' . $th->getLine() );
792 return $response;
793 }
794 }
795
796 /**
797 * Get order count
798 *
799 * @since 3.0.0
800 *
801 * @param array $where Where conditions, sql esc data.
802 * @param string $search_term Search terms, sql esc data.
803 *
804 * @return int
805 */
806 public function get_order_count( $where = array(), string $search_term = '' ) {
807 global $wpdb;
808
809 $search_clause = array();
810 if ( '' !== $search_term ) {
811 foreach ( $this->get_searchable_fields() as $column ) {
812 $search_clause[ $column ] = $search_term;
813 }
814 }
815
816 $join_table = array(
817 array(
818 'type' => 'INNER',
819 'table' => "{$wpdb->users} u",
820 'on' => 'o.user_id = u.ID',
821 ),
822 );
823 $primary_table = "{$this->table_name} o";
824 return QueryHelper::get_joined_count( $primary_table, $join_table, $where, $search_clause );
825 }
826
827 /**
828 * Get order of a user
829 *
830 * @since 3.0.0
831 *
832 * @param string $time_period $time_period Sorting time period,
833 * supported time periods are: today, monthly & yearly.
834 * @param string $start_date $start_date For date range sorting.
835 * @param string $end_date $end_date For date range sorting.
836 * @param int $user_id User id for fetching order list.
837 * @param int $limit Limit to fetch record.
838 * @param int $offset Offset to fetch record.
839 *
840 * @throws \Exception Throw exception if database error occur.
841 *
842 * @return array
843 */
844 public function get_user_orders( $time_period = null, $start_date = null, $end_date = null, int $user_id = 0, $limit = 10, int $offset = 0 ) {
845 $user_id = $user_id ? $user_id : get_current_user_id();
846
847 $response = array(
848 'results' => array(),
849 'total_count' => 0,
850 );
851
852 global $wpdb;
853
854 $time_period_clause = '';
855 $date_range_clause = '';
856
857 if ( $start_date && $end_date ) {
858 $date_range_clause = $wpdb->prepare( 'AND DATE(created_at_gmt) BETWEEN %s AND %s', $start_date, $end_date );
859 } else {
860 if ( $time_period ) {
861 if ( 'today' === $time_period ) {
862 $time_period_clause = 'AND DATE(o.created_at_gmt) = CURDATE()';
863 } elseif ( 'monthly' === $time_period ) {
864 $time_period_clause = 'AND MONTH(o.created_at_gmt) = MONTH(CURDATE()) ';
865 } else {
866 $time_period_clause = 'AND YEAR(o.created_at_gmt) = YEAR(CURDATE()) ';
867 }
868 }
869 }
870
871 $query = $wpdb->prepare(
872 "SELECT
873 SQL_CALC_FOUND_ROWS
874 o.*
875 FROM $this->table_name AS o
876 WHERE o.user_id = %d
877 {$time_period_clause}
878 {$date_range_clause}
879 ORDER BY o.id DESC
880 LIMIT %d OFFSET %d
881 ",
882 $user_id,
883 $limit,
884 $offset
885 );
886
887 $results = $wpdb->get_results( $query );
888
889 if ( $wpdb->last_error ) {
890 throw new \Exception( $wpdb->last_error );
891 } else {
892 $response['results'] = $results;
893 $response['total_count'] = is_array( $results ) && count( $results ) ? (int) $wpdb->get_var( 'SELECT FOUND_ROWS()' ) : 0;
894 }
895
896 return $response;
897 }
898
899 /**
900 * Get total discounts by user_id (instructor), optionally can set period ( today | monthly| yearly )
901 *
902 * Optionally can set start date & end date to get enrollment list from date range
903 *
904 * If period or date range not pass then it will return all time enrollment list
905 *
906 * @since 3.0.0
907 *
908 * @param int $user_id User id, if user not have admin access
909 * then only this user's refund amount will fetched.
910 * @param string $period Time period.
911 * @param string $start_date Start date.
912 * @param string $end_date End date.
913 * @param int $course_id Course id.
914 *
915 * @return array
916 */
917 public function get_discounts_by_user( int $user_id, string $period = '', $start_date = '', string $end_date = '', int $course_id = 0 ): array {
918 $response = array(
919 'discounts' => array(),
920 'total_discounts' => 0,
921 );
922
923 global $wpdb;
924
925 $user_clause = '';
926 $date_range_clause = '';
927 $period_clause = '';
928 $course_clause = '';
929 $group_clause = ' GROUP BY DATE(date_format) ';
930 $discount_clause = 'o.coupon_amount as total';
931
932 if ( $start_date && $end_date ) {
933 $date_range_clause = $wpdb->prepare(
934 'AND o.created_at_gmt BETWEEN %s AND %s',
935 $start_date,
936 $end_date
937 );
938 } else {
939 $period_clause = QueryHelper::get_period_clause( 'o.created_at_gmt', $period );
940 }
941
942 if ( 'today' !== $period ) {
943 $group_clause = ' GROUP BY MONTH(date_format) ';
944 }
945
946 if ( $course_id ) {
947 $course_clause = $wpdb->prepare( 'AND i.item_id = %d', $course_id );
948 $discount_clause = 'i.regular_price - i.discount_price AS total';
949 }
950
951 $item_table = $wpdb->prefix . 'tutor_order_items';
952
953 if ( $course_id ) {
954 if ( $user_id ) {
955 $user_clause = $wpdb->prepare( 'AND c.post_author = %d', $user_id );
956 }
957
958 $discounts = $wpdb->get_results(
959 $wpdb->prepare(
960 "SELECT
961 i.item_id AS course_id,
962 SUM(
963 COALESCE(o.coupon_amount, 0) +
964 COALESCE(
965 IF(
966 o.discount_type = 'percentage',
967 COALESCE(o.subtotal_price * (o.discount_amount / 100), 0),
968 COALESCE(o.discount_amount, 0)
969 ),
970 0
971 )
972 ) AS total,
973 o.created_at_gmt AS date_format
974 FROM
975 {$this->table_name} o
976 JOIN
977 {$item_table} i ON o.id = i.order_id
978 JOIN
979 {$wpdb->posts} c
980 ON c.ID = i.item_id
981 AND c.post_type = %s
982 WHERE
983 1 = 1
984 AND i.item_id = %d
985 {$user_clause}
986 {$period_clause}
987 {$date_range_clause}
988 {$group_clause}
989 ",
990 tutor()->course_post_type,
991 $course_id
992 )
993 );
994 } else {
995 if ( $user_id ) {
996 $user_clause = $wpdb->prepare( "AND %d = (SELECT user_id FROM {$wpdb->tutor_earnings} WHERE order_status = 'completed' LIMIT 1) ", $user_id );
997 }
998
999 $discounts = $wpdb->get_results(
1000 $wpdb->prepare(
1001 "SELECT
1002 SUM(
1003 COALESCE(o.coupon_amount, 0) +
1004 COALESCE(
1005 IF(
1006 o.discount_type = 'percentage',
1007 COALESCE(o.subtotal_price * (o.discount_amount / 100), 0),
1008 COALESCE(o.discount_amount, 0)
1009 ),
1010 0
1011 )
1012 ) AS total,
1013 o.created_at_gmt AS date_format
1014 FROM {$this->table_name} AS o
1015 WHERE 1 = %d
1016 AND o.order_status = 'completed'
1017 {$user_clause}
1018 {$period_clause}
1019 {$date_range_clause}
1020 {$course_clause}
1021 {$group_clause}
1022 HAVING total > 0
1023 ",
1024 1
1025 )
1026 );
1027 }
1028
1029 $total_discount = 0;
1030 $discount_items = array();
1031
1032 $response = array(
1033 'discounts' => array(),
1034 'total_discounts' => 0,
1035 );
1036
1037 if ( $discounts ) {
1038 foreach ( $discounts as $discount ) {
1039 $total_discount += $discount->total;
1040 $discount_items[] = $discount;
1041
1042 // Split each discount.
1043 list( $admin_discount, $instructor_discount ) = array_values( tutor_split_amounts( $discount->total ) );
1044
1045 $discount->total = is_admin() ? $admin_discount : $instructor_discount;
1046 }
1047
1048 list( $admin_total, $instructor_total ) = array_values( tutor_split_amounts( $total_discount ) );
1049
1050 $response['discounts'] = $discount_items;
1051 $response['total_discounts'] = is_admin() ? $admin_total : $instructor_total;
1052 }
1053
1054 return $response;
1055 }
1056
1057 /**
1058 * Get total refunds by user_id (instructor), optionally can set period ( today | monthly| yearly )
1059 *
1060 * Optionally can set start date & end date to get enrollment list from date range
1061 *
1062 * If period or date range not pass then it will return all time enrollment list
1063 *
1064 * @since 3.0.0
1065 *
1066 * @param int $user_id User id, if user not have admin access
1067 * then only this user's refund amount will fetched.
1068 * @param string $period Time period.
1069 * @param string $start_date Start date.
1070 * @param string $end_date End date.
1071 * @param int $course_id Course id.
1072 *
1073 * @return array
1074 */
1075 public function get_refunds_by_user( int $user_id, string $period = '', $start_date = '', string $end_date = '', int $course_id = 0 ): array {
1076 $response = array(
1077 'refunds' => array(),
1078 'total_refunds' => 0,
1079 );
1080
1081 global $wpdb;
1082
1083 $user_clause = '';
1084 $date_range_clause = '';
1085 $period_clause = '';
1086 $course_clause = '';
1087 $commission_clause = '';
1088 $group_clause = ' GROUP BY DATE(o.created_at_gmt) ';
1089
1090 if ( $start_date && $end_date ) {
1091 $date_range_clause = $wpdb->prepare(
1092 'AND o.created_at_gmt BETWEEN %s AND %s',
1093 $start_date,
1094 $end_date
1095 );
1096 $group_clause = ' GROUP BY DATE(o.created_at_gmt) ';
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(o.created_at_gmt) ';
1104 }
1105
1106 if ( $course_id ) {
1107 if ( $user_id ) {
1108 $user_clause = $wpdb->prepare( 'AND c.post_author = %d', $user_id );
1109 }
1110 } else {
1111 if ( $user_id ) {
1112 $user_clause = $wpdb->prepare( 'AND c.post_author = %d', $user_id );
1113 }
1114 }
1115
1116 // Refund query logic remains the same.
1117 $item_table = $wpdb->prefix . 'tutor_order_items';
1118
1119 if ( $course_id ) {
1120 $refunds = $wpdb->get_results(
1121 $wpdb->prepare(
1122 "SELECT
1123 i.item_id AS course_id,
1124 ROUND(
1125 SUM(
1126 o.refund_amount *
1127 (
1128 CASE
1129 WHEN i.discount_price THEN i.discount_price
1130 WHEN i.sale_price > 0 THEN i.sale_price
1131 ELSE i.regular_price
1132 END / o.total_price
1133 )
1134 ), 2
1135 ) AS total
1136 FROM
1137 {$this->table_name} o
1138 JOIN
1139 {$item_table} i ON o.id = i.order_id
1140 JOIN
1141 {$wpdb->posts} c
1142 ON c.ID = i.item_id
1143 AND c.post_type = %s
1144 WHERE
1145 o.refund_amount > 0
1146 AND i.item_id = %d
1147 {$user_clause}
1148 {$period_clause}
1149 {$date_range_clause}
1150 {$group_clause},
1151 i.item_id
1152 ",
1153 tutor()->course_post_type,
1154 $course_id
1155 )
1156 );
1157 } else {
1158 $earning_table = $wpdb->tutor_earnings;
1159 if ( $user_id ) {
1160 $user_clause = "AND {$user_id} = (SELECT user_id FROM {$earning_table} LIMIT 1)";
1161 }
1162
1163 $refunds = $wpdb->get_results(
1164 $wpdb->prepare(
1165 "SELECT
1166 COALESCE(SUM(o.refund_amount), 0) AS total,
1167 created_at_gmt AS date_format
1168 FROM {$this->table_name} AS o
1169 -- LEFT JOIN {$item_table} AS i ON i.order_id = o.id
1170 -- LEFT JOIN {$wpdb->posts} AS c ON c.id = i.item_id
1171 WHERE 1 = %d
1172 AND o.refund_amount > %d
1173 {$user_clause}
1174 {$period_clause}
1175 {$date_range_clause}
1176 {$group_clause},
1177 o.id",
1178 1,
1179 0
1180 )
1181 );
1182 }
1183
1184 $total_refund = 0;
1185
1186 foreach ( $refunds as $refund ) {
1187 $total_refund += $refund->total;
1188
1189 // Update total amount from list.
1190 $split_refund = (object) tutor_split_amounts( $refund->total );
1191 $refund->total = is_admin() ? $split_refund->admin : $split_refund->instructor;
1192 }
1193
1194 $split_total_refund = (object) tutor_split_amounts( $total_refund );
1195
1196 $response = array(
1197 'refunds' => $refunds,
1198 'total_refunds' => is_admin() ? $split_total_refund->admin : $split_total_refund->instructor,
1199 );
1200
1201 return $response;
1202 }
1203
1204 /**
1205 * Update the payment status of an order.
1206 *
1207 * This function updates the payment status and note of an order in the database.
1208 * It uses the QueryHelper class to perform the update operation.
1209 *
1210 * @since 3.0.0
1211 *
1212 * @param object $data An object containing the payment status, note, and order ID.
1213 * - 'payment_status' (string): The new payment status.
1214 * - 'note' (string): A note regarding the payment status update.
1215 * - 'order_id' (int): The ID of the order to update.
1216 *
1217 * @return bool True on successful update, false on failure.
1218 */
1219 public function payment_status_update( object $data ) {
1220 $response = QueryHelper::update(
1221 $this->table_name,
1222 array(
1223 'payment_status' => $data->payment_status,
1224 'note' => $data->note,
1225 ),
1226 array( 'id' => $data->order_id )
1227 );
1228
1229 if ( $response ) {
1230 $activity_controller = new OrderActivitiesController();
1231 $activity_controller->store_order_activity_for_marked_as_paid( $data->order_id );
1232 }
1233
1234 return $response;
1235 }
1236
1237 /**
1238 * Add a discount to an order.
1239 *
1240 * This function updates the order in the database with the provided discount details.
1241 * It updates the discount type, discount amount, and discount reason for the given order ID.
1242 *
1243 * @since 3.0.0
1244 *
1245 * @param object $data An object containing the discount details:
1246 * - $data->order_id (int) The ID of the order.
1247 * - $data->discount_type (string) The type of the discount.
1248 * - $data->discount_amount(float) The amount of the discount.
1249 * - $data->discount_reason(string) The reason for the discount.
1250 *
1251 * @return bool True on successful update, false on failure.
1252 */
1253 public function add_order_discount( object $data ) {
1254 $response = QueryHelper::update(
1255 $this->table_name,
1256 array(
1257 'discount_type' => $data->discount_type,
1258 'discount_amount' => $data->discount_amount,
1259 'discount_reason' => $data->discount_reason,
1260 ),
1261 array( 'id' => $data->order_id )
1262 );
1263
1264 return $response;
1265 }
1266
1267 /**
1268 * Updates the status of an order and logs the activity.
1269 *
1270 * This function updates the status of an order in the database and, if successful, logs the activity
1271 * with a message indicating the status change. The message includes the current user's display name,
1272 * if available.
1273 *
1274 * The possible order statuses include:
1275 * - ORDER_CANCELLED
1276 * - ORDER_COMPLETED
1277 * - ORDER_INCOMPLETE
1278 * - ORDER_TRASH
1279 *
1280 * If the update is successful, an order activity log entry is created with the current date, time,
1281 * and status change message.
1282 *
1283 * @since 3.0.0
1284 *
1285 * @param object $data An object containing:
1286 * - int $order_id The ID of the order to update.
1287 * - string $order_status The new status of the order.
1288 * - string $cancel_reason The reason for the order cancellation (optional).
1289 *
1290 * @return bool True on successful update, false on failure.
1291 */
1292 public function order_status_update( object $data ) {
1293 $response = QueryHelper::update(
1294 $this->table_name,
1295 array(
1296 'order_status' => $data->order_status,
1297 ),
1298 array( 'id' => $data->order_id )
1299 );
1300
1301 if ( $response ) {
1302 $user_name = '';
1303 $current_user = wp_get_current_user();
1304
1305 if ( $current_user->exists() ) {
1306 $user_name = $current_user->display_name;
1307 }
1308
1309 $message = '';
1310
1311 if ( self::ORDER_CANCELLED === $data->order_status ) {
1312 $message = empty( $user_name ) ? __( 'Order marked as cancelled', 'tutor' ) : sprintf( __( 'Order marked as cancelled by %s', 'tutor' ), $user_name );
1313 } elseif ( self::ORDER_COMPLETED === $data->order_status ) {
1314 $message = empty( $user_name ) ? __( 'Order marked as completed', 'tutor' ) : sprintf( __( 'Order marked as completed by %s', 'tutor' ), $user_name );
1315 } elseif ( self::ORDER_INCOMPLETE === $data->order_status ) {
1316 $message = empty( $user_name ) ? __( 'Order marked as incomplete', 'tutor' ) : sprintf( __( 'Order marked as incomplete by %s', 'tutor' ), $user_name );
1317 } elseif ( self::ORDER_TRASH === $data->order_status ) {
1318 $message = empty( $user_name ) ? __( 'Order marked as trash', 'tutor' ) : sprintf( __( 'Order marked as trash by %s', 'tutor' ), $user_name );
1319 }
1320
1321 // insert cancel reason in tutor_ordermeta table.
1322 if ( self::ORDER_CANCELLED === $data->order_status && ! empty( $data->cancel_reason ) ) {
1323 $meta_payload = new \stdClass();
1324 $meta_payload->order_id = $data->order_id;
1325 $meta_payload->meta_key = OrderActivitiesModel::META_KEY_CANCEL_REASON;
1326 $meta_payload->meta_value = $data->cancel_reason;
1327
1328 $order_activities_model = new OrderActivitiesModel();
1329 $order_activities_model->add_order_meta( $meta_payload );
1330 }
1331
1332 if ( $message ) {
1333 $value = wp_json_encode(
1334 array(
1335 'message' => $message,
1336 )
1337 );
1338 OrderActivitiesController::store_order_activity( $data->order_id, OrderActivitiesModel::META_KEY_HISTORY, $value );
1339 }
1340 }
1341
1342 return $response;
1343 }
1344
1345 /**
1346 * Calculate discount amount.
1347 *
1348 * @since 3.0.0
1349 *
1350 * @param string $discount_type The type of discount ('percent' or 'flat').
1351 * @param float $discount_amount The amount of discount to apply.
1352 * @param float $sub_total The subtotal amount before applying the discount.
1353 *
1354 * @return float discount amount.
1355 */
1356 public function calculate_discount_amount( $discount_type, $discount_amount, $sub_total ) {
1357 if ( 'percentage' === $discount_type ) {
1358 $discounted_price = (float) $sub_total * ( ( (float) $discount_amount / 100 ) );
1359 } else {
1360 $discounted_price = (float) $discount_amount;
1361 }
1362 return $discounted_price;
1363 }
1364
1365 /**
1366 * Retrieves the total refund amount for a given order.
1367 *
1368 * This method fetches all refund records for the specified order ID from the database,
1369 * calculates the total refund amount, and returns it. The refund records are retrieved
1370 * from the `tutor_ordermeta` table where the `meta_key` matches the refund meta keys.
1371 *
1372 * @since 3.0.0
1373 *
1374 * @param int $order_id The ID of the order for which the refund amount is to be calculated.
1375 *
1376 * @return float The total refund amount for the order.
1377 */
1378 public function get_refund_amount( $order_id ) {
1379 global $wpdb;
1380
1381 $table = $wpdb->prefix . 'tutor_ordermeta';
1382 $meta_keys = array( OrderActivitiesModel::META_KEY_REFUND, OrderActivitiesModel::META_KEY_PARTIALLY_REFUND );
1383
1384 $where = array(
1385 'meta_key' => $meta_keys,
1386 'order_id' => $order_id,
1387 );
1388 $refund_records = QueryHelper::get_all( $table, $where, 'created_at_gmt' );
1389
1390 $refund_amount = 0;
1391
1392 foreach ( $refund_records as $refund ) {
1393 $refund_data = json_decode( $refund->meta_value );
1394
1395 if ( ! empty( $refund_data->amount ) ) {
1396 $refund_amount += (float) $refund_data->amount;
1397 }
1398 }
1399
1400 return $refund_amount;
1401 }
1402
1403 /**
1404 * Get order status based on the payment status
1405 *
1406 * @since 3.0.0
1407 *
1408 * @param string $payment_status Order payment status.
1409 *
1410 * @return string
1411 */
1412 public function get_order_status_by_payment_status( $payment_status ) {
1413 $status = '';
1414
1415 switch ( $payment_status ) {
1416 case self::PAYMENT_PAID:
1417 $status = self::ORDER_COMPLETED;
1418 break;
1419 case self::PAYMENT_UNPAID:
1420 $status = self::ORDER_INCOMPLETE;
1421 break;
1422 case self::PAYMENT_PARTIALLY_REFUNDED:
1423 $status = self::ORDER_COMPLETED;
1424 break;
1425 case self::PAYMENT_REFUNDED:
1426 $status = self::ORDER_CANCELLED;
1427 break;
1428 case self::PAYMENT_FAILED:
1429 $status = self::ORDER_CANCELLED;
1430 break;
1431 case self::ORDER_TRASH:
1432 $status = self::ORDER_TRASH;
1433 break;
1434 case 'delete':
1435 $status = self::ORDER_CANCELLED;
1436 break;
1437 case self::ORDER_CANCELLED:
1438 $status = self::ORDER_CANCELLED;
1439 break;
1440 }
1441
1442 return $status;
1443 }
1444
1445 /**
1446 * Calculate order price
1447 *
1448 * @since 3.0.0
1449 *
1450 * @param array $items Order items, multi or single dimensional arr.
1451 *
1452 * @return object {subtotal => 10, total => 10}
1453 */
1454 public static function calculate_order_price( array $items ) {
1455 $subtotal = 0;
1456 $total = 0;
1457
1458 if ( isset( $items[0] ) ) {
1459 foreach ( $items as $item ) {
1460 $regular_price = tutor_get_locale_price( $item['regular_price'] );
1461 $sale_price = is_null( $item['sale_price'] ) || '' === $item['sale_price'] ? null : tutor_get_locale_price( $item['sale_price'] );
1462 $discount_price = is_null( $item['discount_price'] ) || '' === $item['discount_price'] ? null : tutor_get_locale_price( $item['discount_price'] );
1463
1464 // Subtotal is the original price (regular price).
1465 $item_subtotal = $regular_price;
1466 $item_total = $regular_price;
1467
1468 // Determine the total based on sale price and discount.
1469 if ( ! is_null( $sale_price ) && $sale_price < $regular_price ) {
1470 $item_subtotal = $sale_price;
1471 $item_total = $sale_price;
1472 } else {
1473 // If there's a discount, apply it to the total price.
1474 if ( ! is_null( $discount_price ) && $discount_price >= 0 ) {
1475 $item_total = max( 0, $discount_price ); // Ensure total doesn't go below 0.
1476 }
1477 }
1478
1479 // $subtotal += $item_subtotal;
1480 $subtotal += $regular_price;
1481 $total += $item_total;
1482 }
1483 } else {
1484 // for single dimensional array.
1485 $regular_price = tutor_get_locale_price( $items['regular_price'] );
1486 $sale_price = is_null( $items['sale_price'] ) || '' === $items['sale_price'] ? null : tutor_get_locale_price( $items['sale_price'] );
1487 $discount_price = is_null( $items['discount_price'] ) || '' === $items['discount_price'] ? null : tutor_get_locale_price( $items['discount_price'] );
1488
1489 // Subtotal is the original price (regular price).
1490 $item_subtotal = $regular_price;
1491 $item_total = $regular_price;
1492
1493 // Determine the total based on sale price and discount.
1494 if ( ! is_null( $sale_price ) && $sale_price < $regular_price ) {
1495 $item_subtotal = $sale_price;
1496 $item_total = $sale_price;
1497 } else {
1498 // If there's a discount, apply it to the total price.
1499 if ( ! is_null( $discount_price ) && $discount_price >= 0 ) {
1500 $item_total = max( 0, $discount_price ); // Ensure total doesn't go below 0.
1501 }
1502 }
1503
1504 // $subtotal = $item_subtotal;
1505 $subtotal = $regular_price;
1506 $total = $item_total;
1507 }
1508
1509 return (object) array(
1510 'subtotal' => tutor_get_locale_price( $subtotal ),
1511 'total' => tutor_get_locale_price( $total ),
1512 );
1513 }
1514
1515 /**
1516 * Check has exclusive type tax.
1517 *
1518 * @since 3.0.0
1519 *
1520 * @param object $order order object.
1521 *
1522 * @return boolean
1523 */
1524 public static function has_exclusive_tax( $order ) {
1525 return self::TAX_TYPE_EXCLUSIVE === $order->tax_type && $order->tax_rate > 0 && $order->tax_amount > 0;
1526 }
1527
1528 /**
1529 * Check has inclusive type tax.
1530 *
1531 * @since 3.0.0
1532 *
1533 * @param object $order order object.
1534 *
1535 * @return boolean
1536 */
1537 public static function has_inclusive_tax( $order ) {
1538 return self::TAX_TYPE_INCLUSIVE === $order->tax_type && $order->tax_rate > 0 && $order->tax_amount > 0;
1539 }
1540
1541 /**
1542 * Get an item
1543 *
1544 * @since 3.0.0
1545 *
1546 * @param integer $item_id Item id.
1547 *
1548 * @return mixed
1549 */
1550 public function get_item( int $item_id ) {
1551 return QueryHelper::get_row(
1552 $this->order_item_table,
1553 array(
1554 'item_id' => $item_id,
1555 ),
1556 'id'
1557 );
1558 }
1559
1560 /**
1561 * Get sellable price
1562 *
1563 * @since 3.0.0
1564 *
1565 * @param mixed $regular_price Regular price.
1566 * @param mixed $sale_price Sale price.
1567 * @param mixed $discount_price Discount price.
1568 *
1569 * @return float item sellable price
1570 */
1571 public static function get_item_sellable_price( $regular_price, $sale_price = null, $discount_price = null ) {
1572 // Ensure prices are numeric and properly formatted.
1573 $sellable_price = (
1574 ! empty( $sale_price )
1575 ? $sale_price
1576 : (
1577 ( ! is_null( $discount_price ) && '' !== $discount_price ) && $discount_price >= 0
1578 ? $discount_price
1579 : $regular_price
1580 )
1581 );
1582
1583 return $sellable_price;
1584 }
1585
1586 /**
1587 * Get item sold price
1588 *
1589 * @since 3.0.0
1590 *
1591 * @param mixed $item_id Item id.
1592 * @param bool $format Item id.
1593 *
1594 * @return mixed item sellable price
1595 */
1596 public static function get_item_sold_price( $item_id, $format = true ) {
1597 $item = ( new self() )->get_item( $item_id );
1598
1599 if ( $item ) {
1600 $sold_price = self::get_item_sellable_price( $item->regular_price, $item->sale_price, $item->discount_price );
1601
1602 return $format ? tutor_get_formatted_price( $sold_price ) : $sold_price;
1603 }
1604
1605 return 0;
1606 }
1607
1608 /**
1609 * Should show pay btn to the user
1610 *
1611 * @since 3.0.0
1612 *
1613 * @param object $order Order object.
1614 *
1615 * @return boolean
1616 */
1617 public static function should_show_pay_btn( object $order ) {
1618 $order_items = ( new self() )->get_order_items_by_id( $order->id );
1619 $is_enrolled_any_course = false;
1620 $is_incomplete_payment = ! empty( $order->payment_method ) && self::ORDER_INCOMPLETE === $order->order_status;
1621 $is_manual_payment = $order->payment_method ? self::is_manual_payment( $order->payment_method ) : true;
1622
1623 if ( $is_incomplete_payment && ! $is_manual_payment && $order_items ) {
1624 if ( self::TYPE_SINGLE_ORDER === $order->order_type ) {
1625 foreach ( $order_items as $item ) {
1626 $course_id = $item->id;
1627 if ( $course_id ) {
1628 $is_enrolled = tutor_utils()->is_enrolled( $course_id );
1629 if ( $is_enrolled ) {
1630 $is_enrolled_any_course = true;
1631 break;
1632 }
1633 }
1634 }
1635 } else {
1636 if ( tutor_utils()->count( $order_items ) ) {
1637 $course_id = apply_filters( 'tutor_subscription_course_by_plan', $order_items[0]->id );
1638 if ( tutor_utils()->is_enrolled( $course_id ) ) {
1639 $is_enrolled_any_course = true;
1640 }
1641 }
1642 }
1643 }
1644
1645 return apply_filters( 'tutor_should_show_pay_btn', $is_incomplete_payment && ! $is_manual_payment && ! $is_enrolled_any_course );
1646 }
1647
1648 /**
1649 * Check is manual payment
1650 *
1651 * @since 3.0.0
1652 *
1653 * @param string $method_name Payment method name.
1654 *
1655 * @return boolean
1656 */
1657 public static function is_manual_payment( $method_name ) {
1658 $payment_methods = tutor_get_manual_payment_gateways();
1659
1660 $is_manual_payment = false;
1661 foreach ( $payment_methods as $payment_method ) {
1662 $is_manual_payment = $payment_method->name === $method_name;
1663 }
1664
1665 return $is_manual_payment;
1666 }
1667
1668 /**
1669 * Render pay button
1670 *
1671 * @since 3.0.1
1672 *
1673 * @param int|object $order Order id or object.
1674 *
1675 * @return void
1676 */
1677 public static function render_pay_button( $order ) {
1678 if ( is_numeric( $order ) ) {
1679 $order = ( new self() )->get_order_by_id( $order );
1680 }
1681
1682 if ( self::should_show_pay_btn( $order ) ) {
1683 ?>
1684 <form method="post">
1685 <?php tutor_nonce_field(); ?>
1686 <input type="hidden" name="tutor_action" value="tutor_pay_incomplete_order">
1687 <input type="hidden" name="order_id" value="<?php echo esc_attr( $order->id ); ?>">
1688 <button type="submit" class="tutor-btn tutor-btn-sm tutor-btn-outline-primary">
1689 <?php esc_html_e( 'Pay', 'tutor' ); ?>
1690 </button>
1691 </form>
1692 <?php
1693 }
1694 }
1695 }
1696