From 251a56039bb6b3a230d537bbc3ab9110b178f4ba Mon Sep 17 00:00:00 2001 From: Jessy Pappachan <32092402+jessy-p@users.noreply.github.com> Date: Fri, 3 May 2024 13:02:03 +0530 Subject: [PATCH 1/4] Update View Report link of Charges data highlight tile. (#8766) Co-authored-by: Jessy --- changelog/fix-8705-charges-view-report | 5 +++++ .../payment-activity/payment-activity-data.tsx | 11 ++++++++++- .../test/__snapshots__/index.test.tsx.snap | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 changelog/fix-8705-charges-view-report diff --git a/changelog/fix-8705-charges-view-report b/changelog/fix-8705-charges-view-report new file mode 100644 index 00000000000..756e6912a52 --- /dev/null +++ b/changelog/fix-8705-charges-view-report @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: Behind fPayment Activity Card feature flag, fix View Report link of Charges tile. + + diff --git a/client/components/payment-activity/payment-activity-data.tsx b/client/components/payment-activity/payment-activity-data.tsx index 788f678dce0..12e94fc1df5 100644 --- a/client/components/payment-activity/payment-activity-data.tsx +++ b/client/components/payment-activity/payment-activity-data.tsx @@ -43,6 +43,7 @@ const searchTermsForViewReportLink = { 'dispute_reversal', 'card_reader_fee', ], + charge: [ 'charge', 'payment' ], }; const getSearchParams = ( searchTerms: string[] ) => { @@ -153,7 +154,15 @@ const PaymentActivityData: React.FC = () => { page: 'wc-admin', path: '/payments/transactions', filter: 'advanced', - type_is: 'charge', + 'date_between[0]': moment( + getDateRange().date_start + ).format( 'YYYY-MM-DD' ), + 'date_between[1]': moment( + getDateRange().date_end + ).format( 'YYYY-MM-DD' ), + ...getSearchParams( + searchTermsForViewReportLink.charge + ), } ) } tracksSource="charges" isLoading={ isLoading } diff --git a/client/components/payment-activity/test/__snapshots__/index.test.tsx.snap b/client/components/payment-activity/test/__snapshots__/index.test.tsx.snap index a45a33735e0..67bfb19e82e 100644 --- a/client/components/payment-activity/test/__snapshots__/index.test.tsx.snap +++ b/client/components/payment-activity/test/__snapshots__/index.test.tsx.snap @@ -136,7 +136,7 @@ exports[`PaymentActivity component should render 1`] = `

View report From 80e006eab4d451e6883abc4b35e5ed81471968aa Mon Sep 17 00:00:00 2001 From: Nagesh Pai <4162931+nagpai@users.noreply.github.com> Date: Fri, 3 May 2024 13:42:51 +0530 Subject: [PATCH 2/4] Reporting: Fix the `View report` link on the Refunds tile of Payment activity widget (#8759) Co-authored-by: Nagesh Pai Co-authored-by: Jessy Pappachan <32092402+jessy-p@users.noreply.github.com> --- changelog/fix-8702-fix-refunds-view-report-link | 5 +++++ .../payment-activity/payment-activity-data.tsx | 12 +++++++++++- .../test/__snapshots__/index.test.tsx.snap | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 changelog/fix-8702-fix-refunds-view-report-link diff --git a/changelog/fix-8702-fix-refunds-view-report-link b/changelog/fix-8702-fix-refunds-view-report-link new file mode 100644 index 00000000000..9cc71e9ee48 --- /dev/null +++ b/changelog/fix-8702-fix-refunds-view-report-link @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: The PR fixes the correct landing links on `View report` for the Refunds tile in Payment activity widget. Changes are behind a feature flag. + + diff --git a/client/components/payment-activity/payment-activity-data.tsx b/client/components/payment-activity/payment-activity-data.tsx index 12e94fc1df5..20ca61265f3 100644 --- a/client/components/payment-activity/payment-activity-data.tsx +++ b/client/components/payment-activity/payment-activity-data.tsx @@ -43,7 +43,15 @@ const searchTermsForViewReportLink = { 'dispute_reversal', 'card_reader_fee', ], + charge: [ 'charge', 'payment' ], + + refunds: [ + 'refund', + 'refund_failure', + 'payment_refund', + 'payment_failure_refund', + ], }; const getSearchParams = ( searchTerms: string[] ) => { @@ -176,13 +184,15 @@ const PaymentActivityData: React.FC = () => { page: 'wc-admin', path: '/payments/transactions', filter: 'advanced', - type_is: 'refund', 'date_between[0]': moment( getDateRange().date_start ).format( 'YYYY-MM-DD' ), 'date_between[1]': moment( getDateRange().date_end ).format( 'YYYY-MM-DD' ), + ...getSearchParams( + searchTermsForViewReportLink.refunds + ), } ) } tracksSource="refunds" isLoading={ isLoading } diff --git a/client/components/payment-activity/test/__snapshots__/index.test.tsx.snap b/client/components/payment-activity/test/__snapshots__/index.test.tsx.snap index 67bfb19e82e..8ffa02adb61 100644 --- a/client/components/payment-activity/test/__snapshots__/index.test.tsx.snap +++ b/client/components/payment-activity/test/__snapshots__/index.test.tsx.snap @@ -165,7 +165,7 @@ exports[`PaymentActivity component should render 1`] = `

View report From d27b16fc59b018683599587f1951ac2bbdfb53c3 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Fri, 3 May 2024 11:33:49 +0100 Subject: [PATCH 3/4] Terminal In-Person Payments auto-renewing subscription support (#8390) Co-authored-by: Vladimir Reznichenko --- .../terminal-payment-subscription-support | 4 + ...ass-wc-rest-payments-orders-controller.php | 42 ++- includes/class-wc-payments-token-service.php | 8 + includes/class-wc-payments.php | 2 +- psalm-baseline.xml | 7 + ...ass-wc-rest-payments-orders-controller.php | 294 ++++++++++++++++-- .../helpers/class-wc-helper-subscriptions.php | 18 ++ 7 files changed, 347 insertions(+), 28 deletions(-) create mode 100644 changelog/terminal-payment-subscription-support diff --git a/changelog/terminal-payment-subscription-support b/changelog/terminal-payment-subscription-support new file mode 100644 index 00000000000..bc5408bb9d4 --- /dev/null +++ b/changelog/terminal-payment-subscription-support @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Support for starting auto-renewing subscriptions for In-Person Payments. diff --git a/includes/admin/class-wc-rest-payments-orders-controller.php b/includes/admin/class-wc-rest-payments-orders-controller.php index 093004d37b2..10068abaf83 100644 --- a/includes/admin/class-wc-rest-payments-orders-controller.php +++ b/includes/admin/class-wc-rest-payments-orders-controller.php @@ -47,6 +47,13 @@ class WC_REST_Payments_Orders_Controller extends WC_Payments_REST_Controller { */ private $order_service; + /** + * WC_Payments_Token instance for working with customer tokens + * + * @var WC_Payments_Token_Service + */ + private $token_service; + /** * WC_Payments_REST_Controller constructor. * @@ -54,12 +61,14 @@ class WC_REST_Payments_Orders_Controller extends WC_Payments_REST_Controller { * @param WC_Payment_Gateway_WCPay $gateway WooCommerce Payments payment gateway. * @param WC_Payments_Customer_Service $customer_service Customer class instance. * @param WC_Payments_Order_Service $order_service Order Service class instance. + * @param WC_Payments_Token_Service $token_service Token Service class instance. */ - public function __construct( WC_Payments_API_Client $api_client, WC_Payment_Gateway_WCPay $gateway, WC_Payments_Customer_Service $customer_service, WC_Payments_Order_Service $order_service ) { + public function __construct( WC_Payments_API_Client $api_client, WC_Payment_Gateway_WCPay $gateway, WC_Payments_Customer_Service $customer_service, WC_Payments_Order_Service $order_service, WC_Payments_Token_Service $token_service ) { parent::__construct( $api_client ); $this->gateway = $gateway; $this->customer_service = $customer_service; $this->order_service = $order_service; + $this->token_service = $token_service; } /** @@ -217,6 +226,30 @@ public function capture_terminal_payment( WP_REST_Request $request ) { } // Store receipt generation URL for mobile applications in order meta-data. $order->add_meta_data( 'receipt_url', get_rest_url( null, sprintf( '%s/payments/readers/receipts/%s', $this->namespace, $intent->get_id() ) ) ); + + // Add payment method for future subscription payments. + $generated_card = $intent->get_charge()->get_payment_method_details()[ Payment_Method::CARD_PRESENT ]['generated_card'] ?? null; + // If we don't get a generated card, e.g. because a digital wallet was used, we can still return that the initial payment was successful. + // The subscription will not be activated and customers will need to provide a new payment method for renewals. + if ( $generated_card ) { + $has_subscriptions = function_exists( 'wcs_order_contains_subscription' ) && + function_exists( 'wcs_get_subscriptions_for_order' ) && + function_exists( 'wcs_is_manual_renewal_required' ) && + wcs_order_contains_subscription( $order_id ); + if ( $has_subscriptions ) { + $token = $this->token_service->add_payment_method_to_user( $generated_card, $order->get_user() ); + $this->gateway->add_token_to_order( $order, $token ); + foreach ( wcs_get_subscriptions_for_order( $order ) as $subscription ) { + $subscription->set_payment_method( WC_Payment_Gateway_WCPay::GATEWAY_ID ); + // Where the setting doesn't force manual renewals, we should turn them off, because we have an auto-renewal token now. + if ( ! wcs_is_manual_renewal_required() ) { + $subscription->set_requires_manual_renewal( false ); + } + $subscription->save(); + } + } + } + // Actualize order status. $this->order_service->mark_terminal_payment_completed( $order, $intent_id, $result['status'] ); @@ -307,15 +340,14 @@ public function capture_authorization( WP_REST_Request $request ) { /** * Returns customer id from order. Create or update customer if needed. - * Use-cases: It was used by older versions of our Mobile apps in their workflows. - * - * @deprecated 3.9.0 + * Use-cases: + * - It was used by older versions of our mobile apps to add the customer details to Payment Intents. + * - It is used by the apps to set customer details on Payment Intents for an order containing subscriptions. Required for capturing renewal payments off session. * * @param WP_REST_Request $request Full data about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function create_customer( $request ) { - wc_deprecated_function( __FUNCTION__, '3.9.0' ); try { $order_id = $request['order_id']; diff --git a/includes/class-wc-payments-token-service.php b/includes/class-wc-payments-token-service.php index 37fa25ced0e..6a8f9e21d0a 100644 --- a/includes/class-wc-payments-token-service.php +++ b/includes/class-wc-payments-token-service.php @@ -81,6 +81,14 @@ public function add_token_to_user( $payment_method, $user ) { $token->set_gateway_id( $gateway_id ); $token->set_email( $payment_method[ Payment_Method::LINK ]['email'] ); break; + case Payment_Method::CARD_PRESENT: + $token = new WC_Payment_Token_CC(); + $token->set_gateway_id( CC_Payment_Gateway::GATEWAY_ID ); + $token->set_expiry_month( $payment_method[ Payment_Method::CARD_PRESENT ]['exp_month'] ); + $token->set_expiry_year( $payment_method[ Payment_Method::CARD_PRESENT ]['exp_year'] ); + $token->set_card_type( strtolower( $payment_method[ Payment_Method::CARD_PRESENT ]['brand'] ) ); + $token->set_last4( $payment_method[ Payment_Method::CARD_PRESENT ]['last4'] ); + break; default: $token = new WC_Payment_Token_CC(); $token->set_gateway_id( CC_Payment_Gateway::GATEWAY_ID ); diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php index e465834adc0..e99fec6dad4 100644 --- a/includes/class-wc-payments.php +++ b/includes/class-wc-payments.php @@ -992,7 +992,7 @@ public static function init_rest_api() { $conn_tokens_controller->register_routes(); include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-orders-controller.php'; - $orders_controller = new WC_REST_Payments_Orders_Controller( self::$api_client, self::get_gateway(), self::$customer_service, self::$order_service ); + $orders_controller = new WC_REST_Payments_Orders_Controller( self::$api_client, self::get_gateway(), self::$customer_service, self::$order_service, self::$token_service ); $orders_controller->register_routes(); include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-fraud-outcomes-controller.php'; diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 17038230df6..b358e46babb 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -100,4 +100,11 @@ $stripe_billing_migrator + + + wcs_get_subscriptions_for_order( $order ) + wcs_is_manual_renewal_required() + wcs_order_contains_subscription( $order_id ) + + diff --git a/tests/unit/admin/test-class-wc-rest-payments-orders-controller.php b/tests/unit/admin/test-class-wc-rest-payments-orders-controller.php index e53b8383e8c..c0aded3df5e 100644 --- a/tests/unit/admin/test-class-wc-rest-payments-orders-controller.php +++ b/tests/unit/admin/test-class-wc-rest-payments-orders-controller.php @@ -44,6 +44,11 @@ class WC_REST_Payments_Orders_Controller_Test extends WCPAY_UnitTestCase { */ private $order_service; + /** + * @var WC_Payments_Token_Service|MockObject + */ + private $mock_token_service; + /** * @var string */ @@ -68,6 +73,7 @@ public function set_up() { $this->mock_api_client = $this->createMock( WC_Payments_API_Client::class ); $this->mock_gateway = $this->createMock( WC_Payment_Gateway_WCPay::class ); $this->mock_customer_service = $this->createMock( WC_Payments_Customer_Service::class ); + $this->mock_token_service = $this->createMock( WC_Payments_Token_Service::class ); $this->order_service = $this->getMockBuilder( 'WC_Payments_Order_Service' ) ->setConstructorArgs( [ $this->mock_api_client ] ) ->setMethods( [ 'attach_intent_info_to_order' ] ) @@ -77,7 +83,8 @@ public function set_up() { $this->mock_api_client, $this->mock_gateway, $this->mock_customer_service, - $this->order_service + $this->order_service, + $this->mock_token_service ); } @@ -868,9 +875,6 @@ public function test_capture_authorization_not_found() { $this->assertSame( 404, $data['status'] ); } - /** - * @expectedDeprecated create_customer - */ public function test_create_customer_invalid_order_id() { $request = new WP_REST_Request( 'POST' ); $request->set_body_params( @@ -887,9 +891,6 @@ public function test_create_customer_invalid_order_id() { $this->assertEquals( 404, $data['status'] ); } - /** - * @expectedDeprecated create_customer - */ public function test_create_customer_from_order_guest_without_customer_id() { $order = WC_Helper_Order::create_order( 0 ); $customer_data = WC_Payments_Customer_Service::map_customer_data( $order ); @@ -956,9 +957,6 @@ function ( $argument ) { ); } - /** - * @expectedDeprecated create_customer - */ public function test_create_customer_from_order_guest_with_customer_id() { $order = WC_Helper_Order::create_order( 0 ); $customer_data = WC_Payments_Customer_Service::map_customer_data( $order ); @@ -1009,9 +1007,6 @@ function ( $argument ) { $this->assertSame( 'cus_guest', $result_order->get_meta( '_stripe_customer_id' ) ); } - /** - * @expectedDeprecated create_customer - */ public function test_create_customer_from_order_non_guest_with_customer_id() { $order = WC_Helper_Order::create_order(); $customer_data = WC_Payments_Customer_Service::map_customer_data( $order ); @@ -1053,9 +1048,6 @@ public function test_create_customer_from_order_non_guest_with_customer_id() { $this->assertSame( 'cus_exist', $result_order->get_meta( '_stripe_customer_id' ) ); } - /** - * @expectedDeprecated create_customer - */ public function test_create_customer_from_order_with_invalid_status() { $order = WC_Helper_Order::create_order(); $order->set_status( Order_Status::COMPLETED ); @@ -1088,9 +1080,6 @@ public function test_create_customer_from_order_with_invalid_status() { $this->assertEquals( 400, $data['status'] ); } - /** - * @expectedDeprecated create_customer - */ public function test_create_customer_from_order_non_guest_with_customer_id_from_order_meta() { $order = WC_Helper_Order::create_order(); $customer_data = WC_Payments_Customer_Service::map_customer_data( $order ); @@ -1133,9 +1122,6 @@ public function test_create_customer_from_order_non_guest_with_customer_id_from_ $this->assertSame( 'cus_exist', $result_order->get_meta( '_stripe_customer_id' ) ); } - /** - * @expectedDeprecated create_customer - */ public function test_create_customer_from_order_non_guest_without_customer_id() { $order = WC_Helper_Order::create_order(); $customer_data = WC_Payments_Customer_Service::map_customer_data( $order ); @@ -1737,4 +1723,268 @@ private function create_charge_object() { return new WC_Payments_API_Charge( $this->mock_charge_id, 1500, $created ); } + + public function test_capture_terminal_payment_with_subscription_product_sets_generated_card_on_user() { + $order = $this->create_mock_order(); + + $subscription = new WC_Subscription(); + $subscription->set_parent( $order ); + $this->mock_wcs_order_contains_subscription( true ); + $this->mock_wcs_get_subscriptions_for_order( [ $subscription ] ); + $this->mock_wcs_is_manual_renewal_required( false ); + + $generated_card_id = 'pm_generatedCardId'; + + $mock_intent = WC_Helper_Intention::create_intention( + [ + 'charge' => [ + 'payment_method_details' => [ + 'type' => 'card_present', + 'card_present' => [ + 'generated_card' => $generated_card_id, + ], + ], + ], + 'metadata' => [ + 'order_id' => $order->get_id(), + ], + 'status' => Intent_Status::REQUIRES_CAPTURE, + ] + ); + + $request = $this->mock_wcpay_request( Get_Intention::class, 1, $this->mock_intent_id ); + + $request->expects( $this->once() ) + ->method( 'format_response' ) + ->willReturn( $mock_intent ); + + $this->mock_gateway + ->expects( $this->once() ) + ->method( 'capture_charge' ) + ->with( $this->isInstanceOf( WC_Order::class ) ) + ->willReturn( + [ + 'status' => Intent_Status::SUCCEEDED, + 'id' => $this->mock_intent_id, + ] + ); + + $this->order_service + ->expects( $this->once() ) + ->method( 'attach_intent_info_to_order' ) + ->with( + $this->isInstanceOf( WC_Order::class ), + $mock_intent, + ); + + $this->mock_token_service + ->expects( $this->once() ) + ->method( 'add_payment_method_to_user' ) + ->with( + $generated_card_id, + $this->isInstanceOf( WP_User::class ) + ); + + $request = new WP_REST_Request( 'POST' ); + $request->set_body_params( + [ + 'order_id' => $order->get_id(), + 'payment_intent_id' => $this->mock_intent_id, + ] + ); + + $response = $this->controller->capture_terminal_payment( $request ); + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 'woocommerce_payments', $subscription->get_payment_method() ); + } + + /** + * @dataProvider provider_capture_terminal_payment_with_subscription_product_sets_manual_renewal + */ + public function test_capture_terminal_payment_with_subscription_product_sets_manual_renewal( bool $manual_renewal_required_setting, bool $initial_subscription_manual_renewal, bool $expected_subscription_manual_renewal ) { + $order = $this->create_mock_order(); + + $subscription = new WC_Subscription(); + $subscription->set_parent( $order ); + $subscription->set_requires_manual_renewal( $initial_subscription_manual_renewal ); + $this->mock_wcs_order_contains_subscription( true ); + $this->mock_wcs_get_subscriptions_for_order( [ $subscription ] ); + $this->mock_wcs_is_manual_renewal_required( $manual_renewal_required_setting ); + + $generated_card_id = 'pm_generatedCardId'; + + $mock_intent = WC_Helper_Intention::create_intention( + [ + 'charge' => [ + 'payment_method_details' => [ + 'type' => 'card_present', + 'card_present' => [ + 'generated_card' => $generated_card_id, + ], + ], + ], + 'metadata' => [ + 'order_id' => $order->get_id(), + ], + 'status' => Intent_Status::REQUIRES_CAPTURE, + ] + ); + + $request = $this->mock_wcpay_request( Get_Intention::class, 1, $this->mock_intent_id ); + + $request->expects( $this->once() ) + ->method( 'format_response' ) + ->willReturn( $mock_intent ); + + $this->mock_gateway + ->expects( $this->once() ) + ->method( 'capture_charge' ) + ->with( $this->isInstanceOf( WC_Order::class ) ) + ->willReturn( + [ + 'status' => Intent_Status::SUCCEEDED, + 'id' => $this->mock_intent_id, + ] + ); + + $this->order_service + ->expects( $this->once() ) + ->method( 'attach_intent_info_to_order' ) + ->with( + $this->isInstanceOf( WC_Order::class ), + $mock_intent, + ); + + $this->mock_token_service + ->expects( $this->once() ) + ->method( 'add_payment_method_to_user' ) + ->with( + $generated_card_id, + $this->isInstanceOf( WP_User::class ) + ); + + $request = new WP_REST_Request( 'POST' ); + $request->set_body_params( + [ + 'order_id' => $order->get_id(), + 'payment_intent_id' => $this->mock_intent_id, + ] + ); + + $response = $this->controller->capture_terminal_payment( $request ); + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( $expected_subscription_manual_renewal, $subscription->is_manual() ); + } + + /** + * bool $manual_renewal_required_setting + * bool $initial_subscription_manual_renewal + * bool $expected_subscription_manual_renewal + */ + public function provider_capture_terminal_payment_with_subscription_product_sets_manual_renewal(): array { + return [ + [ true, true, true ], + [ false, true, false ], + [ true, false, false ], // even if manual_renewal_required, we won't set it to manual_renewal if it started as automatic. + [ false, false, false ], + ]; + } + + /** + * Cleanup after all tests. + */ + public static function tear_down_after_class() { + WC_Subscriptions::set_wcs_order_contains_subscription( null ); + WC_Subscriptions::set_wcs_get_subscriptions_for_order( null ); + WC_Subscriptions::set_wcs_is_manual_renewal_required( null ); + parent::tear_down_after_class(); + } + + private function mock_wcs_order_contains_subscription( $value ) { + WC_Subscriptions::set_wcs_order_contains_subscription( + function ( $order ) use ( $value ) { + return $value; + } + ); + } + + private function mock_wcs_get_subscriptions_for_order( $value ) { + WC_Subscriptions::set_wcs_get_subscriptions_for_order( + function ( $order ) use ( $value ) { + return $value; + } + ); + } + + private function mock_wcs_is_manual_renewal_required( $value ) { + WC_Subscriptions::set_wcs_is_manual_renewal_required( + function () use ( $value ) { + return $value; + } + ); + } + + public function test_capture_terminal_payment_with_subscription_product_returns_success_even_if_no_generated_card() { + $order = $this->create_mock_order(); + + $subscription = new WC_Subscription(); + $subscription->set_parent( $order ); + $this->mock_wcs_order_contains_subscription( true ); + $this->mock_wcs_get_subscriptions_for_order( [ $subscription ] ); + + $mock_intent = WC_Helper_Intention::create_intention( + [ + 'charge' => [ + 'payment_method_details' => [ + 'type' => 'card_present', + 'card_present' => [], + ], + ], + 'metadata' => [ + 'order_id' => $order->get_id(), + ], + 'status' => Intent_Status::REQUIRES_CAPTURE, + ] + ); + + $request = $this->mock_wcpay_request( Get_Intention::class, 1, $this->mock_intent_id ); + + $request->expects( $this->once() ) + ->method( 'format_response' ) + ->willReturn( $mock_intent ); + + $this->mock_gateway + ->expects( $this->once() ) + ->method( 'capture_charge' ) + ->with( $this->isInstanceOf( WC_Order::class ) ) + ->willReturn( + [ + 'status' => Intent_Status::SUCCEEDED, + 'id' => $this->mock_intent_id, + ] + ); + + $this->order_service + ->expects( $this->once() ) + ->method( 'attach_intent_info_to_order' ) + ->with( + $this->isInstanceOf( WC_Order::class ), + $mock_intent, + ); + + $this->mock_token_service + ->expects( $this->never() ) + ->method( 'add_payment_method_to_user' ); + + $request = new WP_REST_Request( 'POST' ); + $request->set_body_params( + [ + 'order_id' => $order->get_id(), + 'payment_intent_id' => $this->mock_intent_id, + ] + ); + + $response = $this->controller->capture_terminal_payment( $request ); + $this->assertSame( 200, $response->status ); + } } diff --git a/tests/unit/helpers/class-wc-helper-subscriptions.php b/tests/unit/helpers/class-wc-helper-subscriptions.php index 3d361a6446d..a2c99a77071 100644 --- a/tests/unit/helpers/class-wc-helper-subscriptions.php +++ b/tests/unit/helpers/class-wc-helper-subscriptions.php @@ -90,6 +90,13 @@ function wcs_get_orders_with_meta_query( $args ) { return ( WC_Subscriptions::$wcs_get_orders_with_meta_query )( $args ); } +function wcs_is_manual_renewal_required() { + if ( ! WC_Subscriptions::$wcs_is_manual_renewal_required ) { + return; + } + return ( WC_Subscriptions::$wcs_is_manual_renewal_required )(); +} + /** * Class WC_Subscriptions. * @@ -187,6 +194,13 @@ class WC_Subscriptions { */ public static $wcs_order_contains_renewal = null; + /** + * wcs_is_manual_renewal_required mock. + * + * @var function + */ + public static $wcs_is_manual_renewal_required = null; + public static function set_wcs_order_contains_subscription( $function ) { self::$wcs_order_contains_subscription = $function; } @@ -234,4 +248,8 @@ public static function wcs_order_contains_renewal( $function ) { public static function is_duplicate_site() { return false; } + + public static function set_wcs_is_manual_renewal_required( $function ) { + self::$wcs_is_manual_renewal_required = $function; + } } From a4c1b31d4a4be6059bf91f31d8829bea1b180a3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Qui=C3=B1ones?= Date: Fri, 3 May 2024 11:27:30 -0500 Subject: [PATCH 4/4] Add notice and notification in Advanced Fraud Settings page (#8762) --- .../fix-6497-advanced-fraud-settings-empty | 4 + .../advanced-settings/index.tsx | 110 +++++++++++------- .../test/__snapshots__/index.test.tsx.snap | 40 +++++++ .../advanced-settings/test/index.test.tsx | 42 +++++++ client/settings/fraud-protection/style.scss | 6 +- 5 files changed, 162 insertions(+), 40 deletions(-) create mode 100644 changelog/fix-6497-advanced-fraud-settings-empty diff --git a/changelog/fix-6497-advanced-fraud-settings-empty b/changelog/fix-6497-advanced-fraud-settings-empty new file mode 100644 index 00000000000..98e38c0b807 --- /dev/null +++ b/changelog/fix-6497-advanced-fraud-settings-empty @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Add notice when no rules are enabled in advanced fraud settings diff --git a/client/settings/fraud-protection/advanced-settings/index.tsx b/client/settings/fraud-protection/advanced-settings/index.tsx index 3c910898159..f18b70a624d 100644 --- a/client/settings/fraud-protection/advanced-settings/index.tsx +++ b/client/settings/fraud-protection/advanced-settings/index.tsx @@ -70,20 +70,28 @@ const observerEventMapping: Record< string, string > = { }; const Breadcrumb = () => ( -

- - { 'WooPayments' } - -  >  - { __( 'Advanced fraud protection', 'woocommerce-payments' ) } -

+ <> +

+ + { 'WooPayments' } + +  >  + { __( 'Advanced fraud protection', 'woocommerce-payments' ) } +

+

+ { __( + 'At least one risk filter needs to be enabled for advanced protection.', + 'woocommerce-payments' + ) } +

+ ); const SaveFraudProtectionSettingsButton: React.FC = ( { children } ) => { @@ -154,42 +162,66 @@ const FraudProtectionAdvancedSettingsPage: React.FC = () => { .every( Boolean ); }; + const checkAnyRuleFilterEnabled = ( + settings: ProtectionSettingsUI + ): boolean => { + return Object.values( settings ).some( ( setting ) => setting.enabled ); + }; + const handleSaveSettings = () => { - if ( validateSettings( protectionSettingsUI ) ) { - if ( ProtectionLevel.ADVANCED !== currentProtectionLevel ) { - updateProtectionLevel( ProtectionLevel.ADVANCED ); - dispatch( 'core/notices' ).createSuccessNotice( + if ( ! validateSettings( protectionSettingsUI ) ) { + window.scrollTo( { + top: 0, + } ); + return; + } + + if ( ! checkAnyRuleFilterEnabled( protectionSettingsUI ) ) { + if ( ProtectionLevel.BASIC === currentProtectionLevel ) { + dispatch( 'core/notices' ).createErrorNotice( __( - 'Current protection level is set to "advanced".', + 'At least one risk filter needs to be enabled for advanced protection.', 'woocommerce-payments' ) ); + return; } - const settings = writeRuleset( protectionSettingsUI ); + updateProtectionLevel( ProtectionLevel.BASIC ); + dispatch( 'core/notices' ).createErrorNotice( + __( + 'Current protection level is set to "basic". At least one risk filter needs to be enabled for advanced protection.', + 'woocommerce-payments' + ) + ); + } else if ( ProtectionLevel.ADVANCED !== currentProtectionLevel ) { + updateProtectionLevel( ProtectionLevel.ADVANCED ); + dispatch( 'core/notices' ).createSuccessNotice( + __( + 'Current protection level is set to "advanced".', + 'woocommerce-payments' + ) + ); + } - // Persist the AVS verification setting until the account cache is updated locally. - if ( - wcpaySettings?.accountStatus?.fraudProtection - ?.declineOnAVSFailure - ) { - wcpaySettings.accountStatus.fraudProtection.declineOnAVSFailure = settings.some( - ( setting ) => setting.key === 'avs_verification' - ); - } + const settings = writeRuleset( protectionSettingsUI ); - updateAdvancedFraudProtectionSettings( settings ); + // Persist the AVS verification setting until the account cache is updated locally. + if ( + wcpaySettings?.accountStatus?.fraudProtection?.declineOnAVSFailure + ) { + wcpaySettings.accountStatus.fraudProtection.declineOnAVSFailure = settings.some( + ( setting ) => setting.key === 'avs_verification' + ); + } - saveSettings(); + updateAdvancedFraudProtectionSettings( settings ); - recordEvent( 'wcpay_fraud_protection_advanced_settings_saved', { - settings: JSON.stringify( settings ), - } ); - } else { - window.scrollTo( { - top: 0, - } ); - } + saveSettings(); + + recordEvent( 'wcpay_fraud_protection_advanced_settings_saved', { + settings: JSON.stringify( settings ), + } ); }; // Hack to make "Payments > Settings" the active selected menu item. diff --git a/client/settings/fraud-protection/advanced-settings/test/__snapshots__/index.test.tsx.snap b/client/settings/fraud-protection/advanced-settings/test/__snapshots__/index.test.tsx.snap index abd09b5d4c7..dffbf33c6b1 100644 --- a/client/settings/fraud-protection/advanced-settings/test/__snapshots__/index.test.tsx.snap +++ b/client/settings/fraud-protection/advanced-settings/test/__snapshots__/index.test.tsx.snap @@ -66,6 +66,11 @@ Object {  >  Advanced fraud protection +

+ At least one risk filter needs to be enabled for advanced protection. +

@@ -1066,6 +1071,11 @@ Object {  >  Advanced fraud protection +

+ At least one risk filter needs to be enabled for advanced protection. +

@@ -2149,6 +2159,11 @@ Object {  >  Advanced fraud protection +

+ At least one risk filter needs to be enabled for advanced protection. +

@@ -3006,6 +3021,11 @@ Object {  >  Advanced fraud protection +

+ At least one risk filter needs to be enabled for advanced protection. +

@@ -3926,6 +3946,11 @@ Object {  >  Advanced fraud protection +

+ At least one risk filter needs to be enabled for advanced protection. +

+

+ At least one risk filter needs to be enabled for advanced protection. +

+

+ At least one risk filter needs to be enabled for advanced protection. +

+

+ At least one risk filter needs to be enabled for advanced protection. +

( { dispatch: jest.fn( () => ( { setIsMatching: jest.fn(), createSuccessNotice: jest.fn(), + createErrorNotice: jest.fn(), onLoad: jest.fn(), } ) ), registerStore: jest.fn(), @@ -438,4 +439,45 @@ describe( 'Advanced fraud protection settings', () => { expect( protectionLevelState.updateState.mock.calls.length ).toBe( 0 ); expect( protectionLevelState.updateState.mock.calls ).toEqual( [] ); } ); + test( 'does not update protection level to advanced when no risk rules are enabled', async () => { + const protectionLevelState = { + state: 'standard', + updateState: jest.fn( ( level ) => { + protectionLevelState.state = level; + } ), + }; + mockUseCurrentProtectionLevel.mockReturnValue( [ + protectionLevelState.state, + protectionLevelState.updateState, + ] ); + mockUseSettings.mockReturnValue( { + settings: { + advanced_fraud_protection_settings: defaultSettings, + }, + isSaving: false, + saveSettings: jest.fn(), + isLoading: false, + } ); + mockUseAdvancedFraudProtectionSettings.mockReturnValue( [ + defaultSettings, + jest.fn(), + ] ); + container = render( +
+
+
+
+ +
+ ); + const [ saveButton ] = await container.findAllByText( 'Save Changes' ); + saveButton.click(); + await waitFor( () => { + expect( mockUseSettings().saveSettings.mock.calls.length ).toBe( + 1 + ); + } ); + + expect( protectionLevelState.state ).toBe( 'basic' ); + } ); } ); diff --git a/client/settings/fraud-protection/style.scss b/client/settings/fraud-protection/style.scss index 70d75d9f0b5..1981d4b3559 100644 --- a/client/settings/fraud-protection/style.scss +++ b/client/settings/fraud-protection/style.scss @@ -43,7 +43,7 @@ } &-header-breadcrumb { margin-top: 0; - margin-bottom: 24px; + margin-bottom: 8px; @media screen and ( min-width: 961px ) { margin-top: -16px; } @@ -261,6 +261,10 @@ ); cursor: pointer; } + &-advanced-settings-notice { + margin-top: 0; + margin-bottom: 16px; + } } .components-modal__header {