| @@ -209,16 +209,15 @@ | ||
| 209 | 209 | if ( ! empty( $subscription_data ) ) { |
| 210 | 210 | $subscription_res = $this->pay_via_subscription( $order, $subscription_data ); |
| 211 | 211 | $paypal_payment_url = $subscription_res['redirect_url'] ?? ''; |
| 212 | 212 | } else { |
| 213 | - $data_token = $this->get_access_token(); | |
| 214 | - $paypal_payment_url = $this->create_payment_url( $order, $data_token ); | |
| 213 | + $paypal_payment_url = $this->create_payment_url( $order ); | |
| 215 | 214 | } |
| 216 | 215 | |
| 217 | - $result['result'] = 'success'; | |
| 218 | - $result['redirect'] = $paypal_payment_url; | |
| 219 | - | |
| 220 | - return $result; | |
| 216 | + return [ | |
| 217 | + 'result' => 'success', | |
| 218 | + 'redirect' => $paypal_payment_url, | |
| 219 | + ]; | |
| 221 | 220 | } |
| 222 | 221 | |
| 223 | 222 | /** |
| 224 | 223 | * Prepare args to send to PayPal |
| @@ -376,24 +375,20 @@ | ||
| 376 | 375 | /** |
| 377 | 376 | * Create Order PayPal and get checkout url |
| 378 | 377 | * |
| 379 | 378 | * @param LP_Order $order |
| 380 | - * @param object $data_token { scope, access_token, token_type, app_id, expires_in, nonce } | |
| 381 | 379 | * |
| 382 | 380 | * @return string |
| 383 | 381 | * @throws Exception |
| 384 | 382 | * @since 4.2.4 |
| 385 | - * @version 1.0.0 | |
| 383 | + * @version 1.0.1 | |
| 386 | 384 | */ |
| 387 | - public function create_payment_url( LP_Order $order, $data_token ): string { | |
| 385 | + public function create_payment_url( LP_Order $order ): string { | |
| 388 | 386 | $checkout_url = ''; |
| 389 | 387 | $params = $this->get_order_args( $order ); |
| 390 | 388 | |
| 391 | - if ( ! isset( $data_token->access_token ) || ! isset( $data_token->token_type ) ) { | |
| 392 | - throw new Exception( __( 'Invalid Paypal access token', 'learnpress' ) ); | |
| 393 | - } | |
| 394 | - | |
| 395 | - $response = wp_remote_post( | |
| 389 | + $data_token = $this->get_access_token(); | |
| 390 | + $response = wp_remote_post( | |
| 396 | 391 | $this->api_url . 'v2/checkout/orders', |
| 397 | 392 | array( |
| 398 | 393 | 'body' => json_encode( $params ), |
| 399 | 394 | 'headers' => array( |
| @@ -443,18 +438,14 @@ | ||
| 443 | 438 | * https://developer.paypal.com/docs/api/orders/v2/#orders_capture |
| 444 | 439 | * |
| 445 | 440 | * @return bool True when capture is completed and order status is updated. |
| 446 | 441 | * @throws Exception |
| 447 | - * @version 1.0.1 | |
| 442 | + * @version 1.0.2 | |
| 448 | 443 | * @since 4.2.4 |
| 449 | 444 | */ |
| 450 | 445 | public function capture_payment_for_order( string $paypal_order_id ): bool { |
| 451 | 446 | $data_token = $this->get_access_token(); |
| 452 | - if ( ! isset( $data_token->access_token ) || ! isset( $data_token->token_type ) ) { | |
| 453 | - return false; | |
| 454 | - } | |
| 455 | - | |
| 456 | - $response = wp_remote_post( | |
| 447 | + $response = wp_remote_post( | |
| 457 | 448 | $this->api_url . 'v2/checkout/orders/' . $paypal_order_id . '/capture', |
| 458 | 449 | array( |
| 459 | 450 | 'headers' => array( |
| 460 | 451 | 'Content-Type' => 'application/json', |
| @@ -464,30 +455,49 @@ | ||
| 464 | 455 | ) |
| 465 | 456 | ); |
| 466 | 457 | |
| 467 | 458 | if ( is_wp_error( $response ) ) { |
| 468 | - return false; | |
| 459 | + throw new Exception( $response->get_error_message() ); | |
| 469 | 460 | } |
| 470 | 461 | |
| 471 | - $response_code = absint( $response['response']['code'] ?? 0 ); | |
| 472 | - if ( 201 !== $response_code ) { | |
| 462 | + $body = wp_remote_retrieve_body( $response ); | |
| 463 | + $response_data = LP_Helper::json_decode( $body ); | |
| 464 | + if ( isset( $response_data->debug_id ) ) { | |
| 465 | + throw new Exception( $response_data->details[0]->description ); | |
| 466 | + } | |
| 467 | + | |
| 468 | + $capture = $response_data->purchase_units[0]->payments->captures[0] ?? null; | |
| 469 | + if ( empty( $capture ) ) { | |
| 473 | 470 | return false; |
| 474 | 471 | } |
| 475 | 472 | |
| 476 | - $body = wp_remote_retrieve_body( $response ); | |
| 477 | - $transaction = LP_Helper::json_decode( $body ); | |
| 478 | - if ( empty( $transaction ) || ! is_object( $transaction ) || ( $transaction->status ?? '' ) !== 'COMPLETED' ) { | |
| 473 | + $order_id = absint( $capture->custom_id ?? 0 ); | |
| 474 | + if ( $order_id <= 0 ) { | |
| 479 | 475 | return false; |
| 480 | 476 | } |
| 481 | 477 | |
| 482 | - $order_id = absint( $transaction->purchase_units[0]->payments->captures[0]->custom_id ?? 0 ); | |
| 483 | - if ( $order_id <= 0 ) { | |
| 478 | + $capture_id = $capture->id ?? ''; | |
| 479 | + if ( empty( $capture_id ) ) { | |
| 484 | 480 | return false; |
| 485 | 481 | } |
| 486 | 482 | |
| 487 | 483 | $lp_order = learn_press_get_order( $order_id ); |
| 488 | - if ( $lp_order instanceof LP_Order ) { | |
| 489 | - $lp_order->update_status( LP_ORDER_COMPLETED ); | |
| 484 | + if ( $lp_order instanceof LP_Order && $capture->status === 'COMPLETED' ) { | |
| 485 | + $transaction_id = $lp_order->get_transaction_id(); | |
| 486 | + if ( $transaction_id === $capture_id ) { | |
| 487 | + return true; | |
| 488 | + } | |
| 489 | + | |
| 490 | + $lp_order->payment_complete( $capture_id ); | |
| 491 | + | |
| 492 | + $lp_order->add_note( | |
| 493 | + sprintf( | |
| 494 | + __( 'PayPal payment %1$s completed at %2$s %3$s', 'learnpress' ), | |
| 495 | + $capture_id, | |
| 496 | + wp_date( 'Y-m-d H:i:s' ), | |
| 497 | + wp_timezone_string() | |
| 498 | + ) | |
| 499 | + ); | |
| 490 | 500 | return true; |
| 491 | 501 | } |
| 492 | 502 | |
| 493 | 503 | return false; |
| @@ -622,9 +632,10 @@ | ||
| 622 | 632 | if ( ! $this->is_subscription_enabled() ) { |
| 623 | 633 | throw new Exception( __( 'PayPal subscriptions are disabled.', 'learnpress' ) ); |
| 624 | 634 | } |
| 625 | 635 | |
| 626 | - $data = $this->validate_data_plan_payload( $data ); | |
| 636 | + $data = $this->validate_data_plan_payload( $data ); | |
| 637 | + $data_token = $this->get_access_token(); | |
| 627 | 638 | |
| 628 | 639 | // PayPal-specific optional keys are normalized at gateway level. |
| 629 | 640 | $description = LP_Helper::sanitize_params_submitted( $data['description'] ?? '', 'html' ); |
| 630 | 641 | $trial_days = absint( $data['trial_days'] ?? 0 ); |
| @@ -629,10 +640,8 @@ | ||
| 629 | 640 | $description = LP_Helper::sanitize_params_submitted( $data['description'] ?? '', 'html' ); |
| 630 | 641 | $trial_days = absint( $data['trial_days'] ?? 0 ); |
| 631 | 642 | $setup_fee = (float) ( $data['setup_fee'] ?? 0 ); |
| 632 | 643 | |
| 633 | - $data_token = $this->get_access_token(); | |
| 634 | - | |
| 635 | 644 | // Create product before create plan |
| 636 | 645 | $product_id = $data['product_id'] ?? ''; |
| 637 | 646 | $product_data = [ 'id' => $product_id ]; |
| 638 | 647 | if ( empty( $product_id ) ) { |
| @@ -949,13 +958,8 @@ | ||
| 949 | 958 | $current_sum = $current['summary']; |
| 950 | 959 | |
| 951 | 960 | $data = $this->validate_data_plan_payload( $data ); |
| 952 | 961 | |
| 953 | - $data_token = $this->get_access_token(); | |
| 954 | - if ( empty( $data_token->access_token ) || empty( $data_token->token_type ) ) { | |
| 955 | - throw new Exception( __( 'Invalid Paypal access token', 'learnpress' ) ); | |
| 956 | - } | |
| 957 | - | |
| 958 | 962 | $patches = array(); |
| 959 | 963 | if ( ! empty( $data['name'] ?? '' ) ) { |
| 960 | 964 | $patches[] = array( |
| 961 | 965 | 'op' => 'replace', |
| @@ -996,8 +1000,9 @@ | ||
| 996 | 1000 | ); |
| 997 | 1001 | } |
| 998 | 1002 | |
| 999 | 1003 | if ( ! empty( $patches ) ) { |
| 1004 | + $data_token = $this->get_access_token(); | |
| 1000 | 1005 | $patch_response = wp_remote_request( |
| 1001 | 1006 | $this->api_url . 'v1/billing/plans/' . rawurlencode( $plan_id ), |
| 1002 | 1007 | array( |
| 1003 | 1008 | 'method' => 'PATCH', |
| @@ -1340,9 +1345,9 @@ | ||
| 1340 | 1345 | ) |
| 1341 | 1346 | ); |
| 1342 | 1347 | |
| 1343 | 1348 | if ( is_wp_error( $response ) ) { |
| 1344 | - throw new Exception( $response->get_error_message(), $response->get_error_code() ); | |
| 1349 | + throw new Exception( $response->get_error_message() ); | |
| 1345 | 1350 | } |
| 1346 | 1351 | |
| 1347 | 1352 | $body = wp_remote_retrieve_body( $response ); |
| 1348 | 1353 | $response_data = LP_Helper::json_decode( $body, true ); |
| @@ -1706,9 +1711,9 @@ | ||
| 1706 | 1711 | ) |
| 1707 | 1712 | ); |
| 1708 | 1713 | |
| 1709 | 1714 | if ( is_wp_error( $response ) ) { |
| 1710 | - throw new Exception( $response->get_error_message(), $response->get_error_code() ); | |
| 1715 | + throw new Exception( $response->get_error_message() ); | |
| 1711 | 1716 | } |
| 1712 | 1717 | |
| 1713 | 1718 | $verify_result = LP_Helper::json_decode( wp_remote_retrieve_body( $response ), true ); |
| 1714 | 1719 | $is_verified = ! empty( $verify_result['verification_status'] ) |
| @@ -1972,8 +1977,87 @@ | ||
| 1972 | 1977 | |
| 1973 | 1978 | $manage_url = trailingslashit( $this->paypal_url ) . 'myaccount/autopay/'; |
| 1974 | 1979 | |
| 1975 | 1980 | return (string) apply_filters( 'learn-press/paypal/subscription/manage-url', $manage_url, $order, $subscription_id, $this ); |
| 1981 | + } | |
| 1982 | + | |
| 1983 | + /** | |
| 1984 | + * Refund PayPal capture via REST API. | |
| 1985 | + * | |
| 1986 | + * @param LP_Order $lp_order | |
| 1987 | + * @param float $amount 0 or empty means full refund. | |
| 1988 | + * @param string $note | |
| 1989 | + * | |
| 1990 | + * @return array | |
| 1991 | + * @throws Exception | |
| 1992 | + * @since 4.4.0 | |
| 1993 | + * @version 1.0.0 | |
| 1994 | + */ | |
| 1995 | + public function refund( $lp_order, float $amount = 0, string $note = '' ): array { | |
| 1996 | + $order_id = $lp_order->get_id(); | |
| 1997 | + $capture_id = $lp_order->get_transaction_id(); | |
| 1998 | + if ( empty( $capture_id ) ) { | |
| 1999 | + throw new Exception( __( 'Missing PayPal capture id to refund.', 'learnpress' ) ); | |
| 2000 | + } | |
| 2001 | + | |
| 2002 | + $refund_args = []; | |
| 2003 | + if ( $amount > 0 ) { | |
| 2004 | + $refund_args['amount'] = [ | |
| 2005 | + 'currency_code' => $lp_order->get_currency(), | |
| 2006 | + 'value' => strval( round( $amount, 2 ) ), | |
| 2007 | + ]; | |
| 2008 | + } | |
| 2009 | + | |
| 2010 | + $note_to_payer = trim( $note ); | |
| 2011 | + if ( ! empty( $note_to_payer ) ) { | |
| 2012 | + $refund_args['note_to_payer'] = $note_to_payer; | |
| 2013 | + } | |
| 2014 | + | |
| 2015 | + $refund_args = apply_filters( 'learn-press/paypal-refund/args', $refund_args, $lp_order, $this ); | |
| 2016 | + $data_token = $this->get_access_token(); | |
| 2017 | + $response = wp_remote_post( | |
| 2018 | + $this->api_url . 'v2/payments/captures/' . rawurlencode( $capture_id ) . '/refund', | |
| 2019 | + [ | |
| 2020 | + 'body' => json_encode( $refund_args ), | |
| 2021 | + 'headers' => [ | |
| 2022 | + 'Authorization' => $data_token->token_type . ' ' . $data_token->access_token, | |
| 2023 | + 'Content-Type' => 'application/json', | |
| 2024 | + ], | |
| 2025 | + 'timeout' => 60, | |
| 2026 | + ] | |
| 2027 | + ); | |
| 2028 | + | |
| 2029 | + if ( is_wp_error( $response ) ) { | |
| 2030 | + throw new Exception( $response->get_error_message() ); | |
| 2031 | + } | |
| 2032 | + | |
| 2033 | + $body = wp_remote_retrieve_body( $response ); | |
| 2034 | + $response_data = LP_Helper::json_decode( $body ); | |
| 2035 | + if ( isset( $response_data->debug_id ) ) { | |
| 2036 | + throw new Exception( $response_data->details[0]->description ); | |
| 2037 | + } | |
| 2038 | + | |
| 2039 | + if ( ! empty( $response_data->status ) && $response_data->status === 'COMPLETED' ) { | |
| 2040 | + update_post_meta( $order_id, '_paypal_refund_id', $response_data->id ); | |
| 2041 | + | |
| 2042 | + $lp_order->add_note( | |
| 2043 | + sprintf( | |
| 2044 | + __( 'PayPal refund completed at %1$s %2$s', 'learnpress' ), | |
| 2045 | + wp_date( 'Y-m-d H:i:s' ), | |
| 2046 | + wp_timezone_string() | |
| 2047 | + ) | |
| 2048 | + ); | |
| 2049 | + do_action( 'learn-press/paypal-refund/success', $lp_order, $response_data, $this ); | |
| 2050 | + } else { | |
| 2051 | + throw new Exception( __( 'PayPal refund something went wrong.', 'learnpress' ) ); | |
| 2052 | + } | |
| 2053 | + | |
| 2054 | + return [ | |
| 2055 | + 'result' => 'success', | |
| 2056 | + 'refund_id' => $response_data->id, | |
| 2057 | + 'status' => $result->status ?? '', | |
| 2058 | + 'response' => $response_data, | |
| 2059 | + ]; | |
| 1976 | 2060 | } |
| 1977 | 2061 | |
| 1978 | 2062 | /** |
| 1979 | 2063 | * Settings form fields for this gateway |