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