From f1a21c81e18ae6a3ef8ba6439b74ace7d6261df4 Mon Sep 17 00:00:00 2001 From: Radoslav Georgiev Date: Thu, 19 Dec 2024 09:06:59 +0200 Subject: [PATCH 1/8] Fix rounding problems with Level 3 data (#9991) --- changelog/fix-9114-level3-rounding | 4 ++ src/Internal/Service/Level3Service.php | 37 +++++++++++++++---- .../Internal/Service/Level3ServiceTest.php | 23 ++++++++++++ 3 files changed, 57 insertions(+), 7 deletions(-) create mode 100644 changelog/fix-9114-level3-rounding diff --git a/changelog/fix-9114-level3-rounding b/changelog/fix-9114-level3-rounding new file mode 100644 index 00000000000..713c8d684cc --- /dev/null +++ b/changelog/fix-9114-level3-rounding @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Add a rounding entry to Level 3 data for rare cases where rounding errors break calculations. diff --git a/src/Internal/Service/Level3Service.php b/src/Internal/Service/Level3Service.php index 67db748debe..b75f3dd1271 100644 --- a/src/Internal/Service/Level3Service.php +++ b/src/Internal/Service/Level3Service.php @@ -80,10 +80,10 @@ public function get_data_from_order( int $order_id ): array { $order_items = array_values( $order->get_items( [ 'line_item', 'fee' ] ) ); $currency = $order->get_currency(); - $process_item = function ( $item ) use ( $currency ) { - return $this->process_item( $item, $currency ); - }; - $items_to_send = array_map( $process_item, $order_items ); + $items_to_send = []; + foreach ( $order_items as $item ) { + $items_to_send = array_merge( $items_to_send, $this->process_item( $item, $currency ) ); + } $level3_data = [ 'merchant_reference' => (string) $order->get_id(), // An alphanumeric string of up to characters in length. This unique value is assigned by the merchant to identify the order. Also known as an “Order ID”. @@ -137,9 +137,9 @@ public function get_data_from_order( int $order_id ): array { * * @param WC_Order_Item_Product|WC_Order_Item_Fee $item Item to process. * @param string $currency Currency to use. - * @return \stdClass + * @return \stdClass[] */ - private function process_item( WC_Order_Item $item, string $currency ): stdClass { + private function process_item( WC_Order_Item $item, string $currency ): array { // Check to see if it is a WC_Order_Item_Product or a WC_Order_Item_Fee. if ( $item instanceof WC_Order_Item_Product ) { $subtotal = $item->get_subtotal(); @@ -164,7 +164,7 @@ private function process_item( WC_Order_Item $item, string $currency ): stdClass $unit_cost = 0; } - return (object) [ + $line_item = (object) [ 'product_code' => (string) $product_code, // Up to 12 characters that uniquely identify the product. 'product_description' => $description, // Up to 26 characters long describing the product. 'unit_cost' => $unit_cost, // Cost of the product, in cents, as a non-negative integer. @@ -172,6 +172,29 @@ private function process_item( WC_Order_Item $item, string $currency ): stdClass 'tax_amount' => $tax_amount, // The amount of tax this item had added to it, in cents, as a non-negative integer. 'discount_amount' => $discount_amount, // The amount an item was discounted—if there was a sale,for example, as a non-negative integer. ]; + $line_items = [ $line_item ]; + + /** + * In edge cases, rounding after division might lead to a slight inconsistency. + * + * For example: 10/3 with 2 decimal places = 3.33, but 3.33*3 = 9.99. + */ + if ( $subtotal > 0 ) { + $prepared_subtotal = $this->prepare_amount( $subtotal, $currency ); + $difference = $prepared_subtotal - ( $unit_cost * $quantity ); + if ( $difference > 0 ) { + $line_items[] = (object) [ + 'product_code' => 'rounding-fix', + 'product_description' => __( 'Rounding fix', 'woocommerce-payments' ), + 'unit_cost' => $difference, + 'quantity' => 1, + 'tax_amount' => 0, + 'discount_amount' => 0, + ]; + } + } + + return $line_items; } /** diff --git a/tests/unit/src/Internal/Service/Level3ServiceTest.php b/tests/unit/src/Internal/Service/Level3ServiceTest.php index fe3eee573c3..dba9766386d 100644 --- a/tests/unit/src/Internal/Service/Level3ServiceTest.php +++ b/tests/unit/src/Internal/Service/Level3ServiceTest.php @@ -167,6 +167,10 @@ protected function mock_level_3_order( $mock_items = array_merge( $mock_items, array_fill( 0, $basket_size - count( $mock_items ), $mock_items[0] ) ); } + $this->mock_order( $mock_items, $shipping_postcode ); + } + + protected function mock_order( array $mock_items, string $shipping_postcode ) { // Setup the order. $mock_order = $this ->getMockBuilder( WC_Order::class ) @@ -434,6 +438,25 @@ public function test_full_level3_data_with_float_quantity() { $this->assertEquals( $expected_data, $level_3_data ); } + public function test_rounding_in_edge_cases() { + $this->mock_account->method( 'get_account_country' )->willReturn( Country_Code::UNITED_STATES ); + + $mock_items = []; + $mock_items[] = $this->create_mock_item( 'Beanie with Addon', 3, 73, 0, 30 ); + $this->mock_order( $mock_items, '98012' ); + + $level_3_data = $this->sut->get_data_from_order( $this->order_id ); + + $this->assertCount( 2, $level_3_data['line_items'] ); + $this->assertEquals( 2433, $level_3_data['line_items'][0]->unit_cost ); + $this->assertEquals( 'rounding-fix', $level_3_data['line_items'][1]->product_code ); + $this->assertEquals( 'Rounding fix', $level_3_data['line_items'][1]->product_description ); + $this->assertEquals( 1, $level_3_data['line_items'][1]->unit_cost ); + $this->assertEquals( 1, $level_3_data['line_items'][1]->quantity ); + $this->assertEquals( 0, $level_3_data['line_items'][1]->tax_amount ); + $this->assertEquals( 0, $level_3_data['line_items'][1]->discount_amount ); + } + public function test_full_level3_data_with_float_quantity_zero() { $expected_data = [ 'merchant_reference' => '210', From 7b6e7efa8aac9cde89a8399a158561173497560e Mon Sep 17 00:00:00 2001 From: Zvonimir Maglica Date: Thu, 19 Dec 2024 09:21:00 +0100 Subject: [PATCH 2/8] Remove hooks from customer service constructor (#9992) --- .../dev-7264-remove-hooks-from-customer-service-constructor | 4 ++++ dev/phpcs/WCPay/ruleset.xml | 4 ---- includes/class-wc-payments-customer-service.php | 5 +++++ includes/class-wc-payments-token-service.php | 5 +++++ includes/class-wc-payments.php | 2 ++ 5 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 changelog/dev-7264-remove-hooks-from-customer-service-constructor diff --git a/changelog/dev-7264-remove-hooks-from-customer-service-constructor b/changelog/dev-7264-remove-hooks-from-customer-service-constructor new file mode 100644 index 00000000000..d912717fc31 --- /dev/null +++ b/changelog/dev-7264-remove-hooks-from-customer-service-constructor @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Remove hooks from customer and token services to dedicated methods diff --git a/dev/phpcs/WCPay/ruleset.xml b/dev/phpcs/WCPay/ruleset.xml index 9806ccfe9e7..7c8cefbd0e3 100644 --- a/dev/phpcs/WCPay/ruleset.xml +++ b/dev/phpcs/WCPay/ruleset.xml @@ -17,10 +17,6 @@ */includes/class-wc-payments-order-success-page.php - - */includes/class-wc-payments-customer-service.php - */includes/class-wc-payments-token-service.php - */includes/class-wc-payments-webhook-reliability-service.php diff --git a/includes/class-wc-payments-customer-service.php b/includes/class-wc-payments-customer-service.php index 05f95c32d31..d0f97e061c0 100644 --- a/includes/class-wc-payments-customer-service.php +++ b/includes/class-wc-payments-customer-service.php @@ -99,7 +99,12 @@ public function __construct( $this->database_cache = $database_cache; $this->session_service = $session_service; $this->order_service = $order_service; + } + /** + * Initialize hooks + */ + public function init_hooks() { /* * Adds the WooCommerce Payments customer ID found in the user session * to the WordPress user as metadata. diff --git a/includes/class-wc-payments-token-service.php b/includes/class-wc-payments-token-service.php index 7bfdc482e18..283a0d7851a 100644 --- a/includes/class-wc-payments-token-service.php +++ b/includes/class-wc-payments-token-service.php @@ -47,7 +47,12 @@ class WC_Payments_Token_Service { public function __construct( WC_Payments_API_Client $payments_api_client, WC_Payments_Customer_Service $customer_service ) { $this->payments_api_client = $payments_api_client; $this->customer_service = $customer_service; + } + /** + * Initializes hooks. + */ + public function init_hooks() { add_action( 'woocommerce_payment_token_deleted', [ $this, 'woocommerce_payment_token_deleted' ], 10, 2 ); add_action( 'woocommerce_payment_token_set_default', [ $this, 'woocommerce_payment_token_set_default' ], 10, 2 ); add_filter( 'woocommerce_get_customer_payment_tokens', [ $this, 'woocommerce_get_customer_payment_tokens' ], 10, 3 ); diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php index 17300478794..32c75df5fc7 100644 --- a/includes/class-wc-payments.php +++ b/includes/class-wc-payments.php @@ -554,6 +554,8 @@ public static function init() { self::$onboarding_service->init_hooks(); self::$incentives_service->init_hooks(); self::$compatibility_service->init_hooks(); + self::$customer_service->init_hooks(); + self::$token_service->init_hooks(); $payment_method_classes = [ CC_Payment_Method::class, From 5005b6bcc01664f1df277dac599c111227b424ad Mon Sep 17 00:00:00 2001 From: Dat Hoang Date: Thu, 19 Dec 2024 15:58:18 +0700 Subject: [PATCH 3/8] Add seller_message to failed order notes (#9934) --- changelog/feat-9810-add-seller-message | 4 ++ includes/class-wc-payment-gateway-wcpay.php | 17 ++++++- includes/class-wc-payments.php | 1 + ...wc-payment-gateway-wcpay-subscriptions.php | 8 ++- .../class-api-merchant-exception.php | 49 +++++++++++++++++++ .../class-wc-payments-api-client.php | 8 +++ .../test-class-wc-payments-api-client.php | 20 ++++++++ 7 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 changelog/feat-9810-add-seller-message create mode 100644 includes/exceptions/class-api-merchant-exception.php diff --git a/changelog/feat-9810-add-seller-message b/changelog/feat-9810-add-seller-message new file mode 100644 index 00000000000..2669c24015b --- /dev/null +++ b/changelog/feat-9810-add-seller-message @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add seller_message to failed order notes diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index 4f579caf2b1..d1be21241b9 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -20,7 +20,19 @@ use WCPay\Constants\Intent_Status; use WCPay\Constants\Payment_Type; use WCPay\Constants\Payment_Method; -use WCPay\Exceptions\{ Add_Payment_Method_Exception, Amount_Too_Small_Exception, Process_Payment_Exception, Intent_Authentication_Exception, API_Exception, Invalid_Address_Exception, Fraud_Prevention_Enabled_Exception, Invalid_Phone_Number_Exception, Rate_Limiter_Enabled_Exception, Order_ID_Mismatch_Exception, Order_Not_Found_Exception, New_Process_Payment_Exception }; +use WCPay\Exceptions\{Add_Payment_Method_Exception, + Amount_Too_Small_Exception, + API_Merchant_Exception, + Process_Payment_Exception, + Intent_Authentication_Exception, + API_Exception, + Invalid_Address_Exception, + Fraud_Prevention_Enabled_Exception, + Invalid_Phone_Number_Exception, + Rate_Limiter_Enabled_Exception, + Order_ID_Mismatch_Exception, + Order_Not_Found_Exception, + New_Process_Payment_Exception}; use WCPay\Core\Server\Request\Cancel_Intention; use WCPay\Core\Server\Request\Capture_Intention; use WCPay\Core\Server\Request\Create_And_Confirm_Intention; @@ -1270,6 +1282,9 @@ public function process_payment( $order_id ) { ); $error_details = esc_html( rtrim( $e->getMessage(), '.' ) ); + if ( $e instanceof API_Merchant_Exception ) { + $error_details = $error_details . '. ' . esc_html( rtrim( $e->get_merchant_message(), '.' ) ); + } if ( $e instanceof API_Exception && 'card_error' === $e->get_error_type() ) { // If the payment failed with a 'card_error' API exception, initialize the fraud meta box diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php index 32c75df5fc7..4ad2d32625e 100644 --- a/includes/class-wc-payments.php +++ b/includes/class-wc-payments.php @@ -354,6 +354,7 @@ public static function init() { include_once __DIR__ . '/exceptions/class-base-exception.php'; include_once __DIR__ . '/exceptions/class-api-exception.php'; + include_once __DIR__ . '/exceptions/class-api-merchant-exception.php'; include_once __DIR__ . '/exceptions/class-connection-exception.php'; include_once __DIR__ . '/core/class-mode.php'; diff --git a/includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php b/includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php index 31ec70bedf8..d2584f9b824 100644 --- a/includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php +++ b/includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php @@ -11,6 +11,7 @@ use WCPay\Core\Server\Request\Get_Intention; use WCPay\Exceptions\API_Exception; +use WCPay\Exceptions\API_Merchant_Exception; use WCPay\Exceptions\Invalid_Payment_Method_Exception; use WCPay\Exceptions\Add_Payment_Method_Exception; use WCPay\Exceptions\Order_Not_Found_Exception; @@ -342,6 +343,11 @@ public function scheduled_subscription_payment( $amount, $renewal_order ) { $renewal_order->update_status( 'failed' ); if ( ! empty( $payment_information ) ) { + $error_details = esc_html( rtrim( $e->getMessage(), '.' ) ); + if ( $e instanceof API_Merchant_Exception ) { + $error_details = $error_details . '. ' . esc_html( rtrim( $e->get_merchant_message(), '.' ) ); + } + $note = sprintf( WC_Payments_Utils::esc_interpolated_html( /* translators: %1: the failed payment amount, %2: error message */ @@ -358,7 +364,7 @@ public function scheduled_subscription_payment( $amount, $renewal_order ) { wc_price( $amount, [ 'currency' => WC_Payments_Utils::get_order_intent_currency( $renewal_order ) ] ), $renewal_order ), - esc_html( rtrim( $e->getMessage(), '.' ) ) + $error_details ); $renewal_order->add_order_note( $note ); } diff --git a/includes/exceptions/class-api-merchant-exception.php b/includes/exceptions/class-api-merchant-exception.php new file mode 100644 index 00000000000..ac10bd271bc --- /dev/null +++ b/includes/exceptions/class-api-merchant-exception.php @@ -0,0 +1,49 @@ +merchant_message = $merchant_message; + + parent::__construct( $message, $error_code, $http_code, $error_type, $decline_code, $code, $previous ); + } + + /** + * Returns the merchant message. + * + * @return string Merchant message. + */ + public function get_merchant_message(): string { + return $this->merchant_message; + } +} diff --git a/includes/wc-payment-api/class-wc-payments-api-client.php b/includes/wc-payment-api/class-wc-payments-api-client.php index b3adf5bf7eb..e90094d57de 100644 --- a/includes/wc-payment-api/class-wc-payments-api-client.php +++ b/includes/wc-payment-api/class-wc-payments-api-client.php @@ -9,6 +9,7 @@ use WCPay\Constants\Intent_Status; use WCPay\Exceptions\API_Exception; +use WCPay\Exceptions\API_Merchant_Exception; use WCPay\Exceptions\Amount_Too_Small_Exception; use WCPay\Exceptions\Amount_Too_Large_Exception; use WCPay\Exceptions\Connection_Exception; @@ -2419,6 +2420,13 @@ protected function check_response_for_errors( $response ) { ); Logger::error( "$error_message ($error_code)" ); + + if ( 'card_declined' === $error_code && isset( $response_body['error']['payment_intent']['charges']['data'][0]['outcome']['seller_message'] ) ) { + $merchant_message = $response_body['error']['payment_intent']['charges']['data'][0]['outcome']['seller_message']; + + throw new API_Merchant_Exception( $message, $error_code, $response_code, $merchant_message, $error_type, $decline_code ); + } + throw new API_Exception( $message, $error_code, $response_code, $error_type, $decline_code ); } } diff --git a/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php b/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php index 3fc4a56c8f6..fb95bcf1591 100644 --- a/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php +++ b/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php @@ -7,7 +7,9 @@ use WCPay\Constants\Country_Code; use WCPay\Constants\Intent_Status; +use WCPay\Core\Server\Request\Create_And_Confirm_Intention; use WCPay\Exceptions\API_Exception; +use WCPay\Exceptions\API_Merchant_Exception; use WCPay\Internal\Logger; use WCPay\Exceptions\Connection_Exception; use WCPay\Fraud_Prevention\Fraud_Prevention_Service; @@ -1195,6 +1197,24 @@ public function test_get_tracking_info() { $this->assertEquals( $expect, $result ); } + public function test_throws_api_merchant_exception() { + $mock_response = []; + $mock_response['error']['code'] = 'card_declined'; + $mock_response['error']['payment_intent']['charges']['data'][0]['outcome']['seller_message'] = 'Bank declined'; + $this->set_http_mock_response( + 401, + $mock_response + ); + + try { + // This is a dummy call to trigger the response so that our test can validate the exception. + $this->payments_api_client->create_subscription(); + } catch ( API_Merchant_Exception $e ) { + $this->assertSame( 'card_declined', $e->get_error_code() ); + $this->assertSame( 'Bank declined', $e->get_merchant_message() ); + } + } + /** * Set up http mock response. * From a64815337185fff3d801562c43401715e34de8f6 Mon Sep 17 00:00:00 2001 From: Jessy Pappachan <32092402+jessy-p@users.noreply.github.com> Date: Thu, 19 Dec 2024 16:10:20 +0530 Subject: [PATCH 4/8] Use the filter param in server-side disputes CSV export (#9988) Co-authored-by: Jessy Co-authored-by: Nagesh Pai --- changelog/fix-9987-filter-csv-disputes | 4 ++++ client/data/disputes/hooks.ts | 15 ++------------- client/data/disputes/resolvers.js | 7 +++++-- client/disputes/index.tsx | 2 ++ 4 files changed, 13 insertions(+), 15 deletions(-) create mode 100644 changelog/fix-9987-filter-csv-disputes diff --git a/changelog/fix-9987-filter-csv-disputes b/changelog/fix-9987-filter-csv-disputes new file mode 100644 index 00000000000..e4a87b24b1b --- /dev/null +++ b/changelog/fix-9987-filter-csv-disputes @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix filtering in async Disputes CSV export diff --git a/client/data/disputes/hooks.ts b/client/data/disputes/hooks.ts index b8a95b1e5e6..5db1c2c3c59 100644 --- a/client/data/disputes/hooks.ts +++ b/client/data/disputes/hooks.ts @@ -17,7 +17,6 @@ import type { } from 'wcpay/types/disputes'; import type { ApiError } from 'wcpay/types/errors'; import { STORE_NAME } from '../constants'; -import { disputeAwaitingResponseStatuses } from 'wcpay/disputes/filters/config'; /** * Returns the dispute object, error object, and loading state. @@ -98,11 +97,6 @@ export const useDisputes = ( { ( select ) => { const { getDisputes, isResolving } = select( STORE_NAME ); - const search = - filter === 'awaiting_response' - ? disputeAwaitingResponseStatuses - : undefined; - const query = { paged: Number.isNaN( parseInt( paged ?? '', 10 ) ) ? '1' @@ -119,7 +113,7 @@ export const useDisputes = ( { dateBetween.sort( ( a, b ) => moment( a ).diff( moment( b ) ) ), - search, + filter, statusIs, statusIsNot, orderBy: orderBy || 'created', @@ -163,11 +157,6 @@ export const useDisputesSummary = ( { ( select ) => { const { getDisputesSummary, isResolving } = select( STORE_NAME ); - const search = - filter === 'awaiting_response' - ? disputeAwaitingResponseStatuses - : undefined; - const query = { paged: Number.isNaN( parseInt( paged ?? '', 10 ) ) ? '1' @@ -180,7 +169,7 @@ export const useDisputesSummary = ( { dateBefore, dateAfter, dateBetween, - search, + filter, statusIs, statusIsNot, }; diff --git a/client/data/disputes/resolvers.js b/client/data/disputes/resolvers.js index bf45770537c..ce748a46562 100644 --- a/client/data/disputes/resolvers.js +++ b/client/data/disputes/resolvers.js @@ -20,6 +20,7 @@ import { updateDisputesSummary, updateErrorForDispute, } from './actions'; +import { disputeAwaitingResponseStatuses } from 'wcpay/disputes/filters/config'; const formatQueryFilters = ( query ) => ( { user_email: query.userEmail, @@ -31,7 +32,10 @@ const formatQueryFilters = ( query ) => ( { formatDateValue( query.dateBetween[ 0 ] ), formatDateValue( query.dateBetween[ 1 ], true ), ], - search: query.search, + search: + query.filter === 'awaiting_response' + ? disputeAwaitingResponseStatuses + : query.search, status_is: query.statusIs, status_is_not: query.statusIsNot, locale: query.locale, @@ -42,7 +46,6 @@ export function getDisputesCSV( query ) { `${ NAMESPACE }/disputes/download`, formatQueryFilters( query ) ); - return path; } diff --git a/client/disputes/index.tsx b/client/disputes/index.tsx index 060afccce35..cdb85131f5d 100644 --- a/client/disputes/index.tsx +++ b/client/disputes/index.tsx @@ -372,6 +372,7 @@ export const DisputesList = (): JSX.Element => { date_after: dateAfter, date_between: dateBetween, match, + filter, status_is: statusIs, status_is_not: statusIsNot, } = getQuery(); @@ -407,6 +408,7 @@ export const DisputesList = (): JSX.Element => { dateBefore, dateBetween, match, + filter, statusIs, statusIsNot, } ), From 771e3abe2a3145d32c493a43eb78d83df1d4831c Mon Sep 17 00:00:00 2001 From: Cvetan Cvetanov Date: Thu, 19 Dec 2024 12:45:15 +0200 Subject: [PATCH 5/8] Enhance Embedded Components and MOX to support custom width and paddings (#9995) Co-authored-by: Dan Paun <82826872+dpaun1985@users.noreply.github.com> --- changelog/update-9919-embedded-components-width | 4 ++++ client/onboarding/style.scss | 8 +++++--- 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 changelog/update-9919-embedded-components-width diff --git a/changelog/update-9919-embedded-components-width b/changelog/update-9919-embedded-components-width new file mode 100644 index 00000000000..ca8fe89ebb7 --- /dev/null +++ b/changelog/update-9919-embedded-components-width @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Update Embedded Components and MOX to support custom width and paddings. diff --git a/client/onboarding/style.scss b/client/onboarding/style.scss index 67a6351aab6..415849c1d06 100644 --- a/client/onboarding/style.scss +++ b/client/onboarding/style.scss @@ -85,10 +85,12 @@ body.wcpay-onboarding__body { } &__content { - max-width: 400px; + max-width: 615px; + width: 100%; - @media screen and ( min-width: $break-mobile ) { - width: 400px; + @media screen and ( max-width: $break-mobile ) { + width: 100%; + padding: 0 $gap; } } From e61393136fb409c73802b0ee68ce49ccdf6968e7 Mon Sep 17 00:00:00 2001 From: Zvonimir Maglica Date: Thu, 19 Dec 2024 15:46:50 +0100 Subject: [PATCH 6/8] Hide transaction fee on admin order view screen when transaction is not captured (#9997) --- ...418-hide-transaction-fees-when-transaction-is-not-captured | 4 ++++ includes/admin/class-wc-payments-admin.php | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changelog/fix-9418-hide-transaction-fees-when-transaction-is-not-captured diff --git a/changelog/fix-9418-hide-transaction-fees-when-transaction-is-not-captured b/changelog/fix-9418-hide-transaction-fees-when-transaction-is-not-captured new file mode 100644 index 00000000000..f524fd812f1 --- /dev/null +++ b/changelog/fix-9418-hide-transaction-fees-when-transaction-is-not-captured @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Hide transaction fee on admin view order screen when transaction is not captured. diff --git a/includes/admin/class-wc-payments-admin.php b/includes/admin/class-wc-payments-admin.php index e7ad01fe210..d78671d1298 100644 --- a/includes/admin/class-wc-payments-admin.php +++ b/includes/admin/class-wc-payments-admin.php @@ -6,6 +6,7 @@ */ use Automattic\Jetpack\Identity_Crisis as Jetpack_Identity_Crisis; +use WCPay\Constants\Intent_Status; use WCPay\Core\Server\Request; use WCPay\Database_Cache; use WCPay\Logger; @@ -1253,7 +1254,7 @@ public function show_woopay_payment_method_name_admin( $order_id ) { */ public function display_wcpay_transaction_fee( $order_id ) { $order = wc_get_order( $order_id ); - if ( ! $order || ! $order->get_meta( '_wcpay_transaction_fee' ) ) { + if ( ! $order || ! $order->get_meta( '_wcpay_transaction_fee' ) || Intent_Status::REQUIRES_CAPTURE === $order->get_meta( WC_Payments_Order_Service::INTENTION_STATUS_META_KEY ) ) { return; } ?> From 7b223259e070bf7ac54d0e9e0715567b013b42f9 Mon Sep 17 00:00:00 2001 From: Francesco Date: Thu, 19 Dec 2024 15:55:08 +0100 Subject: [PATCH 7/8] fix: tokenized ECE item compatibility w/ product bundles (#9994) --- .../fix-tokenized-ece-product-bundles-totals | 5 + .../__tests__/wc-product-bundles.test.js | 253 ++++++++++++++++++ .../compatibility/wc-product-bundles.js | 19 ++ client/tokenized-express-checkout/index.js | 1 + .../transformers/wc-to-stripe.js | 11 +- 5 files changed, 287 insertions(+), 2 deletions(-) create mode 100644 changelog/fix-tokenized-ece-product-bundles-totals create mode 100644 client/tokenized-express-checkout/compatibility/__tests__/wc-product-bundles.test.js create mode 100644 client/tokenized-express-checkout/compatibility/wc-product-bundles.js diff --git a/changelog/fix-tokenized-ece-product-bundles-totals b/changelog/fix-tokenized-ece-product-bundles-totals new file mode 100644 index 00000000000..c003feec46a --- /dev/null +++ b/changelog/fix-tokenized-ece-product-bundles-totals @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: fix: tokenized ECE item compatibility w/ product bundles + + diff --git a/client/tokenized-express-checkout/compatibility/__tests__/wc-product-bundles.test.js b/client/tokenized-express-checkout/compatibility/__tests__/wc-product-bundles.test.js new file mode 100644 index 00000000000..91009ab1a25 --- /dev/null +++ b/client/tokenized-express-checkout/compatibility/__tests__/wc-product-bundles.test.js @@ -0,0 +1,253 @@ +/** + * External dependencies + */ +import { applyFilters } from '@wordpress/hooks'; + +/** + * Internal dependencies + */ +import '../wc-product-bundles'; + +describe( 'ECE product bundles compatibility', () => { + it( 'filters out cart items that are bundled by something else', () => { + const cartData = applyFilters( + 'wcpay.express-checkout.map-line-items', + { + items: [ + { + key: 'd179a6924eafc82d7864f1e0caedbe95', + id: 261, + type: 'bundle', + quantity: 1, + item_data: [ + { + key: 'Includes', + value: 'T-Shirt × 1', + }, + { + key: 'Includes', + value: 'T-Shirt with Logo × 2', + }, + { + key: 'Includes', + value: 'V-Neck T-Shirt - Medium × 1', + }, + ], + extensions: { + bundles: { + bundled_items: [ + 'abda15f782e68dc63bd615d6a05fa3d2', + '4d16fa6ebc10a1d66013b0f85640eb2b', + 'ff279cc5574ef1cf45aa76bde0d66baa', + ], + bundle_data: { + configuration: { + '1': { + product_id: 13, + quantity: 1, + discount: 20, + optional_selected: 'yes', + }, + '2': { + product_id: 30, + quantity: 2, + discount: '', + }, + '3': { + product_id: 10, + quantity: 1, + discount: '', + attributes: { + attribute_size: 'Medium', + }, + variation_id: '25', + }, + '4': { + product_id: 10, + quantity: 0, + discount: '', + optional_selected: 'no', + attributes: [], + }, + }, + is_editable: false, + is_price_hidden: false, + is_subtotal_hidden: false, + is_hidden: false, + is_meta_hidden_in_cart: true, + is_meta_hidden_in_summary: false, + }, + }, + }, + }, + { + key: 'abda15f782e68dc63bd615d6a05fa3d2', + id: 13, + type: 'simple', + quantity: 1, + extensions: { + bundles: { + bundled_by: 'd179a6924eafc82d7864f1e0caedbe95', + bundled_item_data: { + bundle_id: 261, + bundled_item_id: 1, + is_removable: true, + is_indented: true, + is_subtotal_aggregated: true, + is_parent_visible: true, + is_last: false, + is_price_hidden: false, + is_subtotal_hidden: false, + is_thumbnail_hidden: false, + is_hidden_in_cart: false, + is_hidden_in_summary: true, + }, + }, + }, + }, + { + key: '4d16fa6ebc10a1d66013b0f85640eb2b', + id: 30, + type: 'simple', + quantity: 2, + extensions: { + bundles: { + bundled_by: 'd179a6924eafc82d7864f1e0caedbe95', + bundled_item_data: { + bundle_id: 261, + bundled_item_id: 2, + is_removable: false, + is_indented: true, + is_subtotal_aggregated: true, + is_parent_visible: true, + is_last: false, + is_price_hidden: true, + is_subtotal_hidden: true, + is_thumbnail_hidden: false, + is_hidden_in_cart: false, + is_hidden_in_summary: true, + }, + }, + }, + }, + { + key: 'ff279cc5574ef1cf45aa76bde0d66baa', + id: 25, + type: 'variation', + quantity: 1, + extensions: { + bundles: { + bundled_by: 'd179a6924eafc82d7864f1e0caedbe95', + bundled_item_data: { + bundle_id: 261, + bundled_item_id: 3, + is_removable: false, + is_indented: true, + is_subtotal_aggregated: true, + is_parent_visible: true, + is_last: true, + is_price_hidden: true, + is_subtotal_hidden: true, + is_thumbnail_hidden: false, + is_hidden_in_cart: false, + is_hidden_in_summary: true, + }, + }, + }, + }, + { + key: 'c51ce410c124a10e0db5e4b97fc2af39', + id: 13, + type: 'simple', + quantity: 1, + extensions: { + bundles: [], + }, + }, + ], + items_count: 2, + } + ); + + expect( cartData ).toStrictEqual( { + items: [ + { + extensions: { + bundles: { + bundle_data: { + configuration: { + '1': { + discount: 20, + optional_selected: 'yes', + product_id: 13, + quantity: 1, + }, + '2': { + discount: '', + product_id: 30, + quantity: 2, + }, + '3': { + attributes: { + attribute_size: 'Medium', + }, + discount: '', + product_id: 10, + quantity: 1, + variation_id: '25', + }, + '4': { + attributes: [], + discount: '', + optional_selected: 'no', + product_id: 10, + quantity: 0, + }, + }, + is_editable: false, + is_hidden: false, + is_meta_hidden_in_cart: true, + is_meta_hidden_in_summary: false, + is_price_hidden: false, + is_subtotal_hidden: false, + }, + bundled_items: [ + 'abda15f782e68dc63bd615d6a05fa3d2', + '4d16fa6ebc10a1d66013b0f85640eb2b', + 'ff279cc5574ef1cf45aa76bde0d66baa', + ], + }, + }, + id: 261, + item_data: [ + { + key: 'Includes', + value: 'T-Shirt × 1', + }, + { + key: 'Includes', + value: 'T-Shirt with Logo × 2', + }, + { + key: 'Includes', + value: 'V-Neck T-Shirt - Medium × 1', + }, + ], + key: 'd179a6924eafc82d7864f1e0caedbe95', + quantity: 1, + type: 'bundle', + }, + { + extensions: { + bundles: [], + }, + id: 13, + key: 'c51ce410c124a10e0db5e4b97fc2af39', + quantity: 1, + type: 'simple', + }, + ], + items_count: 2, + } ); + } ); +} ); diff --git a/client/tokenized-express-checkout/compatibility/wc-product-bundles.js b/client/tokenized-express-checkout/compatibility/wc-product-bundles.js new file mode 100644 index 00000000000..7a3d2a4dc3c --- /dev/null +++ b/client/tokenized-express-checkout/compatibility/wc-product-bundles.js @@ -0,0 +1,19 @@ +/** + * External dependencies + */ +import { addFilter } from '@wordpress/hooks'; + +addFilter( + 'wcpay.express-checkout.map-line-items', + 'automattic/wcpay/express-checkout', + ( cartData ) => { + return { + ...cartData, + // ensuring that the items that are bundled by another don't appear in the summary. + // otherwise they might be contributing to the wrong order total, creating errors. + items: cartData.items.filter( + ( item ) => ! item.extensions?.bundles?.bundled_by + ), + }; + } +); diff --git a/client/tokenized-express-checkout/index.js b/client/tokenized-express-checkout/index.js index 3b2eb1a8b46..e6c24d02e91 100644 --- a/client/tokenized-express-checkout/index.js +++ b/client/tokenized-express-checkout/index.js @@ -13,6 +13,7 @@ import '../checkout/express-checkout-buttons.scss'; import './compatibility/wc-deposits'; import './compatibility/wc-order-attribution'; import './compatibility/wc-product-page'; +import './compatibility/wc-product-bundles'; import { getExpressCheckoutButtonAppearance, getExpressCheckoutButtonStyleSettings, diff --git a/client/tokenized-express-checkout/transformers/wc-to-stripe.js b/client/tokenized-express-checkout/transformers/wc-to-stripe.js index 6d9b39035ce..794fc5309b1 100644 --- a/client/tokenized-express-checkout/transformers/wc-to-stripe.js +++ b/client/tokenized-express-checkout/transformers/wc-to-stripe.js @@ -8,6 +8,7 @@ import { decodeEntities } from '@wordpress/html-entities'; * Internal dependencies */ import { getExpressCheckoutData } from '../utils'; +import { applyFilters } from '@wordpress/hooks'; /** * GooglePay/ApplePay expect the prices to be formatted in cents. @@ -34,10 +35,16 @@ export const transformPrice = ( price, priceObject ) => { * - https://docs.stripe.com/js/elements_object/express_checkout_element_shippingaddresschange_event * - https://docs.stripe.com/js/elements_object/express_checkout_element_shippingratechange_event * - * @param {Object} cartData Store API Cart response object. + * @param {Object} rawCartData Store API Cart response object. * @return {{pending: boolean, name: string, amount: integer}} `displayItems` for Stripe. */ -export const transformCartDataForDisplayItems = ( cartData ) => { +export const transformCartDataForDisplayItems = ( rawCartData ) => { + // allowing extensions to manipulate the individual items returned by the backend. + const cartData = applyFilters( + 'wcpay.express-checkout.map-line-items', + rawCartData + ); + const displayItems = cartData.items.map( ( item ) => ( { amount: transformPrice( parseInt( item.totals?.line_subtotal || item.prices.price, 10 ), From 863519dde8a28374fcf6d231f2d21161bd34edd9 Mon Sep 17 00:00:00 2001 From: Adam Heckler <5512652+aheckler@users.noreply.github.com> Date: Thu, 19 Dec 2024 10:49:37 -0500 Subject: [PATCH 8/8] Use "currency conversion fee" (#10002) Co-authored-by: Nagesh Pai --- changelog/fix-9996-currency-conversion-fee-phrasing | 4 ++++ client/payment-details/timeline/map-events.js | 4 ++-- .../timeline/test/__snapshots__/map-events.js.snap | 2 +- client/utils/account-fees.tsx | 2 +- client/utils/test/__snapshots__/account-fees.tsx.snap | 4 ++-- includes/class-wc-payments-captured-event-note.php | 4 ++-- tests/fixtures/captured-payments/discount.json | 2 +- tests/fixtures/captured-payments/foreign-card.json | 2 +- tests/fixtures/captured-payments/fx-decimal.json | 2 +- tests/fixtures/captured-payments/fx-partial-capture.json | 2 +- tests/fixtures/captured-payments/fx-with-capped-fee.json | 2 +- tests/fixtures/captured-payments/fx.json | 2 +- tests/fixtures/captured-payments/jpy-payment.json | 2 +- tests/fixtures/captured-payments/subscription.json | 2 +- 14 files changed, 20 insertions(+), 16 deletions(-) create mode 100644 changelog/fix-9996-currency-conversion-fee-phrasing diff --git a/changelog/fix-9996-currency-conversion-fee-phrasing b/changelog/fix-9996-currency-conversion-fee-phrasing new file mode 100644 index 00000000000..bdee2cbc00f --- /dev/null +++ b/changelog/fix-9996-currency-conversion-fee-phrasing @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Use "currency conversion fee" instead "foreign exchange fee" in payment timeline and various other places. diff --git a/client/payment-details/timeline/map-events.js b/client/payment-details/timeline/map-events.js index b76f659ac63..7e5570ce8ce 100644 --- a/client/payment-details/timeline/map-events.js +++ b/client/payment-details/timeline/map-events.js @@ -410,12 +410,12 @@ export const feeBreakdown = ( event ) => { fixedRate !== 0 ? __( /* translators: %1$s% is the fee percentage and %2$s is the fixed rate */ - 'Foreign exchange fee: %1$s%% + %2$s', + 'Currency conversion fee: %1$s%% + %2$s', 'woocommerce-payments' ) : __( /* translators: %1$s% is the fee percentage */ - 'Foreign exchange fee: %1$s%%', + 'Currency conversion fee: %1$s%%', 'woocommerce-payments' ), 'additional-wcpay-subscription': diff --git a/client/payment-details/timeline/test/__snapshots__/map-events.js.snap b/client/payment-details/timeline/test/__snapshots__/map-events.js.snap index 4126cb1ff38..b229ba674be 100644 --- a/client/payment-details/timeline/test/__snapshots__/map-events.js.snap +++ b/client/payment-details/timeline/test/__snapshots__/map-events.js.snap @@ -397,7 +397,7 @@ exports[`mapTimelineEvents single currency events formats captured events with f International card fee: 1.5%
  • - Foreign exchange fee: 2% + Currency conversion fee: 2%
  • Discount diff --git a/client/utils/account-fees.tsx b/client/utils/account-fees.tsx index 711d3d337ed..a1c8056f78d 100644 --- a/client/utils/account-fees.tsx +++ b/client/utils/account-fees.tsx @@ -166,7 +166,7 @@ export const formatMethodFeesTooltip = ( ) } { hasFees( accountFees.fx ) ? (
    -
    Foreign exchange fee
    +
    Currency conversion fee
    { getFeeDescriptionString( accountFees.fx ) }
    ) : ( diff --git a/client/utils/test/__snapshots__/account-fees.tsx.snap b/client/utils/test/__snapshots__/account-fees.tsx.snap index 89321bc7582..d92aa6ae54e 100644 --- a/client/utils/test/__snapshots__/account-fees.tsx.snap +++ b/client/utils/test/__snapshots__/account-fees.tsx.snap @@ -23,7 +23,7 @@ exports[`Account fees utility functions formatMethodFeesTooltip() displays base
    - Foreign exchange fee + Currency conversion fee
    1% @@ -102,7 +102,7 @@ exports[`Account fees utility functions formatMethodFeesTooltip() displays base
    - Foreign exchange fee + Currency conversion fee
    1% diff --git a/includes/class-wc-payments-captured-event-note.php b/includes/class-wc-payments-captured-event-note.php index 10c48567952..07e902d8632 100644 --- a/includes/class-wc-payments-captured-event-note.php +++ b/includes/class-wc-payments-captured-event-note.php @@ -327,9 +327,9 @@ private function fee_label_mapping( int $fixed_rate, bool $is_capped ) { $res['additional-fx'] = 0 !== $fixed_rate /* translators: %1$s% is the fee percentage and %2$s is the fixed rate */ - ? __( 'Foreign exchange fee: %1$s%% + %2$s', 'woocommerce-payments' ) + ? __( 'Currency conversion fee: %1$s%% + %2$s', 'woocommerce-payments' ) /* translators: %1$s% is the fee percentage */ - : __( 'Foreign exchange fee: %1$s%%', 'woocommerce-payments' ); + : __( 'Currency conversion fee: %1$s%%', 'woocommerce-payments' ); $res['additional-wcpay-subscription'] = 0 !== $fixed_rate /* translators: %1$s% is the fee percentage and %2$s is the fixed rate */ diff --git a/tests/fixtures/captured-payments/discount.json b/tests/fixtures/captured-payments/discount.json index 2fa6a911d74..5bf6f936c45 100644 --- a/tests/fixtures/captured-payments/discount.json +++ b/tests/fixtures/captured-payments/discount.json @@ -60,7 +60,7 @@ "feeBreakdown": { "base": "Base fee: 2.9% + $0.30", "additional-international": "International card fee: 1%", - "additional-fx": "Foreign exchange fee: 1%", + "additional-fx": "Currency conversion fee: 1%", "discount": { "label": "Discount", "variable": "Variable fee: -4.9%", diff --git a/tests/fixtures/captured-payments/foreign-card.json b/tests/fixtures/captured-payments/foreign-card.json index 234878b2372..df45c326d62 100644 --- a/tests/fixtures/captured-payments/foreign-card.json +++ b/tests/fixtures/captured-payments/foreign-card.json @@ -53,7 +53,7 @@ "feeBreakdown": { "base": "Base fee: 2.9% + $0.30", "additional-international": "International card fee: 1%", - "additional-fx": "Foreign exchange fee: 1%" + "additional-fx": "Currency conversion fee: 1%" }, "netString": "Net payout: $95.47 USD" } diff --git a/tests/fixtures/captured-payments/fx-decimal.json b/tests/fixtures/captured-payments/fx-decimal.json index b95e9318c84..2f065036122 100644 --- a/tests/fixtures/captured-payments/fx-decimal.json +++ b/tests/fixtures/captured-payments/fx-decimal.json @@ -45,7 +45,7 @@ "feeString": "Fee (3.9% + $0.30): -$4.39", "feeBreakdown": { "base": "Base fee: 2.9% + $0.30", - "additional-fx": "Foreign exchange fee: 1%" + "additional-fx": "Currency conversion fee: 1%" }, "netString": "Net payout: $100.65 USD" } diff --git a/tests/fixtures/captured-payments/fx-partial-capture.json b/tests/fixtures/captured-payments/fx-partial-capture.json index f10ff7aa9e9..691390d4852 100644 --- a/tests/fixtures/captured-payments/fx-partial-capture.json +++ b/tests/fixtures/captured-payments/fx-partial-capture.json @@ -57,7 +57,7 @@ "feeString": "Fee (3.51% + £0.21): -$0.88", "feeBreakdown": { "base": "Base fee: 2.9% + $0.30", - "additional-fx": "Foreign exchange fee: 1%", + "additional-fx": "Currency conversion fee: 1%", "discount": { "label": "Discount", "variable": "Variable fee: -0.39%", diff --git a/tests/fixtures/captured-payments/fx-with-capped-fee.json b/tests/fixtures/captured-payments/fx-with-capped-fee.json index 8c1b602a3eb..4c31a8435d7 100644 --- a/tests/fixtures/captured-payments/fx-with-capped-fee.json +++ b/tests/fixtures/captured-payments/fx-with-capped-fee.json @@ -55,7 +55,7 @@ "feeBreakdown": { "base": "Base fee: capped at $6.00", "additional-international": "International card fee: 1.5%", - "additional-fx": "Foreign exchange fee: 1%" + "additional-fx": "Currency conversion fee: 1%" }, "netString": "Net payout: $971.04 USD" } diff --git a/tests/fixtures/captured-payments/fx.json b/tests/fixtures/captured-payments/fx.json index 8ceee7b7438..f18ca9297ab 100644 --- a/tests/fixtures/captured-payments/fx.json +++ b/tests/fixtures/captured-payments/fx.json @@ -46,7 +46,7 @@ "feeString": "Fee (3.9% + $0.30): -$4.20", "feeBreakdown": { "base": "Base fee: 2.9% + $0.30", - "additional-fx": "Foreign exchange fee: 1%" + "additional-fx": "Currency conversion fee: 1%" }, "netString": "Net payout: $95.84 USD" } diff --git a/tests/fixtures/captured-payments/jpy-payment.json b/tests/fixtures/captured-payments/jpy-payment.json index 6c7a6b3ee05..4b4c6c152c9 100644 --- a/tests/fixtures/captured-payments/jpy-payment.json +++ b/tests/fixtures/captured-payments/jpy-payment.json @@ -57,7 +57,7 @@ "feeBreakdown": { "base": "Base fee: 3.6%", "additional-international": "International card fee: 2%", - "additional-fx": "Foreign exchange fee: 2%" + "additional-fx": "Currency conversion fee: 2%" }, "netString": "Net payout: ¥4,507 JPY" } diff --git a/tests/fixtures/captured-payments/subscription.json b/tests/fixtures/captured-payments/subscription.json index b7312ea0c02..d0e1fe705e4 100644 --- a/tests/fixtures/captured-payments/subscription.json +++ b/tests/fixtures/captured-payments/subscription.json @@ -53,7 +53,7 @@ "feeString": "Fee (4.9% + $0.30): -$3.04", "feeBreakdown": { "base": "Base fee: 2.9% + $0.30", - "additional-fx": "Foreign exchange fee: 1%", + "additional-fx": "Currency conversion fee: 1%", "additional-wcpay-subscription": "Subscription transaction fee: 1%" }, "netString": "Net payout: $52.87 USD"