PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.8
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.8
4.4.8 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.9.1 4.3.9 4.3.8 4.3.7 4.1.6.9 4.1.6.9.1 4.1.6.9.2 4.1.6.9.3 4.1.6.9.4 4.1.7 4.1.7.1 4.1.7.2 4.1.7.3 4.1.7.3.1 4.1.7.3.2 4.2.0 All 139 releases
learnpress / inc / order / class-lp-order.php

class-lp-order.php in LearnPress – WordPress LMS Plugin for Create and Sell Online Courses 4.4.8, at inc/order/class-lp-order.php

1,848 lines 49.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class LP_Order
4 *
5 * @author ThimPress
6 * @package LearnPress/Classes
7 * @version 4.0.1
8 */
9
10 use LearnPress\Databases\Order\LPOrderItemsDB;
11 use LearnPress\Databases\PostDB;
12 use LearnPress\Filters\Order\OrderItemsFilter;
13 use LearnPress\Filters\PostFilter;
14 use LearnPress\Models\UserItems\UserCourseModel;
15 use LearnPress\Models\UserItems\UserItemModel;
16 use LearnPress\Models\UserModel;
17
18 defined( 'ABSPATH' ) || exit();
19
20 if ( ! class_exists( 'LP_Order' ) ) {
21
22 /**
23 * Class LP_Order
24 */
25 class LP_Order extends LP_Abstract_Post_Data {
26
27 /**
28 * @var string
29 */
30 protected $_post_type = LP_ORDER_CPT;
31
32 /**
33 * @var array
34 */
35 protected $_data = array(
36 'user_id' => '',
37 'order_date' => '',
38 'date_modified' => '',
39 'customer_message' => '',
40 'customer_note' => '',
41 'status' => '',
42 'order_key' => '',
43 'total' => 0,
44 'subtotal' => 0,
45 'created_via' => '',
46 'checkout_email' => '',
47 );
48
49 /**
50 * @var array
51 */
52 protected $_meta_keys = array(
53 '_user_id' => 'user',
54 '_order_currency' => 'currency',
55 '_order_subtotal' => 'subtotal',
56 '_order_total' => 'total',
57 '_payment_method' => 'payment_method',
58 '_payment_method_title' => 'payment_method_title',
59 '_order_version' => 'order_version',
60 '_edit_last' => '',
61 '_edit_lock' => '',
62 '_prices_include_tax' => '',
63 '_order_key' => '',
64 '_user_ip' => '',
65 '_checkout_email' => '',
66 );
67
68 const META_KEY_TRANSACTION_ID = '_transaction_id';
69 const META_KEY_REFUND_REQUEST = '_lp_refund_request';
70 const META_KEY_REFUND_REQUEST_REASON = '_lp_refund_request_reason';
71 const META_KEY_REFUNDED_BY = '_lp_refunded_by';
72 const META_KEY_REFUNDED_AT = '_lp_refunded_at';
73 const META_KEY_REFUNDED_AMOUNT = '_lp_refunded_amount';
74
75 /**
76 * Store order status in transactions.
77 *
78 * @var array
79 */
80 protected $_status = array();
81
82 /**
83 * LP_Order constructor.
84 *
85 * @param bool $order_id
86 *
87 * @throws Exception
88 */
89 public function __construct( $order_id = false ) {
90 $this->_curd = new LP_Order_CURD();
91 if ( is_numeric( $order_id ) && $order_id > 0 ) {
92 $this->set_id( $order_id );
93 } elseif ( $order_id instanceof self ) {
94 $this->set_id( absint( $order_id->get_id() ) );
95 } elseif ( ! empty( $order_id->ID ) ) {
96 $this->set_id( absint( $order_id->ID ) );
97 }
98
99 if ( $this->get_id() > 0 ) {
100 $this->load();
101 }
102 }
103
104 /**
105 * Load the order data.
106 * Check if the id is not zero but it's post type does not exists.
107 *
108 * @throws Exception
109 */
110 public function load() {
111 $this->_curd->load( $this );
112 }
113
114 /**
115 * Set order date.
116 *
117 * @param int|string $date Date time string is time zone of WP setting
118 */
119 public function set_order_date( $date ): LP_Order {
120 $this->set_data_date( 'order_date', $date );
121
122 return $this;
123 }
124
125 /**
126 * Get date of this order.
127 *
128 * @param string $context
129 *
130 * @return string|LP_Datetime
131 */
132 public function get_order_date( $context = '' ) {
133 $date = $this->get_data( 'order_date' );
134
135 if ( 'edit' !== $context ) {
136
137 if ( $date instanceof LP_Datetime ) {
138 $strtime = strtotime( $date->toSql() );
139
140 switch ( $context ) {
141 case 'd':
142 $date = date_i18n( 'Y-m-d', $strtime );
143 break;
144 case 'h':
145 $date = date_i18n( 'H', $strtime );
146 break;
147 case 'm':
148 $date = date_i18n( 'i', $strtime );
149 break;
150 case 'timestamp':
151 $date = $strtime;
152 break;
153 default:
154 $post = get_post( $this->get_id() );
155 $m_time = $post->post_date;
156 $time = get_post_time( 'G', true, $post );
157 $time_diff = time() - $time;
158
159 if ( $time_diff > 0 && $time_diff < DAY_IN_SECONDS ) {
160 $date = sprintf( __( '%s ago', 'learnpress' ), human_time_diff( $time ) );
161 } else {
162 $date = mysql2date( get_option( 'date_format' ), $m_time );
163 }
164 }
165 }
166 } elseif ( ! $date instanceof LP_Datetime ) {
167 $date = new LP_Datetime( $date );
168 }
169
170 return $date;
171 }
172
173 /**
174 * Set order key
175 *
176 * @param string $order_key
177 */
178 public function set_order_key( $order_key ) {
179 $this->_set_data( 'order_key', $order_key );
180 }
181
182 /**
183 * Get order key.
184 *
185 * @return array|mixed
186 */
187 public function get_order_key() {
188 return $this->get_data( 'order_key', '' );
189 }
190
191 /**
192 * Get confirm received text
193 *
194 * @return string
195 * @since 3.0.0
196 */
197 public function get_confirm_order_received_text() {
198 return apply_filters( 'learn-press/confirm-order-received-text', __( 'Thank you. Your order has been received.', 'learnpress' ), $this->get_id() );
199 }
200
201 /**
202 * Get thank you message after the order is placed.
203 *
204 * @return mixed
205 * @since 3.0.0
206 */
207 public function get_thankyou_message() {
208 return apply_filters( 'learn-press/confirm-order-received-text', __( 'Thank you. Your order has been received.', 'learnpress' ), $this->get_id() );
209 }
210
211 /**
212 * Checks to see if current order has status as passed.
213 *
214 * @param string|array $status String or an array of statuses
215 *
216 * @return mixed
217 */
218 public function has_status( $status ) {
219 settype( $status, 'array' );
220 $has = in_array( $this->get_status(), $status );
221
222 return apply_filters( 'learn-press/has-order-status', $has, $status, $this->get_id() );
223 }
224
225 /**
226 * Updates order to new status if needed
227 *
228 * @param mixed $new_status
229 * @param bool $manual Is this a manual order status change?.
230 *
231 * @return bool
232 */
233 public function update_status( $new_status = 'pending', $manual = false ): bool {
234 $result = false;
235
236 try {
237 $this->set_status( $new_status );
238 $result = $this->save();
239 } catch ( Throwable $e ) {
240 error_log( $e->getMessage() );
241 }
242
243 return $result;
244 }
245
246 /**
247 * Format order number id
248 *
249 * @return string
250 */
251 public function get_order_number(): string {
252 return learn_press_transaction_order_number( $this->get_id() );
253 }
254
255 /**
256 * Get status of the order
257 *
258 * @return mixed
259 */
260 public function get_order_status() {
261 return $this->get_status();
262 }
263
264 public function get_user_ip_address() {
265 return $this->get_data( 'user_ip_address' );
266 }
267
268 /**
269 * Get current status of order
270 *
271 * @return mixed
272 */
273 public function get_status() {
274 return $this->get_data( 'status', '' );
275 }
276
277 /**
278 * Get label of lp order status
279 *
280 * @param string $status
281 *
282 * @return string
283 * @since 4.2.0
284 * @version 1.0.0
285 */
286 public static function get_status_label( string $status ): string {
287 switch ( $status ) {
288 case LP_ORDER_COMPLETED:
289 $status = __( 'Completed', 'learnpress' );
290 break;
291 case LP_ORDER_PENDING:
292 $status = __( 'Pending', 'learnpress' );
293 break;
294 case LP_ORDER_PROCESSING:
295 $status = __( 'Processing', 'learnpress' );
296 break;
297 case LP_ORDER_CANCELLED:
298 $status = __( 'Cancelled', 'learnpress' );
299 break;
300 case LP_ORDER_FAILED:
301 $status = __( 'Failed', 'learnpress' );
302 break;
303 case LP_ORDER_TRASH:
304 $status = __( 'Trash', 'learnpress' );
305 break;
306 case 'on-hold':
307 $status = __( 'On hold', 'learnpress' );
308 break;
309 case LP_ORDER_REFUNDED:
310 $status = __( 'Refunded', 'learnpress' );
311 break;
312 default:
313 $status = '';
314 break;
315 }
316
317 if ( ! is_string( $status ) ) {
318 $status = '';
319 }
320
321 return $status;
322 }
323
324 /**
325 * Get icons of lp order status
326 *
327 * @return array
328 * @since 4.2.0
329 * @version 1.0.0
330 */
331 public static function get_icons_status(): array {
332 $icons = array(
333 LP_ORDER_COMPLETED => "<i class='dashicons dashicons-yes-alt'></i>",
334 LP_ORDER_PENDING => "<i class='dashicons dashicons-flag'></i>",
335 LP_ORDER_PROCESSING => "<i class='dashicons dashicons-clock'></i>",
336 LP_ORDER_CANCELLED => "<i class='dashicons dashicons-dismiss'></i>",
337 LP_ORDER_FAILED => "<i class='dashicons dashicons-warning'></i>",
338 LP_ORDER_REFUNDED => "<i class='dashicons dashicons-undo'></i>",
339 );
340
341 return apply_filters( 'lp/order/status/icons', $icons );
342 }
343
344 /**
345 * Set order status.
346 * $new_status shouldn't have prefix 'lp-'
347 *
348 * @param string $new_status
349 * @param string $note - Optional. Note for changing status.
350 *
351 * @sicne 3.0.0
352 * @version 1.0.1
353 */
354 public function set_status( string $new_status = '', string $note = '' ) {
355 // Ensure status not has prefix 'lp-'.
356 $new_status = str_replace( 'lp-', '', $new_status );
357 $valid_statuses = array_values( self::get_order_statuses() );
358 if ( ! in_array( $new_status, $valid_statuses ) && 'trash' !== $new_status ) {
359 $new_status = LP_ORDER_PENDING;
360 }
361
362 $this->_set_data( 'status', $new_status );
363 }
364
365 public function get_order_status_html() {
366 $order_status = self::get_status_label( $this->get_status() );
367 $status = ucfirst( $order_status );
368 $class = 'order-status order-status-' . sanitize_title( $status );
369 $html = sprintf( '<span class="%s">%s</span>', apply_filters( 'learn_press_order_status_class', $class, $status, $this ), $status );
370
371 return apply_filters( 'learn_press_order_status_html', $html, $this );
372 }
373
374 /**
375 * Mark order as complete
376 *
377 * @param string $transaction_id
378 *
379 * @return bool
380 */
381 public function payment_complete( $transaction_id = '' ): bool {
382 do_action( 'learn-press/payment-pre-complete', $this->get_id() );
383
384 $valid_order_statuses = apply_filters(
385 'learn-press/valid-order-statuses-for-payment-complete',
386 array(
387 'pending',
388 'processing',
389 ),
390 $this
391 );
392
393 if ( $this->get_id() && $this->has_status( $valid_order_statuses ) ) {
394
395 $this->update_status( 'completed' );
396
397 if ( ! empty( $transaction_id ) ) {
398 add_post_meta( $this->get_id(), '_transaction_id', $transaction_id, true );
399 }
400
401 do_action( 'learn-press/payment-complete', $this->get_id() );
402 } else {
403 do_action( 'learn-press/payment-complete-order-status-' . $this->get_status(), $this->get_id() );
404 }
405
406 return true;
407 }
408
409 /**
410 * Get checkout order successful url.
411 *
412 * @return string
413 */
414 public function get_checkout_order_received_url() {
415 $received_url = learn_press_get_endpoint_url( 'lp-order-received', $this->get_id(), learn_press_get_page_link( 'checkout' ) );
416
417 $received_url = add_query_arg( 'key', $this->get_order_key(), $received_url );
418
419 $received_url = apply_filters( 'learn_press_get_checkout_order_received_url', $received_url, $this );
420
421 /**
422 * @since 3.0.0
423 */
424 return apply_filters( 'learn-press/checkout-order-received-url', $received_url, $this );
425 }
426
427 /*********************************/
428
429 /**
430 * Get customer name of the order
431 */
432 public function get_customer_name() {
433 $customer_name = '';
434
435 if ( 'auto-draft' !== get_post_status( $this->get_id() ) ) {
436 $user_ids = $this->get_users();
437 if ( ! empty( $user_ids ) ) {
438 $customer_name = array();
439 foreach ( $user_ids as $uid ) {
440 $customer = learn_press_get_user( $uid );
441 if ( $customer && $customer->is_exists() ) {
442 if ( $customer->get_data( 'display_name' ) ) {
443 $customer_name[] = $customer->get_data( 'display_name' );
444 } elseif ( $customer->get_data( 'user_nicename' ) ) {
445 $customer_name[] = $customer->get_data( 'user_nicename' );
446 } elseif ( $customer->get_data( 'user_login' ) ) {
447 $customer_name[] = $customer->get_data( 'user_login' );
448 }
449 } else {
450 $customer_name[] = $this->get_guest_customer_name();
451 }
452 }
453
454 $customer_name = join( ', ', $customer_name );
455 }
456 }
457
458 if ( ! $customer_name ) {
459 $customer_name = $this->get_guest_customer_name();
460 }
461
462 return $customer_name;
463 }
464
465 public function get_guest_customer_name() {
466 $checkout_email = $this->get_checkout_email();
467 if ( $checkout_email ) {
468 $customer_name = sprintf( __( '%s (Guest)', 'learnpress' ), $checkout_email );
469 } else {
470 $customer_name = sprintf( __( '(Guest)', 'learnpress' ), $checkout_email );
471 }
472
473 return apply_filters( 'learn-press/order/guest-customer-name', $customer_name );
474 }
475
476 /*
477 public function customer_exists() {
478 return false !== get_userdata( $this->get_data( 'user_id' ) );
479 }*/
480
481 /**
482 * Get items of the order only to show.
483 *
484 * @return mixed
485 */
486 public function get_items() {
487 $items = $this->_curd->read_items( $this );
488
489 return apply_filters( 'learn-press/order-items', $items );
490 }
491
492 /**
493 * Get all items of the order.
494 * Only use for add/remove items to learn (learn_press_user_items) table.
495 * Where call this function, must be careful, must set_time_limit( 0 );.
496 *
497 * @return array|null
498 * @version 1.0.1
499 * @since 4.2.7.2
500 */
501 public function get_all_items() {
502 global $wpdb;
503 $lp_order_items = LP_Database::getInstance();
504 $table_order_items = $lp_order_items->tb_lp_order_items;
505
506 $query = $wpdb->prepare(
507 "SELECT order_item_id, order_item_name, item_id, item_type
508 From $table_order_items
509 WHERE order_id = %d",
510 $this->get_id()
511 );
512
513 return $wpdb->get_results( $query, ARRAY_A );
514 }
515
516 /**
517 * Get items of the order by filter
518 *
519 * @param array $filter
520 *
521 * @return mixed
522 * @author tungnx
523 */
524 public function get_items_filter( $filter = array() ) {
525 $items = $this->_curd->read_items_filter( $this, $filter );
526
527 return apply_filters( 'learn-press/order-items', $items );
528 }
529
530 public function is_child() {
531 return $this->get_parent_id();
532 }
533
534 public function get_parent() {
535 return $this->get_parent_id() ? learn_press_get_order( $this->get_parent_id() ) : false;
536 }
537
538 /**
539 * Get list of course ids from order.
540 *
541 * @return array|bool
542 * @editor tungnx
543 */
544 public function get_item_ids() {
545 $items = $this->get_all_items();
546
547 if ( $items ) {
548 $course_ids = array();
549 foreach ( $items as $item ) {
550 if ( $item['item_type'] === LP_COURSE_CPT ) {
551 $course_ids[] = (int) $item['item_id'];
552 }
553 }
554
555 return $course_ids;
556 /**
557 * Comment by tungnx. Because it will error if item didn't have key 'course_id'.
558 * Ex: case buy certificate, will not have that key
559 */
560 // return @wp_list_pluck( $items, 'course_id' );
561 }
562
563 return false;
564 }
565
566 /**
567 * Check is order of Guest (user not login)
568 *
569 * @return bool
570 */
571 public function is_guest(): bool {
572 return ! get_user_by( 'ID', $this->get_user_id() );
573 }
574
575 public function get_item_meta( &$item ) {
576 $metas = get_metadata( 'learnpress_order_item', $item['id'] );
577 if ( $metas ) {
578 foreach ( $metas as $k => $v ) {
579 $item[ preg_replace( '!^_!', '', $k ) ] = LP_Helper::maybe_unserialize( $v[0] );
580 }
581 }
582 }
583
584 /**
585 * Remove all items from an order
586 */
587 public function remove_order_items() {
588 global $wpdb;
589 $wpdb->query(
590 $wpdb->prepare(
591 "
592 DELETE FROM itemmeta
593 USING $wpdb->learnpress_order_itemmeta itemmeta
594 INNER JOIN $wpdb->learnpress_order_items items
595 WHERE itemmeta.learnpress_order_item_id = items.order_item_id
596 AND items.order_id = %d",
597 $this->get_id()
598 )
599 );
600 $wpdb->query(
601 $wpdb->prepare(
602 "
603 DELETE FROM {$wpdb->learnpress_order_items}
604 WHERE order_id = %d",
605 $this->get_id()
606 )
607 );
608 $wpdb->query( $wpdb->prepare( "ALTER TABLE {$wpdb->learnpress_order_itemmeta} AUTO_INCREMENT = %d", 1 ) );
609 $wpdb->query( $wpdb->prepare( "ALTER TABLE {$wpdb->learnpress_order_items} AUTO_INCREMENT = %d", 1 ) );
610 }
611
612 /**
613 * Add a new item to order.
614 *
615 * @param array|int $item
616 *
617 * @return int
618 * @throws Exception
619 * @since 1.0.0
620 * @version 4.2.5
621 */
622 public function add_item( $item ): int {
623 global $wpdb;
624 $order_item_id = 0;
625
626 try {
627 if ( is_numeric( $item ) ) {
628 $item = array(
629 'item_id' => absint( $item ),
630 'order_item_name' => get_the_title( $item ),
631 );
632 }
633
634 $item_type = $item['item_type'] ?? get_post_type( $item['item_id'] );
635 if ( ! in_array( $item_type, learn_press_get_item_types_can_purchase() ) ) {
636 return false;
637 }
638
639 $item = wp_parse_args(
640 $item,
641 array(
642 'item_id' => 0,
643 'item_type' => '',
644 'order_item_name' => '',
645 'quantity' => 1,
646 'subtotal' => 0,
647 'total' => 0,
648 'meta' => array(),
649 )
650 );
651
652 switch ( $item_type ) {
653 case LP_COURSE_CPT:
654 $course = learn_press_get_course( $item['item_id'] );
655 $item['subtotal'] = apply_filters( 'learnpress/order/item/subtotal', $course->get_price() * $item['quantity'], $course, $item, $this );
656 $item['total'] = apply_filters( 'learnpress/order/item/total', $course->get_price() * $item['quantity'], $course, $item, $this );
657 $item['order_item_name'] = apply_filters( 'learnpress/order/item/title', get_post_field( 'post_title', $item['item_id'], 'raw' ), $course, $item, $this );
658 $item['meta']['_course_id'] = $item['item_id'];
659 break;
660 default:
661 $item = apply_filters( 'learnpress/order/add-item/item_type_' . $item_type, $item );
662 break;
663 }
664
665 // Insert new order item
666 $wpdb->insert(
667 $wpdb->learnpress_order_items,
668 array(
669 'order_item_name' => $item['order_item_name'],
670 'order_id' => $this->get_id(),
671 'item_id' => $item['item_id'],
672 'item_type' => $item_type,
673 ),
674 array(
675 '%s',
676 '%d',
677 '%d',
678 '%s',
679 )
680 );
681 $order_item_id = absint( $wpdb->insert_id );
682 // Clear cache
683 $key = "order/{$this->get_id()}/{$this->get_status()}/items";
684 LP_Cache::cache_load_first( 'clear', $key );
685 // End insert new order item
686
687 // Add learnpress_order_itemmeta
688 $item['meta']['_quantity'] = $item['quantity'] ?? 1;
689 $item['meta']['_subtotal'] = $item['subtotal'] ?? 0;
690 $item['meta']['_total'] = $item['total'] ?? 0;
691
692 if ( is_array( $item['meta'] ) ) {
693 foreach ( $item['meta'] as $k => $v ) {
694 learn_press_add_order_item_meta( $order_item_id, $k, $v );
695 }
696 }
697
698 do_action( 'learn-press/added-order-item-data', $order_item_id, $item, $this->get_id() );
699 } catch ( Throwable $e ) {
700 error_log( __FUNCTION__ . ': ' . $e->getMessage() );
701 }
702
703 return $order_item_id;
704 }
705
706 /**
707 * Set total
708 *
709 * @param int|float $total
710 */
711 public function set_total( $total = 0 ) {
712 $this->_set_data( 'total', $total );
713 }
714
715 /**
716 * Get total
717 *
718 * @return int|float
719 */
720 public function get_total() {
721 return $this->get_data( 'total', 0 );
722 }
723
724 /**
725 * @param float|int $subtotal
726 */
727 public function set_subtotal( $subtotal = 0 ) {
728 $this->_set_data( 'subtotal', $subtotal );
729 }
730
731 /**
732 * Get subtotal
733 *
734 * @return float
735 */
736 public function get_subtotal() {
737 return $this->get_data( 'subtotal' );
738 }
739
740 /**
741 * Remove an item from database and it's data.
742 *
743 * @param $order_item_id
744 *
745 * @return bool
746 * @throws Exception
747 */
748 public function remove_item( $order_item_id ): bool {
749 global $wpdb;
750
751 $item_id = absint( $order_item_id );
752 $order_item_id = absint( $order_item_id );
753
754 if ( ! $item_id ) {
755 return false;
756 }
757
758 do_action( 'learn_press_before_delete_order_item', $item_id );
759
760 /**
761 * @since 3.0.0
762 */
763 do_action( 'learn-press/before-delete-order-item', $item_id, $this->get_id() );
764
765 $filter = new OrderItemsFilter();
766 $filter->order_item_id = $order_item_id;
767 $filter->return_string_query = true;
768 $filter->run_query_count = false;
769 $order_items_db = LPOrderItemsDB::getInstance();
770 $order_items_db->get_query_single_row( $filter );
771 $query_single_row = $order_items_db->get_items( $filter );
772 $itemObj = $order_items_db->wpdb->get_row( $query_single_row );
773
774 if ( $itemObj && $itemObj->item_type === LP_COURSE_CPT ) {
775 $course_id = $itemObj->item_id;
776 $user_ids = $this->get_user_id();
777 if ( is_array( $user_ids ) ) {
778 foreach ( $user_ids as $user_id ) {
779 // Delete course item on learnpress_user_items if exists
780 $userCourseItem = UserItemModel::find_user_item(
781 $user_id,
782 $course_id,
783 LP_COURSE_CPT,
784 $this->get_id(),
785 LP_ORDER_CPT,
786 true
787 );
788 if ( $userCourseItem instanceof UserItemModel ) {
789 $userCourseModel = new UserCourseModel( $userCourseItem );
790 $userCourseModel->delete();
791 }
792 }
793 } else {
794 // Delete course item on learnpress_user_items if exists
795 $userCourseItem = UserItemModel::find_user_item(
796 $user_ids,
797 $course_id,
798 LP_COURSE_CPT,
799 $this->get_id(),
800 LP_ORDER_CPT,
801 true
802 );
803 if ( $userCourseItem instanceof UserItemModel ) {
804 $userCourseModel = new UserCourseModel( $userCourseItem );
805 $userCourseModel->delete();
806 }
807 }
808 }
809
810 $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}learnpress_order_items WHERE order_item_id = %d", $item_id ) );
811 $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}learnpress_order_itemmeta WHERE learnpress_order_item_id = %d", $item_id ) );
812
813 // Clear cache
814 $key = "order/{$this->get_id()}/{$this->get_status()}/items";
815 LP_Cache::cache_load_first( 'clear', $key );
816
817 /**
818 * @since 3.0.0
819 */
820 do_action( 'learn-press/deleted-order-item', $item_id, $this->get_id() );
821
822 do_action( 'learn_press_delete_order_item', $item_id );
823
824 return true;
825 }
826
827 /**
828 * Add multiple items.
829 *
830 * @param array $items
831 *
832 * @return array
833 */
834 public function add_items( $items ) {
835 settype( $items, 'array' );
836 $item_ids = array();
837 try {
838 foreach ( $items as $item ) {
839 $item_id = $this->add_item( $item );
840 if ( $item_id ) {
841 $item_ids[] = $item_id;
842 }
843 }
844 } catch ( Throwable $e ) {
845 error_log( $e->getMessage() );
846 }
847
848 return $item_ids;
849 }
850
851 /**
852 * @param string $field
853 *
854 * @return array|bool|int|LP_User|mixed
855 */
856 public function get_user( $field = '' ) {
857
858 $users = $this->get_users();
859 $uid = reset( $users );
860
861 $user = learn_press_get_user( $uid );
862 if ( false === $user ) {
863 return false;
864 }
865
866 if ( strtolower( $field ) == 'id' ) {
867 return $user->get_id();
868 }
869
870 if ( $field && $user ) {
871 return $user->get_data( $field );
872 }
873
874 return $user;
875 }
876
877 /**
878 * Get user id in array.
879 * user_id = 0 -> User type Guest
880 *
881 * @return int[]
882 * @editor tungnx
883 * @modify 4.1.4
884 * @version 1.0.1
885 */
886 public function get_users(): array {
887 $users = $this->get_user_id();
888 if ( $users !== - 1 ) {
889 settype( $users, 'array' );
890
891 $users = array_map(
892 function ( $user ) {
893 return absint( $user );
894 },
895 $users
896 );
897
898 $users = array_unique( $users );
899 } else {
900 $users = array();
901 }
902
903 return $users;
904 }
905
906 public function dropdown_users() {
907 $order_users = $this->get_users();
908 $users = get_users( array() );
909 echo '<select name="order-customer[]" id="order-customer" multiple="multiple">';
910 foreach ( (array) $users as $user ) {
911 // $user->get_id() = (int) $user->get_id();
912 if ( in_array( $user->get_id(), $order_users ) ) {
913 $found_selected = true;
914 } else {
915 $found_selected = false;
916 }
917 printf( '<option value="%d" %s>%s</option>', esc_attr( $user->get_id() ), selected( $found_selected, true, false ), esc_html( $user->user_login ) );
918 }
919 echo '</select>';
920 }
921
922
923 public function get_checkout_payment_url() {
924 }
925
926 public function get_formatted_order_subtotal() {
927 $currency_symbol = learn_press_get_currency_symbol( $this->get_currency() );
928
929 return learn_press_format_price( $this->get_subtotal(), $currency_symbol );
930 }
931
932 public function get_formatted_order_total() {
933 $currency_symbol = learn_press_get_currency_symbol( $this->get_currency() );
934
935 return learn_press_format_price( $this->get_total(), $currency_symbol );
936 }
937
938 public function get_currency() {
939 return $this->get_data( 'currency' ) ? $this->get_data( 'currency' ) : learn_press_get_currency();
940 }
941
942 public function set_currency( $value ) {
943 $this->_set_data( 'currency', $value );
944 }
945
946 /**
947 * Get payment method title.
948 *
949 * @return array|mixed
950 */
951 public function get_payment_method_title() {
952 return esc_html( $this->get_data( 'payment_method_title', '' ) );
953 }
954
955 /**
956 * Get view order detail url.
957 *
958 * @return string
959 */
960 public function get_view_order_url() {
961 global $wp_query;
962
963 $userModel = UserModel::find( get_current_user_id(), true );
964 $view_order_endpoint = LP_Settings::instance()->get( 'profile_endpoints.order-details' );
965
966 if ( ! $view_order_endpoint ) {
967 $view_order_endpoint = 'order-details';
968 }
969
970 if ( ! $userModel ) {
971 return '';
972 }
973
974 $user_slug = $userModel->user_nicename;
975 $link_profile = learn_press_get_page_link( 'profile' );
976
977 $view_order_endpoint = urlencode( $view_order_endpoint );
978 if ( get_option( 'permalink_structure' ) ) {
979 $view_order_url = $link_profile . $user_slug . '/' . $view_order_endpoint . '/' . $this->get_id() . '/';
980 } else {
981 $args = array(
982 'user' => $user_slug,
983 );
984 $args['view'] = $view_order_endpoint;
985
986 if ( $view_order_endpoint ) {
987 $args['id'] = $this->get_id();
988 }
989 $view_order_url = add_query_arg(
990 $args,
991 $link_profile
992 );
993 }
994
995 return apply_filters( 'learn_press_view_order_url', $view_order_url, $this );
996 }
997
998 /**
999 * Get cancel url if it's status is pending.
1000 *
1001 * @param bool $force
1002 *
1003 * @return mixed
1004 */
1005 public function get_cancel_order_url( $force = false ) {
1006
1007 $url = false;
1008 if ( $this->has_status( 'pending' ) ) {
1009 $user_id = get_current_user_id();
1010 $profile = LP_Profile::instance( $user_id );
1011 $url = $profile->get_tab_link( LP_Settings::instance()->get( 'profile_endpoints.orders' ) );
1012 if ( ! $force ) {
1013 $url = esc_url_raw( add_query_arg( 'cancel-order', $this->get_id(), $url ) );
1014 } else {
1015 $url = esc_url_raw( add_query_arg( 'cancelled-order', $this->get_id(), $url ) );
1016 }
1017
1018 $url = wp_nonce_url( $url, 'cancel-order', 'lp-nonce' );
1019 }
1020
1021 return apply_filters( 'learn-press/order-cancel-url', $url, $this->get_id() );
1022 }
1023
1024 /**
1025 * Get refund URL if order can be refunded by customer.
1026 *
1027 * @since 4.3.4
1028 * @version 1.0.0
1029 * @return bool|string
1030 */
1031 /*public function get_refund_order_url() {
1032 $url = false;
1033 $user = learn_press_get_current_user();
1034 if ( ! $user instanceof LP_User || $user->get_id() <= 0 ) {
1035 return apply_filters( 'learn-press/order-refund-url', $url, $this->get_id() );
1036 }
1037
1038 $eligibility = learn_press_get_order_refund_eligibility( $this, $user->get_id() );
1039 if ( empty( $eligibility['eligible'] ) ) {
1040 return apply_filters( 'learn-press/order-refund-url', $url, $this->get_id() );
1041 }
1042
1043 $url = learn_press_user_profile_link(
1044 $user->get_id(),
1045 LP_Settings::instance()->get( 'profile_endpoints.orders', 'orders' )
1046 );
1047 $url = esc_url_raw( $url );
1048
1049 return apply_filters( 'learn-press/order-refund-url', $url, $this->get_id() );
1050 }*/
1051
1052 /**
1053 * Get profile order's actions.
1054 *
1055 * @return array|mixed
1056 */
1057 public function get_profile_order_actions() {
1058 $actions = [];
1059
1060 $userModel = UserModel::find( get_current_user_id(), true );
1061 if ( ! $userModel instanceof UserModel ) {
1062 return $actions;
1063 }
1064
1065 $order_user_id = $this->get_users();
1066 if ( ! in_array( $userModel->get_id(), $order_user_id ) ) {
1067 return $actions;
1068 }
1069
1070 $actions = array(
1071 'view' => array(
1072 'url' => $this->get_view_order_url(),
1073 'text' => __( 'View', 'learnpress' ),
1074 ),
1075 );
1076
1077 $cancel_url = $this->get_cancel_order_url();
1078 if ( $cancel_url ) {
1079 $actions['cancel'] = array(
1080 'url' => $this->get_cancel_order_url(),
1081 'text' => __( 'Cancel', 'learnpress' ),
1082 );
1083 }
1084
1085 $refund_request_status = $this->get_refund_request();
1086 $can_send_request_refund = $this->can_send_request_refund( $userModel );
1087 if ( 'pending' === $refund_request_status ) {
1088 $actions['refund-requested'] = array(
1089 'url' => '',
1090 'text' => __( 'Refund Requested', 'learnpress' ),
1091 );
1092 } elseif ( $can_send_request_refund === true ) {
1093 $require_reason = 'yes' === learn_press_get_refund_setting( 'require_refund_reason', 'no' );
1094
1095 $actions['refund'] = array(
1096 'url' => learn_press_user_profile_link(
1097 $userModel->get_id(),
1098 LP_Settings::instance()->get( 'profile_endpoints.orders', 'orders' )
1099 ),
1100 'text' => __( 'Refund', 'learnpress' ),
1101 'class' => 'lp-refund-order-action',
1102 'data' => array(
1103 'order_id' => $this->get_id(),
1104 'require_reason' => $require_reason ? 'yes' : 'no',
1105 'reason_prompt' => __( 'Enter your refund reason', 'learnpress' ),
1106 'reason_placeholder' => __( 'Please describe why you want a refund.', 'learnpress' ),
1107 'reason_required' => __( 'Refund reason is required.', 'learnpress' ),
1108 'confirm_title' => __( 'Request a refund?', 'learnpress' ),
1109 'confirm_text' => __( 'This request will be sent and processed according to payment settings.', 'learnpress' ),
1110 'confirm_button' => __( 'Submit Request', 'learnpress' ),
1111 'cancel_button' => __( 'Cancel', 'learnpress' ),
1112 ),
1113 );
1114 }
1115
1116 $actions = apply_filters( 'learn-press/profile-order-actions', $actions, $this->get_id() );
1117
1118 return $actions;
1119 }
1120
1121 public function add_note( $note = null ) {
1122 $comment_author = '';
1123 $comment_author_email = '';
1124 if ( is_user_logged_in() ) {
1125 $user = get_user_by( 'id', get_current_user_id() );
1126 $comment_author = $user->display_name;
1127 $comment_author_email = $user->user_email;
1128 }
1129
1130 $comment_post_ID = $this->get_id();
1131 $comment_author_url = '';
1132 $comment_content = $note;
1133 $comment_agent = 'LearnPress';
1134 $comment_type = 'lp_order_note';
1135 $comment_parent = 0;
1136 $comment_approved = 1;
1137
1138 $commentdata = apply_filters(
1139 'learn_press_new_order_note_data',
1140 compact( 'comment_post_ID', 'comment_author', 'comment_author_email', 'comment_author_url', 'comment_content', 'comment_agent', 'comment_type', 'comment_parent', 'comment_approved' ),
1141 $this->get_id()
1142 );
1143
1144 return wp_insert_comment( $commentdata );
1145 }
1146
1147 /**
1148 * Get user_name
1149 *
1150 * @param int $user_id
1151 *
1152 * @return string
1153 */
1154 public function get_user_name( int $user_id = 0 ): string {
1155 $user = learn_press_get_user( $user_id );
1156
1157 if ( $user && ! $user instanceof LP_User_Guest ) {
1158 $user_name = $user->get_display_name();
1159 } else {
1160 $user_name = $this->get_checkout_email();
1161 }
1162
1163 return apply_filters( 'learn-press/order/user-name', sprintf( _x( '%1$s', 'full name', 'learnpress' ), $user_name ) );
1164 }
1165
1166 /**
1167 * Check to see if this order is for multi users
1168 *
1169 * @return bool
1170 * @since 2.1.5
1171 */
1172 public function is_multi_users(): bool {
1173 return is_array( $this->get_user_id() );
1174 }
1175
1176 /**
1177 * Get email of user has bought this order.
1178 * In case this order is for multi users return an array with multi email addresses.
1179 *
1180 * @return array
1181 * @since 2.1.5
1182 */
1183 public function get_user_data(): array {
1184 $data = array();
1185
1186 $user_ids = $this->get_users();
1187 if ( ! empty( $user_ids ) ) {
1188 foreach ( $user_ids as $user_id ) {
1189 $user = learn_press_get_user( $user_id );
1190 if ( $user && $user->is_exists() ) {
1191 $data[ $user_id ] = $user->get_data(
1192 array(
1193 'id',
1194 'email',
1195 'user_login',
1196 'description',
1197 'first_name',
1198 'last_name',
1199 'nickname',
1200 'display_name',
1201 )
1202 );
1203 }
1204 }
1205 }
1206
1207 return $data;
1208 }
1209
1210 /**
1211 * Get email's user by user_id
1212 *
1213 * @param int $user_id
1214 *
1215 * @return string
1216 * @editor tungnx
1217 * @modify 4.1.3
1218 * @version 1.0.3
1219 */
1220 public function get_user_email( int $user_id = 0 ): string {
1221 if ( $user_id == 0 ) {
1222 $user_id = (int) $this->get_user_id();
1223 }
1224
1225 $userModel = UserModel::find( $user_id, true );
1226 if ( $userModel instanceof UserModel ) {
1227 $email = $userModel->get_email();
1228 } else {
1229 $email = $this->get_checkout_email();
1230 }
1231
1232 return $email;
1233 }
1234
1235 /**
1236 * Order title
1237 *
1238 * @param string $context
1239 *
1240 * @return array|mixed
1241 */
1242 public function get_title( $context = '' ) {
1243 return $this->get_data( 'order_title', __( 'Order on', 'learnpress' ) . ' ' . current_time( 'l jS F Y h:i:s A' ) );
1244 }
1245
1246 public function get_parent_id() {
1247 return $this->get_data( 'parent_id', 0 );
1248 }
1249
1250 public function set_parent_id( $parent_id ) {
1251 $this->_set_data( 'parent_id', $parent_id );
1252 }
1253
1254 public function set_user_id( $user_id ) {
1255 $this->_set_data( 'user_id', $user_id );
1256 }
1257
1258 /**
1259 * Set 1 for is user guest
1260 *
1261 * @param int $user_type_guest
1262 */
1263 public function set_user_type_guest( int $user_type_guest ) {
1264 $this->_set_data( 'user_type_guest', $user_type_guest );
1265 }
1266
1267 /**
1268 * Get user's ids of order.
1269 *
1270 * @return array|int
1271 */
1272 public function get_user_id() {
1273 return $this->get_data( 'user_id', - 1 );
1274 }
1275
1276 /**
1277 * Get date modified of order.
1278 *
1279 * @return LP_Datetime
1280 */
1281 public function get_date_modified() {
1282 return $this->get_data( 'date_modified' );
1283 }
1284
1285 /**
1286 * Set date modified of order.
1287 *
1288 * @param mixed $date
1289 */
1290 public function set_date_modified( $date ) {
1291 $this->_set_data( 'date_modified', $date );
1292 }
1293
1294 /**
1295 * Set method for creating the order, such as: checkout
1296 *
1297 * @param string $created_via
1298 */
1299 public function set_created_via( $created_via ) {
1300 $this->_set_data( 'created_via', $created_via );
1301 }
1302
1303 /**
1304 * Get method which order is created, such as: checkout
1305 *
1306 * @return string
1307 */
1308 public function get_created_via(): string {
1309 return $this->get_data( 'created_via', '' );
1310 }
1311
1312 /**
1313 * Save order data.
1314 *
1315 * @return mixed
1316 *
1317 * @throws Exception
1318 */
1319 public function save() {
1320 $old_status = '';
1321 $is_created = ! $this->get_id();
1322
1323 if ( $this->get_id() ) {
1324 $old_status_post = get_post_status( $this->get_id() );
1325 if ( ! in_array( $old_status_post, array( 'trash', 'auto-draft' ) ) ) {
1326 $old_status = str_replace( 'lp-', '', $old_status_post );
1327 } else {
1328 $old_status = $old_status_post;
1329 }
1330
1331 $return = $this->_curd->update( $this );
1332 } else {
1333 $return = $this->_curd->create( $this );
1334 $this->_id = $return;
1335 }
1336
1337 $new_status_post = get_post_status( $this->get_id() );
1338 if ( ! in_array( $new_status_post, array( 'trash', 'auto-draft' ) ) ) {
1339 $new_status = str_replace( 'lp-', '', $new_status_post );
1340 } else {
1341 $new_status = $new_status_post;
1342 }
1343
1344 $order_id = $this->get_id();
1345
1346 if ( $is_created && $order_id ) {
1347 do_action( 'learn-press/order/created', $order_id, $this );
1348 }
1349
1350 if ( $new_status !== $old_status ) {
1351 do_action( 'learn-press/order/status-changed', $order_id, $old_status, $new_status );
1352 do_action( 'learn-press/order/status-' . $new_status, $order_id, $old_status );
1353 do_action( 'learn-press/order/status-' . $old_status . '-to-' . $new_status, $order_id );
1354 }
1355
1356 // $this->_save_status();
1357
1358 return $return;
1359 }
1360
1361 /**
1362 * Get an instance of LP_Order by post ID or WP_Post object
1363 *
1364 * @param $order
1365 * @param $force
1366 *
1367 * @return LP_Order
1368 */
1369 public static function instance( $order, $force = true ) {
1370 // learn_press_deprecated_function( 'new LP_Order', '3.0', 'learn_press_get_order' );
1371
1372 return learn_press_get_order( $order );
1373 }
1374
1375 /** Getter/Setter **/
1376 public function set_customer_message( $message ) {
1377 $this->_set_data( 'customer_message', $message );
1378 }
1379
1380 public function set_customer_note( $note ) {
1381 $this->_set_data( 'customer_note', $note );
1382 }
1383
1384 public function get_customer_note() {
1385 return $this->get_data( 'customer_note' ) . '';
1386 }
1387
1388 public function set_user_ip_address( $value ) {
1389 $this->_set_data( 'user_ip_address', $value );
1390 }
1391
1392 public function set_user_agent( $value ) {
1393 $this->_set_data( 'user_agent', $value );
1394 }
1395
1396 public function get_user_agent() {
1397 return $this->get_data( 'user_agent' );
1398 }
1399
1400 public function set_checkout_email( $email ) {
1401 $this->_set_data( 'checkout_email', $email );
1402 }
1403
1404 /**
1405 * Get email checkout for case Guest
1406 *
1407 * @return string
1408 */
1409 public function get_checkout_email(): string {
1410 return $this->get_data( 'checkout_email', '' );
1411 }
1412
1413 /**
1414 * Short function to check order is completed.
1415 *
1416 * @return bool
1417 */
1418 public function is_completed(): bool {
1419 return $this->get_status() === LP_ORDER_COMPLETED;
1420 }
1421
1422 /**
1423 * Check Order is created manual.
1424 *
1425 * @return bool
1426 * @since 4.1.3
1427 * @author tungnx
1428 * @version 1.0.0
1429 */
1430 public function is_manual(): bool {
1431 return $this->get_created_via() === LP_ORDER_CREATED_VIA_MANUAL;
1432 }
1433
1434 /**
1435 * Check can delete lp_user_items old
1436 *
1437 * @param LP_Course $course
1438 *
1439 * @return bool
1440 * @throws Exception
1441 * @author tungnx
1442 * @since 4.1.4
1443 * @version 1.0.0
1444 */
1445 public function check_can_delete_item_old( LP_Course $course ): bool {
1446 $can_delete = false;
1447
1448 $lp_user_items_db = LP_User_Items_DB::getInstance();
1449
1450 /**
1451 * For case user buy on frontend (not LP Order manual)
1452 * And course enable repurchase and repurchase_type is keep
1453 */
1454 $allow_repurchase_type = '';
1455
1456 $filter = new LP_User_Items_Filter();
1457 $filter->user_id = get_current_user_id();
1458 $filter->item_id = $course->get_id();
1459 $user_course = $lp_user_items_db->get_last_user_course( $filter );
1460
1461 if ( $user_course && isset( $user_course->user_item_id ) ) {
1462 $latest_user_item_id = $user_course->user_item_id;
1463
1464 /** Get allow_repurchase_type for reset or update. Add in: rest-api/v1/frontend/class-lp-courses-controller.php: purchase_course */
1465 $allow_repurchase_type = learn_press_get_user_item_meta( $latest_user_item_id, '_lp_allow_repurchase_type' );
1466 }
1467
1468 /**
1469 * Course is free
1470 * Or not allow repurchase
1471 * Or repurchase not type 'Keep'
1472 * And else
1473 * Will deleted lp_user_items old
1474 */
1475 if ( $course->is_free() || empty( $allow_repurchase_type ) || ! $course->allow_repurchase() || $allow_repurchase_type != 'keep' ) {
1476 $can_delete = true;
1477 }
1478
1479 return apply_filters( 'learnpress/order/can_delete_old_item', $can_delete, $course );
1480 }
1481
1482 /**
1483 * Get list status of LP Order
1484 *
1485 * @return array
1486 */
1487 public static function get_order_statuses(): array {
1488 $order_statuses = array(
1489 LP_ORDER_COMPLETED_DB => LP_ORDER_COMPLETED,
1490 LP_ORDER_PROCESSING_DB => LP_ORDER_PROCESSING,
1491 LP_ORDER_PENDING_DB => LP_ORDER_PENDING,
1492 LP_ORDER_CANCELLED_DB => LP_ORDER_CANCELLED,
1493 LP_ORDER_FAILED_DB => LP_ORDER_FAILED,
1494 LP_ORDER_REFUNDED_DB => LP_ORDER_REFUNDED,
1495 );
1496
1497 return apply_filters( 'lp/order/statuses', $order_statuses );
1498 }
1499
1500 /**
1501 * Query list orders.
1502 *
1503 * @param PostFilter $post_filter
1504 * @param array $param [ 'author' => int, 'post_status' => string|array, 'refund_request_status' => string, 's' => string, 'm' => string, 'posts_per_page' => int, 'paged' => int, 'orderby' => string, 'order' => string ]
1505 *
1506 * @return void
1507 * @throws Exception
1508 * @since 4.3.2.8
1509 * @version 1.0.2
1510 */
1511 public static function handle_params_query_list_orders( PostFilter &$post_filter, array $param = array() ) {
1512 $post_db = PostDB::getInstance();
1513 $user_of_order = absint( $param['author'] ?? 0 );
1514 $status = $param['post_status'] ?? '';
1515 $key = $param['s'] ?? '';
1516 $month = $param['m'] ?? '';
1517 $limit = $param['posts_per_page'] ?? 20;
1518 $paged = $param['paged'] ?? 1;
1519 $refund_request_status = sanitize_key( (string) ( $param['refund_request_status'] ?? '' ) );
1520 $order_by = $param['orderby'] ?? 'menu_order';
1521 if ( empty( $order_by ) ) {
1522 $order_by = 'ID';
1523 } else {
1524 switch ( $order_by ) {
1525 case 'date':
1526 $order_by = 'post_date';
1527 break;
1528 default:
1529 $allowed_key = array(
1530 'ID',
1531 'post_title',
1532 'post_author',
1533 'post_status',
1534 'order_total',
1535 'menu_order'
1536 );
1537 if ( ! in_array( $order_by, $allowed_key ) ) {
1538 $order_by = 'ID';
1539 }
1540 break;
1541 }
1542 }
1543
1544 $order = $param['order'] ?? 'DESC';
1545 // End convert params
1546
1547 if ( $order_by === 'order_total' ) {
1548 $post_filter->join[] = "INNER JOIN {$post_db->tb_postmeta} pm2 ON p.ID = pm2.post_id AND pm2.meta_key = '_order_total'";
1549 $post_filter->order_by = 'CAST(pm2.meta_value AS DECIMAL(10,2))';
1550 } else {
1551 $post_filter->order_by = $order_by;
1552 }
1553
1554 if ( ! empty( $key ) ) {
1555 $pattern = '/^#\d+$/';
1556 $is_order_id_sure = false;
1557 if ( preg_match( $pattern, $key ) ) {
1558 $is_order_id_sure = true;
1559 $key = str_replace( '#', '', $key );
1560 }
1561
1562 $pattern2 = '#^0+.*\d+$#';
1563 if ( preg_match( $pattern2, $key ) ) {
1564 $key = (int) $key;
1565 }
1566
1567 $key = trim( $key );
1568
1569 if ( $is_order_id_sure ) {
1570 $post_filter->where[] = $post_db->wpdb->prepare( 'AND p.ID = %d', $key );
1571 } else {
1572 $post_filter->join[] = "INNER JOIN {$post_db->tb_lp_order_items} lpori ON p.ID = lpori.order_id";
1573 $post_filter->where[] = $post_db->wpdb->prepare(
1574 'AND (p.ID = %d OR lpori.order_item_name like %s)',
1575 $key,
1576 '%' . $key . '%'
1577 );
1578 }
1579 }
1580
1581 if ( ! empty( $month ) ) {
1582 $year = substr( $month, 0, 4 );
1583 $post_filter->where[] = $post_db->wpdb->prepare(
1584 'AND YEAR(p.post_date) = %s',
1585 $year
1586 );
1587 if ( strlen( $month ) > 5 ) {
1588 $mon = substr( $month, 4, 2 );
1589 $post_filter->where[] = $post_db->wpdb->prepare(
1590 'AND MONTH(p.post_date) = %s',
1591 $mon
1592 );
1593 }
1594 }
1595
1596 $post_filter->order = $order;
1597 $post_filter->limit = $limit;
1598 $post_filter->page = $paged;
1599
1600 if ( ! empty( $user_of_order ) ) {
1601 $user_id = absint( $user_of_order );
1602 $post_filter->join[] = "INNER JOIN {$post_db->tb_postmeta} pm1 ON p.ID = pm1.post_id AND pm1.meta_key = '_user_id'";
1603 $post_filter->where[] = "AND ( pm1.meta_value like '%\"$user_id\"%' OR pm1.meta_value = $user_id )";
1604 }
1605
1606 if ( ! empty( $refund_request_status ) ) {
1607 $post_filter->where[] = $post_db->wpdb->prepare(
1608 "AND EXISTS (
1609 SELECT 1 FROM {$post_db->tb_postmeta} refund_request_pm
1610 WHERE refund_request_pm.post_id = p.ID
1611 AND refund_request_pm.meta_key = %s
1612 AND refund_request_pm.meta_value = %s
1613 )",
1614 LP_Order::META_KEY_REFUND_REQUEST,
1615 $refund_request_status
1616 );
1617 }
1618 if ( ! empty( $status ) && $status !== 'all' ) {
1619 $post_filter->post_status = (array) $status;
1620 } else {
1621 $post_filter->where[] = $post_db->wpdb->prepare( 'AND p.post_status != %s', LP_ORDER_TRASH );
1622 }
1623 }
1624
1625 /**
1626 * Get transaction id from order meta
1627 *
1628 * @since 4.4.0
1629 * @version 1.0.0
1630 * @return string
1631 */
1632 public function get_transaction_id(): string {
1633 return (string) $this->get_meta( self::META_KEY_TRANSACTION_ID );
1634 }
1635
1636 /**
1637 * Get refund request status from order meta.
1638 *
1639 * @since 4.4.0
1640 * @version 1.0.0
1641 * @return string pending|approved|auto-approved|denied|''
1642 */
1643 public function get_refund_request(): string {
1644 return (string) get_post_meta( $this->get_id(), self::META_KEY_REFUND_REQUEST, true );
1645 }
1646
1647 /**
1648 * Check the order can be refunded.
1649 *
1650 * Validates multiple conditions required for refund eligibility:
1651 * - Refund requests must be enabled in settings
1652 * - Order must not already be refunded
1653 * - Order must be in 'completed' status
1654 * - Refund amount must not exceed order total (including previously refunded amount)
1655 * - Payment gateway must have refund method implemented (override from abstract)
1656 *
1657 * @param float $amount The amount to refund..
1658 *
1659 * @return bool|WP_Error Returns true if refund is allowed, WP_Error with specific error code otherwise.
1660 *
1661 * Error codes:
1662 * - 'refund_disabled': Refund feature is disabled in settings
1663 * - 'order_refunded': Order already has refunded status
1664 * - 'order_not_completed': Order is not in completed status
1665 * - 'order_refund_amount_exceeds_total': Generic error (amount exceeds total or gateway unavailable)
1666 * - 'order_refund_gateway_unavailable': Gateway does not support refund
1667 *
1668 * @since 4.4.0
1669 * @version 1.0.0
1670 */
1671 public function can_refund( $amount = 0 ) {
1672 try {
1673 $error_code = '';
1674 if ( 'yes' !== learn_press_get_refund_setting( 'enable_refund_requests', 'no' ) ) {
1675 $error_code = 'refund_disabled';
1676 throw new Exception( __( 'Refund requests are currently disabled.', 'learnpress' ) );
1677 }
1678
1679 if ( $this->has_status( LP_ORDER_REFUNDED ) ) {
1680 $error_code = 'order_refunded';
1681 throw new Exception( __( 'Order has refunded', 'learnpress' ) );
1682 }
1683
1684 if ( ! $this->has_status( LP_ORDER_COMPLETED ) ) {
1685 $error_code = 'order_not_completed';
1686 throw new Exception( __( 'Order is not completed', 'learnpress' ) );
1687 }
1688
1689 $refunded_amount = $this->get_meta( LP_Order::META_KEY_REFUNDED_AMOUNT );
1690 if ( $refunded_amount > 0 ) {
1691 $amount = $refunded_amount + $amount;
1692 }
1693
1694 if ( $amount > $this->get_total() ) {
1695 $error_code = 'order_refund_amount_exceeds_total';
1696 throw new Exception( __( 'Refund amount is greater than order total.', 'learnpress' ) );
1697 }
1698
1699 $payment_method = strtolower( $this->get_data( 'payment_method', '' ) );
1700 $gateway = LP_Gateways::instance()->get_gateway( $payment_method );
1701
1702 $has_refund_override = false;
1703 if ( $gateway && method_exists( $gateway, 'refund' ) ) {
1704 // Check gateway has refund method override
1705 $reflection = new ReflectionMethod( $gateway, 'refund' );
1706 $has_refund_override = $reflection->class === get_class( $gateway );
1707 }
1708
1709 if ( ! $gateway || ! $has_refund_override ) {
1710 $error_code = 'order_refund_gateway_unavailable';
1711 throw new Exception( __( 'Refund gateway is unavailable.', 'learnpress' ) );
1712 }
1713 } catch ( Throwable $e ) {
1714 if ( empty( $error_code ) ) {
1715 $error_code = 'order_can_not_refund';
1716 }
1717
1718 return new WP_Error( $error_code, $e->getMessage() );
1719 }
1720
1721 return true;
1722 }
1723
1724 /**
1725 * Check user can send request refund
1726 *
1727 * Apply for cache 1 user 1 order, only user of this order can send request refund
1728 *
1729 * @since 4.4.0
1730 * @version 1.0.0
1731 * @return bool|WP_Error
1732 */
1733 public function can_send_request_refund( UserModel $userModel ) {
1734 try {
1735 $error_code = '';
1736
1737 // Check user of this order
1738 $order_user = (int) $this->get_user_id();
1739 if ( $order_user !== $userModel->get_id() ) {
1740 $error_code = 'request_refund_user_invalid';
1741 throw new Exception(
1742 __( 'You do not have permission to refund this order.', 'learnpress' )
1743 );
1744 }
1745
1746 // Check refund requests are enabled
1747 if ( 'yes' !== learn_press_get_refund_setting( 'enable_refund_requests', 'no' ) ) {
1748 $error_code = 'refund_disabled';
1749 throw new Exception( __( 'Refund requests are currently disabled.', 'learnpress' ) );
1750 }
1751
1752 // Check order total > 0
1753 if ( $this->get_total() <= 0 ) {
1754 $error_code = 'order_total_invalid';
1755 throw new Exception( __( 'Order total is not valid.', 'learnpress' ) );
1756 }
1757
1758 // Check request refund status exists
1759 $request_status = $this->get_refund_request();
1760 if ( 'rejected' === $request_status ) {
1761 if ( 'yes' !== learn_press_get_refund_setting( 'allow_resend_after_rejected', 'no' ) ) {
1762 $error_code = 'request_refund_is_rejected';
1763 throw new Exception( __( 'Request refund is rejected!.', 'learnpress' ) );
1764 }
1765 } elseif ( ! empty( $request_status ) ) {
1766 $error_code = 'request_refund_sent';
1767 throw new Exception(
1768 sprintf(
1769 __( 'Request refund has %s', 'learnpress' ),
1770 $request_status
1771 )
1772 );
1773 }
1774
1775 // Check time limit for refund request
1776 $refund_time_limit = learn_press_get_refund_setting( 'refund_time_limit', 30 );
1777 if ( $refund_time_limit > 0 ) {
1778 $order_time = absint( $this->get_order_date( 'timestamp' ) );
1779 if ( $order_time > 0 ) {
1780 $now = current_time( 'timestamp' );
1781 $allowed_time_limit = strtotime( '+ ' . $refund_time_limit . ' day', $order_time );
1782 if ( $allowed_time_limit < $now ) {
1783 $error_code = 'request_refund_time_expired';
1784 throw new Exception( __( 'Time for refund request expired', 'learnpress' ) );
1785 }
1786 }
1787 }
1788
1789 // Check gateway support refund
1790 $payment_method = strtolower( $this->get_data( 'payment_method', '' ) );
1791 $gateway = LP_Gateways::instance()->get_gateway( $payment_method );
1792
1793 $gateway_has_refund_override = false;
1794 if ( $gateway && method_exists( $gateway, 'refund' ) ) {
1795 // Check gateway has refund method override
1796 $reflection = new ReflectionMethod( $gateway, 'refund' );
1797 $gateway_has_refund_override = $reflection->class === get_class( $gateway );
1798 }
1799
1800 if ( ! $gateway || ! $gateway_has_refund_override ) {
1801 $error_code = 'order_refund_gateway_unavailable';
1802 throw new Exception( __( 'Refund gateway is unavailable.', 'learnpress' ) );
1803 }
1804 } catch ( Throwable $e ) {
1805 if ( empty( $error_code ) ) {
1806 $error_code = 'user_can_not_send_request_refund';
1807 }
1808
1809 return new WP_Error( $error_code, $e->getMessage() );
1810 }
1811
1812 return true;
1813 }
1814
1815 /**
1816 * Refund order with amount
1817 *
1818 * @return void
1819 * @throws Exception
1820 */
1821 public function refund( $amount ) {
1822 $can_refund = $this->can_refund( $amount );
1823 if ( is_wp_error( $can_refund ) ) {
1824 throw new Exception( $can_refund->get_error_message() );
1825 }
1826
1827 $payment_method = strtolower( $this->get_data( 'payment_method', '' ) );
1828 $gateway = LP_Gateways::instance()->get_gateway( $payment_method );
1829 /** @var LP_Gateway_Abstract|LP_Gateway_Paypal $gateway */
1830 $gateway->refund( $this, $amount );
1831
1832 $user_id = get_current_user_id();
1833 update_post_meta( $this->get_id(), self::META_KEY_REFUNDED_AMOUNT, (float) $amount );
1834 update_post_meta( $this->get_id(), self::META_KEY_REFUNDED_BY, $user_id );
1835 update_post_meta( $this->get_id(), self::META_KEY_REFUNDED_AT, gmdate( LP_Datetime::$format, time() ) );
1836 $this->update_status( LP_ORDER_REFUNDED );
1837
1838 $this->add_note(
1839 sprintf(
1840 __( 'Order %1$s has been refunded %2$s.', 'learnpress' ),
1841 $this->get_order_number(),
1842 learn_press_format_price( $amount, learn_press_get_currency_symbol( $this->get_currency() ) )
1843 )
1844 );
1845 }
1846 }
1847 }
1848