PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.0
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.0
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.0, at inc/order/class-lp-order.php

1,828 lines 49.4 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
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
1322 if ( $this->get_id() ) {
1323 $old_status_post = get_post_status( $this->get_id() );
1324 if ( ! in_array( $old_status_post, array( 'trash', 'auto-draft' ) ) ) {
1325 $old_status = str_replace( 'lp-', '', $old_status_post );
1326 } else {
1327 $old_status = $old_status_post;
1328 }
1329
1330 $return = $this->_curd->update( $this );
1331 } else {
1332 $return = $this->_curd->create( $this );
1333 $this->_id = $return;
1334 }
1335
1336 $new_status_post = get_post_status( $this->get_id() );
1337 if ( ! in_array( $new_status_post, array( 'trash', 'auto-draft' ) ) ) {
1338 $new_status = str_replace( 'lp-', '', $new_status_post );
1339 } else {
1340 $new_status = $new_status_post;
1341 }
1342
1343 $order_id = $this->get_id();
1344
1345 if ( $new_status !== $old_status ) {
1346 do_action( 'learn-press/order/status-changed', $order_id, $old_status, $new_status );
1347 do_action( 'learn-press/order/status-' . $new_status, $order_id, $old_status );
1348 do_action( 'learn-press/order/status-' . $old_status . '-to-' . $new_status, $order_id );
1349 }
1350
1351 // $this->_save_status();
1352
1353 return $return;
1354 }
1355
1356 /**
1357 * Get an instance of LP_Order by post ID or WP_Post object
1358 *
1359 * @param $order
1360 * @param $force
1361 *
1362 * @return LP_Order
1363 */
1364 public static function instance( $order, $force = true ) {
1365 // learn_press_deprecated_function( 'new LP_Order', '3.0', 'learn_press_get_order' );
1366
1367 return learn_press_get_order( $order );
1368 }
1369
1370 /** Getter/Setter **/
1371 public function set_customer_message( $message ) {
1372 $this->_set_data( 'customer_message', $message );
1373 }
1374
1375 public function set_customer_note( $note ) {
1376 $this->_set_data( 'customer_note', $note );
1377 }
1378
1379 public function get_customer_note() {
1380 return $this->get_data( 'customer_note' ) . '';
1381 }
1382
1383 public function set_user_ip_address( $value ) {
1384 $this->_set_data( 'user_ip_address', $value );
1385 }
1386
1387 public function set_user_agent( $value ) {
1388 $this->_set_data( 'user_agent', $value );
1389 }
1390
1391 public function get_user_agent() {
1392 return $this->get_data( 'user_agent' );
1393 }
1394
1395 public function set_checkout_email( $email ) {
1396 $this->_set_data( 'checkout_email', $email );
1397 }
1398
1399 /**
1400 * Get email checkout for case Guest
1401 *
1402 * @return string
1403 */
1404 public function get_checkout_email(): string {
1405 return $this->get_data( 'checkout_email', '' );
1406 }
1407
1408 /**
1409 * Short function to check order is completed.
1410 *
1411 * @return bool
1412 */
1413 public function is_completed(): bool {
1414 return $this->get_status() === LP_ORDER_COMPLETED;
1415 }
1416
1417 /**
1418 * Check Order is created manual.
1419 *
1420 * @return bool
1421 * @since 4.1.3
1422 * @author tungnx
1423 * @version 1.0.0
1424 */
1425 public function is_manual(): bool {
1426 return $this->get_created_via() === LP_ORDER_CREATED_VIA_MANUAL;
1427 }
1428
1429 /**
1430 * Check can delete lp_user_items old
1431 *
1432 * @param LP_Course $course
1433 *
1434 * @return bool
1435 * @throws Exception
1436 * @author tungnx
1437 * @since 4.1.4
1438 * @version 1.0.0
1439 */
1440 public function check_can_delete_item_old( LP_Course $course ): bool {
1441 $can_delete = false;
1442
1443 $lp_user_items_db = LP_User_Items_DB::getInstance();
1444
1445 /**
1446 * For case user buy on frontend (not LP Order manual)
1447 * And course enable repurchase and repurchase_type is keep
1448 */
1449 $allow_repurchase_type = '';
1450
1451 $filter = new LP_User_Items_Filter();
1452 $filter->user_id = get_current_user_id();
1453 $filter->item_id = $course->get_id();
1454 $user_course = $lp_user_items_db->get_last_user_course( $filter );
1455
1456 if ( $user_course && isset( $user_course->user_item_id ) ) {
1457 $latest_user_item_id = $user_course->user_item_id;
1458
1459 /** Get allow_repurchase_type for reset or update. Add in: rest-api/v1/frontend/class-lp-courses-controller.php: purchase_course */
1460 $allow_repurchase_type = learn_press_get_user_item_meta( $latest_user_item_id, '_lp_allow_repurchase_type' );
1461 }
1462
1463 /**
1464 * Course is free
1465 * Or not allow repurchase
1466 * Or repurchase not type 'Keep'
1467 * And else
1468 * Will deleted lp_user_items old
1469 */
1470 if ( $course->is_free() || empty( $allow_repurchase_type ) || ! $course->allow_repurchase() || $allow_repurchase_type != 'keep' ) {
1471 $can_delete = true;
1472 }
1473
1474 return apply_filters( 'learnpress/order/can_delete_old_item', $can_delete, $course );
1475 }
1476
1477 /**
1478 * Get list status of LP Order
1479 *
1480 * @return array
1481 */
1482 public static function get_order_statuses(): array {
1483 $order_statuses = array(
1484 LP_ORDER_COMPLETED_DB => LP_ORDER_COMPLETED,
1485 LP_ORDER_PROCESSING_DB => LP_ORDER_PROCESSING,
1486 LP_ORDER_PENDING_DB => LP_ORDER_PENDING,
1487 LP_ORDER_CANCELLED_DB => LP_ORDER_CANCELLED,
1488 LP_ORDER_FAILED_DB => LP_ORDER_FAILED,
1489 LP_ORDER_REFUNDED_DB => LP_ORDER_REFUNDED,
1490 );
1491
1492 return apply_filters( 'lp/order/statuses', $order_statuses );
1493 }
1494
1495 /**
1496 * Query list orders.
1497 *
1498 * @param PostFilter $post_filter
1499 * @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 ]
1500 *
1501 * @return void
1502 * @throws Exception
1503 * @since 4.3.2.8
1504 * @version 1.0.0
1505 */
1506 public static function handle_params_query_list_orders( PostFilter &$post_filter, array $param = array() ) {
1507 $post_db = PostDB::getInstance();
1508 $user_of_order = absint( $param['author'] ?? 0 );
1509 $status = $param['post_status'] ?? '';
1510 $key = $param['s'] ?? '';
1511 $month = $param['m'] ?? '';
1512 $limit = $param['posts_per_page'] ?? 20;
1513 $paged = $param['paged'] ?? 1;
1514 $refund_request_status = sanitize_key( (string) ( $param['refund_request_status'] ?? '' ) );
1515 $order_by = $param['orderby'] ?? 'date';
1516 if ( empty( $order_by ) ) {
1517 $order_by = 'ID';
1518 } else {
1519 switch ( $order_by ) {
1520 case 'date':
1521 $order_by = 'post_date';
1522 break;
1523 case 'title':
1524 $order_by = 'ID';
1525 break;
1526 }
1527 }
1528
1529 $order = $param['order'] ?? 'DESC';
1530 // End convert params
1531
1532 if ( $order_by === 'order_total' ) {
1533 $post_filter->join[] = "INNER JOIN {$post_db->tb_postmeta} pm2 ON p.ID = pm2.post_id AND pm2.meta_key = '_order_total'";
1534 $post_filter->where[] = 'AND CAST(pm2.meta_value AS UNSIGNED)';
1535 $post_filter->order_by = 'pm2.meta_value';
1536 } else {
1537 $post_filter->order_by = $order_by;
1538 }
1539
1540 if ( ! empty( $key ) ) {
1541 $pattern = '/^#\d+$/';
1542 $is_order_id_sure = false;
1543 if ( preg_match( $pattern, $key ) ) {
1544 $is_order_id_sure = true;
1545 $key = str_replace( '#', '', $key );
1546 }
1547
1548 $pattern2 = '#^0+.*\d+$#';
1549 if ( preg_match( $pattern2, $key ) ) {
1550 $key = (int) $key;
1551 }
1552
1553 $key = trim( $key );
1554
1555 if ( $is_order_id_sure ) {
1556 $post_filter->where[] = $post_db->wpdb->prepare( 'AND p.ID = %d', $key );
1557 } else {
1558 $post_filter->join[] = "INNER JOIN {$post_db->tb_lp_order_items} lpori ON p.ID = lpori.order_id";
1559 $post_filter->where[] = $post_db->wpdb->prepare(
1560 'AND (p.ID = %d OR lpori.order_item_name like %s)',
1561 $key,
1562 '%' . $key . '%'
1563 );
1564 }
1565 }
1566
1567 if ( ! empty( $month ) ) {
1568 $year = substr( $month, 0, 4 );
1569 $post_filter->where[] = "AND YEAR(p.post_date) = $year";
1570 if ( strlen( $month ) > 5 ) {
1571 $mon = substr( $month, 4, 2 );
1572 $post_filter->where[] = "AND MONTH(p.post_date) = $mon";
1573 }
1574 }
1575
1576 $post_filter->order = $order;
1577 $post_filter->limit = $limit;
1578 $post_filter->page = $paged;
1579
1580 if ( ! empty( $user_of_order ) ) {
1581 $user_id = absint( $user_of_order );
1582 $post_filter->join[] = "INNER JOIN {$post_db->tb_postmeta} pm1 ON p.ID = pm1.post_id AND pm1.meta_key = '_user_id'";
1583 $post_filter->where[] = "AND ( pm1.meta_value like '%\"$user_id\"%' OR pm1.meta_value = $user_id )";
1584 }
1585
1586 if ( ! empty( $refund_request_status ) ) {
1587 $post_filter->where[] = $post_db->wpdb->prepare(
1588 "AND EXISTS (
1589 SELECT 1 FROM {$post_db->tb_postmeta} refund_request_pm
1590 WHERE refund_request_pm.post_id = p.ID
1591 AND refund_request_pm.meta_key = %s
1592 AND refund_request_pm.meta_value = %s
1593 )",
1594 LP_Order::META_KEY_REFUND_REQUEST,
1595 $refund_request_status
1596 );
1597 }
1598 if ( ! empty( $status ) && $status !== 'all' ) {
1599 $post_filter->post_status = (array) $status;
1600 } else {
1601 $post_filter->where[] = $post_db->wpdb->prepare( 'AND p.post_status != %s', LP_ORDER_TRASH );
1602 }
1603 }
1604
1605 /**
1606 * Get transaction id from order meta
1607 *
1608 * @since 4.4.0
1609 * @version 1.0.0
1610 * @return string
1611 */
1612 public function get_transaction_id(): string {
1613 return (string) $this->get_meta( self::META_KEY_TRANSACTION_ID );
1614 }
1615
1616 /**
1617 * Get refund request status from order meta.
1618 *
1619 * @since 4.4.0
1620 * @version 1.0.0
1621 * @return string pending|approved|auto-approved|denied|''
1622 */
1623 public function get_refund_request(): string {
1624 return (string) get_post_meta( $this->get_id(), self::META_KEY_REFUND_REQUEST, true );
1625 }
1626
1627 /**
1628 * Check the order can be refunded.
1629 *
1630 * Validates multiple conditions required for refund eligibility:
1631 * - Refund requests must be enabled in settings
1632 * - Order must not already be refunded
1633 * - Order must be in 'completed' status
1634 * - Refund amount must not exceed order total (including previously refunded amount)
1635 * - Payment gateway must have refund method implemented (override from abstract)
1636 *
1637 * @param float $amount The amount to refund..
1638 *
1639 * @return bool|WP_Error Returns true if refund is allowed, WP_Error with specific error code otherwise.
1640 *
1641 * Error codes:
1642 * - 'refund_disabled': Refund feature is disabled in settings
1643 * - 'order_refunded': Order already has refunded status
1644 * - 'order_not_completed': Order is not in completed status
1645 * - 'order_refund_amount_exceeds_total': Generic error (amount exceeds total or gateway unavailable)
1646 * - 'order_refund_gateway_unavailable': Gateway does not support refund
1647 *
1648 * @since 4.4.0
1649 * @version 1.0.0
1650 */
1651 public function can_refund( $amount = 0 ) {
1652 try {
1653 $error_code = '';
1654 if ( 'yes' !== learn_press_get_refund_setting( 'enable_refund_requests', 'no' ) ) {
1655 $error_code = 'refund_disabled';
1656 throw new Exception( __( 'Refund requests are currently disabled.', 'learnpress' ) );
1657 }
1658
1659 if ( $this->has_status( LP_ORDER_REFUNDED ) ) {
1660 $error_code = 'order_refunded';
1661 throw new Exception( __( 'Order has refunded', 'learnpress' ) );
1662 }
1663
1664 if ( ! $this->has_status( LP_ORDER_COMPLETED ) ) {
1665 $error_code = 'order_not_completed';
1666 throw new Exception( __( 'Order is not completed', 'learnpress' ) );
1667 }
1668
1669 $refunded_amount = $this->get_meta( LP_Order::META_KEY_REFUNDED_AMOUNT );
1670 if ( $refunded_amount > 0 ) {
1671 $amount = $refunded_amount + $amount;
1672 }
1673
1674 if ( $amount > $this->get_total() ) {
1675 $error_code = 'order_refund_amount_exceeds_total';
1676 throw new Exception( __( 'Refund amount is greater than order total.', 'learnpress' ) );
1677 }
1678
1679 $payment_method = strtolower( $this->get_data( 'payment_method', '' ) );
1680 $gateway = LP_Gateways::instance()->get_gateway( $payment_method );
1681
1682 $has_refund_override = false;
1683 if ( $gateway && method_exists( $gateway, 'refund' ) ) {
1684 // Check gateway has refund method override
1685 $reflection = new ReflectionMethod( $gateway, 'refund' );
1686 $has_refund_override = $reflection->class === get_class( $gateway );
1687 }
1688
1689 if ( ! $gateway || ! $has_refund_override ) {
1690 $error_code = 'order_refund_gateway_unavailable';
1691 throw new Exception( __( 'Refund gateway is unavailable.', 'learnpress' ) );
1692 }
1693 } catch ( Throwable $e ) {
1694 if ( empty( $error_code ) ) {
1695 $error_code = 'order_can_not_refund';
1696 }
1697
1698 return new WP_Error( $error_code, $e->getMessage() );
1699 }
1700
1701 return true;
1702 }
1703
1704 /**
1705 * Check user can send request refund
1706 *
1707 * Apply for cache 1 user 1 order, only user of this order can send request refund
1708 *
1709 * @since 4.4.0
1710 * @version 1.0.0
1711 * @return bool|WP_Error
1712 */
1713 public function can_send_request_refund( UserModel $userModel ) {
1714 try {
1715 $error_code = '';
1716
1717 // Check user of this order
1718 $order_user = (int) $this->get_user_id();
1719 if ( $order_user !== $userModel->get_id() ) {
1720 $error_code = 'request_refund_user_invalid';
1721 throw new Exception(
1722 __( 'You do not have permission to refund this order.', 'learnpress' )
1723 );
1724 }
1725
1726 // Check refund requests are enabled
1727 if ( 'yes' !== learn_press_get_refund_setting( 'enable_refund_requests', 'no' ) ) {
1728 $error_code = 'refund_disabled';
1729 throw new Exception( __( 'Refund requests are currently disabled.', 'learnpress' ) );
1730 }
1731
1732 // Check order total > 0
1733 if ( $this->get_total() <= 0 ) {
1734 $error_code = 'order_total_invalid';
1735 throw new Exception( __( 'Order total is not valid.', 'learnpress' ) );
1736 }
1737
1738 // Check request refund status exists
1739 $request_status = $this->get_refund_request();
1740 if ( 'rejected' === $request_status ) {
1741 if ( 'yes' !== learn_press_get_refund_setting( 'allow_resend_after_rejected', 'no' ) ) {
1742 $error_code = 'request_refund_is_rejected';
1743 throw new Exception( __( 'Request refund is rejected!.', 'learnpress' ) );
1744 }
1745 } elseif ( ! empty( $request_status ) ) {
1746 $error_code = 'request_refund_sent';
1747 throw new Exception(
1748 sprintf(
1749 __( 'Request refund has %s', 'learnpress' ),
1750 $request_status
1751 )
1752 );
1753 }
1754
1755 // Check time limit for refund request
1756 $refund_time_limit = learn_press_get_refund_setting( 'refund_time_limit', 30 );
1757 if ( $refund_time_limit > 0 ) {
1758 $order_time = absint( $this->get_order_date( 'timestamp' ) );
1759 if ( $order_time > 0 ) {
1760 $now = current_time( 'timestamp' );
1761 $allowed_time_limit = strtotime( '+ ' . $refund_time_limit . ' day', $order_time );
1762 if ( $allowed_time_limit < $now ) {
1763 $error_code = 'request_refund_time_expired';
1764 throw new Exception( __( 'Time for refund request expired', 'learnpress' ) );
1765 }
1766 }
1767 }
1768
1769 // Check gateway support refund
1770 $payment_method = strtolower( $this->get_data( 'payment_method', '' ) );
1771 $gateway = LP_Gateways::instance()->get_gateway( $payment_method );
1772
1773 $gateway_has_refund_override = false;
1774 if ( $gateway && method_exists( $gateway, 'refund' ) ) {
1775 // Check gateway has refund method override
1776 $reflection = new ReflectionMethod( $gateway, 'refund' );
1777 $gateway_has_refund_override = $reflection->class === get_class( $gateway );
1778 }
1779
1780 if ( ! $gateway || ! $gateway_has_refund_override ) {
1781 $error_code = 'order_refund_gateway_unavailable';
1782 throw new Exception( __( 'Refund gateway is unavailable.', 'learnpress' ) );
1783 }
1784 } catch ( Throwable $e ) {
1785 if ( empty( $error_code ) ) {
1786 $error_code = 'user_can_not_send_request_refund';
1787 }
1788
1789 return new WP_Error( $error_code, $e->getMessage() );
1790 }
1791
1792 return true;
1793 }
1794
1795 /**
1796 * Refund order with amount
1797 *
1798 * @return void
1799 * @throws Exception
1800 */
1801 public function refund( $amount ) {
1802 $can_refund = $this->can_refund( $amount );
1803 if ( is_wp_error( $can_refund ) ) {
1804 throw new Exception( $can_refund->get_error_message() );
1805 }
1806
1807 $payment_method = strtolower( $this->get_data( 'payment_method', '' ) );
1808 $gateway = LP_Gateways::instance()->get_gateway( $payment_method );
1809 /** @var LP_Gateway_Abstract|LP_Gateway_Paypal $gateway */
1810 $gateway->refund( $this, $amount );
1811
1812 $user_id = get_current_user_id();
1813 update_post_meta( $this->get_id(), self::META_KEY_REFUNDED_AMOUNT, (float) $amount );
1814 update_post_meta( $this->get_id(), self::META_KEY_REFUNDED_BY, $user_id );
1815 update_post_meta( $this->get_id(), self::META_KEY_REFUNDED_AT, gmdate( LP_Datetime::$format, time() ) );
1816 $this->update_status( LP_ORDER_REFUNDED );
1817
1818 $this->add_note(
1819 sprintf(
1820 __( 'Order %1$s has been refunded %2$s.', 'learnpress' ),
1821 $this->get_order_number(),
1822 learn_press_format_price( $amount, learn_press_get_currency_symbol( $this->get_currency() ) )
1823 )
1824 );
1825 }
1826 }
1827 }
1828