From b93416a253c641de5a8177b4bcbcaae78fd5bfc3 Mon Sep 17 00:00:00 2001 From: Miguel Gasca Date: Wed, 8 Jan 2025 19:17:51 +0100 Subject: [PATCH 01/12] Add specifics to the amount too small exception --- ...ass-wc-rest-payments-orders-controller.php | 11 +++++++--- includes/class-wc-payment-gateway-wcpay.php | 20 +++++++++++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/includes/admin/class-wc-rest-payments-orders-controller.php b/includes/admin/class-wc-rest-payments-orders-controller.php index 04c86f54197..83e5d4de32e 100644 --- a/includes/admin/class-wc-rest-payments-orders-controller.php +++ b/includes/admin/class-wc-rest-payments-orders-controller.php @@ -204,15 +204,20 @@ public function capture_terminal_payment( WP_REST_Request $request ) { $result = $is_intent_captured ? $result_for_captured_intent : $this->gateway->capture_charge( $order, false, $intent_metadata ); if ( Intent_Status::SUCCEEDED !== $result['status'] ) { - $http_code = $result['http_code'] ?? 502; + $http_code = $result['http_code'] ?? 502; + $error_code = $result['error_code'] ?? null; + $extra_details = $result['extra_details'] ?? []; return new WP_Error( - 'wcpay_capture_error', + 'amount_too_small' === $error_code ? 'wcpay_capture_error_amount_too_small' : 'wcpay_capture_error', sprintf( // translators: %s: the error message. __( 'Payment capture failed to complete with the following message: %s', 'woocommerce-payments' ), $result['message'] ?? __( 'Unknown error', 'woocommerce-payments' ) ), - [ 'status' => $http_code ] + [ + 'status' => $http_code, + 'extra_details' => $extra_details, + ] ); } // Store receipt generation URL for mobile applications in order meta-data. diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index d1be21241b9..1a09733cb69 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -3377,6 +3377,15 @@ public function capture_charge( $order, $include_level3 = true, $intent_metadata try { $error_message = $e->getMessage(); $http_code = $e->get_http_code(); + $error_code = $e->get_error_code(); + $extra_details = []; + + if ( $e instanceof Amount_Too_Small_Exception ) { + $extra_details = [ + 'minimum_amount' => $e->get_minimum_amount(), + 'minimum_amount_currency' => $e->get_currency(), + ]; + } $request = Get_Intention::create( $intent_id ); $request->set_hook_args( $order ); @@ -3392,6 +3401,7 @@ public function capture_charge( $order, $include_level3 = true, $intent_metadata $status = null; $error_message = $e->getMessage(); $http_code = $e->get_http_code(); + $error_code = $e->get_error_code(); } } @@ -3418,10 +3428,12 @@ public function capture_charge( $order, $include_level3 = true, $intent_metadata } return [ - 'status' => $status ?? 'failed', - 'id' => ! empty( $intent ) ? $intent->get_id() : null, - 'message' => $error_message, - 'http_code' => $http_code, + 'status' => $status ?? 'failed', + 'id' => ! empty( $intent ) ? $intent->get_id() : null, + 'message' => $error_message, + 'http_code' => $http_code, + 'error_code' => $error_code, + 'extra_details' => $extra_details, ]; } From 1d73dbd53745153eea73c179cf9827419953101c Mon Sep 17 00:00:00 2001 From: Miguel Gasca Date: Wed, 8 Jan 2025 19:53:24 +0100 Subject: [PATCH 02/12] Add unit test --- includes/class-wc-payment-gateway-wcpay.php | 3 +- ...ass-wc-rest-payments-orders-controller.php | 51 +++++++++++ .../test-class-wc-payment-gateway-wcpay.php | 90 +++++++++++-------- 3 files changed, 107 insertions(+), 37 deletions(-) diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index 1a09733cb69..8f67840007c 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -3355,6 +3355,7 @@ public function capture_charge( $order, $include_level3 = true, $intent_metadata $status = null; $error_message = null; $http_code = null; + $error_code = null; try { $intent_id = $order->get_transaction_id(); @@ -3433,7 +3434,7 @@ public function capture_charge( $order, $include_level3 = true, $intent_metadata 'message' => $error_message, 'http_code' => $http_code, 'error_code' => $error_code, - 'extra_details' => $extra_details, + 'extra_details' => $extra_details ?? [], ]; } 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 48c94e75869..fd2b18d57c2 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 @@ -1998,4 +1998,55 @@ public function test_capture_terminal_payment_with_subscription_product_returns_ $response = $this->controller->capture_terminal_payment( $request ); $this->assertSame( 200, $response->status ); } + + public function test_capture_terminal_payment_error_amount_too_small() { + $order = $this->create_mock_order(); + $mock_intent = WC_Helper_Intention::create_intention( + [ + 'status' => Intent_Status::REQUIRES_CAPTURE, + 'metadata' => [ + 'order_id' => $order->get_id(), + ], + ] + ); + + $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::REQUIRES_CAPTURE, + 'id' => $this->mock_intent_id, + 'http_code' => 400, + 'error_code' => 'amount_too_small', + 'extra_details' => [ + 'minimum_amount' => 50, + 'minimum_amount_currency' => 'USD', + ], + ] + ); + + $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->assertInstanceOf( 'WP_Error', $response ); + $this->assertEquals( 'wcpay_capture_error_amount_too_small', $response->get_error_code() ); + $this->assertStringContainsString( 'Payment capture failed to complete', $response->get_error_message() ); + $this->assertEquals( 400, $response->get_error_data()['status'] ); + $this->assertEquals( 50, $response->get_error_data()['extra_details']['minimum_amount'] ); + $this->assertEquals( 'USD', $response->get_error_data()['extra_details']['minimum_amount_currency'] ); + } } diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay.php b/tests/unit/test-class-wc-payment-gateway-wcpay.php index 1827041a1fc..e4102d7d8c1 100644 --- a/tests/unit/test-class-wc-payment-gateway-wcpay.php +++ b/tests/unit/test-class-wc-payment-gateway-wcpay.php @@ -1488,10 +1488,12 @@ public function test_capture_charge_success() { // Assert the returned data contains fields required by the REST endpoint. $this->assertEquals( [ - 'status' => Intent_Status::SUCCEEDED, - 'id' => $intent_id, - 'message' => null, - 'http_code' => 200, + 'status' => Intent_Status::SUCCEEDED, + 'id' => $intent_id, + 'message' => null, + 'http_code' => 200, + 'error_code' => null, + 'extra_details' => [], ], $result ); @@ -1542,10 +1544,12 @@ public function test_capture_charge_success_non_usd() { // Assert the returned data contains fields required by the REST endpoint. $this->assertEquals( [ - 'status' => Intent_Status::SUCCEEDED, - 'id' => $intent_id, - 'message' => null, - 'http_code' => 200, + 'status' => Intent_Status::SUCCEEDED, + 'id' => $intent_id, + 'message' => null, + 'http_code' => 200, + 'error_code' => null, + 'extra_details' => [], ], $result ); @@ -1588,10 +1592,12 @@ public function test_capture_charge_failure() { // Assert the returned data contains fields required by the REST endpoint. $this->assertEquals( [ - 'status' => Intent_Status::REQUIRES_CAPTURE, - 'id' => $intent_id, - 'message' => null, - 'http_code' => 502, + 'status' => Intent_Status::REQUIRES_CAPTURE, + 'id' => $intent_id, + 'message' => null, + 'http_code' => 502, + 'error_code' => null, + 'extra_details' => [], ], $result ); @@ -1642,10 +1648,12 @@ public function test_capture_charge_failure_non_usd() { // Assert the returned data contains fields required by the REST endpoint. $this->assertEquals( [ - 'status' => Intent_Status::REQUIRES_CAPTURE, - 'id' => $intent_id, - 'message' => null, - 'http_code' => 502, + 'status' => Intent_Status::REQUIRES_CAPTURE, + 'id' => $intent_id, + 'message' => null, + 'http_code' => 502, + 'error_code' => null, + 'extra_details' => [], ], $result ); @@ -1694,10 +1702,12 @@ public function test_capture_charge_api_failure() { // Assert the returned data contains fields required by the REST endpoint. $this->assertEquals( [ - 'status' => 'failed', - 'id' => $intent_id, - 'message' => 'test exception', - 'http_code' => 500, + 'status' => 'failed', + 'id' => $intent_id, + 'message' => 'test exception', + 'http_code' => 500, + 'error_code' => 'server_error', + 'extra_details' => [], ], $result ); @@ -1755,10 +1765,12 @@ public function test_capture_charge_api_failure_non_usd() { // Assert the returned data contains fields required by the REST endpoint. $this->assertEquals( [ - 'status' => 'failed', - 'id' => $intent_id, - 'message' => 'test exception', - 'http_code' => 500, + 'status' => 'failed', + 'id' => $intent_id, + 'message' => 'test exception', + 'http_code' => 500, + 'error_code' => 'server_error', + 'extra_details' => [], ], $result ); @@ -1808,10 +1820,12 @@ public function test_capture_charge_expired() { // Assert the returned data contains fields required by the REST endpoint. $this->assertEquals( [ - 'status' => 'failed', - 'id' => $intent_id, - 'message' => 'test exception', - 'http_code' => 500, + 'status' => 'failed', + 'id' => $intent_id, + 'message' => 'test exception', + 'http_code' => 500, + 'error_code' => 'server_error', + 'extra_details' => [], ], $result ); @@ -1863,10 +1877,12 @@ public function test_capture_charge_metadata() { // Assert the returned data contains fields required by the REST endpoint. $this->assertSame( [ - 'status' => Intent_Status::SUCCEEDED, - 'id' => $intent_id, - 'message' => null, - 'http_code' => 200, + 'status' => Intent_Status::SUCCEEDED, + 'id' => $intent_id, + 'message' => null, + 'http_code' => 200, + 'error_code' => null, + 'extra_details' => [], ], $result ); @@ -1914,10 +1930,12 @@ public function test_capture_charge_without_level3() { // Assert the returned data contains fields required by the REST endpoint. $this->assertEquals( [ - 'status' => Intent_Status::SUCCEEDED, - 'id' => $intent_id, - 'message' => null, - 'http_code' => 200, + 'status' => Intent_Status::SUCCEEDED, + 'id' => $intent_id, + 'message' => null, + 'http_code' => 200, + 'error_code' => null, + 'extra_details' => [], ], $result ); From a49b4305ad799f481a00db9577e74a185c054f04 Mon Sep 17 00:00:00 2001 From: Miguel Gasca Date: Thu, 9 Jan 2025 16:15:13 +0100 Subject: [PATCH 03/12] Add minimum capturable amount to the error details when capturing authorizations --- .../admin/class-wc-rest-payments-orders-controller.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/includes/admin/class-wc-rest-payments-orders-controller.php b/includes/admin/class-wc-rest-payments-orders-controller.php index 83e5d4de32e..c33c670e3b6 100644 --- a/includes/admin/class-wc-rest-payments-orders-controller.php +++ b/includes/admin/class-wc-rest-payments-orders-controller.php @@ -309,14 +309,19 @@ public function capture_authorization( WP_REST_Request $request ) { $result = $this->gateway->capture_charge( $order, true, $intent_metadata ); if ( Intent_Status::SUCCEEDED !== $result['status'] ) { + $error_code = $result['error_code'] ?? null; + $extra_details = $result['extra_details'] ?? []; return new WP_Error( - 'wcpay_capture_error', + 'amount_too_small' === $error_code ? 'wcpay_capture_error_amount_too_small' : 'wcpay_capture_error', sprintf( // translators: %s: the error message. __( 'Payment capture failed to complete with the following message: %s', 'woocommerce-payments' ), $result['message'] ?? __( 'Unknown error', 'woocommerce-payments' ) ), - [ 'status' => $result['http_code'] ?? 502 ] + [ + 'status' => $result['http_code'] ?? 502, + 'extra_details' => $extra_details, + ] ); } From 5bc97843c8ce146762e3f317ef280e82c8d007f9 Mon Sep 17 00:00:00 2001 From: Miguel Gasca Date: Thu, 9 Jan 2025 16:16:51 +0100 Subject: [PATCH 04/12] Append minimum amount to the error message. --- includes/class-wc-payment-gateway-wcpay.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index 8f67840007c..bd019a063d4 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -3382,10 +3382,17 @@ public function capture_charge( $order, $include_level3 = true, $intent_metadata $extra_details = []; if ( $e instanceof Amount_Too_Small_Exception ) { - $extra_details = [ + $extra_details = [ 'minimum_amount' => $e->get_minimum_amount(), 'minimum_amount_currency' => $e->get_currency(), ]; + $minimum_amount_details = sprintf( + /* translators: %1$s: minimum amount, %2$s: currency */ + __( 'The minimum amount to capture is %1$s %2$s.', 'woocommerce-payments' ), + WC_Payments_Utils::interpret_stripe_amount( $e->get_minimum_amount(), $e->get_currency() ), + strtoupper( $e->get_currency() ) + ); + $error_message = $error_message . ' ' . $minimum_amount_details; } $request = Get_Intention::create( $intent_id ); From a5d7d9c3050b35bb82136c7a50af3af4a4d3f6b4 Mon Sep 17 00:00:00 2001 From: Miguel Gasca Date: Thu, 9 Jan 2025 16:18:03 +0100 Subject: [PATCH 05/12] Surface the amount too small error in the front end when capturing authorizations --- client/data/authorizations/actions.ts | 50 ++++++++++++-- .../data/authorizations/test/actions.test.ts | 66 +++++++++++++++++++ 2 files changed, 112 insertions(+), 4 deletions(-) diff --git a/client/data/authorizations/actions.ts b/client/data/authorizations/actions.ts index ace7f3c6fed..84c274ff17a 100644 --- a/client/data/authorizations/actions.ts +++ b/client/data/authorizations/actions.ts @@ -18,13 +18,46 @@ import { } from 'wcpay/types/authorizations'; import { STORE_NAME } from '../constants'; import { ApiError } from 'wcpay/types/errors'; +import { formatCurrency } from 'multi-currency/utils/currency'; -const getErrorMessage = ( apiError: { +interface WCPayError { code?: string; message?: string; -} ): string => { + data?: { + status?: number; + extra_details?: { + minimum_amount?: number; + minimum_amount_currency?: string; + }; + }; +} + +const getErrorMessage = ( apiError: WCPayError ): string => { // Map specific error codes to user-friendly messages - const errorMessages: Record< string, string > = { + const getAmountTooSmallError = ( error: WCPayError ): string => { + const currency = + error.data?.extra_details?.minimum_amount_currency ?? 'USD'; + + const amount = formatCurrency( + error.data?.extra_details?.minimum_amount ?? 0, + currency + ); + + return sprintf( + /* translators: %1$s: minimum amount, %2$s: currency code */ + __( + 'The minimum amount to capture is %1$s %2$s.', + 'woocommerce-payments' + ), + amount, + currency.toUpperCase() + ); + }; + + const errorMessages: Record< + string, + string | ( ( error: WCPayError ) => string ) + > = { wcpay_missing_order: __( 'The order could not be found.', 'woocommerce-payments' @@ -53,10 +86,15 @@ const getErrorMessage = ( apiError: { 'An unexpected error occurred. Please try again later.', 'woocommerce-payments' ), + wcpay_capture_error_amount_too_small: getAmountTooSmallError, }; + const errorHandler = errorMessages[ apiError.code ?? '' ]; + if ( typeof errorHandler === 'function' ) { + return errorHandler( apiError ); + } return ( - errorMessages[ apiError.code ?? '' ] ?? + errorHandler ?? __( 'Unable to process the payment. Please try again later.', 'woocommerce-payments' @@ -231,6 +269,10 @@ export function* submitCaptureAuthorization( message?: string; data?: { status?: number; + extra_details?: { + minimum_amount?: number; + minimum_amount_currency?: string; + }; }; }; diff --git a/client/data/authorizations/test/actions.test.ts b/client/data/authorizations/test/actions.test.ts index 36527d1836a..363d80b2c7e 100644 --- a/client/data/authorizations/test/actions.test.ts +++ b/client/data/authorizations/test/actions.test.ts @@ -18,6 +18,25 @@ import { import authorizationsFixture from './authorizations.fixture.json'; import { STORE_NAME } from 'wcpay/data/constants'; +declare const global: { + wcpaySettings: { + zeroDecimalCurrencies: string[]; + connect: { + country: string; + }; + currencyData: { + [ key: string ]: { + code: string; + symbol: string; + symbolPosition: string; + thousandSeparator: string; + decimalSeparator: string; + precision: number; + }; + }; + }; +}; + describe( 'Authorizations actions', () => { describe( 'submitCaptureAuthorization', () => { const { @@ -168,6 +187,25 @@ describe( 'Authorizations actions', () => { } ); describe( 'error handling', () => { + beforeEach( () => { + global.wcpaySettings = { + zeroDecimalCurrencies: [], + connect: { + country: 'US', + }, + currencyData: { + USD: { + code: 'USD', + symbol: '$', + symbolPosition: 'left', + thousandSeparator: ',', + decimalSeparator: '.', + precision: 2, + }, + }, + }; + } ); + it( 'should create error notice with API error message', () => { const generator = submitCaptureAuthorization( 'pi_123', 123 ); @@ -225,6 +263,34 @@ describe( 'Authorizations actions', () => { ); } ); + it( 'should create error notice with amount too small error details', () => { + const generator = submitCaptureAuthorization( 'pi_123', 123 ); + + // Skip initial dispatch calls + generator.next(); + generator.next(); + + // Mock API error for amount too small + const apiError = { + code: 'wcpay_capture_error_amount_too_small', + data: { + status: 400, + extra_details: { + minimum_amount: 50, + minimum_amount_currency: 'USD', + }, + }, + }; + + expect( generator.throw( apiError ).value ).toEqual( + controls.dispatch( + 'core/notices', + 'createErrorNotice', + 'There has been an error capturing the payment for order #123. The minimum amount to capture is $0.50 USD.' + ) + ); + } ); + it( 'should create error notice with fallback message when API error has no message', () => { const generator = submitCaptureAuthorization( 'pi_123', 123 ); From e696976a4cd8fcbe20cc897a1ba255f965b6dbb8 Mon Sep 17 00:00:00 2001 From: Miguel Gasca Date: Thu, 9 Jan 2025 16:18:25 +0100 Subject: [PATCH 06/12] Add wcpay_capture_error_amount_too_small error code to the docs --- docs/rest-api/source/includes/wp-api-v3/order.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/rest-api/source/includes/wp-api-v3/order.md b/docs/rest-api/source/includes/wp-api-v3/order.md index 6a6527c1023..fc3ff4dd7ca 100644 --- a/docs/rest-api/source/includes/wp-api-v3/order.md +++ b/docs/rest-api/source/includes/wp-api-v3/order.md @@ -68,6 +68,7 @@ Capture the funds of an in-person payment intent. Given an intent ID and an orde - `wcpay_refunded_order_uncapturable` - Payment cannot be captured for partially or fully refunded orders - `wcpay_payment_uncapturable` - The payment cannot be captured if intent status is not one of 'processing', 'requires_capture', or 'succeeded' - `wcpay_capture_error` - Unknown error +- `wcpay_capture_error_amount_too_small` - The payment cannot be captured because the amount is too small ### HTTP request @@ -124,6 +125,7 @@ Capture the funds of an existing uncaptured payment intent that was marked to be - `wcpay_payment_uncapturable` - The payment cannot be captured if intent status is not one of 'processing', 'requires_capture', or 'succeeded' - `wcpay_intent_order_mismatch` - Payment cannot be captured because the order id does not match - `wcpay_capture_error` - Unknown error +- `wcpay_capture_error_amount_too_small` - The payment cannot be captured because the amount is too small ### HTTP request From d0d056ac0b5271d8bfa12c6ceff1750c494c8780 Mon Sep 17 00:00:00 2001 From: Miguel Gasca Date: Thu, 9 Jan 2025 17:50:43 +0100 Subject: [PATCH 07/12] Add changelog --- ...error-response-for-the-payments-below-min-supported-amount | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelog/add-10092-add-specific-type-and-min-supported-amount-in-error-response-for-the-payments-below-min-supported-amount diff --git a/changelog/add-10092-add-specific-type-and-min-supported-amount-in-error-response-for-the-payments-below-min-supported-amount b/changelog/add-10092-add-specific-type-and-min-supported-amount-in-error-response-for-the-payments-below-min-supported-amount new file mode 100644 index 00000000000..671ad26ad97 --- /dev/null +++ b/changelog/add-10092-add-specific-type-and-min-supported-amount-in-error-response-for-the-payments-below-min-supported-amount @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Add wcpay_capture_error_amount_too_small error type and minimum amount details when capturing payments below the supported threshold. From 01795d0ee816fb8d5314bcd1dbb6ff73e15b97d8 Mon Sep 17 00:00:00 2001 From: Miguel Gasca Date: Fri, 10 Jan 2025 13:21:18 +0100 Subject: [PATCH 08/12] PR feedback: do not use term capture --- client/data/authorizations/actions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/data/authorizations/actions.ts b/client/data/authorizations/actions.ts index 84c274ff17a..096f375ee7f 100644 --- a/client/data/authorizations/actions.ts +++ b/client/data/authorizations/actions.ts @@ -46,7 +46,7 @@ const getErrorMessage = ( apiError: WCPayError ): string => { return sprintf( /* translators: %1$s: minimum amount, %2$s: currency code */ __( - 'The minimum amount to capture is %1$s %2$s.', + 'The minimum amount that can be processed is %1$s %2$s.', 'woocommerce-payments' ), amount, From 7cf9528e21015eba0993939b19b19f350c9670c6 Mon Sep 17 00:00:00 2001 From: Miguel Gasca Date: Fri, 10 Jan 2025 13:29:50 +0100 Subject: [PATCH 09/12] PR feedback: fallback error message when amount details are not present --- client/data/authorizations/actions.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/client/data/authorizations/actions.ts b/client/data/authorizations/actions.ts index 096f375ee7f..67f7dda18de 100644 --- a/client/data/authorizations/actions.ts +++ b/client/data/authorizations/actions.ts @@ -35,11 +35,19 @@ interface WCPayError { const getErrorMessage = ( apiError: WCPayError ): string => { // Map specific error codes to user-friendly messages const getAmountTooSmallError = ( error: WCPayError ): string => { - const currency = - error.data?.extra_details?.minimum_amount_currency ?? 'USD'; + if ( + ! error.data?.extra_details?.minimum_amount || + ! error.data?.extra_details?.minimum_amount_currency + ) { + return __( + 'The payment amount is too small to be processed.', + 'woocommerce-payments' + ); + } + const currency = error.data.extra_details.minimum_amount_currency; const amount = formatCurrency( - error.data?.extra_details?.minimum_amount ?? 0, + error.data.extra_details.minimum_amount, currency ); From 282d4e1da6eb3578ffd6fbd91703cf699ab33ab5 Mon Sep 17 00:00:00 2001 From: Miguel Gasca Date: Fri, 10 Jan 2025 13:34:17 +0100 Subject: [PATCH 10/12] Fix tests --- .../data/authorizations/test/actions.test.ts | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/client/data/authorizations/test/actions.test.ts b/client/data/authorizations/test/actions.test.ts index 363d80b2c7e..a9c7c35605e 100644 --- a/client/data/authorizations/test/actions.test.ts +++ b/client/data/authorizations/test/actions.test.ts @@ -286,7 +286,31 @@ describe( 'Authorizations actions', () => { controls.dispatch( 'core/notices', 'createErrorNotice', - 'There has been an error capturing the payment for order #123. The minimum amount to capture is $0.50 USD.' + 'There has been an error capturing the payment for order #123. The minimum amount that can be processed is $0.50 USD.' + ) + ); + } ); + + it( 'should create error notice with amount too small when amount details are missing', () => { + const generator = submitCaptureAuthorization( 'pi_123', 123 ); + + // Skip initial dispatch calls + generator.next(); + generator.next(); + + // Mock API error for amount too small + const apiError = { + code: 'wcpay_capture_error_amount_too_small', + data: { + status: 400, + }, + }; + + expect( generator.throw( apiError ).value ).toEqual( + controls.dispatch( + 'core/notices', + 'createErrorNotice', + 'There has been an error capturing the payment for order #123. The payment amount is too small to be processed.' ) ); } ); From 8691f1e1536f576441250a1ebe6b3a9ba30dc923 Mon Sep 17 00:00:00 2001 From: Miguel Gasca Date: Mon, 13 Jan 2025 13:09:09 +0100 Subject: [PATCH 11/12] PR feedback: return currency in uppercase to comply with standard --- includes/class-wc-payment-gateway-wcpay.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index bd019a063d4..ecd5a46dd34 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -3384,7 +3384,7 @@ public function capture_charge( $order, $include_level3 = true, $intent_metadata if ( $e instanceof Amount_Too_Small_Exception ) { $extra_details = [ 'minimum_amount' => $e->get_minimum_amount(), - 'minimum_amount_currency' => $e->get_currency(), + 'minimum_amount_currency' => strtoupper( $e->get_currency() ), ]; $minimum_amount_details = sprintf( /* translators: %1$s: minimum amount, %2$s: currency */ From 654f64f6a3f750547c2debdf8129db00561a5537 Mon Sep 17 00:00:00 2001 From: Miguel Gasca Date: Mon, 13 Jan 2025 13:56:22 +0100 Subject: [PATCH 12/12] PR feedback: Add error type as metadata --- .../admin/class-wc-rest-payments-orders-controller.php | 6 ++++-- .../test-class-wc-rest-payments-orders-controller.php | 9 +++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/includes/admin/class-wc-rest-payments-orders-controller.php b/includes/admin/class-wc-rest-payments-orders-controller.php index c33c670e3b6..e56961eb514 100644 --- a/includes/admin/class-wc-rest-payments-orders-controller.php +++ b/includes/admin/class-wc-rest-payments-orders-controller.php @@ -208,7 +208,7 @@ public function capture_terminal_payment( WP_REST_Request $request ) { $error_code = $result['error_code'] ?? null; $extra_details = $result['extra_details'] ?? []; return new WP_Error( - 'amount_too_small' === $error_code ? 'wcpay_capture_error_amount_too_small' : 'wcpay_capture_error', + 'wcpay_capture_error', sprintf( // translators: %s: the error message. __( 'Payment capture failed to complete with the following message: %s', 'woocommerce-payments' ), @@ -217,6 +217,7 @@ public function capture_terminal_payment( WP_REST_Request $request ) { [ 'status' => $http_code, 'extra_details' => $extra_details, + 'error_type' => $error_code, ] ); } @@ -312,7 +313,7 @@ public function capture_authorization( WP_REST_Request $request ) { $error_code = $result['error_code'] ?? null; $extra_details = $result['extra_details'] ?? []; return new WP_Error( - 'amount_too_small' === $error_code ? 'wcpay_capture_error_amount_too_small' : 'wcpay_capture_error', + 'wcpay_capture_error', sprintf( // translators: %s: the error message. __( 'Payment capture failed to complete with the following message: %s', 'woocommerce-payments' ), @@ -321,6 +322,7 @@ public function capture_authorization( WP_REST_Request $request ) { [ 'status' => $result['http_code'] ?? 502, 'extra_details' => $extra_details, + 'error_type' => $error_code, ] ); } 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 fd2b18d57c2..cd90665d850 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 @@ -2043,10 +2043,11 @@ public function test_capture_terminal_payment_error_amount_too_small() { $response = $this->controller->capture_terminal_payment( $request ); $this->assertInstanceOf( 'WP_Error', $response ); - $this->assertEquals( 'wcpay_capture_error_amount_too_small', $response->get_error_code() ); + $this->assertSame( 'wcpay_capture_error', $response->get_error_code() ); $this->assertStringContainsString( 'Payment capture failed to complete', $response->get_error_message() ); - $this->assertEquals( 400, $response->get_error_data()['status'] ); - $this->assertEquals( 50, $response->get_error_data()['extra_details']['minimum_amount'] ); - $this->assertEquals( 'USD', $response->get_error_data()['extra_details']['minimum_amount_currency'] ); + $this->assertSame( 400, $response->get_error_data()['status'] ); + $this->assertSame( 50, $response->get_error_data()['extra_details']['minimum_amount'] ); + $this->assertSame( 'USD', $response->get_error_data()['extra_details']['minimum_amount_currency'] ); + $this->assertSame( 'amount_too_small', $response->get_error_data()['error_type'] ); } }