| @@ -5,8 +5,10 @@ | ||
| 5 | 5 | * @author ThimPress |
| 6 | 6 | * @package LearnPress/Functions |
| 7 | 7 | * @version 1.0 |
| 8 | 8 | */ |
| 9 | +use LearnPress\Models\UserItems\UserCourseModel; | |
| 10 | +use LearnPress\Models\UserModel; | |
| 9 | 11 | |
| 10 | 12 | defined( 'ABSPATH' ) || exit; |
| 11 | 13 | |
| 12 | 14 | /** |
| @@ -177,9 +179,9 @@ | ||
| 177 | 179 | |
| 178 | 180 | /** |
| 179 | 181 | * Format price with currency and other settings. |
| 180 | 182 | * |
| 181 | - * @param float $price | |
| 183 | + * @param float $price | |
| 182 | 184 | * @param string $currency |
| 183 | 185 | * |
| 184 | 186 | * @return string |
| 185 | 187 | */ |
| @@ -320,8 +322,16 @@ | ||
| 320 | 322 | 'show_in_admin_all_list' => true, |
| 321 | 323 | 'show_in_admin_status_list' => true, |
| 322 | 324 | 'label_count' => _n_noop( 'Failed <span class="count">(%s)</span>', 'Failed <span class="count">(%s)</span>', 'learnpress' ), |
| 323 | 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 | + ); | |
| 324 | 334 | $order_statues['trash'] = array( |
| 325 | 335 | 'label' => _x( 'Trash', 'Order status', 'learnpress' ), |
| 326 | 336 | 'public' => false, |
| 327 | 337 | 'exclude_from_search' => false, |
| @@ -333,18 +343,20 @@ | ||
| 333 | 343 | return $order_statues; |
| 334 | 344 | } |
| 335 | 345 | |
| 336 | 346 | function _learn_press_get_order_status_description( $status ) { |
| 347 | + | |
| 348 | + $status = str_replace( 'lp-', '', (string) $status ); | |
| 337 | 349 | $descriptions = array( |
| 338 | 350 | 'pending' => __( 'Order received in case a user purchases a course but doesn\'t finalize the order.', 'learnpress' ), |
| 339 | 351 | 'processing' => __( 'Payment received and the order is awaiting fulfillment.', 'learnpress' ), |
| 340 | 352 | 'completed' => __( 'The order is fulfilled and completed.', 'learnpress' ), |
| 341 | 353 | 'cancelled' => __( 'The order is cancelled by an admin or the customer.', 'learnpress' ), |
| 354 | + 'refunded' => __( 'Order was refunded to the customer.', 'learnpress' ), | |
| 342 | 355 | ); |
| 343 | 356 | |
| 344 | 357 | return apply_filters( 'learn_press_order_status_description', ! empty( $descriptions[ $status ] ) ? $descriptions[ $status ] : '' ); |
| 345 | 358 | } |
| 346 | - | |
| 347 | 359 | /** |
| 348 | 360 | * Get status of an order by the ID. |
| 349 | 361 | * |
| 350 | 362 | * @param int $order_id |
| @@ -351,8 +363,9 @@ | ||
| 351 | 363 | * |
| 352 | 364 | * @return bool|string |
| 353 | 365 | */ |
| 354 | 366 | function learn_press_get_order_status( $order_id ) { |
| 367 | + | |
| 355 | 368 | $order = learn_press_get_order( $order_id ); |
| 356 | 369 | |
| 357 | 370 | if ( $order ) { |
| 358 | 371 | return $order->get_status(); |
| @@ -360,8 +373,74 @@ | ||
| 360 | 373 | |
| 361 | 374 | return false; |
| 362 | 375 | } |
| 363 | 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 | + | |
| 364 | 443 | if ( ! function_exists( 'learn_press_cancel_order_process' ) ) { |
| 365 | 444 | /** |
| 366 | 445 | * Process action allows user to cancel an order is pending |
| 367 | 446 | * in their profile. |
| @@ -366,8 +445,9 @@ | ||
| 366 | 445 | * Process action allows user to cancel an order is pending |
| 367 | 446 | * in their profile. |
| 368 | 447 | */ |
| 369 | 448 | function learn_press_cancel_order_process() { |
| 449 | + | |
| 370 | 450 | if ( empty( $_REQUEST['cancel-order'] ) || empty( $_REQUEST['lp-nonce'] ) || |
| 371 | 451 | ! wp_verify_nonce( $_REQUEST['lp-nonce'], 'cancel-order' ) || is_admin() ) { |
| 372 | 452 | return; |
| 373 | 453 | } |
| @@ -378,12 +458,12 @@ | ||
| 378 | 458 | LP_Settings::instance()->get( 'profile_endpoints.orders', 'orders' ) |
| 379 | 459 | ); |
| 380 | 460 | |
| 381 | 461 | try { |
| 382 | - $message = [ | |
| 462 | + $message = array( | |
| 383 | 463 | 'status' => 'error', |
| 384 | 464 | 'content' => '', |
| 385 | - ]; | |
| 465 | + ); | |
| 386 | 466 | |
| 387 | 467 | $order_id = absint( $_REQUEST['cancel-order'] ); |
| 388 | 468 | $order = learn_press_get_order( $order_id ); |
| 389 | 469 | |
| @@ -403,10 +483,12 @@ | ||
| 403 | 483 | $message['status'] = 'success'; |
| 404 | 484 | $message['content'] = sprintf( __( 'Order number <strong>%s</strong> has been cancelled', 'learnpress' ), $order->get_order_number() ); |
| 405 | 485 | } else { |
| 406 | 486 | throw new Exception( |
| 407 | - __( 'The order number <strong>%s</strong> can not be cancelled.', 'learnpress' ), | |
| 408 | - $order->get_order_number() | |
| 487 | + sprintf( | |
| 488 | + __( 'The order number <strong>%s</strong> can not be cancelled.', 'learnpress' ), | |
| 489 | + $order->get_order_number() | |
| 490 | + ) | |
| 409 | 491 | ); |
| 410 | 492 | } |
| 411 | 493 | } catch ( Throwable $e ) { |
| 412 | 494 | $message['content'] = $e->getMessage(); |
| @@ -417,15 +499,155 @@ | ||
| 417 | 499 | exit(); |
| 418 | 500 | } |
| 419 | 501 | } |
| 420 | 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 | + } | |
| 421 | 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 | + } | |
| 422 | 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 | +} | |
| 423 | 571 | /** |
| 424 | - * get total price order complete | |
| 572 | + * Render pending refund request panel on order detail. | |
| 425 | 573 | * |
| 574 | + * @since 4.3.4 | |
| 575 | + * @version 1.0.0 | |
| 576 | + * @param LP_Order $order | |
| 426 | 577 | */ |
| 578 | +function learn_press_admin_order_refund_request_panel( $order ) { | |
| 427 | 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 | + */ | |
| 428 | 650 | function learn_press_get_total_price_order_complete() { |
| 429 | 651 | global $wpdb; |
| 430 | 652 | |
| 431 | 653 | $query = $wpdb->prepare( |