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

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

668 lines 19.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Defines functions related to order
4 *
5 * @author ThimPress
6 * @package LearnPress/Functions
7 * @version 1.0
8 */
9 use LearnPress\Models\UserItems\UserCourseModel;
10 use LearnPress\Models\UserModel;
11
12 defined( 'ABSPATH' ) || exit;
13
14 /**
15 * Generate unique key for an order.
16 *
17 * @return mixed
18 */
19 function learn_press_generate_order_key() {
20 return apply_filters( 'learn-press/order-key', strtoupper( uniqid( 'ORDER' ) ) );
21 }
22
23 /**
24 * Update Order status
25 *
26 * @param int
27 * @param string
28 *
29 * @return bool
30 */
31 function learn_press_update_order_status( $order_id, $status = '' ) {
32 $order = new LP_Order( $order_id );
33 if ( $order ) {
34 return $order->update_status( $status );
35 }
36
37 return false;
38 }
39
40 /**
41 * Add order item meta data.
42 *
43 * @param int $item_id
44 * @param string $meta_key
45 * @param mixed $meta_value
46 * @param string $prev_value
47 *
48 * @return false|int
49 */
50 function learn_press_add_order_item_meta( $item_id, $meta_key, $meta_value, $prev_value = '' ) {
51 return add_metadata( 'learnpress_order_item', $item_id, $meta_key, $meta_value, $prev_value );
52 }
53
54 /**
55 * Update order item meta data.
56 *
57 * @param int $item_id
58 * @param string $meta_key
59 * @param mixed $meta_value
60 * @param string $prev_value
61 *
62 * @return bool|int
63 */
64 function learn_press_update_order_item_meta( $item_id, $meta_key, $meta_value, $prev_value = '' ) {
65 return update_metadata( 'learnpress_order_item', $item_id, $meta_key, $meta_value, $prev_value );
66 }
67
68 /**
69 * Delete order item meta data.
70 *
71 * @param int $item_id
72 * @param string $meta_key
73 * @param mixed $meta_value
74 * @param bool $delete_all
75 *
76 * @return bool
77 */
78 function learn_press_delete_order_item_meta( $item_id, $meta_key, $meta_value, $delete_all = false ) {
79 return delete_metadata( 'learnpress_order_item', $item_id, $meta_key, $meta_value, $delete_all );
80 }
81
82 /**
83 * Get order item meta data.
84 *
85 * @param int $item_id
86 * @param string $meta_key
87 * @param bool $single
88 *
89 * @return mixed
90 */
91 function learn_press_get_order_item_meta( $item_id, $meta_key, $single = true ) {
92 return get_metadata( 'learnpress_order_item', $item_id, $meta_key, $single );
93 }
94
95 /**
96 * Get order
97 *
98 * @param mixed $the_order
99 *
100 * @return LP_Order|bool object instance
101 */
102 function learn_press_get_order( $the_order = false ) {
103 global $post;
104 $the_id = 0;
105 if ( false === $the_order && is_a( $post, 'WP_Post' ) && LP_ORDER_CPT === get_post_type( $post ) ) {
106 $the_id = $post->ID;
107 } elseif ( is_numeric( $the_order ) ) {
108 $the_id = $the_order;
109 } elseif ( $the_order instanceof LP_Order ) {
110 $the_id = $the_order->get_id();
111 } elseif ( ! empty( $the_order->ID ) ) {
112 $the_id = $the_order->ID;
113 }
114
115 if ( LP_ORDER_CPT != get_post_type( $the_id ) ) {
116 return false;
117 }
118
119 return new LP_Order( $the_id );
120 }
121
122 /**
123 * Count orders by it's status
124 *
125 * @param array $args
126 * @Todo tungnx review to rewrite query
127 * @return array
128 */
129 function learn_press_count_orders( $args = array() ) {
130 if ( is_string( $args ) ) {
131 $args = array( 'status' => $args );
132 } else {
133 $args = wp_parse_args(
134 $args,
135 array(
136 'status' => '',
137 )
138 );
139 }
140 global $wpdb;
141 $statuses = $args['status'];
142
143 if ( ! $statuses ) {
144 $statuses = array_keys( LP_Order::get_order_statuses() );
145 }
146
147 settype( $statuses, 'array' );
148 $size_of_status = sizeof( $statuses );
149
150 foreach ( $statuses as $k => $status ) {
151 $statuses[ $k ] = ! preg_match( '~^lp-~', $status ) ? 'lp-' . $status : $status;
152 }
153
154 $format = array_fill( 0, $size_of_status, '%s' );
155 $counts = array_fill_keys( $statuses, 0 );
156 $statuses[] = LP_ORDER_CPT;
157 $query = $wpdb->prepare(
158 "
159 SELECT COUNT(ID) AS count, post_status AS status
160 FROM {$wpdb->posts} o
161 WHERE post_status IN(" . join( ',', $format ) . ')
162 AND post_type = %s
163 GROUP BY o.post_status
164 ',
165 $statuses
166 );
167
168 $results = $wpdb->get_results( $query );
169 if ( $results ) {
170 foreach ( $results as $result ) {
171 if ( array_key_exists( $result->status, $counts ) ) {
172 $counts[ $result->status ] = absint( $result->count );
173 }
174 }
175 }
176
177 return $size_of_status > 1 ? $counts : reset( $counts );
178 }
179
180 /**
181 * Format price with currency and other settings.
182 *
183 * @param float $price
184 * @param string $currency
185 *
186 * @return string
187 */
188 function learn_press_format_price( $price = 0, $currency = '' ): string {
189 if ( ! is_numeric( $price ) ) {
190 $price = 0;
191 }
192
193 $before = $after = '';
194
195 $currency = esc_html(
196 is_string( $currency ) && '' !== $currency
197 ? $currency
198 : learn_press_get_currency_symbol()
199 );
200 $thousands_separator = esc_html( LP_Settings::get_option( 'thousands_separator', ',' ) );
201 $number_of_decimals = esc_html( LP_Settings::get_option( 'number_of_decimals', 2 ) );
202 $decimals_separator = esc_html( LP_Settings::get_option( 'decimals_separator', '.' ) );
203
204 switch ( LP_Settings::get_option( 'currency_pos' ) ) {
205 default:
206 $before = $currency;
207 break;
208 case 'left_with_space':
209 $before = $currency . ' ';
210 break;
211 case 'right':
212 $after = $currency;
213 break;
214 case 'right_with_space':
215 $after = ' ' . $currency;
216 }
217
218 return $before . number_format( $price, $number_of_decimals, $decimals_separator, $thousands_separator ) . $after;
219 }
220
221 /**
222 * Update
223 *
224 * @param $order_id
225 *
226 * @return array|bool
227 */
228 function learn_press_update_order_items( $order_id ) {
229 $order = learn_press_get_order( $order_id );
230 if ( ! $order ) {
231 return false;
232 }
233
234 $subtotal = 0;
235 $total = 0;
236 $items = $order->get_items();
237
238 if ( $items ) {
239 foreach ( $items as $item ) {
240 $subtotal += $item['subtotal'];
241 $total += $item['total'];
242 }
243 }
244
245 update_post_meta( $order_id, '_order_currency', learn_press_get_currency() );
246 update_post_meta( $order_id, '_prices_include_tax', 'no' );
247 update_post_meta( $order_id, '_order_subtotal', $subtotal );
248 update_post_meta( $order_id, '_order_total', $total );
249 update_post_meta( $order_id, '_order_key', learn_press_generate_order_key() );
250 update_post_meta( $order_id, '_payment_method', '' );
251 update_post_meta( $order_id, '_payment_method_title', '' );
252 update_post_meta( $order_id, '_order_version', '1.0' );
253
254 return array(
255 'subtotal' => $subtotal,
256 'total' => $total,
257 'currency' => learn_press_get_currency(),
258 );
259 }
260
261 /**
262 * Format order's ID in ten numbers. Eg: 0000000XXX.
263 *
264 * @param int $order_number
265 *
266 * @since 2.0.0
267 *
268 * @return string
269 */
270 function learn_press_transaction_order_number( $order_number ) {
271 $formatted_number = apply_filters( 'learn_press_get_order_number', '#' . sprintf( "%'.010d", $order_number ), $order_number );
272
273 return apply_filters( 'learn-press/order-number-formatted', $formatted_number, $order_number );
274 }
275
276 /**
277 * Get list of registered order's statues for registering with wp post's status.
278 *
279 * @since 2.0.0
280 *
281 * @return array
282 */
283 function learn_press_get_register_order_statuses() {
284 $order_statues = array();
285
286 $order_statues['lp-completed'] = array(
287 'label' => _x( 'Completed', 'Order status', 'learnpress' ),
288 'public' => false,
289 'exclude_from_search' => false,
290 'show_in_admin_all_list' => true,
291 'show_in_admin_status_list' => true,
292 'label_count' => _n_noop( 'Completed <span class="count">(%s)</span>', 'Completed <span class="count">(%s)</span>', 'learnpress' ),
293 );
294 $order_statues['lp-pending'] = array(
295 'label' => _x( 'Pending', 'Order status', 'learnpress' ),
296 'public' => false,
297 'exclude_from_search' => false,
298 'show_in_admin_all_list' => true,
299 'show_in_admin_status_list' => true,
300 'label_count' => _n_noop( 'Pending Payment <span class="count">(%s)</span>', 'Pending Payment <span class="count">(%s)</span>', 'learnpress' ),
301 );
302 $order_statues['lp-processing'] = array(
303 'label' => _x( 'Processing', 'Order status', 'learnpress' ),
304 'public' => false,
305 'exclude_from_search' => false,
306 'show_in_admin_all_list' => true,
307 'show_in_admin_status_list' => true,
308 'label_count' => _n_noop( 'Processing <span class="count">(%s)</span>', 'Processing <span class="count">(%s)</span>', 'learnpress' ),
309 );
310 $order_statues['lp-cancelled'] = array(
311 'label' => _x( 'Cancelled', 'Order status', 'learnpress' ),
312 'public' => false,
313 'exclude_from_search' => false,
314 'show_in_admin_all_list' => true,
315 'show_in_admin_status_list' => true,
316 'label_count' => _n_noop( 'Cancelled <span class="count">(%s)</span>', 'Cancelled <span class="count">(%s)</span>', 'learnpress' ),
317 );
318 $order_statues['lp-failed'] = array(
319 'label' => _x( 'Failed', 'Order status', 'learnpress' ),
320 'public' => false,
321 'exclude_from_search' => false,
322 'show_in_admin_all_list' => true,
323 'show_in_admin_status_list' => true,
324 'label_count' => _n_noop( 'Failed <span class="count">(%s)</span>', 'Failed <span class="count">(%s)</span>', 'learnpress' ),
325 );
326 $order_statues['lp-refunded'] = array(
327 'label' => _x( 'Refunded', 'Order status', 'learnpress' ),
328 'public' => false,
329 'exclude_from_search' => false,
330 'show_in_admin_all_list' => true,
331 'show_in_admin_status_list' => true,
332 'label_count' => _n_noop( 'Refunded <span class="count">(%s)</span>', 'Refunded <span class="count">(%s)</span>', 'learnpress' ),
333 );
334 $order_statues['trash'] = array(
335 'label' => _x( 'Trash', 'Order status', 'learnpress' ),
336 'public' => false,
337 'exclude_from_search' => false,
338 'show_in_admin_all_list' => true,
339 'show_in_admin_status_list' => true,
340 'label_count' => _n_noop( 'Trash <span class="count">(%s)</span>', 'Trash <span class="count">(%s)</span>', 'learnpress' ),
341 );
342
343 return $order_statues;
344 }
345
346 function _learn_press_get_order_status_description( $status ) {
347
348 $status = str_replace( 'lp-', '', (string) $status );
349 $descriptions = array(
350 'pending' => __( 'Order received in case a user purchases a course but doesn\'t finalize the order.', 'learnpress' ),
351 'processing' => __( 'Payment received and the order is awaiting fulfillment.', 'learnpress' ),
352 'completed' => __( 'The order is fulfilled and completed.', 'learnpress' ),
353 'cancelled' => __( 'The order is cancelled by an admin or the customer.', 'learnpress' ),
354 'refunded' => __( 'Order was refunded to the customer.', 'learnpress' ),
355 );
356
357 return apply_filters( 'learn_press_order_status_description', ! empty( $descriptions[ $status ] ) ? $descriptions[ $status ] : '' );
358 }
359 /**
360 * Get status of an order by the ID.
361 *
362 * @param int $order_id
363 *
364 * @return bool|string
365 */
366 function learn_press_get_order_status( $order_id ) {
367
368 $order = learn_press_get_order( $order_id );
369
370 if ( $order ) {
371 return $order->get_status();
372 }
373
374 return false;
375 }
376
377 if ( ! function_exists( 'learn_press_get_refund_setting' ) ) {
378 /**
379 * Get refund setting value with safe defaults.
380 *
381 * @param string $key
382 * @param mixed $default
383 *
384 * @since 4.3.4
385 * @version 1.0.0
386 * @return mixed
387 */
388 function learn_press_get_refund_setting( string $key, $default = null ) {
389 $defaults = array(
390 'enable_refund_requests' => 'no',
391 'auto_refund' => 'no',
392 'refund_time_limit' => 30,
393 'require_refund_reason' => 'no',
394 'allow_resend_after_rejected' => 'no',
395 'refund_max_completion' => 0,
396 );
397
398 if ( null === $default && array_key_exists( $key, $defaults ) ) {
399 $default = $defaults[ $key ];
400 }
401
402 return LP_Settings::get_option( $key, $default );
403 }
404 }
405
406 if ( ! function_exists( 'learn_press_get_order_refund_supported_gateways' ) ) {
407 /**
408 * Get list gateways that support refund.
409 *
410 * @since 4.3.4
411 * @version 1.1.0
412 * @return array
413 */
414 function learn_press_get_order_refund_supported_gateways(): array {
415 if ( ! class_exists( 'LP_Gateways' ) ) {
416 return array();
417 }
418
419 $gateways_instance = LP_Gateways::instance();
420 if ( ! $gateways_instance || ! method_exists( $gateways_instance, 'get_gateways' ) ) {
421 return array();
422 }
423
424 $supported_gateways = array();
425 $all_gateways = (array) $gateways_instance->get_gateways();
426 foreach ( $all_gateways as $gateway_id => $gateway ) {
427 $gateway_id = sanitize_key( (string) $gateway_id );
428 if ( empty( $gateway_id ) ) {
429 continue;
430 }
431
432 if ( ! is_object( $gateway ) || ! is_callable( array( $gateway, 'refund' ) ) ) {
433 continue;
434 }
435
436 $supported_gateways[] = $gateway_id;
437 }
438
439 return array_values( array_unique( $supported_gateways ) );
440 }
441 }
442
443 if ( ! function_exists( 'learn_press_cancel_order_process' ) ) {
444 /**
445 * Process action allows user to cancel an order is pending
446 * in their profile.
447 */
448 function learn_press_cancel_order_process() {
449
450 if ( empty( $_REQUEST['cancel-order'] ) || empty( $_REQUEST['lp-nonce'] ) ||
451 ! wp_verify_nonce( $_REQUEST['lp-nonce'], 'cancel-order' ) || is_admin() ) {
452 return;
453 }
454
455 $user_id = get_current_user_id();
456 $profile = LP_Profile::instance( $user_id );
457 $url = $profile->get_tab_link(
458 LP_Settings::instance()->get( 'profile_endpoints.orders', 'orders' )
459 );
460
461 try {
462 $message = array(
463 'status' => 'error',
464 'content' => '',
465 );
466
467 $order_id = absint( $_REQUEST['cancel-order'] );
468 $order = learn_press_get_order( $order_id );
469
470 if ( ! $order ) {
471 throw new Exception( sprintf( __( 'Order number <strong>%s</strong> not found', 'learnpress' ), $order_id ) );
472 }
473
474 $user_ids = (array) $order->get_user_id();
475 if ( ! in_array( $user_id, $user_ids ) ) {
476 throw new Exception( __( 'You do not have permission to cancel this order.', 'learnpress' ) );
477 }
478
479 if ( $order->has_status( LP_ORDER_PENDING ) ) {
480 $order->update_status( LP_ORDER_CANCELLED );
481 $order->add_note( __( 'The order is cancelled by the customer', 'learnpress' ) );
482
483 $message['status'] = 'success';
484 $message['content'] = sprintf( __( 'Order number <strong>%s</strong> has been cancelled', 'learnpress' ), $order->get_order_number() );
485 } else {
486 throw new Exception(
487 sprintf(
488 __( 'The order number <strong>%s</strong> can not be cancelled.', 'learnpress' ),
489 $order->get_order_number()
490 )
491 );
492 }
493 } catch ( Throwable $e ) {
494 $message['content'] = $e->getMessage();
495 }
496
497 learn_press_set_message( $message );
498 wp_safe_redirect( $url );
499 exit();
500 }
501 }
502 add_action( 'init', 'learn_press_cancel_order_process' );
503 if ( ! function_exists( 'learn_press_get_order_refund_event_data' ) ) {
504 /**
505 * Build normalized refund event payload.
506 *
507 * @since 4.3.5
508 * @version 1.0.1
509 *
510 * @param LP_Order $order
511 * @param array $overrides
512 *
513 * @return array
514 */
515 function learn_press_get_order_refund_event_data( LP_Order $order, array $overrides = array() ): array {
516 $order_id = $order->get_id();
517 $requested_at = get_post_meta( $order_id, '_lp_refund_requested_at', true );
518 if ( ! empty( $requested_at ) ) {
519 $requested_at = new LP_Datetime( $requested_at );
520 $requested_at = sprintf(
521 esc_html__( '%1$s %2$s', 'learnpress' ),
522 $requested_at->format( LP_Datetime::I18N_FORMAT_HAS_TIME ),
523 LP_Datetime::get_timezone_string()
524 );
525 }
526
527 $user_id = (int) $order->get_user_id();
528 $userOrderModel = UserModel::find( $user_id, true );
529 if ( $userOrderModel instanceof UserModel ) {
530 $requested_by = $userOrderModel->get_display_name();
531 } else {
532 $requested_by = $order->get_checkout_email();
533 }
534
535 $data = array(
536 'order_id' => $order_id,
537 'order_number' => $order->get_order_number(),
538 'order_key' => $order->get_order_key(),
539 'order_status' => $order->get_status(),
540 'request_status' => $order->get_refund_request(),
541 'requested_by' => $requested_by,
542 'requested_at' => $requested_at,
543 'reviewed_by' => absint( get_post_meta( $order_id, '_lp_refund_reviewed_by', true ) ),
544 'reviewed_at' => (string) get_post_meta( $order_id, '_lp_refund_reviewed_at', true ),
545 'reason' => (string) get_post_meta( $order_id, LP_Order::META_KEY_REFUND_REQUEST_REASON, true ),
546 'requester_email' => '',
547 'admin_order_edit_url' => add_query_arg(
548 array(
549 'post' => $order_id,
550 'action' => 'edit',
551 ),
552 admin_url( 'post.php' )
553 ),
554 );
555
556 $requested_user_id = absint( $data['requested_by'] );
557 if ( ! empty( $requested_user_id ) ) {
558 $requested_user = get_user_by( 'id', $requested_user_id );
559 if ( $requested_user instanceof WP_User ) {
560 $data['requester_email'] = $requested_user->user_email;
561 }
562 }
563
564 if ( ! empty( $overrides ) ) {
565 $data = array_merge( $data, $overrides );
566 }
567
568 return $data;
569 }
570 }
571 /**
572 * Render pending refund request panel on order detail.
573 *
574 * @since 4.3.4
575 * @version 1.0.0
576 * @param LP_Order $order
577 */
578 function learn_press_admin_order_refund_request_panel( $order ) {
579
580 if ( ! $order instanceof LP_Order ) {
581 return;
582 }
583
584 $order_id = $order->get_id();
585 if ( ! current_user_can( 'edit_post', $order_id ) ) {
586 return;
587 }
588
589 $refund_event_data = learn_press_get_order_refund_event_data( $order );
590 $refund_request_status = sanitize_key( (string) ( $refund_event_data['request_status'] ?? '' ) );
591 $requested_by = $refund_event_data['requested_by'] ?? '';
592 $requested_at = $refund_event_data['requested_at'] ?? '';
593 $refund_reason = $refund_event_data['reason'] ?? '';
594 $requester_email = '';
595
596 $status_labels = array(
597 'pending' => __( 'Pending review', 'learnpress' ),
598 'approved' => __( 'Approved', 'learnpress' ),
599 'auto-approved' => __( 'Auto approved', 'learnpress' ),
600 'rejected' => __( 'Rejected', 'learnpress' ),
601 );
602 $status_label = $status_labels[ $refund_request_status ] ?? '';
603 if ( empty( $status_label ) && ! empty( $refund_request_status ) ) {
604 $status_label = ucwords( str_replace( '-', ' ', $refund_request_status ) );
605 }
606
607 $render_statuses = array( 'pending', 'approved', 'auto-approved' );
608 if ( ! in_array( $refund_request_status, $render_statuses, true ) ) {
609 return;
610 }
611
612 $is_pending = ( 'pending' === $refund_request_status );
613 $currency_symbol = learn_press_get_currency_symbol( $order->get_currency() );
614
615 $order_total = round( max( 0, floatval( $order->get_total() ) ), 2 );
616 $order_total_formatted = learn_press_format_price( $order_total, $currency_symbol );
617
618 // Refund amount is only meaningful once the refund has been executed
619 // (approved/auto-approved), when _lp_refund_amount has been written.
620 $refund_amount_formatted = '';
621 if ( ! $is_pending ) {
622 $refund_amount = get_post_meta( $order_id, LP_Order::META_KEY_REFUNDED_AMOUNT, true );
623 if ( '' === $refund_amount || null === $refund_amount ) {
624 $refund_amount = $order_total; // Fallback for full refunds without a stored amount.
625 }
626 $refund_amount = round( max( 0, floatval( $refund_amount ) ), 2 );
627 $refund_amount_formatted = learn_press_format_price( $refund_amount, $currency_symbol );
628 }
629
630 learn_press_admin_view(
631 'meta-boxes/order/refund-request-panel',
632 compact(
633 'order_id',
634 'is_pending',
635 'order_total',
636 'order_total_formatted',
637 'refund_amount_formatted',
638 'requested_by',
639 'requested_at',
640 'requester_email',
641 'refund_reason',
642 'status_label'
643 )
644 );
645 }
646
647 /**
648 * get total price order complete
649 */
650 function learn_press_get_total_price_order_complete() {
651 global $wpdb;
652
653 $query = $wpdb->prepare(
654 "SELECT SUM(meta_value) as order_total From `{$wpdb->prefix}postmeta` as mt
655 INNER JOIN `{$wpdb->prefix}posts` as p ON p.id = mt.post_id
656 WHERE p.post_type = %s AND mt.meta_key = %s
657 AND p.post_status = %s
658 ",
659 LP_ORDER_CPT,
660 '_order_total',
661 'lp-completed'
662 );
663
664 $total = $wpdb->get_results( $query )[0]->order_total;
665
666 return learn_press_format_price( $total, true );
667 }
668