PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.10
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.10
4.4.10 4.4.9.1 4.4.9 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 All 142 releases
← All changes | inc/gateways/paypal/class-lp-gateway-paypal.php +352 -231 4.3.7 → 4.4.10 View file →
@@ -103,8 +103,9 @@
103 103 }
104 104
105 105 /**
106 106 * Init.
107 + * @throws Exception
107 108 */
108 109 public function init() {
109 110 if ( $this->is_enabled() ) {
110 111 if ( $this->settings->get( 'paypal_sandbox', 'no' ) === 'no' ) {
@@ -135,13 +136,13 @@
135 136 *
136 137 * @return bool
137 138 */
138 139 public function is_subscription_enabled(): bool {
139 -
140 140 return $this->settings->get( 'enable_subscriptions', 'no' ) === 'yes';
141 141 }
142 +
142 143 /**
143 - * Listen callback, webhook form PayPal.
144 + * Listen callback, webhook payment from PayPal.
144 145 */
145 146 public function check_webhook_callback() {
146 147 if ( ! isset( $_GET['paypay_express_checkout'] ) ) {
147 148 return;
@@ -151,9 +152,13 @@
151 152 if ( empty( $paypal_order_id ) ) {
152 153 return;
153 154 }
154 155
155 - $this->capture_payment_for_order( $paypal_order_id );
156 + try {
157 + $this->capture_payment_for_order( $paypal_order_id );
158 + } catch ( Throwable $e ) {
159 + LP_Debug::error_log( $e );
160 + }
156 161 }
157 162
158 163 /**
159 164 * https://developer.paypal.com/api/nvp-soap/ipn/IPNImplementation/#link-ipnlistenerrequestresponseflow
@@ -159,10 +164,11 @@
159 164 * https://developer.paypal.com/api/nvp-soap/ipn/IPNImplementation/#link-ipnlistenerrequestresponseflow
160 165 * Check validate IPN.
161 166 *
162 167 * @return bool
168 + * @deprecated 4.3.8
163 169 */
164 - public function validate_ipn(): bool {
170 + /*public function validate_ipn(): bool {
165 171 $validate_ipn = array( 'cmd' => '_notify-validate' );
166 172 $validate_ipn += wp_unslash( $_POST );
167 173
168 174 $params = array(
@@ -183,9 +189,9 @@
183 189 error_log( 'Error code paypal validate ipn: ' . $response->get_error_message() );
184 190 }
185 191
186 192 return false;
187 - }
193 + }*/
188 194
189 195 /**
190 196 * Handle payment.
191 197 *
@@ -196,31 +202,22 @@
196 202 */
197 203 public function process_payment( $order_id = 0 ): array {
198 204 $order = new LP_Order( $order_id );
199 205
200 - $subscription_data = $this->resolve_subscription_payment_data( $order );
206 + //$subscription_data = $this->resolve_subscription_payment_data( $order );
201 207
208 + $subscription_data = $this->is_data_for_payment_subscription( $order );
202 209 if ( ! empty( $subscription_data ) ) {
203 - $subscription_res = $this->pay_subscription( $subscription_data );
204 -
205 - update_post_meta( $order_id, self::META_SUBSCRIPTION_STATUS, 'pending' );
206 - if ( ! empty( $subscription_res['subscription_id'] ) ) {
207 - update_post_meta( $order_id, self::META_SUBSCRIPTION_ID, sanitize_text_field( (string) $subscription_res['subscription_id'] ) );
208 - }
209 -
210 - return array(
211 - 'result' => 'success',
212 - 'redirect' => esc_url_raw( (string) ( $subscription_res['redirect_url'] ?? '' ) ),
213 - );
210 + $subscription_res = $this->pay_via_subscription( $order, $subscription_data );
211 + $paypal_payment_url = $subscription_res['redirect_url'] ?? '';
212 + } else {
213 + $paypal_payment_url = $this->create_payment_url( $order );
214 214 }
215 215
216 - $data_token = $this->get_access_token();
217 - $paypal_payment_url = $this->create_payment_url( $order, $data_token );
218 -
219 - $result['result'] = 'success';
220 - $result['redirect'] = $paypal_payment_url;
221 -
222 - return $result;
216 + return [
217 + 'result' => 'success',
218 + 'redirect' => $paypal_payment_url,
219 + ];
223 220 }
224 221
225 222 /**
226 223 * Prepare args to send to PayPal
@@ -229,10 +226,11 @@
229 226 *
230 227 * @return array
231 228 * @since 3.0.0
232 229 * @version 1.0.1
230 + * @deprecated 4.3.8
233 231 */
234 - public function get_paypal_args( LP_Order $order ): array {
232 + /*public function get_paypal_args( LP_Order $order ): array {
235 233 $checkout = LearnPress::instance()->checkout();
236 234 $custom = array(
237 235 'order_id' => $order->get_id(),
238 236 'order_key' => $order->get_order_key(),
@@ -263,9 +261,9 @@
263 261 $item_arg
264 262 );
265 263
266 264 return apply_filters( 'learn-press/paypal/args', $args );
267 - }
265 + }*/
268 266
269 267 /**
270 268 * Get access token from PayPal
271 269 *
@@ -377,24 +375,20 @@
377 375 /**
378 376 * Create Order PayPal and get checkout url
379 377 *
380 378 * @param LP_Order $order
381 - * @param object $data_token { scope, access_token, token_type, app_id, expires_in, nonce }
382 379 *
383 380 * @return string
384 381 * @throws Exception
385 382 * @since 4.2.4
386 - * @version 1.0.0
383 + * @version 1.0.1
387 384 */
388 - public function create_payment_url( LP_Order $order, $data_token ): string {
385 + public function create_payment_url( LP_Order $order ): string {
389 386 $checkout_url = '';
390 387 $params = $this->get_order_args( $order );
391 388
392 - if ( ! isset( $data_token->access_token ) || ! isset( $data_token->token_type ) ) {
393 - throw new Exception( __( 'Invalid Paypal access token', 'learnpress' ) );
394 - }
395 -
396 - $response = wp_remote_post(
389 + $data_token = $this->get_access_token();
390 + $response = wp_remote_post(
397 391 $this->api_url . 'v2/checkout/orders',
398 392 array(
399 393 'body' => json_encode( $params ),
400 394 'headers' => array(
@@ -444,18 +438,14 @@
444 438 * https://developer.paypal.com/docs/api/orders/v2/#orders_capture
445 439 *
446 440 * @return bool True when capture is completed and order status is updated.
447 441 * @throws Exception
448 - * @version 1.0.1
442 + * @version 1.0.2
449 443 * @since 4.2.4
450 444 */
451 445 public function capture_payment_for_order( string $paypal_order_id ): bool {
452 446 $data_token = $this->get_access_token();
453 - if ( ! isset( $data_token->access_token ) || ! isset( $data_token->token_type ) ) {
454 - return false;
455 - }
456 -
457 - $response = wp_remote_post(
447 + $response = wp_remote_post(
458 448 $this->api_url . 'v2/checkout/orders/' . $paypal_order_id . '/capture',
459 449 array(
460 450 'headers' => array(
461 451 'Content-Type' => 'application/json',
@@ -465,30 +455,49 @@
465 455 )
466 456 );
467 457
468 458 if ( is_wp_error( $response ) ) {
469 - return false;
459 + throw new Exception( $response->get_error_message() );
470 460 }
471 461
472 - $response_code = absint( $response['response']['code'] ?? 0 );
473 - 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 ) ) {
474 470 return false;
475 471 }
476 472
477 - $body = wp_remote_retrieve_body( $response );
478 - $transaction = LP_Helper::json_decode( $body );
479 - if ( empty( $transaction ) || ! is_object( $transaction ) || ( $transaction->status ?? '' ) !== 'COMPLETED' ) {
473 + $order_id = absint( $capture->custom_id ?? 0 );
474 + if ( $order_id <= 0 ) {
480 475 return false;
481 476 }
482 477
483 - $order_id = absint( $transaction->purchase_units[0]->payments->captures[0]->custom_id ?? 0 );
484 - if ( $order_id <= 0 ) {
478 + $capture_id = $capture->id ?? '';
479 + if ( empty( $capture_id ) ) {
485 480 return false;
486 481 }
487 482
488 483 $lp_order = learn_press_get_order( $order_id );
489 - if ( $lp_order instanceof LP_Order ) {
490 - $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 + );
491 500 return true;
492 501 }
493 502
494 503 return false;
@@ -503,10 +512,11 @@
503 512 * @param array $data
504 513 *
505 514 * @return array
506 515 * @throws Exception
516 + * @deprecated 4.3.8
507 517 */
508 - protected function validate_subscription_payload( array $data ): array {
518 + /*protected function validate_subscription_payload( array $data ): array {
509 519 $data = parent::validate_subscription_payload( $data );
510 520
511 521 // PayPal subscription API expects a plan id string and positive quantity.
512 522 $data['price_id'] = (string) $data['price_id'];
@@ -512,9 +522,9 @@
512 522 $data['price_id'] = (string) $data['price_id'];
513 523 $data['quantity'] = max( 1, absint( $data['quantity'] ) );
514 524
515 525 return $data;
516 - }
526 + }*/
517 527
518 528 /**
519 529 * Convert generic interval to PayPal interval unit.
520 530 *
@@ -520,10 +530,11 @@
520 530 *
521 531 * @param string $interval
522 532 *
523 533 * @return string
534 + * @deprecated 4.3.8
524 535 */
525 - protected function map_paypal_interval_unit( string $interval ): string {
536 + /*protected function map_paypal_interval_unit( string $interval ): string {
526 537
527 538 $map = array(
528 539 'day' => 'DAY',
529 540 'week' => 'WEEK',
@@ -531,9 +542,9 @@
531 542 'year' => 'YEAR',
532 543 );
533 544
534 545 return $map[ $interval ] ?? 'MONTH';
535 - }
546 + }*/
536 547
537 548 /**
538 549 * Convert PayPal interval unit to LearnPress interval slug.
539 550 *
@@ -539,10 +550,11 @@
539 550 *
540 551 * @param string $interval_unit
541 552 *
542 553 * @return string
554 + * @deprecated 4.3.8
543 555 */
544 - protected function map_paypal_interval_slug( string $interval_unit ): string {
556 + /*protected function map_paypal_interval_slug( string $interval_unit ): string {
545 557
546 558 $map = array(
547 559 'DAY' => 'day',
548 560 'WEEK' => 'week',
@@ -552,47 +564,48 @@
552 564
553 565 $interval_unit = strtoupper( sanitize_text_field( $interval_unit ) );
554 566
555 567 return $map[ $interval_unit ] ?? 'month';
556 - }
568 + }*/
557 569
558 570 /**
559 571 * Build normalized summary from PayPal plan payload.
560 572 *
561 - * @param object $plan_body
573 + * @param array $plan_data
562 574 *
563 575 * @return array
576 + * @since 4.3.7
577 + * @version 1.0.1
564 578 */
565 - protected function build_paypal_plan_summary( object $plan_body ): array {
566 -
579 + protected function build_paypal_plan_summary( array $plan_data ): array {
567 580 $summary = array(
568 - 'plan_id' => (string) ( $plan_body->id ?? '' ),
569 - 'status' => strtolower( (string) ( $plan_body->status ?? '' ) ),
581 + 'plan_id' => $plan_data['id'] ?? '',
582 + 'status' => strtolower( (string) ( $plan_data['status'] ?? '' ) ),
570 583 'amount' => 0.0,
571 584 'currency' => '',
572 585 'interval' => '',
573 586 'interval_count' => 1,
574 587 'setup_fee' => 0.0,
575 - 'product_id' => (string) ( $plan_body->product_id ?? '' ),
588 + 'product_id' => (string) ( $plan_data['product_id'] ?? '' ),
576 589 );
577 590
578 - if ( ! empty( $plan_body->payment_preferences->setup_fee->value ) ) {
579 - $summary['setup_fee'] = (float) $plan_body->payment_preferences->setup_fee->value;
591 + if ( ! empty( $plan_data['payment_preferences']['setup_fee']['value'] ) ) {
592 + $summary['setup_fee'] = (float) $plan_data['payment_preferences']['setup_fee']['value'];
580 593 }
581 594
582 - if ( ! empty( $plan_body->billing_cycles ) && is_array( $plan_body->billing_cycles ) ) {
583 - foreach ( $plan_body->billing_cycles as $billing_cycle ) {
584 - if ( ! is_object( $billing_cycle ) ) {
595 + if ( ! empty( $plan_data['billing_cycles'] ) && is_array( $plan_data['billing_cycles'] ) ) {
596 + foreach ( $plan_data['billing_cycles'] as $billing_cycle ) {
597 + if ( ! is_array( $billing_cycle ) ) {
585 598 continue;
586 599 }
587 - if ( ( $billing_cycle->tenure_type ?? '' ) !== 'REGULAR' ) {
600 + if ( ( $billing_cycle['tenure_type'] ?? '' ) !== 'REGULAR' ) {
588 601 continue;
589 602 }
590 603
591 - $summary['amount'] = (float) ( $billing_cycle->pricing_scheme->fixed_price->value ?? 0 );
592 - $summary['currency'] = strtoupper( (string) ( $billing_cycle->pricing_scheme->fixed_price->currency_code ?? '' ) );
593 - $summary['interval'] = $this->map_paypal_interval_slug( (string) ( $billing_cycle->frequency->interval_unit ?? '' ) );
594 - $summary['interval_count'] = max( 1, absint( $billing_cycle->frequency->interval_count ?? 1 ) );
604 + $summary['amount'] = (float) ( $billing_cycle['pricing_scheme']['fixed_price']['value'] ?? 0 );
605 + $summary['currency'] = strtolower( (string) ( $billing_cycle['pricing_scheme']['fixed_price']['currency_code'] ?? '' ) );
606 + $summary['interval'] = strtolower( (string) ( $billing_cycle['frequency']['interval_unit'] ?? '' ) );
607 + $summary['interval_count'] = max( 1, absint( $billing_cycle['frequency']['interval_count'] ?? 1 ) );
595 608 break;
596 609 }
597 610 }
598 611
@@ -597,51 +610,53 @@
597 610 }
598 611
599 612 return $summary;
600 613 }
614 +
601 615 /**
602 - * Create PayPal billing plan resource (catalog product + billing plan).
616 + * Create a PayPal subscription plan (creates product first if needed).
603 617 *
604 - * Returned `plan` object contains PayPal `plan_id` (`$plan->id`) for later
605 - * subscription checkout via `pay_subscription()`.
618 + * - If `product_id` is empty, creates a new PayPal product via v1/catalogs/products API,
619 + * then creates a billing plan via v1/billing/plans API.
620 + * - Supports trial period and setup fee.
621 + * - Returns full PayPal product and plan data on success.
606 622 *
607 - * @param array $data
623 + * @param array $data Required: name, amount, currency, interval, interval_count.
624 + * Optional: description, trial_days, setup_fee, product_id, metadata.
608 625 *
609 - * @return array
610 - * @throws Exception
626 + * @return array With keys: status, product, plan, message.
627 + * @throws Exception On API errors or validation failures. Includes full PayPal error details if available.
628 + * @since 4.3.7
629 + * @version 1.0.1
611 630 */
612 631 public function create_plan( array $data ): array {
613 -
614 632 if ( ! $this->is_subscription_enabled() ) {
615 633 throw new Exception( __( 'PayPal subscriptions are disabled.', 'learnpress' ) );
616 634 }
617 635
618 - $data = $this->validate_data_plan_payload( $data );
636 + $data = $this->validate_data_plan_payload( $data );
637 + $data_token = $this->get_access_token();
619 638
620 639 // PayPal-specific optional keys are normalized at gateway level.
621 - $description = sanitize_text_field( wp_unslash( (string) ( $data['description'] ?? '' ) ) );
640 + $description = LP_Helper::sanitize_params_submitted( $data['description'] ?? '', 'html' );
622 641 $trial_days = absint( $data['trial_days'] ?? 0 );
623 642 $setup_fee = (float) ( $data['setup_fee'] ?? 0 );
624 643
625 - // PayPal billing plans require a plan name.
626 - if ( empty( $data['name'] ) ) {
627 - throw new Exception( __( 'Missing subscription plan name.', 'learnpress' ) );
628 - }
629 - $data_token = $this->get_access_token();
630 - if ( empty( $data_token->access_token ) || empty( $data_token->token_type ) ) {
631 - throw new Exception( __( 'Invalid Paypal access token', 'learnpress' ) );
632 - }
633 -
634 - $product_id = (string) $data['product_id'];
635 - $product_body = (object) array( 'id' => $product_id );
644 + // Create product before create plan
645 + $product_id = $data['product_id'] ?? '';
646 + $product_data = [ 'id' => $product_id ];
636 647 if ( empty( $product_id ) ) {
637 648 $product_payload = array(
638 - 'name' => (string) $data['name'],
649 + 'name' => $data['name'] ?? '',
639 650 'type' => 'SERVICE',
640 651 );
652 +
653 + // Add description if not empty
641 654 if ( ! empty( $description ) ) {
642 - $product_payload['description'] = (string) $description;
655 + $product_payload['description'] = $description;
643 656 }
657 +
658 + // Call API create product
644 659 $product_response = wp_remote_post(
645 660 $this->api_url . 'v1/catalogs/products',
646 661 array(
647 662 'body' => wp_json_encode( $product_payload ),
@@ -655,24 +670,31 @@
655 670 if ( is_wp_error( $product_response ) ) {
656 671 throw new Exception( $product_response->get_error_message() );
657 672 }
658 673
659 - $product_body = LP_Helper::json_decode( wp_remote_retrieve_body( $product_response ) );
660 - if ( empty( $product_body->id ) ) {
661 - $error_message = __( 'Invalid PayPal product response.', 'learnpress' );
662 - if ( ! empty( $product_body->message ) ) {
663 - $error_message = (string) $product_body->message;
664 - }
674 + $product_body = wp_remote_retrieve_body( $product_response );
675 + $product_data = LP_Helper::json_decode( $product_body, true );
665 676
666 - throw new Exception( $error_message );
677 + // Error create product return from PayPal
678 + if ( ! empty( $product_data['debug_id'] ) ) {
679 + throw new Exception(
680 + __( 'Create Product: ' ) . $product_data['details'][0]['description'],
681 + (int) $product_data['debug_id']
682 + );
667 683 }
668 684
669 - $product_id = (string) $product_body->id;
685 + if ( empty( $product_data['id'] ) ) {
686 + throw new Exception( __( 'Invalid PayPal product response.', 'learnpress' ) );
687 + }
688 +
689 + $product_id = $product_data['id'];
670 690 }
671 691
672 - $currency_code = (string) $data['currency'];
692 + $currency_code = strtoupper( (string) $data['currency'] );
673 693 $billing_cycles = array();
674 694 $sequence = 1;
695 +
696 + // Set trial period
675 697 if ( $trial_days > 0 ) {
676 698 $billing_cycles[] = array(
677 699 'frequency' => array(
678 700 'interval_unit' => 'DAY',
@@ -690,12 +712,13 @@
690 712 );
691 713 $sequence = 2;
692 714 }
693 715
716 + // Set regular price
694 717 $billing_cycles[] = array(
695 718 'frequency' => array(
696 - 'interval_unit' => $this->map_paypal_interval_unit( (string) $data['interval'] ),
697 - 'interval_count' => max( 1, absint( $data['interval_count'] ) ),
719 + 'interval_unit' => strtoupper( (string) $data['interval'] ?? 'MONTH' ),
720 + 'interval_count' => max( 1, absint( $data['interval_count'] ?? 0 ) ),
698 721 ),
699 722 'tenure_type' => 'REGULAR',
700 723 'sequence' => $sequence,
701 724 'total_cycles' => 0,
@@ -708,9 +731,9 @@
708 731 );
709 732
710 733 $plan_payload = array(
711 734 'product_id' => $product_id,
712 - 'name' => (string) $data['name'],
735 + 'name' => $data['name'] ?? '',
713 736 'status' => 'ACTIVE',
714 737 'billing_cycles' => $billing_cycles,
715 738 'payment_preferences' => array(
716 739 'auto_bill_outstanding' => true,
@@ -721,11 +744,15 @@
721 744 'setup_fee_failure_action' => 'CONTINUE',
722 745 'payment_failure_threshold' => 3,
723 746 ),
724 747 );
748 +
749 + // Add description if not empty
725 750 if ( ! empty( $description ) ) {
726 - $plan_payload['description'] = (string) $description;
751 + $plan_payload['description'] = $description;
727 752 }
753 +
754 + // Call API create plan
728 755 $plan_response = wp_remote_post(
729 756 $this->api_url . 'v1/billing/plans',
730 757 array(
731 758 'body' => wp_json_encode( $plan_payload ),
@@ -739,22 +766,27 @@
739 766 if ( is_wp_error( $plan_response ) ) {
740 767 throw new Exception( $plan_response->get_error_message() );
741 768 }
742 769
743 - $plan_body = LP_Helper::json_decode( wp_remote_retrieve_body( $plan_response ) );
744 - if ( empty( $plan_body->id ) ) {
745 - $error_message = __( 'Invalid PayPal plan response.', 'learnpress' );
746 - if ( ! empty( $plan_body->message ) ) {
747 - $error_message = (string) $plan_body->message;
748 - }
770 + $body = wp_remote_retrieve_body( $plan_response );
771 + $response_data = LP_Helper::json_decode( $body, true );
749 772
750 - throw new Exception( $error_message );
773 + // Error return from PayPal
774 + if ( ! empty( $response_data['debug_id'] ) ) {
775 + throw new Exception(
776 + __( 'Create Plan: ' ) . $response_data['details'][0]['description'],
777 + (int) $response_data['debug_id']
778 + );
751 779 }
752 780
781 + if ( empty( $response_data['id'] ) ) {
782 + throw new Exception( __( 'Invalid PayPal plan response.', 'learnpress' ) );
783 + }
784 +
753 785 return array(
754 786 'status' => 'success',
755 - 'product' => $product_body,
756 - 'plan' => $plan_body,
787 + 'product' => $product_data,
788 + 'plan' => $response_data,
757 789 'message' => __( 'PayPal subscription plan created.', 'learnpress' ),
758 790 );
759 791 }
760 792
@@ -764,10 +796,11 @@
764 796 * @param array $args Supported: page, page_size, total_required, product_id.
765 797 *
766 798 * @return array
767 799 * @throws Exception
800 + * @deprecated 4.3.8
768 801 */
769 - public function list_plans( array $args = array() ): array {
802 + /*public function list_plans( array $args = array() ): array {
770 803
771 804 if ( ! $this->is_subscription_enabled() ) {
772 805 throw new Exception( __( 'PayPal subscriptions are disabled.', 'learnpress' ) );
773 806 }
@@ -836,9 +869,9 @@
836 869 'total_items' => absint( $plans_body->total_items ?? count( $plans ) ),
837 870 'total_pages' => absint( $plans_body->total_pages ?? 1 ),
838 871 'message' => __( 'PayPal subscription plans fetched.', 'learnpress' ),
839 872 );
840 - }
873 + }*/
841 874
842 875 /**
843 876 * Get PayPal billing plan details by plan id.
844 877 *
@@ -850,22 +883,18 @@
850 883 * @return array
851 884 * @throws Exception
852 885 */
853 886 public function get_plan( string $plan_id ): array {
854 -
855 887 if ( ! $this->is_subscription_enabled() ) {
856 888 throw new Exception( __( 'PayPal subscriptions are disabled.', 'learnpress' ) );
857 889 }
858 890
859 - $plan_id = sanitize_text_field( wp_unslash( $plan_id ) );
891 + $plan_id = LP_Helper::sanitize_params_submitted( $plan_id );
860 892 if ( empty( $plan_id ) ) {
861 893 throw new Exception( __( 'Missing subscription plan id.', 'learnpress' ) );
862 894 }
863 895
864 896 $data_token = $this->get_access_token();
865 - if ( empty( $data_token->access_token ) || empty( $data_token->token_type ) ) {
866 - throw new Exception( __( 'Invalid Paypal access token', 'learnpress' ) );
867 - }
868 897
869 898 $plan_response = wp_remote_get(
870 899 $this->api_url . 'v1/billing/plans/' . rawurlencode( $plan_id ),
871 900 array(
@@ -880,23 +909,21 @@
880 909 if ( is_wp_error( $plan_response ) ) {
881 910 throw new Exception( $plan_response->get_error_message() );
882 911 }
883 912
884 - $plan_body = LP_Helper::json_decode( wp_remote_retrieve_body( $plan_response ) );
885 - if ( empty( $plan_body->id ) ) {
886 - $error_message = __( 'Invalid PayPal plan response.', 'learnpress' );
887 - if ( ! empty( $plan_body->message ) ) {
888 - $error_message = (string) $plan_body->message;
889 - }
890 -
891 - throw new Exception( $error_message );
913 + $plan_data = LP_Helper::json_decode( wp_remote_retrieve_body( $plan_response ), true );
914 + if ( isset( $plan_data['debug_id'] ) ) {
915 + throw new Exception(
916 + __( 'Get plan: ', 'learnpress' ) . $plan_data['details'][0]['description'],
917 + (int) $plan_data['debug_id']
918 + );
892 919 }
893 920
894 - $summary = $this->build_paypal_plan_summary( $plan_body );
921 + $summary = $this->build_paypal_plan_summary( $plan_data );
895 922
896 923 return array(
897 924 'status' => 'success',
898 - 'plan' => $plan_body,
925 + 'plan' => $plan_data,
899 926 'summary' => $summary,
900 927 'message' => __( 'PayPal subscription plan fetched.', 'learnpress' ),
901 928 );
902 929 }
@@ -912,16 +939,17 @@
912 939 * @param array $data
913 940 *
914 941 * @return array
915 942 * @throws Exception
943 + * @since 4.3.7
944 + * @version 1.0.1
916 945 */
917 946 public function update_plan( string $plan_id, array $data ): array {
918 -
919 947 if ( ! $this->is_subscription_enabled() ) {
920 948 throw new Exception( __( 'PayPal subscriptions are disabled.', 'learnpress' ) );
921 949 }
922 950
923 - $plan_id = sanitize_text_field( wp_unslash( $plan_id ) );
951 + $plan_id = LP_Helper::sanitize_params_submitted( $plan_id );
924 952 if ( empty( $plan_id ) ) {
925 953 throw new Exception( __( 'Missing subscription plan id.', 'learnpress' ) );
926 954 }
927 955
@@ -928,29 +956,12 @@
928 956 $current = $this->get_plan( $plan_id );
929 957 $current_plan = $current['plan'];
930 958 $current_sum = $current['summary'];
931 959
932 - $data = wp_parse_args(
933 - $data,
934 - array(
935 - 'name' => null,
936 - 'description' => null,
937 - 'status' => null,
938 - 'amount' => null,
939 - 'currency' => '',
940 - 'interval' => '',
941 - 'interval_count' => null,
942 - 'setup_fee' => null,
943 - )
944 - );
960 + $data = $this->validate_data_plan_payload( $data );
945 961
946 - $data_token = $this->get_access_token();
947 - if ( empty( $data_token->access_token ) || empty( $data_token->token_type ) ) {
948 - throw new Exception( __( 'Invalid Paypal access token', 'learnpress' ) );
949 - }
950 -
951 962 $patches = array();
952 - if ( null !== $data['name'] ) {
963 + if ( ! empty( $data['name'] ?? '' ) ) {
953 964 $patches[] = array(
954 965 'op' => 'replace',
955 966 'path' => '/name',
956 967 'value' => sanitize_text_field( wp_unslash( (string) $data['name'] ) ),
@@ -955,9 +966,9 @@
955 966 'path' => '/name',
956 967 'value' => sanitize_text_field( wp_unslash( (string) $data['name'] ) ),
957 968 );
958 969 }
959 - if ( null !== $data['description'] ) {
970 + if ( ! empty( $data['description'] ?? '' ) ) {
960 971 $patches[] = array(
961 972 'op' => 'replace',
962 973 'path' => '/description',
963 974 'value' => sanitize_text_field( wp_unslash( (string) $data['description'] ) ),
@@ -962,9 +973,9 @@
962 973 'path' => '/description',
963 974 'value' => sanitize_text_field( wp_unslash( (string) $data['description'] ) ),
964 975 );
965 976 }
966 - if ( null !== $data['status'] ) {
977 + if ( ! empty( $data['status'] ?? '' ) ) {
967 978 $status = strtoupper( sanitize_text_field( wp_unslash( (string) $data['status'] ) ) );
968 979 if ( ! in_array( $status, array( 'ACTIVE', 'INACTIVE' ), true ) ) {
969 980 throw new Exception( __( 'Invalid PayPal subscription plan status.', 'learnpress' ) );
970 981 }
@@ -973,9 +984,9 @@
973 984 'path' => '/status',
974 985 'value' => $status,
975 986 );
976 987 }
977 - if ( null !== $data['setup_fee'] ) {
988 + if ( isset( $data['setup_fee'] ) && '' !== $data['setup_fee'] ) {
978 989 $setup_fee = (float) $data['setup_fee'];
979 990 if ( $setup_fee < 0 ) {
980 991 throw new Exception( __( 'Invalid subscription setup fee.', 'learnpress' ) );
981 992 }
@@ -983,58 +994,77 @@
983 994 'op' => 'replace',
984 995 'path' => '/payment_preferences/setup_fee',
985 996 'value' => array(
986 997 'value' => number_format( $setup_fee, 2, '.', '' ),
987 - 'currency_code' => (string) $current_sum['currency'],
998 + 'currency_code' => strtoupper( $current_sum['currency'] ?? '' ),
988 999 ),
989 1000 );
990 1001 }
991 1002
992 1003 if ( ! empty( $patches ) ) {
993 - $patch_response = wp_remote_request(
994 - $this->api_url . 'v1/billing/plans/' . rawurlencode( $plan_id ),
995 - array(
996 - 'method' => 'PATCH',
997 - 'body' => wp_json_encode( $patches ),
998 - 'headers' => array(
999 - 'Authorization' => $data_token->token_type . ' ' . $data_token->access_token,
1000 - 'Content-Type' => 'application/json',
1001 - ),
1002 - 'timeout' => 60,
1003 - )
1004 - );
1004 + $data_token = $this->get_access_token();
1005 + $patch_response = wp_remote_request(
1006 + $this->api_url . 'v1/billing/plans/' . rawurlencode( $plan_id ),
1007 + array(
1008 + 'method' => 'PATCH',
1009 + 'body' => wp_json_encode( $patches ),
1010 + 'headers' => array(
1011 + 'Authorization' => $data_token->token_type . ' ' . $data_token->access_token,
1012 + 'Content-Type' => 'application/json',
1013 + ),
1014 + 'timeout' => 60,
1015 + )
1016 + );
1005 1017 if ( is_wp_error( $patch_response ) ) {
1006 - throw new Exception( $patch_response->get_error_message() );
1018 + throw new Exception( $patch_response->get_error_message() );
1007 1019 }
1020 +
1021 + // Empty is for success, not empty is for error
1022 + $patch_response_body = wp_remote_retrieve_body( $patch_response );
1023 + if ( ! empty( $patch_response_body ) ) {
1024 + $patched_plan = LP_Helper::json_decode( $patch_response_body, true );
1025 + if ( isset( $patched_plan['debug_id'] ) ) {
1026 + throw new Exception(
1027 + __( 'Update plan: ', 'learnpress' ) . $patched_plan['details'][0]['description'],
1028 + (int) $patched_plan['debug_id']
1029 + );
1030 + }
1031 + }
1008 1032 }
1009 1033
1010 - if ( null !== $data['interval_count'] || ! empty( $data['interval'] ) ) {
1011 - $new_interval = ! empty( $data['interval'] ) ? strtolower( sanitize_key( (string) $data['interval'] ) ) : (string) $current_sum['interval'];
1012 - $new_interval_count = null !== $data['interval_count'] ? max( 1, absint( $data['interval_count'] ) ) : (int) $current_sum['interval_count'];
1013 - if ( $new_interval !== (string) $current_sum['interval'] || $new_interval_count !== (int) $current_sum['interval_count'] ) {
1034 + // Check if interval is changed will not allow to change
1035 + if ( ! empty( $data['interval'] ?? '' ) ) {
1036 + if ( $data['interval'] !== $current_sum['interval'] ) {
1014 1037 throw new Exception( __( 'PayPal plan interval cannot be changed. Create a new plan instead.', 'learnpress' ) );
1015 1038 }
1016 1039 }
1017 1040
1018 - if ( null !== $data['amount'] ) {
1041 + // Check if interval count is changed will not allow to change
1042 + if ( ! empty( $data['interval_count'] ?? '' ) ) {
1043 + if ( $data['interval_count'] !== $current_sum['interval_count'] ) {
1044 + throw new Exception( __( 'PayPal plan interval count cannot be changed. Create a new plan instead.', 'learnpress' ) );
1045 + }
1046 + }
1047 +
1048 + if ( ! empty( $data['amount'] ?? '' ) ) {
1019 1049 $new_amount = (float) $data['amount'];
1020 1050 if ( $new_amount <= 0 ) {
1021 1051 throw new Exception( __( 'Invalid subscription amount.', 'learnpress' ) );
1022 1052 }
1023 1053
1024 - $currency_code = ! empty( $data['currency'] ) ? strtoupper( sanitize_text_field( wp_unslash( (string) $data['currency'] ) ) ) : (string) $current_sum['currency'];
1054 + $currency_code = strtoupper( $data['currency'] ?? '' );
1025 1055 if ( empty( $currency_code ) ) {
1026 - throw new Exception( __( 'Missing subscription currency.', 'learnpress' ) );
1056 + throw new Exception( __( 'Missing subscription currency.', 'learnpress' ) );
1027 1057 }
1028 1058
1029 1059 $regular_sequence = 1;
1030 - if ( ! empty( $current_plan->billing_cycles ) && is_array( $current_plan->billing_cycles ) ) {
1031 - foreach ( $current_plan->billing_cycles as $billing_cycle ) {
1032 - if ( ! is_object( $billing_cycle ) ) {
1060 + if ( ! empty( $current_plan['billing_cycles'] ) && is_array( $current_plan['billing_cycles'] ) ) {
1061 + foreach ( $current_plan['billing_cycles'] as $billing_cycle ) {
1062 + if ( ! is_array( $billing_cycle ) ) {
1033 1063 continue;
1034 1064 }
1035 - if ( ( $billing_cycle->tenure_type ?? '' ) === 'REGULAR' ) {
1036 - $regular_sequence = max( 1, absint( $billing_cycle->sequence ?? 1 ) );
1065 + if ( ( $billing_cycle['tenure_type'] ?? '' ) === 'REGULAR' ) {
1066 + $regular_sequence = max( 1, absint( $billing_cycle['sequence'] ?? 1 ) );
1037 1067 break;
1038 1068 }
1039 1069 }
1040 1070 }
@@ -1066,8 +1096,15 @@
1066 1096 );
1067 1097 if ( is_wp_error( $pricing_response ) ) {
1068 1098 throw new Exception( $pricing_response->get_error_message() );
1069 1099 }
1100 +
1101 + if ( isset( $pricing_response['debug_id'] ) ) {
1102 + throw new Exception(
1103 + __( 'Update plan pricing: ', 'learnpress' ) . $pricing_response['details'][0]['description'],
1104 + (int) $pricing_response['debug_id']
1105 + );
1106 + }
1070 1107 }
1071 1108
1072 1109 $updated = $this->get_plan( $plan_id );
1073 1110 $updated['message'] = __( 'PayPal subscription plan updated.', 'learnpress' );
@@ -1086,9 +1123,8 @@
1086 1123 * @return array
1087 1124 * @throws Exception
1088 1125 */
1089 1126 public function delete_plan( string $plan_id ): array {
1090 -
1091 1127 $deleted = $this->update_plan(
1092 1128 $plan_id,
1093 1129 array(
1094 1130 'status' => 'INACTIVE',
@@ -1116,10 +1152,11 @@
1116 1152 * subscription_id:string,
1117 1153 * message:string
1118 1154 * }
1119 1155 * @throws Exception
1156 + * @deprecated 4.3.8 Use pay_via_subscription() instead.
1120 1157 */
1121 - public function pay_subscription( array $data ): array {
1158 + /*public function pay_subscription( array $data ): array {
1122 1159 if ( ! $this->is_subscription_enabled() ) {
1123 1160 throw new Exception( __( 'PayPal subscriptions are disabled.', 'learnpress' ) );
1124 1161 }
1125 1162
@@ -1199,9 +1236,9 @@
1199 1236 'provider_reference' => (string) $data->id,
1200 1237 'subscription_id' => (string) $data->id,
1201 1238 'message' => __( 'Redirecting to PayPal subscription checkout.', 'learnpress' ),
1202 1239 );
1203 - }
1240 + }*/
1204 1241
1205 1242 /**
1206 1243 * Create PayPal subscription checkout and return redirect payload.
1207 1244 *
@@ -1209,20 +1246,17 @@
1209 1246 * - plan_id = subscription price/plan id configured in PayPal.
1210 1247 * - custom_id = LearnPress parent order id for reconciliation.
1211 1248 * - return/cancel URLs = checkout callbacks.
1212 1249 *
1213 - * @param array $data Normalized payload from get_subscription_context().
1250 + * @param array $data [ 'plan_id' => string, 'quantity' => int, 'success_url' => string, 'cancel_url' => string ]
1251 + * Required: plan_id
1214 1252 *
1215 - * @return array{
1216 - * status:string,
1217 - * redirect_url:string,
1218 - * provider_reference:string,
1219 - * subscription_id:string,
1220 - * message:string
1221 - * }
1253 + * @return array [ 'redirect_url' => string, and data from v1/billing/subscriptions ]
1222 1254 * @throws Exception
1255 + * @since 4.3.7
1256 + * @version 1.0.1
1223 1257 */
1224 - public function pay_via_subscription( array $data ): array {
1258 + public function pay_via_subscription( LP_Order $lp_order, array $data ): array {
1225 1259 if ( ! $this->is_subscription_enabled() ) {
1226 1260 throw new Exception( __( 'PayPal subscriptions are disabled.', 'learnpress' ) );
1227 1261 }
1228 1262
@@ -1230,34 +1264,33 @@
1230 1264 if ( empty( $plan_id ) ) {
1231 1265 throw new Exception( __( 'PayPal subscription plan ID is invalid.', 'learnpress' ) );
1232 1266 }
1233 1267
1234 - $lp_order_id = $data['lp_order_id'] ?? '';
1235 - if ( empty( $lp_order_id ) ) {
1236 - throw new Exception( __( 'LearnPress order ID is invalid.', 'learnpress' ) );
1237 - }
1268 + $lp_order_id = $lp_order->get_id();
1269 + $data_token = $this->get_access_token();
1238 1270
1239 - $lp_order = learn_press_get_order( (int) $lp_order_id );
1240 - if ( ! $lp_order ) {
1241 - throw new Exception( __( 'LearnPress order is invalid.', 'learnpress' ) );
1242 - }
1243 -
1244 - $data_token = $this->get_access_token();
1245 -
1246 1271 $request_body = array(
1247 - 'plan_id' => (string) ( $data['plan_id'] ?? '' ),
1248 - 'quantity' => (string) max( 1, absint( $data['quantity'] ?? 0 ) ),
1249 - 'custom_id' => (string) ( $data['lp_order_id'] ?? '' ),
1272 + 'plan_id' => $plan_id,
1273 + 'quantity' => max( 1, absint( $data['quantity'] ?? 0 ) ),
1274 + 'custom_id' => $lp_order_id,
1250 1275 'application_context' => array(
1251 1276 'brand_name' => ! empty( get_bloginfo() ) ? get_bloginfo() : 'LearnPress',
1252 - 'return_url' => esc_url_raw( (string) ( $data['success_url'] ?? '' ) ),
1253 - 'cancel_url' => esc_url_raw( (string) ( $data['cancel_url'] ?? '' ) ),
1277 + 'return_url' => esc_url_raw( $data['success_url'] ?? '' ),
1278 + 'cancel_url' => esc_url_raw( $data['cancel_url'] ?? '' ),
1254 1279 ),
1255 1280 );
1256 1281
1257 1282 // If the user already completed a trial for this plan on a previous order.
1258 - $user_has_trial_done = get_user_meta( $lp_order->get_user_id(), 'lp_subscription_trial', true );
1259 - if ( $user_has_trial_done ) {
1283 + $user_has_trial_done = get_user_meta( $lp_order->get_user_id(), 'user_plan_trial', true );
1284 + $user_has_trial_done = apply_filters(
1285 + 'learn-press/subscription/user-has-trial',
1286 + $user_has_trial_done,
1287 + $lp_order,
1288 + $plan_id,
1289 + $this
1290 + );
1291 + if ( $user_has_trial_done && $user_has_trial_done === $plan_id ) {
1292 + LP_Debug::log_to_comment( 'Pay renew for user trial done: ' . $plan_id );
1260 1293 // Fetch plan details to find the REGULAR billing cycle and its pricing.
1261 1294 $plan_response = wp_remote_get(
1262 1295 $this->api_url . 'v1/billing/plans/' . rawurlencode( $plan_id ),
1263 1296 array(
@@ -1312,20 +1345,22 @@
1312 1345 )
1313 1346 );
1314 1347
1315 1348 if ( is_wp_error( $response ) ) {
1316 - throw new Exception( $response->get_error_message(), $response->get_error_code() );
1349 + throw new Exception( $response->get_error_message() );
1317 1350 }
1318 1351
1319 1352 $body = wp_remote_retrieve_body( $response );
1320 1353 $response_data = LP_Helper::json_decode( $body, true );
1321 1354
1322 - //error_log( 'Pay subscription: ' . $body );
1323 1355 LP_Debug::log_to_comment( 'Pay subscription: ' . $body );
1324 1356
1325 1357 // Error return from PayPal
1326 1358 if ( ! empty( $response_data['debug_id'] ) ) {
1327 - throw new Exception( $response_data['details'][0]['description'] );
1359 + throw new Exception(
1360 + __( 'Pay PayPal subscription: ', 'learnpress' ) . $response_data['details'][0]['description'],
1361 + (int) $response_data['debug_id']
1362 + );
1328 1363 }
1329 1364
1330 1365 if ( empty( $response_data['id'] ) ) {
1331 1366 throw new Exception( __( 'Invalid PayPal subscription response.', 'learnpress' ) );
@@ -1351,10 +1386,11 @@
1351 1386 * @param array $webhook_data Webhook payload + headers extracted by listener.
1352 1387 *
1353 1388 * @return array Verified webhook payload array on success.
1354 1389 * @throws Exception
1390 + * @deprecated 4.3.8
1355 1391 */
1356 - public function verify_subscription_webhook( array $webhook_data ): array {
1392 + /*public function verify_subscription_webhook( array $webhook_data ): array {
1357 1393
1358 1394 if ( empty( $this->subscription_webhook_id ) ) {
1359 1395 throw new Exception( __( 'PayPal subscription webhook id is missing.', 'learnpress' ), 500 );
1360 1396 }
@@ -1432,9 +1468,9 @@
1432 1468 throw new Exception( __( 'PayPal webhook verification failed.', 'learnpress' ), 400 );
1433 1469 }
1434 1470
1435 1471 return $payload;
1436 - }
1472 + }*/
1437 1473
1438 1474 /**
1439 1475 * Normalize PayPal webhook event into LearnPress subscription event schema.
1440 1476 *
@@ -1444,10 +1480,11 @@
1444 1480 *
1445 1481 * @param array|object $provider_event
1446 1482 *
1447 1483 * @return array
1484 + * @deprecated 4.3.8 Use normalize_subscription_data instead.
1448 1485 */
1449 - public function normalize_subscription_event( $provider_event ): array {
1486 + /*public function normalize_subscription_event( $provider_event ): array {
1450 1487 $event = parent::normalize_subscription_event( $provider_event );
1451 1488 if ( is_object( $provider_event ) ) {
1452 1489 $provider_event = (array) $provider_event;
1453 1490 }
@@ -1524,9 +1561,9 @@
1524 1561 break;
1525 1562 }
1526 1563
1527 1564 return $event;
1528 - }
1565 + }*/
1529 1566
1530 1567 /**
1531 1568 * Verify, normalize, and dispatch PayPal subscription webhook event.
1532 1569 *
@@ -1536,10 +1573,11 @@
1536 1573 * @param WP_REST_Request $request
1537 1574 *
1538 1575 * @return array
1539 1576 * @throws Exception
1577 + * @deprecated 4.3.8 Use capture_subscription_webhook instead.
1540 1578 */
1541 - public function listen_webhook_subscription( WP_REST_Request $request ): array {
1579 + /*public function listen_webhook_subscription( WP_REST_Request $request ): array {
1542 1580
1543 1581 $required_headers = array(
1544 1582 'paypal-auth-algo',
1545 1583 'paypal-cert-url',
@@ -1564,9 +1602,9 @@
1564 1602 throw new Exception( __( 'Subscription manager is not available.', 'learnpress' ), 500 );
1565 1603 }
1566 1604
1567 1605 return LP_Subscription_Manager::instance()->process_webhook_event( $this, $event );
1568 - }
1606 + }*/
1569 1607
1570 1608 /**
1571 1609 * Receive data from webhook.
1572 1610 * Verify, normalize, and dispatch PayPal subscription webhook event.
@@ -1596,9 +1634,9 @@
1596 1634 ];
1597 1635 $this->verify_data_from_webhook_subscription( $webhook_data_verify );
1598 1636
1599 1637 // Check lp order exists
1600 - // SUBSCRIPTION return custom_id, PAYMENT.SALE return custom
1638 + // SUBSCRIPTION return 'custom_id', PAYMENT.SALE return 'custom'
1601 1639 $lp_order_id = $webhook_data['resource']['custom'] ?? $webhook_data['resource']['custom_id'] ?? '';
1602 1640 $lp_order = learn_press_get_order( $lp_order_id );
1603 1641 if ( ! $lp_order ) {
1604 1642 error_log( 'LearnPress order is invalid: ' . json_encode( $webhook_data, JSON_UNESCAPED_UNICODE ) );
@@ -1612,9 +1650,9 @@
1612 1650 return;
1613 1651 }
1614 1652
1615 1653 // Capture payment setup fee or renewal
1616 - $this->capturePaymentSetupFeeOrRenewal( $lp_order, $webhook_data );
1654 + $this->capture_payment_setup_fee_or_renewal( $lp_order, $webhook_data );
1617 1655 // Capture subscription data
1618 1656 $this->capture_subscription_data( $webhook_data );
1619 1657
1620 1658 $lp_payment_success = $lp_order->get_meta( self::META_SUBSCRIPTION_DATA_PAYMENT_SUCCESS );
@@ -1620,21 +1658,25 @@
1620 1658 $lp_payment_success = $lp_order->get_meta( self::META_SUBSCRIPTION_DATA_PAYMENT_SUCCESS );
1621 1659 $lp_subscription_status_tmp = $lp_order->get_meta( self::META_SUBSCRIPTION_STATUS_TMP );
1622 1660 $lp_subscription_status = $lp_order->get_meta( self::META_SUBSCRIPTION_STATUS );
1623 1661
1624 - // Check lp order status is activated will renew
1662 + // Check lp order status is activated/trial will renew
1625 1663 if ( $lp_order->is_completed() ) {
1626 - if ( $lp_subscription_status === LP_Subscription_Manager::STATUS_ACTIVATED ) {
1627 - LP_Debug::log_to_comment( 'Activated to renew' );
1664 + if ( in_array(
1665 + $lp_subscription_status,
1666 + [ LP_Subscription_Manager::STATUS_ACTIVATED, LP_Subscription_Manager::STATUS_TRIAL ]
1667 + ) ) {
1668 + LP_Debug::log_to_comment( 'Activated/Trial to renew' );
1669 + update_post_meta( $lp_order_id, self::META_SUBSCRIPTION_STATUS, LP_Subscription_Manager::STATUS_ACTIVATED );
1628 1670 $lp_subscription_status_set_to_handle = LP_Subscription_Manager::STATUS_RENEWED;
1629 1671 }
1630 1672 } elseif ( ! empty( $lp_payment_success )
1631 - && $lp_subscription_status_tmp === LP_Subscription_Manager::STATUS_ACTIVATED ) {
1673 + && $lp_subscription_status_tmp === LP_Subscription_Manager::STATUS_ACTIVATED ) {
1632 1674 // If payment success and subscription status tmp is not empty
1633 1675 LP_Debug::log_to_comment( 'Payment success and subscription status tmp is not empty' );
1634 1676 $lp_subscription_status_set_to_handle = LP_Subscription_Manager::STATUS_ACTIVATED;
1635 1677 } elseif ( $webhook_data['lp_subscription_status'] !== LP_Subscription_Manager::STATUS_ACTIVATED ) {
1636 - // If subscription status is not activated, use subscription status from webhook
1678 + // If subscription status is not activated, use subscription status from webhook data switch
1637 1679 $lp_subscription_status_set_to_handle = $webhook_data['lp_subscription_status'] ?? '';
1638 1680 LP_Debug::log_to_comment( 'Subscription status is not activated: ' . $lp_subscription_status_set_to_handle );
1639 1681 }
1640 1682
@@ -1669,9 +1711,9 @@
1669 1711 )
1670 1712 );
1671 1713
1672 1714 if ( is_wp_error( $response ) ) {
1673 - throw new Exception( $response->get_error_message(), $response->get_error_code() );
1715 + throw new Exception( $response->get_error_message() );
1674 1716 }
1675 1717
1676 1718 $verify_result = LP_Helper::json_decode( wp_remote_retrieve_body( $response ), true );
1677 1719 $is_verified = ! empty( $verify_result['verification_status'] )
@@ -1818,9 +1860,9 @@
1818 1860 * @throws Exception When the sale resource is invalid or does not match the stored subscription id.
1819 1861 * @since 4.3.7
1820 1862 * @version 1.0.0
1821 1863 */
1822 - public function capturePaymentSetupFeeOrRenewal( $lp_order, array &$webhook_data ) {
1864 + public function capture_payment_setup_fee_or_renewal( $lp_order, array &$webhook_data ) {
1823 1865 $resource_type = $webhook_data['resource_type'] ?? '';
1824 1866 $event_type = $webhook_data['event_type'] ?? '';
1825 1867 if ( 'sale' !== $resource_type || 'PAYMENT.SALE.COMPLETED' !== $event_type ) {
1826 1868 return;
@@ -1935,8 +1977,87 @@
1935 1977
1936 1978 $manage_url = trailingslashit( $this->paypal_url ) . 'myaccount/autopay/';
1937 1979
1938 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 + ];
1939 2060 }
1940 2061
1941 2062 /**
1942 2063 * Settings form fields for this gateway