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
← All changes | inc/order/class-lp-order.php +387 -52 4.3.74.4.8 View file →
@@ -6,10 +6,14 @@
6 6 * @package LearnPress/Classes
7 7 * @version 4.0.1
8 8 */
9 9
10 +use LearnPress\Databases\Order\LPOrderItemsDB;
10 11 use LearnPress\Databases\PostDB;
12 +use LearnPress\Filters\Order\OrderItemsFilter;
11 13 use LearnPress\Filters\PostFilter;
14 +use LearnPress\Models\UserItems\UserCourseModel;
15 +use LearnPress\Models\UserItems\UserItemModel;
12 16 use LearnPress\Models\UserModel;
13 17
14 18 defined( 'ABSPATH' ) || exit();
15 19
@@ -60,8 +64,15 @@
60 64 '_user_ip' => '',
61 65 '_checkout_email' => '',
62 66 );
63 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 +
64 75 /**
65 76 * Store order status in transactions.
66 77 *
67 78 * @var array
@@ -102,9 +113,9 @@
102 113
103 114 /**
104 115 * Set order date.
105 116 *
106 - * @param int|string $date
117 + * @param int|string $date Date time string is time zone of WP setting
107 118 */
108 119 public function set_order_date( $date ): LP_Order {
109 120 $this->set_data_date( 'order_date', $date );
110 121
@@ -214,9 +225,9 @@
214 225 /**
215 226 * Updates order to new status if needed
216 227 *
217 228 * @param mixed $new_status
218 - * @param bool $manual Is this a manual order status change?.
229 + * @param bool $manual Is this a manual order status change?.
219 230 *
220 231 * @return bool
221 232 */
222 233 public function update_status( $new_status = 'pending', $manual = false ): bool {
@@ -294,9 +305,9 @@
294 305 break;
295 306 case 'on-hold':
296 307 $status = __( 'On hold', 'learnpress' );
297 308 break;
298 - case 'refunded':
309 + case LP_ORDER_REFUNDED:
299 310 $status = __( 'Refunded', 'learnpress' );
300 311 break;
301 312 default:
302 313 $status = '';
@@ -317,15 +328,16 @@
317 328 * @since 4.2.0
318 329 * @version 1.0.0
319 330 */
320 331 public static function get_icons_status(): array {
321 - $icons = [
332 + $icons = array(
322 333 LP_ORDER_COMPLETED => "<i class='dashicons dashicons-yes-alt'></i>",
323 334 LP_ORDER_PENDING => "<i class='dashicons dashicons-flag'></i>",
324 335 LP_ORDER_PROCESSING => "<i class='dashicons dashicons-clock'></i>",
325 336 LP_ORDER_CANCELLED => "<i class='dashicons dashicons-dismiss'></i>",
326 337 LP_ORDER_FAILED => "<i class='dashicons dashicons-warning'></i>",
327 - ];
338 + LP_ORDER_REFUNDED => "<i class='dashicons dashicons-undo'></i>",
339 + );
328 340
329 341 return apply_filters( 'lp/order/status/icons', $icons );
330 342 }
331 343
@@ -341,9 +353,9 @@
341 353 */
342 354 public function set_status( string $new_status = '', string $note = '' ) {
343 355 // Ensure status not has prefix 'lp-'.
344 356 $new_status = str_replace( 'lp-', '', $new_status );
345 - $valid_statuses = array_values( LP_Order::get_order_statuses() );
357 + $valid_statuses = array_values( self::get_order_statuses() );
346 358 if ( ! in_array( $new_status, $valid_statuses ) && 'trash' !== $new_status ) {
347 359 $new_status = LP_ORDER_PENDING;
348 360 }
349 361
@@ -460,9 +472,10 @@
460 472
461 473 return apply_filters( 'learn-press/order/guest-customer-name', $customer_name );
462 474 }
463 475
464 - /*public function customer_exists() {
476 + /*
477 + public function customer_exists() {
465 478 return false !== get_userdata( $this->get_data( 'user_id' ) );
466 479 }*/
467 480
468 481 /**
@@ -543,9 +556,9 @@
543 556 /**
544 557 * Comment by tungnx. Because it will error if item didn't have key 'course_id'.
545 558 * Ex: case buy certificate, will not have that key
546 559 */
547 - //return @wp_list_pluck( $items, 'course_id' );
560 + // return @wp_list_pluck( $items, 'course_id' );
548 561 }
549 562
550 563 return false;
551 564 }
@@ -796,9 +809,9 @@
796 809
797 810 $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}learnpress_order_items WHERE order_item_id = %d", $item_id ) );
798 811 $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}learnpress_order_itemmeta WHERE learnpress_order_item_id = %d", $item_id ) );
799 812
800 - //Clear cache
813 + // Clear cache
801 814 $key = "order/{$this->get_id()}/{$this->get_status()}/items";
802 815 LP_Cache::cache_load_first( 'clear', $key );
803 816
804 817 /**
@@ -863,8 +876,9 @@
863 876
864 877 /**
865 878 * Get user id in array.
866 879 * user_id = 0 -> User type Guest
880 + *
867 881 * @return int[]
868 882 * @editor tungnx
869 883 * @modify 4.1.4
870 884 * @version 1.0.1
@@ -899,9 +913,9 @@
899 913 $found_selected = true;
900 914 } else {
901 915 $found_selected = false;
902 916 }
903 - echo sprintf( '<option value="%d" %s>%s</option>', esc_attr( $user->get_id() ), selected( $found_selected, true, false ), esc_html( $user->user_login ) );
917 + printf( '<option value="%d" %s>%s</option>', esc_attr( $user->get_id() ), selected( $found_selected, true, false ), esc_html( $user->user_login ) );
904 918 }
905 919 echo '</select>';
906 920 }
907 921
@@ -937,13 +951,17 @@
937 951 public function get_payment_method_title() {
938 952 return esc_html( $this->get_data( 'payment_method_title', '' ) );
939 953 }
940 954
955 + /**
956 + * Get view order detail url.
957 + *
958 + * @return string
959 + */
941 960 public function get_view_order_url() {
942 961 global $wp_query;
943 962
944 - $view_order_url = learn_press_get_endpoint_url( 'view-order', $this->get_id(), learn_press_get_page_link( 'profile' ) );
945 - $user = learn_press_get_current_user();
963 + $userModel = UserModel::find( get_current_user_id(), true );
946 964 $view_order_endpoint = LP_Settings::instance()->get( 'profile_endpoints.order-details' );
947 965
948 966 if ( ! $view_order_endpoint ) {
949 967 $view_order_endpoint = 'order-details';
@@ -948,14 +966,21 @@
948 966 if ( ! $view_order_endpoint ) {
949 967 $view_order_endpoint = 'order-details';
950 968 }
951 969
970 + if ( ! $userModel ) {
971 + return '';
972 + }
973 +
974 + $user_slug = $userModel->user_nicename;
975 + $link_profile = learn_press_get_page_link( 'profile' );
976 +
952 977 $view_order_endpoint = urlencode( $view_order_endpoint );
953 978 if ( get_option( 'permalink_structure' ) ) {
954 - $view_order_url = learn_press_get_page_link( 'profile' ) . $user->get_data( 'user_login' ) . '/' . $view_order_endpoint . '/' . $this->get_id() . '/';
979 + $view_order_url = $link_profile . $user_slug . '/' . $view_order_endpoint . '/' . $this->get_id() . '/';
955 980 } else {
956 981 $args = array(
957 - 'user' => $user->get_data( 'user_login' ),
982 + 'user' => $user_slug,
958 983 );
959 984 $args['view'] = $view_order_endpoint;
960 985
961 986 if ( $view_order_endpoint ) {
@@ -962,9 +987,9 @@
962 987 $args['id'] = $this->get_id();
963 988 }
964 989 $view_order_url = add_query_arg(
965 990 $args,
966 - learn_press_get_page_link( 'profile' )
991 + $link_profile
967 992 );
968 993 }
969 994
970 995 return apply_filters( 'learn_press_view_order_url', $view_order_url, $this );
@@ -996,13 +1021,53 @@
996 1021 return apply_filters( 'learn-press/order-cancel-url', $url, $this->get_id() );
997 1022 }
998 1023
999 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 + /**
1000 1053 * Get profile order's actions.
1001 1054 *
1002 1055 * @return array|mixed
1003 1056 */
1004 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 +
1005 1070 $actions = array(
1006 1071 'view' => array(
1007 1072 'url' => $this->get_view_order_url(),
1008 1073 'text' => __( 'View', 'learnpress' ),
@@ -1016,21 +1081,37 @@
1016 1081 'text' => __( 'Cancel', 'learnpress' ),
1017 1082 );
1018 1083 }
1019 1084
1020 - $payment_method = sanitize_key( (string) get_post_meta( $this->get_id(), '_payment_method', true ) );
1021 - $subscription_status = get_post_meta( $this->get_id(), LP_Gateway_Abstract::META_SUBSCRIPTION_STATUS, true );
1022 - if ( ! empty( $payment_method ) && in_array( $subscription_status, array( 'active', 'trialing', 'past_due' ), true ) ) {
1023 - $gateway = LP_Gateways::instance()->get_gateway( $payment_method );
1024 - if ( $gateway instanceof LP_Gateway_Abstract ) {
1025 - $manage_url = $gateway->get_manage_subscription_url( $this );
1026 - if ( ! empty( $manage_url ) ) {
1027 - $actions['manage-subscription'] = array(
1028 - 'url' => esc_url_raw( $manage_url ),
1029 - 'text' => __( 'Manage subscription', 'learnpress' ),
1030 - );
1031 - }
1032 - }
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 + );
1033 1114 }
1034 1115
1035 1116 $actions = apply_filters( 'learn-press/profile-order-actions', $actions, $this->get_id() );
1036 1117
@@ -1236,12 +1317,13 @@
1236 1317 * @throws Exception
1237 1318 */
1238 1319 public function save() {
1239 1320 $old_status = '';
1321 + $is_created = ! $this->get_id();
1240 1322
1241 1323 if ( $this->get_id() ) {
1242 1324 $old_status_post = get_post_status( $this->get_id() );
1243 - if ( ! in_array( $old_status_post, [ 'trash', 'auto-draft' ] ) ) {
1325 + if ( ! in_array( $old_status_post, array( 'trash', 'auto-draft' ) ) ) {
1244 1326 $old_status = str_replace( 'lp-', '', $old_status_post );
1245 1327 } else {
1246 1328 $old_status = $old_status_post;
1247 1329 }
@@ -1252,9 +1334,9 @@
1252 1334 $this->_id = $return;
1253 1335 }
1254 1336
1255 1337 $new_status_post = get_post_status( $this->get_id() );
1256 - if ( ! in_array( $new_status_post, [ 'trash', 'auto-draft' ] ) ) {
1338 + if ( ! in_array( $new_status_post, array( 'trash', 'auto-draft' ) ) ) {
1257 1339 $new_status = str_replace( 'lp-', '', $new_status_post );
1258 1340 } else {
1259 1341 $new_status = $new_status_post;
1260 1342 }
@@ -1260,8 +1342,12 @@
1260 1342 }
1261 1343
1262 1344 $order_id = $this->get_id();
1263 1345
1346 + if ( $is_created && $order_id ) {
1347 + do_action( 'learn-press/order/created', $order_id, $this );
1348 + }
1349 +
1264 1350 if ( $new_status !== $old_status ) {
1265 1351 do_action( 'learn-press/order/status-changed', $order_id, $old_status, $new_status );
1266 1352 do_action( 'learn-press/order/status-' . $new_status, $order_id, $old_status );
1267 1353 do_action( 'learn-press/order/status-' . $old_status . '-to-' . $new_status, $order_id );
@@ -1266,9 +1352,9 @@
1266 1352 do_action( 'learn-press/order/status-' . $new_status, $order_id, $old_status );
1267 1353 do_action( 'learn-press/order/status-' . $old_status . '-to-' . $new_status, $order_id );
1268 1354 }
1269 1355
1270 - //$this->_save_status();
1356 + // $this->_save_status();
1271 1357
1272 1358 return $return;
1273 1359 }
1274 1360
@@ -1286,9 +1372,8 @@
1286 1372 return learn_press_get_order( $order );
1287 1373 }
1288 1374
1289 1375 /** Getter/Setter **/
1290 -
1291 1376 public function set_customer_message( $message ) {
1292 1377 $this->_set_data( 'customer_message', $message );
1293 1378 }
1294 1379
@@ -1399,15 +1484,16 @@
1399 1484 *
1400 1485 * @return array
1401 1486 */
1402 1487 public static function get_order_statuses(): array {
1403 - $order_statuses = [
1488 + $order_statuses = array(
1404 1489 LP_ORDER_COMPLETED_DB => LP_ORDER_COMPLETED,
1405 1490 LP_ORDER_PROCESSING_DB => LP_ORDER_PROCESSING,
1406 1491 LP_ORDER_PENDING_DB => LP_ORDER_PENDING,
1407 1492 LP_ORDER_CANCELLED_DB => LP_ORDER_CANCELLED,
1408 1493 LP_ORDER_FAILED_DB => LP_ORDER_FAILED,
1409 - ];
1494 + LP_ORDER_REFUNDED_DB => LP_ORDER_REFUNDED,
1495 + );
1410 1496
1411 1497 return apply_filters( 'lp/order/statuses', $order_statuses );
1412 1498 }
1413 1499
@@ -1414,25 +1500,25 @@
1414 1500 /**
1415 1501 * Query list orders.
1416 1502 *
1417 1503 * @param PostFilter $post_filter
1418 - * @param array $param [ 'author' => int, 'post_status' => string|array, 's' => string, 'm' => string, 'posts_per_page' => int, 'paged' => int, 'orderby' => string, 'order' => string ]
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 ]
1419 1505 *
1420 1506 * @return void
1421 1507 * @throws Exception
1422 1508 * @since 4.3.2.8
1423 - * @version 1.0.0
1509 + * @version 1.0.2
1424 1510 */
1425 - public static function handle_params_query_list_orders( PostFilter &$post_filter, array $param = [] ) {
1426 - $post_db = PostDB::getInstance();
1427 - $user_of_order = absint( $param['author'] ?? 0 );
1428 - $status = $param['post_status'] ?? '';
1429 - $key = $param['s'] ?? '';
1430 - $month = $param['m'] ?? '';
1431 - $limit = $param['posts_per_page'] ?? 20;
1432 - $paged = $param['paged'] ?? 1;
1433 -
1434 - $order_by = $param['orderby'] ?? 'date';
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';
1435 1521 if ( empty( $order_by ) ) {
1436 1522 $order_by = 'ID';
1437 1523 } else {
1438 1524 switch ( $order_by ) {
@@ -1438,10 +1524,20 @@
1438 1524 switch ( $order_by ) {
1439 1525 case 'date':
1440 1526 $order_by = 'post_date';
1441 1527 break;
1442 - case 'title':
1443 - $order_by = 'ID';
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 + }
1444 1540 break;
1445 1541 }
1446 1542 }
1447 1543
@@ -1449,10 +1545,9 @@
1449 1545 // End convert params
1450 1546
1451 1547 if ( $order_by === 'order_total' ) {
1452 1548 $post_filter->join[] = "INNER JOIN {$post_db->tb_postmeta} pm2 ON p.ID = pm2.post_id AND pm2.meta_key = '_order_total'";
1453 - $post_filter->where[] = 'AND CAST(pm2.meta_value AS UNSIGNED)';
1454 - $post_filter->order_by = 'pm2.meta_value';
1549 + $post_filter->order_by = 'CAST(pm2.meta_value AS DECIMAL(10,2))';
1455 1550 } else {
1456 1551 $post_filter->order_by = $order_by;
1457 1552 }
1458 1553
@@ -1484,12 +1579,18 @@
1484 1579 }
1485 1580
1486 1581 if ( ! empty( $month ) ) {
1487 1582 $year = substr( $month, 0, 4 );
1488 - $post_filter->where[] = "AND YEAR(p.post_date) = $year";
1583 + $post_filter->where[] = $post_db->wpdb->prepare(
1584 + 'AND YEAR(p.post_date) = %s',
1585 + $year
1586 + );
1489 1587 if ( strlen( $month ) > 5 ) {
1490 1588 $mon = substr( $month, 4, 2 );
1491 - $post_filter->where[] = "AND MONTH(p.post_date) = $mon";
1589 + $post_filter->where[] = $post_db->wpdb->prepare(
1590 + 'AND MONTH(p.post_date) = %s',
1591 + $mon
1592 + );
1492 1593 }
1493 1594 }
1494 1595
1495 1596 $post_filter->order = $order;
@@ -1501,12 +1602,246 @@
1501 1602 $post_filter->join[] = "INNER JOIN {$post_db->tb_postmeta} pm1 ON p.ID = pm1.post_id AND pm1.meta_key = '_user_id'";
1502 1603 $post_filter->where[] = "AND ( pm1.meta_value like '%\"$user_id\"%' OR pm1.meta_value = $user_id )";
1503 1604 }
1504 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 + }
1505 1618 if ( ! empty( $status ) && $status !== 'all' ) {
1506 1619 $post_filter->post_status = (array) $status;
1507 1620 } else {
1508 1621 $post_filter->where[] = $post_db->wpdb->prepare( 'AND p.post_status != %s', LP_ORDER_TRASH );
1509 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 + );
1510 1845 }
1511 1846 }
1512 1847 }