| @@ -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 | } |