From 0620455775fb477821a76f5ecabf7107c97b7789 Mon Sep 17 00:00:00 2001 From: Malith Senaweera <6216000+malithsen@users.noreply.github.com> Date: Mon, 9 Oct 2023 15:27:45 -0500 Subject: [PATCH 01/60] Add shopper Tracks events (#7268) --- changelog/add-shopper-tracks-events | 4 ++ client/checkout/woopay/email-input-iframe.js | 2 + .../components/woopay/save-user/agreement.js | 13 +++++++ .../save-user/checkout-page-save-user.js | 20 ++++++++++ .../blocks/payment-request-express.js | 33 +++++++++++++++- client/payment-request/index.js | 19 +++++++++ client/tracks/index.js | 9 +++++ ...ayments-payment-request-button-handler.php | 26 +++++++++++++ includes/class-woopay-tracker.php | 39 +++++++++++++++++++ 9 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 changelog/add-shopper-tracks-events diff --git a/changelog/add-shopper-tracks-events b/changelog/add-shopper-tracks-events new file mode 100644 index 00000000000..130620c53ae --- /dev/null +++ b/changelog/add-shopper-tracks-events @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Add Shopper Tracks events diff --git a/client/checkout/woopay/email-input-iframe.js b/client/checkout/woopay/email-input-iframe.js index a30e7cc26ce..d710af82dd7 100644 --- a/client/checkout/woopay/email-input-iframe.js +++ b/client/checkout/woopay/email-input-iframe.js @@ -340,6 +340,8 @@ export const handleWooPayEmailInput = async ( parentDiv.removeChild( errorMessage ); } + wcpayTracks.recordUserEvent( wcpayTracks.events.WOOPAY_EMAIL_CHECK ); + request( buildAjaxURL( getConfig( 'wcAjaxUrl' ), 'get_woopay_signature' ), { diff --git a/client/components/woopay/save-user/agreement.js b/client/components/woopay/save-user/agreement.js index 6c11824f990..65948e956f3 100644 --- a/client/components/woopay/save-user/agreement.js +++ b/client/components/woopay/save-user/agreement.js @@ -4,6 +4,7 @@ */ import { __ } from '@wordpress/i18n'; import interpolateComponents from '@automattic/interpolate-components'; +import wcpayTracks from 'tracks'; const Agreement = () => { return ( @@ -19,6 +20,12 @@ const Agreement = () => { target="_blank" href="https://wordpress.com/tos/" rel="noopener noreferrer" + onClick={ () => { + wcpayTracks.recordUserEvent( + wcpayTracks.events + .WOOPAY_SAVE_MY_INFO_TOS_CLICK + ); + } } > { __( 'Terms of Service', 'woocommerce-payments' ) } @@ -28,6 +35,12 @@ const Agreement = () => { target="_blank" href="https://automattic.com/privacy/" rel="noopener noreferrer" + onClick={ () => { + wcpayTracks.recordUserEvent( + wcpayTracks.events + .WOOPAY_SAVE_MY_INFO_PRIVACY_CLICK + ); + } } > { __( 'Privacy Policy', 'woocommerce-payments' ) } diff --git a/client/components/woopay/save-user/checkout-page-save-user.js b/client/components/woopay/save-user/checkout-page-save-user.js index fffe352092f..77c541786d9 100644 --- a/client/components/woopay/save-user/checkout-page-save-user.js +++ b/client/components/woopay/save-user/checkout-page-save-user.js @@ -30,6 +30,8 @@ const CheckoutPageSaveUser = ( { isBlocksCheckout } ) => { const [ isPhoneValid, onPhoneValidationChange ] = useState( null ); const [ userDataSent, setUserDataSent ] = useState( false ); const [ isInfoFlyoutVisible, setIsInfoFlyoutVisible ] = useState( false ); + const [ hasShownInfoFlyout, setHasShownInfoFlyout ] = useState( false ); + const setInfoFlyoutVisible = useCallback( () => setIsInfoFlyoutVisible( true ), [] @@ -114,6 +116,18 @@ const CheckoutPageSaveUser = ( { isBlocksCheckout } ) => { ); }; + useEffect( () => { + // Record Tracks event when user hovers over the info icon for the first time. + if ( isInfoFlyoutVisible && ! hasShownInfoFlyout ) { + setHasShownInfoFlyout( true ); + wcpayTracks.recordUserEvent( + wcpayTracks.events.WOOPAY_SAVE_MY_INFO_TOOLTIP_HOVER + ); + } else if ( ! isInfoFlyoutVisible && ! hasShownInfoFlyout ) { + setHasShownInfoFlyout( false ); + } + }, [ isInfoFlyoutVisible, hasShownInfoFlyout ] ); + useEffect( () => { const formSubmitButton = isBlocksCheckout ? document.querySelector( @@ -261,6 +275,12 @@ const CheckoutPageSaveUser = ( { isBlocksCheckout } ) => { target="_blank" href="https://woocommerce.com/document/woopay-customer-documentation/" rel="noopener noreferrer" + onClick={ () => { + wcpayTracks.recordUserEvent( + wcpayTracks.events + .WOOPAY_SAVE_MY_INFO_TOOLTIP_LEARN_MORE_CLICK + ); + } } > { __( 'Learn more', diff --git a/client/payment-request/blocks/payment-request-express.js b/client/payment-request/blocks/payment-request-express.js index 2c91b17b938..a44cc527b16 100644 --- a/client/payment-request/blocks/payment-request-express.js +++ b/client/payment-request/blocks/payment-request-express.js @@ -8,6 +8,7 @@ import { Elements, PaymentRequestButtonElement } from '@stripe/react-stripe-js'; */ import { useInitialization } from './use-initialization'; import { getPaymentRequestData } from '../utils'; +import wcpayTracks from 'tracks'; /** * PaymentRequestExpressComponent @@ -53,9 +54,39 @@ const PaymentRequestExpressComponent = ( { return null; } + let paymentRequestType = ''; + + // Check the availability of the Payment Request API first. + paymentRequest.canMakePayment().then( ( result ) => { + if ( ! result ) { + return; + } + + // Set the payment request type. + if ( result.applePay ) { + paymentRequestType = 'apple_pay'; + } else if ( result.googlePay ) { + paymentRequestType = 'google_pay'; + } + } ); + + const onPaymentRequestButtonClick = () => { + onButtonClick(); + + const paymentRequestTypeEvents = { + google_pay: wcpayTracks.events.GOOGLEPAY_BUTTON_CLICK, + apple_pay: wcpayTracks.events.APPLEPAY_BUTTON_CLICK, + }; + + if ( paymentRequestTypeEvents.hasOwnProperty( paymentRequestType ) ) { + const event = paymentRequestTypeEvents[ paymentRequestType ]; + wcpayTracks.recordUserEvent( event, { source: 'checkout' } ); + } + }; + return ( { let paymentRequestType; + // Track the payment request button click event. + const trackPaymentRequestButtonClick = ( source ) => { + const paymentRequestTypeEvents = { + google_pay: wcpayTracks.events.GOOGLEPAY_BUTTON_CLICK, + apple_pay: wcpayTracks.events.APPLEPAY_BUTTON_CLICK, + }; + + if ( paymentRequestTypeEvents.hasOwnProperty( paymentRequestType ) ) { + const event = paymentRequestTypeEvents[ paymentRequestType ]; + wcpayTracks.recordUserEvent( event, { source } ); + } + }; + /** * Object to handle Stripe payment forms. */ @@ -341,6 +355,8 @@ jQuery( ( $ ) => { const addToCartButton = $( '.single_add_to_cart_button' ); prButton.on( 'click', ( evt ) => { + trackPaymentRequestButtonClick( 'product' ); + // If login is required for checkout, display redirect confirmation dialog. if ( wcpayPaymentRequestParams.login_confirmation ) { evt.preventDefault(); @@ -459,6 +475,9 @@ jQuery( ( $ ) => { evt.preventDefault(); displayLoginConfirmation( paymentRequestType ); } + trackPaymentRequestButtonClick( + wcpayPaymentRequestParams.button_context + ); } ); }, diff --git a/client/tracks/index.js b/client/tracks/index.js index 4dc253734ec..0c38e8e77ee 100644 --- a/client/tracks/index.js +++ b/client/tracks/index.js @@ -60,6 +60,7 @@ function recordUserEvent( eventName, eventProperties, isLegacy = false ) { } const events = { + APPLEPAY_BUTTON_CLICK: 'applepay_button_click', CONNECT_ACCOUNT_CLICKED: 'wcpay_connect_account_clicked', CONNECT_ACCOUNT_VIEW: 'page_view', CONNECT_ACCOUNT_LEARN_MORE: 'wcpay_welcome_learn_more', @@ -74,6 +75,7 @@ const events = { DISPUTE_INQUIRY_REFUND_CLICK: 'wcpay_dispute_inquiry_refund_click', DISPUTE_INQUIRY_REFUND_MODAL_VIEW: 'wcpay_dispute_inquiry_refund_modal_view', + GOOGLEPAY_BUTTON_CLICK: 'gpay_button_click', ORDER_DISPUTE_NOTICE_BUTTON_CLICK: 'wcpay_order_dispute_notice_action_click', OVERVIEW_BALANCES_CURRENCY_CLICK: @@ -104,6 +106,7 @@ const events = { SUBSCRIPTIONS_ACCOUNT_NOT_CONNECTED_PRODUCT_MODAL_DISMISS: 'wcpay_subscriptions_account_not_connected_product_modal_dismiss', TRANSACTIONS_DOWNLOAD_CSV_CLICK: 'wcpay_transactions_download_csv_click', + WOOPAY_EMAIL_CHECK: 'checkout_email_address_woopay_check', WOOPAY_OFFERED: 'woopay_offered', WOOPAY_OTP_START: 'woopay_otp_prompt_start', WOOPAY_OTP_COMPLETE: 'woopay_otp_prompt_complete', @@ -113,6 +116,12 @@ const events = { WOOPAY_BUTTON_LOAD: 'woopay_button_load', WOOPAY_BUTTON_CLICK: 'woopay_button_click', WOOPAY_SAVE_MY_INFO_CLICK: 'checkout_save_my_info_click', + WOOPAY_SAVE_MY_INFO_TOS_CLICK: 'checkout_save_my_info_tos_click', + WOOPAY_SAVE_MY_INFO_PRIVACY_CLICK: + 'checkout_save_my_info_privacy_policy_click', + WOOPAY_SAVE_MY_INFO_TOOLTIP_HOVER: 'checkout_save_my_info_tooltip_hover', + WOOPAY_SAVE_MY_INFO_TOOLTIP_LEARN_MORE_CLICK: + 'checkout_save_my_info_tooltip_learn_more_click', // Onboarding flow. ONBOARDING_FLOW_STARTED: 'wcpay_onboarding_flow_started', ONBOARDING_FLOW_MODE_SELECTED: 'wcpay_onboarding_flow_mode_selected', diff --git a/includes/class-wc-payments-payment-request-button-handler.php b/includes/class-wc-payments-payment-request-button-handler.php index 56179967d20..0d07d09f17a 100644 --- a/includes/class-wc-payments-payment-request-button-handler.php +++ b/includes/class-wc-payments-payment-request-button-handler.php @@ -678,6 +678,31 @@ public function is_available_at( $location ) { return false; } + /** + * Gets the context for where the button is being displayed. + * + * @return string + */ + public function get_button_context() { + if ( $this->is_product() ) { + return 'product'; + } + + if ( $this->is_cart() ) { + return 'cart'; + } + + if ( $this->is_checkout() ) { + return 'checkout'; + } + + if ( $this->is_pay_for_order_page() ) { + return 'pay_for_order'; + } + + return ''; + } + /** * Get product from product page or product_page shortcode. * @@ -753,6 +778,7 @@ public function scripts() { 'button' => $this->get_button_settings(), 'login_confirmation' => $this->get_login_confirmation_settings(), 'is_product_page' => $this->is_product(), + 'button_context' => $this->get_button_context(), 'is_pay_for_order' => $this->is_pay_for_order_page(), 'has_block' => has_block( 'woocommerce/cart' ) || has_block( 'woocommerce/checkout' ), 'product' => $this->get_product_data(), diff --git a/includes/class-woopay-tracker.php b/includes/class-woopay-tracker.php index b6c8e15a621..2006fb19906 100644 --- a/includes/class-woopay-tracker.php +++ b/includes/class-woopay-tracker.php @@ -63,7 +63,10 @@ public function __construct( $http ) { // Actions that should result in recorded Tracks events. add_action( 'woocommerce_after_checkout_form', [ $this, 'classic_checkout_start' ] ); + add_action( 'woocommerce_after_cart', [ $this, 'classic_cart_page_view' ] ); + add_action( 'woocommerce_after_single_product', [ $this, 'classic_product_page_view' ] ); add_action( 'woocommerce_blocks_enqueue_checkout_block_scripts_after', [ $this, 'blocks_checkout_start' ] ); + add_action( 'woocommerce_blocks_enqueue_cart_block_scripts_after', [ $this, 'blocks_cart_page_view' ] ); add_action( 'woocommerce_checkout_order_processed', [ $this, 'checkout_order_processed' ] ); add_action( 'woocommerce_blocks_checkout_order_processed', [ $this, 'checkout_order_processed' ] ); add_action( 'woocommerce_payments_save_user_in_woopay', [ $this, 'must_save_payment_method_to_platform' ] ); @@ -384,6 +387,42 @@ public function blocks_checkout_start() { ); } + /** + * Record a Tracks event that the classic cart page has loaded. + */ + public function classic_cart_page_view() { + $this->maybe_record_wcpay_shopper_event( + 'cart_page_view', + [ + 'theme_type' => 'short_code', + ] + ); + } + + /** + * Record a Tracks event that the blocks cart page has loaded. + */ + public function blocks_cart_page_view() { + $this->maybe_record_wcpay_shopper_event( + 'cart_page_view', + [ + 'theme_type' => 'blocks', + ] + ); + } + + /** + * Record a Tracks event that the classic cart product has loaded. + */ + public function classic_product_page_view() { + $this->maybe_record_wcpay_shopper_event( + 'product_page_view', + [ + 'theme_type' => 'short_code', + ] + ); + } + /** * Record a Tracks event that the order has been processed. */ From 1f62d4251a7cbc0982a30a274f4ed2d4878e22aa Mon Sep 17 00:00:00 2001 From: jessy <32092402+jessy-p@users.noreply.github.com> Date: Tue, 10 Oct 2023 21:59:05 +0530 Subject: [PATCH 02/60] Add Reporting Authorization API endpoint (#7066) Co-authored-by: Jessy --- changelog/add-6342-reports-authorisations-api | 4 + includes/class-wc-payments.php | 5 + .../request/class-list-transactions.php | 39 +- .../server/request/class-request-utils.php | 50 +++ ...ents-reports-authorizations-controller.php | 390 ++++++++++++++++++ tests/unit/bootstrap.php | 1 + ...ents-reports-authorizations-controller.php | 331 +++++++++++++++ ...yments-reports-transactions-controller.php | 2 +- 8 files changed, 786 insertions(+), 36 deletions(-) create mode 100644 changelog/add-6342-reports-authorisations-api create mode 100644 includes/core/server/request/class-request-utils.php create mode 100644 includes/reports/class-wc-rest-payments-reports-authorizations-controller.php create mode 100644 tests/unit/reports/test-class-wc-rest-payments-reports-authorizations-controller.php rename tests/unit/{admin => reports}/test-class-wc-rest-payments-reports-transactions-controller.php (99%) diff --git a/changelog/add-6342-reports-authorisations-api b/changelog/add-6342-reports-authorisations-api new file mode 100644 index 00000000000..fdeb47ad449 --- /dev/null +++ b/changelog/add-6342-reports-authorisations-api @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Added Authorizations reporting endpoint. diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php index bee75837a4a..b0882e7e03d 100644 --- a/includes/class-wc-payments.php +++ b/includes/class-wc-payments.php @@ -376,6 +376,7 @@ public static function init() { include_once __DIR__ . '/core/server/request/class-refund-charge.php'; include_once __DIR__ . '/core/server/request/class-list-charge-refunds.php'; include_once __DIR__ . '/core/server/request/class-get-request.php'; + include_once __DIR__ . '/core/server/request/class-request-utils.php'; include_once __DIR__ . '/woopay/services/class-checkout-service.php'; @@ -1089,6 +1090,10 @@ public static function init_rest_api() { $reports_transactions_controller = new WC_REST_Payments_Reports_Transactions_Controller( self::$api_client ); $reports_transactions_controller->register_routes(); + include_once WCPAY_ABSPATH . 'includes/reports/class-wc-rest-payments-reports-authorizations-controller.php'; + $reports_authorizations_controller = new WC_REST_Payments_Reports_Authorizations_Controller( self::$api_client ); + $reports_authorizations_controller->register_routes(); + } /** diff --git a/includes/core/server/request/class-list-transactions.php b/includes/core/server/request/class-list-transactions.php index ef340c37d9f..4d10a3555ab 100644 --- a/includes/core/server/request/class-list-transactions.php +++ b/includes/core/server/request/class-list-transactions.php @@ -7,12 +7,11 @@ namespace WCPay\Core\Server\Request; -use DateTime; -use DateTimeZone; use WC_Payments_API_Client; use WC_Payments_DB; use WC_Payments_Utils; use WCPay\Core\Server\Response; +use WCPay\Core\Server\Request\Request_Utils; use WP_REST_Request; /** @@ -69,7 +68,7 @@ public static function from_rest_request( $request ) { if ( ! is_null( $date_between_filter ) ) { $date_between_filter = array_map( function ( $transaction_date ) use ( $user_timezone ) { - return List_Transactions::format_transaction_date_with_timestamp( $transaction_date, $user_timezone ); + return Request_Utils::format_transaction_date_by_timezone( $transaction_date, $user_timezone ); }, $date_between_filter ); @@ -77,8 +76,8 @@ function ( $transaction_date ) use ( $user_timezone ) { $filters = [ 'match' => $request->get_param( 'match' ), - 'date_before' => self::format_transaction_date_with_timestamp( $request->get_param( 'date_before' ), $user_timezone ), - 'date_after' => self::format_transaction_date_with_timestamp( $request->get_param( 'date_after' ), $user_timezone ), + 'date_before' => Request_Utils::format_transaction_date_by_timezone( $request->get_param( 'date_before' ), $user_timezone ), + 'date_after' => Request_Utils::format_transaction_date_by_timezone( $request->get_param( 'date_after' ), $user_timezone ), 'date_between' => $date_between_filter, 'type_is' => $request->get_param( 'type_is' ), 'type_is_not' => $request->get_param( 'type_is_not' ), @@ -239,34 +238,4 @@ public function format_response( $response ) { return new Response( $response ); } - /** - * Formats the incoming transaction date as per the blog's timezone. - * - * @param string|null $transaction_date Transaction date to format. - * @param string|null $user_timezone User's timezone passed from client. - * - * @return string|null The formatted transaction date as per timezone. - */ - public static function format_transaction_date_with_timestamp( $transaction_date, $user_timezone ) { - if ( is_null( $transaction_date ) || is_null( $user_timezone ) ) { - return $transaction_date; - } - - // Get blog timezone. - $blog_time = new DateTime( $transaction_date ); - $blog_time->setTimezone( new DateTimeZone( wp_timezone_string() ) ); - - // Get local timezone. - $local_time = new DateTime( $transaction_date ); - $local_time->setTimezone( new DateTimeZone( $user_timezone ) ); - - // Compute time difference in minutes. - $time_difference = ( strtotime( $local_time->format( 'Y-m-d H:i:s' ) ) - strtotime( $blog_time->format( 'Y-m-d H:i:s' ) ) ) / 60; - - // Shift date by time difference. - $formatted_date = new DateTime( $transaction_date ); - date_modify( $formatted_date, $time_difference . 'minutes' ); - - return $formatted_date->format( 'Y-m-d H:i:s' ); - } } diff --git a/includes/core/server/request/class-request-utils.php b/includes/core/server/request/class-request-utils.php new file mode 100644 index 00000000000..0090ad7ec90 --- /dev/null +++ b/includes/core/server/request/class-request-utils.php @@ -0,0 +1,50 @@ +setTimezone( new DateTimeZone( wp_timezone_string() ) ); + + // Get local timezone. + $local_time = new DateTime( $transaction_date ); + $local_time->setTimezone( new DateTimeZone( $user_timezone ) ); + + // Compute time difference in minutes. + $time_difference = ( strtotime( $local_time->format( 'Y-m-d H:i:s' ) ) - strtotime( $blog_time->format( 'Y-m-d H:i:s' ) ) ) / 60; + + // Shift date by time difference. + $formatted_date = new DateTime( $transaction_date ); + date_modify( $formatted_date, $time_difference . 'minutes' ); + + return $formatted_date->format( 'Y-m-d H:i:s' ); + } + +} diff --git a/includes/reports/class-wc-rest-payments-reports-authorizations-controller.php b/includes/reports/class-wc-rest-payments-reports-authorizations-controller.php new file mode 100644 index 00000000000..09c2e191816 --- /dev/null +++ b/includes/reports/class-wc-rest-payments-reports-authorizations-controller.php @@ -0,0 +1,390 @@ +namespace, + '/' . $this->rest_base, + [ + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [ $this, 'get_authorizations' ], + 'permission_callback' => [ $this, 'check_permission' ], + 'args' => $this->get_collection_params(), + ], + 'schema' => [ $this, 'get_item_schema' ], + ] + ); + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P\w+)', + [ + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [ $this, 'get_authorization' ], + 'permission_callback' => [ $this, 'check_permission' ], + ], + 'schema' => [ $this, 'get_item_schema' ], + ] + ); + } + + /** + * Retrieve transactions to respond with via API. + * + * @param WP_REST_Request $request Full data about the request. + */ + public function get_authorizations( $request ) { + + $wcpay_request = List_Authorizations::from_rest_request( $request ); + $wcpay_request->set_page_size( $request->get_param( 'per_page' ) ?? 25 ); + + $date_between_filter = $request->get_param( 'date_between' ); + $user_timezone = $request->get_param( 'user_timezone' ); + $filters = [ + 'match' => $request->get_param( 'match' ), + 'order_id_is' => $request->get_param( 'order_id' ), + 'customer_email_is' => $request->get_param( 'customer_email' ), + 'source_is' => $request->get_param( 'payment_method_type' ), + ]; + if ( $request->get_param( 'date_before' ) ) { + $filters['from_date'] = strtotime( Request_Utils::format_transaction_date_by_timezone( $request->get_param( 'date_before' ), $user_timezone ) ); + } + if ( $request->get_param( 'date_after' ) ) { + $filters['to_date'] = strtotime( Request_Utils::format_transaction_date_by_timezone( $request->get_param( 'date_after' ), $user_timezone ) ); + } + $wcpay_request->set_filters( $filters ); + + $response = $wcpay_request->handle_rest_request( 'wcpay_list_authorizations_request' ); + if ( is_wp_error( $response ) ) { + return $response; + } + $data = []; + foreach ( $response['data'] ?? [] as $authorization ) { + $response = $this->prepare_item_for_response( $authorization, $request ); + $data[] = $this->prepare_response_for_collection( $response ); + } + + return rest_ensure_response( $data ); + + } + + /** + * Retrieve transaction to respond with via API. + * + * @param WP_REST_Request $request Full data about the request. + */ + public function get_authorization( $request ) { + $wcpay_request = List_Authorizations::create(); + $wcpay_request->set_filters( [ 'charge_id_is' => $request->get_param( 'id' ) ] ); + $wcpay_request->set_page_size( 1 ); // Set page size to limit to only one record. + + $response = $wcpay_request->handle_rest_request( 'wcpay_list_authorizations_request' ); + if ( is_wp_error( $response ) ) { + return $response; + } + $authorization = $response['data'][0] ?? null; + if ( ! $authorization ) { + return rest_ensure_response( [] ); + } + $response = $this->prepare_item_for_response( $authorization, $request ); + $response = rest_ensure_response( $this->prepare_response_for_collection( $response ) ); + + return $response; + } + + /** + * Prepare each item for response. + * + * @param array|mixed $item Item to prepare. + * @param WP_REST_Request $request Request instance. + * + * @return WP_REST_Response|WP_Error|WP_REST_Response + */ + public function prepare_item_for_response( $item, $request ) { + + $prepared_item = []; + + $prepared_item['authorization_id'] = $item['charge_id']; + $prepared_item['date'] = $item['created']; + $prepared_item['payment_id'] = $item['payment_intent_id']; + $prepared_item['channel'] = $item['channel']; + $prepared_item['payment_method'] = [ + 'type' => $item['source'], + ]; + $prepared_item['currency'] = $item['currency']; + $prepared_item['amount'] = $item['amount']; + $prepared_item['amount_captured'] = $item['amount_captured']; + $prepared_item['fees'] = $item['fees']; + $prepared_item['customer'] = [ + 'name' => $item['customer_name'], + 'email' => $item['customer_email'], + 'country' => $item['customer_country'], + ]; + $prepared_item['net_amount'] = $item['net']; + $prepared_item['order_id'] = $item['order_id']; + $prepared_item['risk_level'] = $item['risk_level']; + + $context = $request['context'] ?? 'view'; + $prepared_item = $this->add_additional_fields_to_object( $prepared_item, $request ); + $prepared_item = $this->filter_response_by_context( $prepared_item, $context ); + + return rest_ensure_response( $prepared_item ); + } + + /** + * Collection args params. + * + * @return array[] + */ + public function get_collection_params() { + return [ + 'date_before' => [ + 'description' => __( 'Filter transactions before this date.', 'woocommerce-payments' ), + 'type' => 'string', + 'format' => 'date-time', + 'required' => false, + ], + 'date_after' => [ + 'description' => __( 'Filter transactions after this date.', 'woocommerce-payments' ), + 'type' => 'string', + 'format' => 'date-time', + 'required' => false, + ], + 'date_between' => [ + 'description' => __( 'Filter transactions between these dates.', 'woocommerce-payments' ), + 'type' => 'array', + ], + 'order_id' => [ + 'description' => __( 'Filter transactions based on the associated order ID.', 'woocommerce-payments' ), + 'type' => 'integer', + 'required' => false, + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ], + 'deposit_id' => [ + 'description' => __( 'Filter transactions based on the associated deposit ID.', 'woocommerce-payments' ), + 'type' => 'string', + 'required' => false, + 'validate_callback' => 'rest_validate_request_arg', + ], + 'customer_email' => [ + 'description' => __( 'Filter transactions based on the customer email.', 'woocommerce-payments' ), + 'type' => 'string', + 'required' => false, + 'validate_callback' => 'rest_validate_request_arg', + ], + 'payment_method_type' => [ + 'description' => __( 'Filter transactions based on the payment method used.', 'woocommerce-payments' ), + 'type' => 'string', + 'required' => false, + 'validate_callback' => 'rest_validate_request_arg', + ], + 'type' => [ + 'description' => __( 'Filter transactions where type is a specific value.', 'woocommerce-payments' ), + 'type' => 'string', + 'required' => false, + 'validate_callback' => 'rest_validate_request_arg', + ], + 'match' => [ + 'description' => __( 'Match filter for the transactions.', 'woocommerce-payments' ), + 'type' => 'string', + 'required' => false, + ], + 'user_timezone' => [ + 'description' => __( 'Include timezone into date filtering.', 'woocommerce-payments' ), + 'type' => 'string', + 'required' => false, + ], + 'page' => [ + 'description' => __( 'Page number.', 'woocommerce-payments' ), + 'type' => 'integer', + 'required' => false, + 'default' => 1, + 'minimum' => 1, + ], + 'per_page' => [ + 'description' => __( 'Page size.', 'woocommerce-payments' ), + 'type' => 'integer', + 'required' => false, + 'default' => 25, + 'minimum' => 1, + 'maximum' => 100, + ], + 'sort' => [ + 'description' => __( 'Field on which to sort.', 'woocommerce-payments' ), + 'type' => 'string', + 'required' => false, + 'default' => 'date', + ], + 'direction' => [ + 'description' => __( 'Direction on which to sort.', 'woocommerce-payments' ), + 'type' => 'string', + 'required' => false, + 'default' => 'desc', + ], + ]; + } + + + /** + * Item schema. + * + * @return array + */ + public function get_item_schema() { + $schema = [ + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'transaction', + 'type' => 'object', + 'properties' => [ + 'date' => [ + 'description' => __( 'The date and time when the transaction was created.', 'woocommerce-payments' ), + 'type' => 'string', + 'format' => 'date-time', + 'context' => [ 'view' ], + ], + 'transaction_id' => [ + 'description' => __( 'A unique identifier for each transaction based on its transaction type.', 'woocommerce-payments' ), + 'type' => 'string', + 'context' => [ 'view' ], + ], + 'source_id' => [ + 'description' => __( 'A unique source id for each transaction.', 'woocommerce-payments' ), + 'type' => 'string', + 'context' => [ 'view' ], + ], + 'channel' => [ + 'description' => __( 'Indicates whether the transaction was made online or offline.', 'woocommerce-payments' ), + 'type' => 'string', + 'context' => [ 'view' ], + ], + 'payment_method' => [ + 'description' => __( 'Specifies whether the payment method used was a card (Visa, Mastercard, etc.) or an Alternative Payment Method (APM) or Local Payment Method (LPM) (iDEAL, Apple Pay, Google Pay, etc.).', 'woocommerce-payments' ), + 'type' => 'object', + 'context' => [ 'view' ], + 'properties' => [ + 'type' => [ + 'description' => __( 'Specifies whether the payment method used was a card (Visa, Mastercard, etc.) or an Alternative Payment Method (APM) or Local Payment Method (LPM) (iDEAL, Apple Pay, Google Pay, etc.).', 'woocommerce-payments' ), + 'type' => 'string', + 'context' => [ 'view' ], + ], + 'id' => [ + 'description' => __( 'The payment method ID used to create the transaction type.', 'woocommerce-payments' ), + 'type' => 'string', + 'context' => [ 'view' ], + ], + ], + ], + 'type' => [ + 'description' => __( 'The type of the transaction.', 'woocommerce-payments' ), + 'type' => 'string', + 'context' => [ 'view' ], + ], + 'transaction_currency' => [ + 'description' => __( 'The currency of the transaction.', 'woocommerce-payments' ), + 'type' => 'string', + 'context' => [ 'view' ], + ], + 'amount' => [ + 'description' => __( 'The amount of the transaction.', 'woocommerce-payments' ), + 'type' => 'number', + 'context' => [ 'view' ], + ], + 'exchange_rate' => [ + 'description' => __( 'The exchange rate of the transaction.', 'woocommerce-payments' ), + 'type' => 'number', + 'context' => [ 'view' ], + ], + 'deposit_currency' => [ + 'description' => __( 'The currency of the store.', 'woocommerce-payments' ), + 'type' => 'string', + 'context' => [ 'view' ], + ], + 'fees' => [ + 'description' => __( 'Transaction fees.', 'woocommerce-payments' ), + 'type' => 'number', + 'context' => [ 'view' ], + ], + 'customer' => [ + 'description' => __( 'Customer details.', 'woocommerce-payments' ), + 'type' => 'object', + 'context' => [ 'view' ], + 'properties' => [ + 'name' => [ + 'name' => __( 'Customer name.', 'woocommerce-payments' ), + 'type' => 'string', + 'context' => [ 'view' ], + ], + 'email' => [ + 'description' => __( 'Customer email.', 'woocommerce-payments' ), + 'type' => 'string', + 'context' => [ 'view' ], + ], + 'country' => [ + 'description' => __( 'Customer country.', 'woocommerce-payments' ), + 'type' => 'string', + 'context' => [ 'view' ], + ], + ], + ], + 'net_amount' => [ + 'description' => __( 'Net amount.', 'woocommerce-payments' ), + 'type' => 'number', + 'context' => [ 'view' ], + ], + 'order_id' => [ + 'description' => __( 'The identifier of the WooCommerce order associated with this transaction.', 'woocommerce-payments' ), + 'type' => 'number', + 'context' => [ 'view' ], + ], + 'risk_level' => [ + 'description' => __( 'Fraud risk level.', 'woocommerce-payments' ), + 'type' => 'number', + 'context' => [ 'view' ], + ], + 'deposit_date' => [ + 'description' => __( 'Deposit date of transaction', 'woocommerce-payments' ), + 'type' => 'string', + 'context' => [ 'view' ], + ], + 'deposit_id' => [ + 'description' => __( 'A unique identifier for the deposit.', 'woocommerce-payments' ), + 'type' => 'string', + 'context' => [ 'view' ], + ], + 'deposit_status' => [ + 'description' => __( 'The status of the deposit', 'woocommerce-payments' ), + 'type' => 'string', + 'context' => [ 'view' ], + ], + ], + ]; + + return $this->add_additional_fields_schema( $schema ); + } + +} diff --git a/tests/unit/bootstrap.php b/tests/unit/bootstrap.php index 468c5ed0f98..b986ab01ef4 100755 --- a/tests/unit/bootstrap.php +++ b/tests/unit/bootstrap.php @@ -94,6 +94,7 @@ function() { require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-reader-controller.php'; require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-files-controller.php'; require_once $_plugin_dir . 'includes/reports/class-wc-rest-payments-reports-transactions-controller.php'; + require_once $_plugin_dir . 'includes/reports/class-wc-rest-payments-reports-authorizations-controller.php'; require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-payment-intents-controller.php'; require_once $_plugin_dir . 'includes/class-woopay-tracker.php'; require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-customer-controller.php'; diff --git a/tests/unit/reports/test-class-wc-rest-payments-reports-authorizations-controller.php b/tests/unit/reports/test-class-wc-rest-payments-reports-authorizations-controller.php new file mode 100644 index 00000000000..57f9be53cbe --- /dev/null +++ b/tests/unit/reports/test-class-wc-rest-payments-reports-authorizations-controller.php @@ -0,0 +1,331 @@ +mock_api_client = $this->createMock( WC_Payments_API_Client::class ); + $this->controller = new WC_REST_Payments_Reports_Authorizations_Controller( $this->mock_api_client ); + } + + + public function test_get_authorizations_success() { + $request = new WP_REST_Request( 'POST' ); + $request->set_param( 'per_page', 3 ); + + $mock_request = $this->mock_wcpay_request( List_Authorizations::class ); + $mock_request->expects( $this->once() ) + ->method( 'format_response' ) + ->willReturn( $this->get_authorizations_list_from_server() ); + + // check that in the end, page size is set correctly. + $mock_request->expects( $this->any() ) + ->method( 'set_page_size' ) + ->withConsecutive( + [ $this->anything() ], + [ '3' ] + ); + + $response = $this->controller->get_authorizations( $request ); + $this->assertEquals( $this->get_authorizations_list(), $response->get_data() ); + } + + public function test_get_authorizations_response_error() { + $request = new WP_REST_Request( 'POST' ); + + $mock_request = $this->mock_wcpay_request( List_Authorizations::class ); + $mock_request->expects( $this->once() ) + ->method( 'format_response' ) + ->will( + $this->throwException( + new Connection_Exception( + 'Test error.', + 'wcpay_http_request_failed', + 400 + ) + ) + ); + + $response = $this->controller->get_authorizations( $request ); + $expected = new WP_Error( 'wcpay_http_request_failed', 'Test error.' ); + $this->assertEquals( $expected, $response ); + } + + public function test_get_authorizations_filters() { + $request = new WP_REST_Request( 'POST' ); + $request->set_param( 'match', 'any' ); + $request->set_param( 'order_id', 123 ); + $request->set_param( 'customer_email', 'test@woocommerce.com' ); + $request->set_param( 'payment_method_type', 'visa' ); + + $mock_request = $this->mock_wcpay_request( List_Authorizations::class ); + $mock_request->expects( $this->once() ) + ->method( 'set_filters' ) + ->with( + [ + 'match' => 'any', + 'order_id_is' => 123, + 'customer_email_is' => 'test@woocommerce.com', + 'source_is' => 'visa', + ], + ); + + $response = $this->controller->get_authorizations( $request ); + } + + + public function test_get_authorizations_date_filters() { + $request = new WP_REST_Request( 'POST' ); + $request->set_param( 'date_after', '2023-08-20 00:00:00' ); + $request->set_param( 'date_before', '2023-08-21 23:59:59' ); + $request->set_param( 'user_timezone', '-2:30' ); + + // the date minus 2:30 hrs in timestamp. + $translated_timestamp_date_after = strtotime( '2023-08-20 00:00:00' ) - 9000; + $translated_timestamp_date_before = strtotime( '2023-08-21 23:59:59' ) - 9000; + + $mock_request = $this->mock_wcpay_request( List_Authorizations::class ); + $mock_request->expects( $this->once() ) + ->method( 'set_filters' ) + ->with( + $this->callback( + function ( $filters ) use ( $translated_timestamp_date_after, $translated_timestamp_date_before ): bool { + $this->assertSame( $translated_timestamp_date_after, $filters['to_date'] ); + $this->assertSame( $translated_timestamp_date_before, $filters['from_date'] ); + return true; + } + ) + ); + $response = $this->controller->get_authorizations( $request ); + } + + public function test_get_authorization_success() { + $request = new WP_REST_Request( 'POST' ); + $request->set_param( 'id', 'ch_123' ); + + $mock_request = $this->mock_wcpay_request( List_Authorizations::class ); + // test the params are set correctly. + $mock_request->expects( $this->once() ) + ->method( 'set_filters' ) + ->with( + [ 'charge_id_is' => 'ch_123' ] + ); + $mock_request->expects( $this->once() ) + ->method( 'set_page_size' ) + ->with( 1 ); + + $mock_request->expects( $this->once() ) + ->method( 'format_response' ) + ->willReturn( [ 'data' => [ $this->get_authorizations_list_from_server()['data'][0] ] ] ); + + $response = $this->controller->get_authorization( $request ); + $this->assertEquals( $this->get_authorizations_list()[0], $response->get_data() ); + } + + public function test_get_authorization_response_error() { + $request = new WP_REST_Request( 'POST' ); + + $mock_request = $this->mock_wcpay_request( List_Authorizations::class ); + $mock_request->expects( $this->once() ) + ->method( 'format_response' ) + ->will( + $this->throwException( + new Connection_Exception( + 'Test error.', + 'wcpay_http_request_failed', + 400 + ) + ) + ); + + $response = $this->controller->get_authorization( $request ); + $expected = new WP_Error( 'wcpay_http_request_failed', 'Test error.' ); + $this->assertEquals( $expected, $response ); + } + + public function test_get_authorization_empty_result() { + $request = new WP_REST_Request( 'POST' ); + + $mock_request = $this->mock_wcpay_request( List_Authorizations::class ); + $mock_request->expects( $this->once() ) + ->method( 'format_response' ) + ->willReturn( [ 'data' => [] ] ); + + $response = $this->controller->get_authorization( $request ); + $this->assertEquals( [], $response->get_data() ); + } + + private function get_authorizations_list_from_server() { + return [ + 'data' => [ + [ + 'charge_id' => 'ch_123', + 'transaction_id' => null, + 'amount' => 7300, + 'net' => 6988, + 'amount_captured' => 0, + 'amount_refunded' => 0, + 'is_capture¯d' => false, + 'created' => '2023-08-26 00:51:42', + 'modified' => '2023-08-28 13:09:19', + 'channel' => 'online', + 'source' => 'visa', + 'source_identifier' => '4242', + 'customer_name' => 'Test One', + 'customer_email' => 'test1@woocommerce.com', + 'customer_country' => 'US', + 'fees' => 312, + 'currency' => 'eur', + 'risk_level' => 0, + 'payment_intent_id' => 'pi_321', + 'refunded' => false, + 'order_id' => 123, + 'outcome_type' => 'authorized', + 'status' => 'succeeded', + ], + [ + 'charge_id' => 'ch_345', + 'transaction_id' => null, + 'amount' => 1800, + 'net' => 1702, + 'amount_captured' => 0, + 'amount_refunded' => 0, + 'is_captured' => false, + 'created' => '2023-08-27 00:48:44', + 'modified' => '2023-08-28 13:09:05', + 'channel' => 'online', + 'source' => 'visa', + 'source_identifier' => '4242', + 'customer_name' => 'Test Two', + 'customer_email' => 'test2@woocommerce.com', + 'customer_country' => 'US', + 'fees' => 98, + 'currency' => 'eur', + 'risk_level' => 0, + 'payment_intent_id' => 'pi_345', + 'refunded' => false, + 'order_id' => 456, + 'outcome_type' => 'authorized', + 'status' => 'succeeded', + ], + [ + 'charge_id' => 'ch_567', + 'transaction_id' => null, + 'amount' => 7300, + 'net' => 6988, + 'amount_captured' => 0, + 'amount_refunded' => 0, + 'is_captured' => false, + 'created' => '2023-08-27 00:51:42', + 'modified' => '2023-08-28 13:09:11', + 'channel' => 'online', + 'source' => 'mastercard', + 'source_identifier' => '4242', + 'customer_name' => 'Test One', + 'customer_email' => 'test1@woocommerce.com', + 'customer_country' => 'US', + 'fees' => 312, + 'currency' => 'eur', + 'risk_level' => 0, + 'payment_intent_id' => 'pi_567', + 'refunded' => false, + 'order_id' => 789, + 'outcome_type' => 'authorized', + 'status' => 'succeeded', + ], + ], + ]; + } + + private function get_authorizations_list() { + return [ + [ + 'authorization_id' => 'ch_123', + 'date' => '2023-08-26 00:51:42', + 'payment_id' => 'pi_321', + 'channel' => 'online', + 'payment_method' => [ + 'type' => 'visa', + ], + 'currency' => 'eur', + 'amount' => 7300, + 'amount_captured' => 0, + 'fees' => 312, + 'customer' => [ + 'name' => 'Test One', + 'email' => 'test1@woocommerce.com', + 'country' => 'US', + ], + 'net_amount' => 6988, + 'order_id' => 123, + 'risk_level' => 0, + ], + [ + 'authorization_id' => 'ch_345', + 'date' => '2023-08-27 00:48:44', + 'payment_id' => 'pi_345', + 'channel' => 'online', + 'payment_method' => [ + 'type' => 'visa', + ], + 'currency' => 'eur', + 'amount' => 1800, + 'amount_captured' => 0, + 'fees' => 98, + 'customer' => [ + 'name' => 'Test Two', + 'email' => 'test2@woocommerce.com', + 'country' => 'US', + ], + 'net_amount' => 1702, + 'order_id' => 456, + 'risk_level' => 0, + ], + [ + 'authorization_id' => 'ch_567', + 'date' => '2023-08-27 00:51:42', + 'payment_id' => 'pi_567', + 'channel' => 'online', + 'payment_method' => [ + 'type' => 'mastercard', + ], + 'currency' => 'eur', + 'amount' => 7300, + 'amount_captured' => 0, + 'fees' => 312, + 'customer' => [ + 'name' => 'Test One', + 'email' => 'test1@woocommerce.com', + 'country' => 'US', + ], + 'net_amount' => 6988, + 'order_id' => 789, + 'risk_level' => 0, + ], + ]; + } + +} diff --git a/tests/unit/admin/test-class-wc-rest-payments-reports-transactions-controller.php b/tests/unit/reports/test-class-wc-rest-payments-reports-transactions-controller.php similarity index 99% rename from tests/unit/admin/test-class-wc-rest-payments-reports-transactions-controller.php rename to tests/unit/reports/test-class-wc-rest-payments-reports-transactions-controller.php index 9228d1bf9ca..22146b607ae 100644 --- a/tests/unit/admin/test-class-wc-rest-payments-reports-transactions-controller.php +++ b/tests/unit/reports/test-class-wc-rest-payments-reports-transactions-controller.php @@ -1,6 +1,6 @@ Date: Wed, 11 Oct 2023 00:10:58 +0530 Subject: [PATCH 03/60] Fix for issue during plugin update (#7447) Co-authored-by: Jessy P --- changelog/update-6705-temp-fix-arg-issue | 5 + ...st-payments-payment-intents-controller.php | 341 ---------------- ...ents-payment-intents-create-controller.php | 376 ++++++++++++++++++ includes/class-wc-payments.php | 8 +- ...st-payments-payment-intents-controller.php | 10 +- 5 files changed, 392 insertions(+), 348 deletions(-) create mode 100644 changelog/update-6705-temp-fix-arg-issue create mode 100644 includes/admin/class-wc-rest-payments-payment-intents-create-controller.php diff --git a/changelog/update-6705-temp-fix-arg-issue b/changelog/update-6705-temp-fix-arg-issue new file mode 100644 index 00000000000..c94fd968d6a --- /dev/null +++ b/changelog/update-6705-temp-fix-arg-issue @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Temp fix by reverting controller constructor and moving code to new file + + diff --git a/includes/admin/class-wc-rest-payments-payment-intents-controller.php b/includes/admin/class-wc-rest-payments-payment-intents-controller.php index 58ea31b456b..53d02e8afa3 100644 --- a/includes/admin/class-wc-rest-payments-payment-intents-controller.php +++ b/includes/admin/class-wc-rest-payments-payment-intents-controller.php @@ -19,27 +19,6 @@ */ class WC_REST_Payments_Payment_Intents_Controller extends WC_Payments_REST_Controller { - /** - * Instance of WC_Payment_Gateway_WCPay - * - * @var WC_Payment_Gateway_WCPay - */ - private $gateway; - - /** - * Order service instance. - * - * @var OrderService - */ - private $order_service; - - /** - * Level3 service instance. - * - * @var Level3Service - */ - private $level3_service; - /** * Endpoint path. * @@ -60,37 +39,6 @@ public function register_routes() { 'permission_callback' => [ $this, 'check_permission' ], ] ); - register_rest_route( - $this->namespace, - '/' . $this->rest_base, - [ - 'methods' => WP_REST_Server::CREATABLE, - 'callback' => [ $this, 'create_payment_intent' ], - 'permission_callback' => [ $this, 'check_permission' ], - 'schema' => [ $this, 'get_item_schema' ], - ] - ); - } - - /** - * WC_REST_Payments_Payment_Intents_Controller constructor. - * - * @param WC_Payments_API_Client $api_client WooCommerce Payments API client. - * @param WC_Payment_Gateway_WCPay $gateway WooCommerce Payments payment gateway. - * @param OrderService $order_service The new order servie. - * @param Level3Service $level3_service Level3 service instance. - */ - public function __construct( - WC_Payments_API_Client $api_client, - WC_Payment_Gateway_WCPay $gateway, - OrderService $order_service, - Level3Service $level3_service - ) { - parent::__construct( $api_client ); - - $this->gateway = $gateway; - $this->order_service = $order_service; - $this->level3_service = $level3_service; } /** @@ -104,293 +52,4 @@ public function get_payment_intent( $request ) { return $this->forward_request( 'get_intent', [ $payment_intent_id ] ); } - /** - * Create a payment intent. - * - * @param WP_REST_Request $request data about the request. - * - * @throws Rest_Request_Exception - */ - public function create_payment_intent( $request ) { - try { - - $order_id = $request->get_param( 'order_id' ); - $order = wc_get_order( $order_id ); - if ( ! $order ) { - throw new Rest_Request_Exception( __( 'Order not found', 'woocommerce-payments' ) ); - } - - $wcpay_server_request = Create_And_Confirm_Intention::create(); - - $currency = strtolower( $order->get_currency() ); - $amount = WC_Payments_Utils::prepare_amount( $order->get_total(), $currency ); - $wcpay_server_request->set_currency_code( $currency ); - $wcpay_server_request->set_amount( $amount ); - - $metadata = $this->order_service->get_payment_metadata( $order_id, Payment_Type::SINGLE() ); - $wcpay_server_request->set_metadata( $metadata ); - - $wcpay_server_request->set_customer( $request->get_param( 'customer' ) ); - $wcpay_server_request->set_level3( $this->level3_service->get_data_from_order( $order_id ) ); - $wcpay_server_request->set_payment_method( $request->get_param( 'payment_method' ) ); - $wcpay_server_request->set_payment_method_types( [ 'card' ] ); - $wcpay_server_request->set_off_session( true ); - $wcpay_server_request->set_capture_method( $this->gateway->get_option( 'manual_capture' ) && ( 'yes' === $this->gateway->get_option( 'manual_capture' ) ) ); - - $wcpay_server_request->assign_hook( 'wcpay_create_and_confirm_intent_request_api' ); - $intent = $wcpay_server_request->send(); - - $response = $this->prepare_item_for_response( $intent, $request ); - return rest_ensure_response( $this->prepare_response_for_collection( $response ) ); - - } catch ( \Throwable $e ) { - Logger::error( 'Failed to create an intention via REST API: ' . $e ); - return new WP_Error( 'wcpay_server_error', $e->getMessage(), [ 'status' => 500 ] ); - } - } - - - /** - * Item schema. - * - * @return array - */ - public function get_item_schema() { - return [ - '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => 'payment_intent', - 'type' => 'object', - 'properties' => [ - 'id' => [ - 'description' => __( 'ID for the payment intent.', 'woocommerce-payments' ), - 'type' => 'string', - 'context' => [ 'view' ], - ], - 'amount' => [ - 'description' => __( 'The amount of the transaction.', 'woocommerce-payments' ), - 'type' => 'integer', - 'context' => [ 'view' ], - ], - 'currency' => [ - 'description' => __( 'The currency of the transaction.', 'woocommerce-payments' ), - 'type' => 'string', - 'context' => [ 'view' ], - ], - 'created' => [ - 'description' => __( 'Timestamp for when the payment intent was created.', 'woocommerce-payments' ), - 'type' => 'string', - 'context' => [ 'view' ], - ], - 'customer' => [ - 'description' => __( 'The customer id of the intent', 'woocommerce-payments' ), - 'type' => 'string', - 'context' => [ 'view' ], - ], - 'status' => [ - 'description' => __( 'The status of the payment intent.', 'woocommerce-payments' ), - 'type' => 'string', - 'context' => [ 'view' ], - ], - 'charge' => [ - 'description' => __( 'Charge object associated with this payment intention.', 'woocommerce-payments' ), - 'type' => 'object', - 'context' => [ 'view' ], - 'properties' => [ - 'id' => [ - 'description' => 'ID for the charge.', - 'type' => 'string', - 'context' => [ 'view' ], - ], - 'amount' => [ - 'description' => 'The amount of the charge.', - 'type' => 'integer', - 'context' => [ 'view' ], - ], - 'payment_method_details' => [ - 'description' => 'Details for the payment method used for the charge.', - 'type' => 'object', - 'properties' => [ - 'card' => [ - 'description' => 'Details for a card payment method.', - 'type' => 'object', - 'properties' => [ - 'amount_authorized' => [ - 'description' => 'The amount authorized by the card.', - 'type' => 'integer', - ], - 'brand' => [ - 'description' => 'The brand of the card.', - 'type' => 'string', - ], - 'capture_before' => [ - 'description' => 'Timestamp for when the authorization must be captured.', - 'type' => 'string', - ], - 'country' => [ - 'description' => 'The ISO country code.', - 'type' => 'string', - ], - 'exp_month' => [ - 'description' => 'The expiration month of the card.', - 'type' => 'integer', - ], - 'exp_year' => [ - 'description' => 'The expiration year of the card.', - 'type' => 'integer', - ], - 'last4' => [ - 'description' => 'The last 4 digits of the card.', - 'type' => 'string', - ], - 'three_d_secure' => [ - 'description' => 'Details for 3D Secure authentication.', - 'type' => 'object', - ], - ], - ], - ], - ], - 'billing_details' => [ - 'description' => __( 'Billing details for the payment method.', 'woocommerce-payments' ), - 'type' => 'object', - 'context' => [ 'view' ], - 'properties' => [ - 'address' => [ - 'description' => __( 'Address associated with the billing details.', 'woocommerce-payments' ), - 'type' => 'object', - 'context' => [ 'view' ], - 'properties' => [ - 'city' => [ - 'description' => __( 'City of the billing address.', 'woocommerce-payments' ), - 'type' => 'string', - 'context' => [ 'view' ], - ], - 'country' => [ - 'description' => __( 'Country of the billing address.', 'woocommerce-payments' ), - 'type' => 'string', - 'context' => [ 'view' ], - ], - 'line1' => [ - 'description' => __( 'Line 1 of the billing address.', 'woocommerce-payments' ), - 'type' => 'string', - 'context' => [ 'view' ], - ], - 'line2' => [ - 'description' => __( 'Line 2 of the billing address.', 'woocommerce-payments' ), - 'type' => 'string', - 'context' => [ 'view' ], - ], - 'postal_code' => [ - 'description' => __( 'Postal code of the billing address.', 'woocommerce-payments' ), - 'type' => 'string', - 'context' => [ 'view' ], - ], - 'state' => [ - 'description' => __( 'State of the billing address.', 'woocommerce-payments' ), - 'type' => 'string', - 'context' => [ 'view' ], - ], - ], - ], - 'email' => [ - 'description' => __( 'Email associated with the billing details.', 'woocommerce-payments' ), - 'type' => 'string', - 'format' => 'email', - 'context' => [ 'view' ], - ], - 'name' => [ - 'description' => __( 'Name associated with the billing details.', 'woocommerce-payments' ), - 'type' => 'string', - 'context' => [ 'view' ], - ], - 'phone' => [ - 'description' => __( 'Phone number associated with the billing details.', 'woocommerce-payments' ), - 'type' => 'string', - 'context' => [ 'view' ], - ], - ], - ], - 'payment_method' => [ - 'description' => 'The payment method associated with this charge.', - 'type' => 'string', - 'context' => [ 'view' ], - ], - 'application_fee_amount' => [ - 'description' => 'The application fee amount.', - 'type' => 'integer', - 'context' => [ 'view' ], - ], - 'status' => [ - 'description' => 'The status of the payment intent created.', - 'type' => 'string', - 'context' => [ 'view' ], - ], - ], - ], - - ], - ]; - } - - /** - * Prepare each item for response. - * - * @param array|mixed $item Item to prepare. - * @param WP_REST_Request $request Request instance. - * - * @return WP_REST_Response|WP_Error|WP_REST_Response - */ - public function prepare_item_for_response( $item, $request ) { - $prepared_item = []; - $prepared_item['id'] = $item->get_id(); - $prepared_item['amount'] = $item->get_amount(); - $prepared_item['currency'] = $item->get_currency(); - $prepared_item['created'] = $item->get_created()->getTimestamp(); - $prepared_item['customer'] = $item->get_customer_id(); - $prepared_item['payment_method'] = $item->get_payment_method_id(); - $prepared_item['status'] = $item->get_status(); - - try { - $charge = $item->get_charge(); - $prepared_item['charge']['id'] = $charge->get_id(); - $prepared_item['charge']['amount'] = $charge->get_amount(); - $prepared_item['charge']['application_fee_amount'] = $charge->get_application_fee_amount(); - $prepared_item['charge']['status'] = $charge->get_status(); - - $billing_details = $charge->get_billing_details(); - if ( isset( $billing_details['address'] ) ) { - $prepared_item['charge']['billing_details']['address']['city'] = $billing_details['address']['city'] ?? ''; - $prepared_item['charge']['billing_details']['address']['country'] = $billing_details['address']['country'] ?? ''; - $prepared_item['charge']['billing_details']['address']['line1'] = $billing_details['address']['line1'] ?? ''; - $prepared_item['charge']['billing_details']['address']['line2'] = $billing_details['address']['line2'] ?? ''; - $prepared_item['charge']['billing_details']['address']['postal_code'] = $billing_details['address']['postal_code'] ?? ''; - $prepared_item['charge']['billing_details']['address']['state'] = $billing_details['address']['state'] ?? ''; - } - $prepared_item['charge']['billing_details']['email'] = $billing_details['email'] ?? ''; - $prepared_item['charge']['billing_details']['name'] = $billing_details['name'] ?? ''; - $prepared_item['charge']['billing_details']['phone'] = $billing_details['phone'] ?? ''; - - $payment_method_details = $charge->get_payment_method_details(); - if ( isset( $payment_method_details['card'] ) ) { - $prepared_item['charge']['payment_method_details']['card']['amount_authorized'] = $payment_method_details['card']['amount_authorized'] ?? ''; - $prepared_item['charge']['payment_method_details']['card']['brand'] = $payment_method_details['card']['brand'] ?? ''; - $prepared_item['charge']['payment_method_details']['card']['capture_before'] = $payment_method_details['card']['capture_before'] ?? ''; - $prepared_item['charge']['payment_method_details']['card']['country'] = $payment_method_details['card']['country'] ?? ''; - $prepared_item['charge']['payment_method_details']['card']['exp_month'] = $payment_method_details['card']['exp_month'] ?? ''; - $prepared_item['charge']['payment_method_details']['card']['exp_year'] = $payment_method_details['card']['exp_year'] ?? ''; - $prepared_item['charge']['payment_method_details']['card']['last4'] = $payment_method_details['card']['last4'] ?? ''; - $prepared_item['charge']['payment_method_details']['card']['three_d_secure'] = $payment_method_details['card']['three_d_secure'] ?? ''; - } - } catch ( \Throwable $e ) { - Logger::error( 'Failed to prepare payment intent for response: ' . $e ); - } - - $context = $request['context'] ?? 'view'; - $prepared_item = $this->add_additional_fields_to_object( $prepared_item, $request ); - $prepared_item = $this->filter_response_by_context( $prepared_item, $context ); - - return rest_ensure_response( $prepared_item ); - } - - } diff --git a/includes/admin/class-wc-rest-payments-payment-intents-create-controller.php b/includes/admin/class-wc-rest-payments-payment-intents-create-controller.php new file mode 100644 index 00000000000..37b711ebc2e --- /dev/null +++ b/includes/admin/class-wc-rest-payments-payment-intents-create-controller.php @@ -0,0 +1,376 @@ +namespace, + '/' . $this->rest_base, + [ + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => [ $this, 'create_payment_intent' ], + 'permission_callback' => [ $this, 'check_permission' ], + 'schema' => [ $this, 'get_item_schema' ], + ] + ); + } + + /** + * WC_REST_Payments_Payment_Intents_Create_Controller constructor. + * + * @param WC_Payments_API_Client $api_client WooCommerce Payments API client. + * @param WC_Payment_Gateway_WCPay $gateway WooCommerce Payments payment gateway. + * @param OrderService $order_service The new order servie. + * @param Level3Service $level3_service Level3 service instance. + */ + public function __construct( + WC_Payments_API_Client $api_client, + WC_Payment_Gateway_WCPay $gateway, + OrderService $order_service, + Level3Service $level3_service + ) { + parent::__construct( $api_client ); + + $this->gateway = $gateway; + $this->order_service = $order_service; + $this->level3_service = $level3_service; + } + + /** + * Create a payment intent. + * + * @param WP_REST_Request $request data about the request. + * + * @throws Rest_Request_Exception + */ + public function create_payment_intent( $request ) { + try { + + $order_id = $request->get_param( 'order_id' ); + $order = wc_get_order( $order_id ); + if ( ! $order ) { + throw new Rest_Request_Exception( __( 'Order not found', 'woocommerce-payments' ) ); + } + + $wcpay_server_request = Create_And_Confirm_Intention::create(); + + $currency = strtolower( $order->get_currency() ); + $amount = WC_Payments_Utils::prepare_amount( $order->get_total(), $currency ); + $wcpay_server_request->set_currency_code( $currency ); + $wcpay_server_request->set_amount( $amount ); + + $metadata = $this->order_service->get_payment_metadata( $order_id, Payment_Type::SINGLE() ); + $wcpay_server_request->set_metadata( $metadata ); + + $wcpay_server_request->set_customer( $request->get_param( 'customer' ) ); + $wcpay_server_request->set_level3( $this->level3_service->get_data_from_order( $order_id ) ); + $wcpay_server_request->set_payment_method( $request->get_param( 'payment_method' ) ); + $wcpay_server_request->set_payment_method_types( [ 'card' ] ); + $wcpay_server_request->set_off_session( true ); + $wcpay_server_request->set_capture_method( $this->gateway->get_option( 'manual_capture' ) && ( 'yes' === $this->gateway->get_option( 'manual_capture' ) ) ); + + $wcpay_server_request->assign_hook( 'wcpay_create_and_confirm_intent_request_api' ); + $intent = $wcpay_server_request->send(); + + $response = $this->prepare_item_for_response( $intent, $request ); + return rest_ensure_response( $this->prepare_response_for_collection( $response ) ); + + } catch ( \Throwable $e ) { + Logger::error( 'Failed to create an intention via REST API: ' . $e ); + return new WP_Error( 'wcpay_server_error', $e->getMessage(), [ 'status' => 500 ] ); + } + } + + + /** + * Item schema. + * + * @return array + */ + public function get_item_schema() { + return [ + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'payment_intent', + 'type' => 'object', + 'properties' => [ + 'id' => [ + 'description' => __( 'ID for the payment intent.', 'woocommerce-payments' ), + 'type' => 'string', + 'context' => [ 'view' ], + ], + 'amount' => [ + 'description' => __( 'The amount of the transaction.', 'woocommerce-payments' ), + 'type' => 'integer', + 'context' => [ 'view' ], + ], + 'currency' => [ + 'description' => __( 'The currency of the transaction.', 'woocommerce-payments' ), + 'type' => 'string', + 'context' => [ 'view' ], + ], + 'created' => [ + 'description' => __( 'Timestamp for when the payment intent was created.', 'woocommerce-payments' ), + 'type' => 'string', + 'context' => [ 'view' ], + ], + 'customer' => [ + 'description' => __( 'The customer id of the intent', 'woocommerce-payments' ), + 'type' => 'string', + 'context' => [ 'view' ], + ], + 'status' => [ + 'description' => __( 'The status of the payment intent.', 'woocommerce-payments' ), + 'type' => 'string', + 'context' => [ 'view' ], + ], + 'charge' => [ + 'description' => __( 'Charge object associated with this payment intention.', 'woocommerce-payments' ), + 'type' => 'object', + 'context' => [ 'view' ], + 'properties' => [ + 'id' => [ + 'description' => 'ID for the charge.', + 'type' => 'string', + 'context' => [ 'view' ], + ], + 'amount' => [ + 'description' => 'The amount of the charge.', + 'type' => 'integer', + 'context' => [ 'view' ], + ], + 'payment_method_details' => [ + 'description' => 'Details for the payment method used for the charge.', + 'type' => 'object', + 'properties' => [ + 'card' => [ + 'description' => 'Details for a card payment method.', + 'type' => 'object', + 'properties' => [ + 'amount_authorized' => [ + 'description' => 'The amount authorized by the card.', + 'type' => 'integer', + ], + 'brand' => [ + 'description' => 'The brand of the card.', + 'type' => 'string', + ], + 'capture_before' => [ + 'description' => 'Timestamp for when the authorization must be captured.', + 'type' => 'string', + ], + 'country' => [ + 'description' => 'The ISO country code.', + 'type' => 'string', + ], + 'exp_month' => [ + 'description' => 'The expiration month of the card.', + 'type' => 'integer', + ], + 'exp_year' => [ + 'description' => 'The expiration year of the card.', + 'type' => 'integer', + ], + 'last4' => [ + 'description' => 'The last 4 digits of the card.', + 'type' => 'string', + ], + 'three_d_secure' => [ + 'description' => 'Details for 3D Secure authentication.', + 'type' => 'object', + ], + ], + ], + ], + ], + 'billing_details' => [ + 'description' => __( 'Billing details for the payment method.', 'woocommerce-payments' ), + 'type' => 'object', + 'context' => [ 'view' ], + 'properties' => [ + 'address' => [ + 'description' => __( 'Address associated with the billing details.', 'woocommerce-payments' ), + 'type' => 'object', + 'context' => [ 'view' ], + 'properties' => [ + 'city' => [ + 'description' => __( 'City of the billing address.', 'woocommerce-payments' ), + 'type' => 'string', + 'context' => [ 'view' ], + ], + 'country' => [ + 'description' => __( 'Country of the billing address.', 'woocommerce-payments' ), + 'type' => 'string', + 'context' => [ 'view' ], + ], + 'line1' => [ + 'description' => __( 'Line 1 of the billing address.', 'woocommerce-payments' ), + 'type' => 'string', + 'context' => [ 'view' ], + ], + 'line2' => [ + 'description' => __( 'Line 2 of the billing address.', 'woocommerce-payments' ), + 'type' => 'string', + 'context' => [ 'view' ], + ], + 'postal_code' => [ + 'description' => __( 'Postal code of the billing address.', 'woocommerce-payments' ), + 'type' => 'string', + 'context' => [ 'view' ], + ], + 'state' => [ + 'description' => __( 'State of the billing address.', 'woocommerce-payments' ), + 'type' => 'string', + 'context' => [ 'view' ], + ], + ], + ], + 'email' => [ + 'description' => __( 'Email associated with the billing details.', 'woocommerce-payments' ), + 'type' => 'string', + 'format' => 'email', + 'context' => [ 'view' ], + ], + 'name' => [ + 'description' => __( 'Name associated with the billing details.', 'woocommerce-payments' ), + 'type' => 'string', + 'context' => [ 'view' ], + ], + 'phone' => [ + 'description' => __( 'Phone number associated with the billing details.', 'woocommerce-payments' ), + 'type' => 'string', + 'context' => [ 'view' ], + ], + ], + ], + 'payment_method' => [ + 'description' => 'The payment method associated with this charge.', + 'type' => 'string', + 'context' => [ 'view' ], + ], + 'application_fee_amount' => [ + 'description' => 'The application fee amount.', + 'type' => 'integer', + 'context' => [ 'view' ], + ], + 'status' => [ + 'description' => 'The status of the payment intent created.', + 'type' => 'string', + 'context' => [ 'view' ], + ], + ], + ], + + ], + ]; + } + + /** + * Prepare each item for response. + * + * @param array|mixed $item Item to prepare. + * @param WP_REST_Request $request Request instance. + * + * @return WP_REST_Response|WP_Error|WP_REST_Response + */ + public function prepare_item_for_response( $item, $request ) { + $prepared_item = []; + $prepared_item['id'] = $item->get_id(); + $prepared_item['amount'] = $item->get_amount(); + $prepared_item['currency'] = $item->get_currency(); + $prepared_item['created'] = $item->get_created()->getTimestamp(); + $prepared_item['customer'] = $item->get_customer_id(); + $prepared_item['payment_method'] = $item->get_payment_method_id(); + $prepared_item['status'] = $item->get_status(); + + try { + $charge = $item->get_charge(); + $prepared_item['charge']['id'] = $charge->get_id(); + $prepared_item['charge']['amount'] = $charge->get_amount(); + $prepared_item['charge']['application_fee_amount'] = $charge->get_application_fee_amount(); + $prepared_item['charge']['status'] = $charge->get_status(); + + $billing_details = $charge->get_billing_details(); + if ( isset( $billing_details['address'] ) ) { + $prepared_item['charge']['billing_details']['address']['city'] = $billing_details['address']['city'] ?? ''; + $prepared_item['charge']['billing_details']['address']['country'] = $billing_details['address']['country'] ?? ''; + $prepared_item['charge']['billing_details']['address']['line1'] = $billing_details['address']['line1'] ?? ''; + $prepared_item['charge']['billing_details']['address']['line2'] = $billing_details['address']['line2'] ?? ''; + $prepared_item['charge']['billing_details']['address']['postal_code'] = $billing_details['address']['postal_code'] ?? ''; + $prepared_item['charge']['billing_details']['address']['state'] = $billing_details['address']['state'] ?? ''; + } + $prepared_item['charge']['billing_details']['email'] = $billing_details['email'] ?? ''; + $prepared_item['charge']['billing_details']['name'] = $billing_details['name'] ?? ''; + $prepared_item['charge']['billing_details']['phone'] = $billing_details['phone'] ?? ''; + + $payment_method_details = $charge->get_payment_method_details(); + if ( isset( $payment_method_details['card'] ) ) { + $prepared_item['charge']['payment_method_details']['card']['amount_authorized'] = $payment_method_details['card']['amount_authorized'] ?? ''; + $prepared_item['charge']['payment_method_details']['card']['brand'] = $payment_method_details['card']['brand'] ?? ''; + $prepared_item['charge']['payment_method_details']['card']['capture_before'] = $payment_method_details['card']['capture_before'] ?? ''; + $prepared_item['charge']['payment_method_details']['card']['country'] = $payment_method_details['card']['country'] ?? ''; + $prepared_item['charge']['payment_method_details']['card']['exp_month'] = $payment_method_details['card']['exp_month'] ?? ''; + $prepared_item['charge']['payment_method_details']['card']['exp_year'] = $payment_method_details['card']['exp_year'] ?? ''; + $prepared_item['charge']['payment_method_details']['card']['last4'] = $payment_method_details['card']['last4'] ?? ''; + $prepared_item['charge']['payment_method_details']['card']['three_d_secure'] = $payment_method_details['card']['three_d_secure'] ?? ''; + } + } catch ( \Throwable $e ) { + Logger::error( 'Failed to prepare payment intent for response: ' . $e ); + } + + $context = $request['context'] ?? 'view'; + $prepared_item = $this->add_additional_fields_to_object( $prepared_item, $request ); + $prepared_item = $this->filter_response_by_context( $prepared_item, $context ); + + return rest_ensure_response( $prepared_item ); + } + + +} diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php index b0882e7e03d..5d11dcf3c2f 100644 --- a/includes/class-wc-payments.php +++ b/includes/class-wc-payments.php @@ -1074,13 +1074,17 @@ public static function init_rest_api() { } include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-payment-intents-controller.php'; - $payment_intents_controller = new WC_REST_Payments_Payment_Intents_Controller( + $payment_intents_controller = new WC_REST_Payments_Payment_Intents_Controller( self::$api_client ); + $payment_intents_controller->register_routes(); + + include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-payment-intents-create-controller.php'; + $payment_intents_create_controller = new WC_REST_Payments_Payment_Intents_Create_Controller( self::$api_client, self::get_gateway(), wcpay_get_container()->get( OrderService::class ), wcpay_get_container()->get( Level3Service::class ) ); - $payment_intents_controller->register_routes(); + $payment_intents_create_controller->register_routes(); include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-authorizations-controller.php'; $authorizations_controller = new WC_REST_Payments_Authorizations_Controller( self::$api_client ); diff --git a/tests/unit/admin/test-class-wc-rest-payments-payment-intents-controller.php b/tests/unit/admin/test-class-wc-rest-payments-payment-intents-controller.php index 731727a3daf..1bdb96fe54b 100644 --- a/tests/unit/admin/test-class-wc-rest-payments-payment-intents-controller.php +++ b/tests/unit/admin/test-class-wc-rest-payments-payment-intents-controller.php @@ -1,6 +1,6 @@ mock_order_service = $this->createMock( OrderService::class ); $this->mock_level3_service = $this->createMock( Level3Service::class ); - $this->controller = new WC_REST_Payments_Payment_Intents_Controller( + $this->controller = new WC_REST_Payments_Payment_Intents_Create_Controller( $this->mock_api_client, $this->mock_gateway, $this->mock_order_service, From cea60b317ccbcaf7caeeb89f70a5c05d0185f48f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ismael=20Mart=C3=ADn=20Alabarce?= Date: Wed, 11 Oct 2023 14:13:00 +0200 Subject: [PATCH 04/60] Expose `is_account_partially_onboarded` to be used in WC Core (#7443) --- changelog/fix-7119-setup-woopayments-task-completion | 5 +++++ includes/class-wc-payment-gateway-wcpay.php | 10 ++++++++++ 2 files changed, 15 insertions(+) create mode 100644 changelog/fix-7119-setup-woopayments-task-completion diff --git a/changelog/fix-7119-setup-woopayments-task-completion b/changelog/fix-7119-setup-woopayments-task-completion new file mode 100644 index 00000000000..a87938cb18a --- /dev/null +++ b/changelog/fix-7119-setup-woopayments-task-completion @@ -0,0 +1,5 @@ +Significance: patch +Type: add +Comment: Exposing a function to be used in WC Core. + + diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index cc33a732379..b4e41089219 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -511,6 +511,16 @@ public function is_connected() { return $this->account->is_stripe_connected(); } + /** + * Checks if the account has not completed onboarding due to users abandoning the process half way. + * Also used by WC Core to complete the task "Set up WooPayments". + * + * @return bool + */ + public function is_account_partially_onboarded(): bool { + return $this->account->is_account_partially_onboarded(); + } + /** * Returns true if the gateway needs additional configuration, false if it's ready to use. * From 28df3ba496b77e568b9eadf84d72fbeac6ce6528 Mon Sep 17 00:00:00 2001 From: Oleksandr Aratovskyi <79862886+oaratovskyi@users.noreply.github.com> Date: Wed, 11 Oct 2023 15:49:15 +0300 Subject: [PATCH 05/60] Add additional security checks (#7442) --- .../fix-3579-add-additional-security-checks | 4 ++++ includes/admin/class-wc-payments-admin.php | 16 +++++++++++++++- .../unit/admin/test-class-wc-payments-admin.php | 3 +++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 changelog/fix-3579-add-additional-security-checks diff --git a/changelog/fix-3579-add-additional-security-checks b/changelog/fix-3579-add-additional-security-checks new file mode 100644 index 00000000000..84c5b874892 --- /dev/null +++ b/changelog/fix-3579-add-additional-security-checks @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Add additional security checks in the plugin diff --git a/includes/admin/class-wc-payments-admin.php b/includes/admin/class-wc-payments-admin.php index 2e2f309a4fb..0a875747f17 100644 --- a/includes/admin/class-wc-payments-admin.php +++ b/includes/admin/class-wc-payments-admin.php @@ -328,6 +328,9 @@ public function add_payments_menu_for_treatment() { * Add payments menu items. */ public function add_payments_menu() { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return; + } global $submenu; try { @@ -540,7 +543,9 @@ public function add_payments_menu() { * Register the CSS and JS scripts */ public function register_payments_scripts() { - // TODO: Add check to see if user can manage_woocommerce and exit early if they cannot. + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return; + } WC_Payments::register_script_with_dependencies( 'WCPAY_DASH_APP', 'dist/index' ); @@ -1025,6 +1030,9 @@ private function get_settings_menu_item_name() { * if it is not and the user is attempting to view a WCPay admin page. */ public function maybe_redirect_to_onboarding() { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return; + } if ( wp_doing_ajax() ) { return; } @@ -1071,6 +1079,9 @@ public function maybe_redirect_to_onboarding() { * @see self::add_payments_menu() */ public function maybe_redirect_overview_to_connect() { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return; + } if ( wp_doing_ajax() ) { return; } @@ -1105,6 +1116,9 @@ public function maybe_redirect_overview_to_connect() { * Redirect back to the connect page with an error message. */ public function maybe_redirect_onboarding_flow_to_connect(): void { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return; + } $url_params = wp_unslash( $_GET ); // phpcs:ignore WordPress.Security.NonceVerification if ( isset( $url_params['page'] ) && 'wc-admin' === $url_params['page'] && isset( $url_params['path'] ) && '/payments/onboarding' === $url_params['path'] && ! $this->payments_api_client->is_server_connected() ) { diff --git a/tests/unit/admin/test-class-wc-payments-admin.php b/tests/unit/admin/test-class-wc-payments-admin.php index 10079f3838b..c70e580c99b 100644 --- a/tests/unit/admin/test-class-wc-payments-admin.php +++ b/tests/unit/admin/test-class-wc-payments-admin.php @@ -206,6 +206,7 @@ private function mock_current_user_is_admin() { * @dataProvider data_maybe_redirect_to_onboarding */ public function test_maybe_redirect_to_onboarding( $expected_times_redirect_called, $is_stripe_connected, $get_params ) { + $this->mock_current_user_is_admin(); $_GET = $get_params; $this->mock_account @@ -283,6 +284,7 @@ public function data_maybe_redirect_to_onboarding() { */ public function test_maybe_redirect_overview_to_connect( $expected_times_redirect_called, $is_wc_registered_page, $get_params ) { global $wp_actions; + $this->mock_current_user_is_admin(); // Avoid WP doing_it_wrong warnings. // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $wp_actions['current_screen'] = true; @@ -384,6 +386,7 @@ public function data_maybe_redirect_overview_to_connect() { * @dataProvider data_maybe_redirect_onboarding_flow_to_connect */ public function test_maybe_redirect_onboarding_flow_to_connect( $expected_times_redirect_called, $is_server_connected, $get_params ) { + $this->mock_current_user_is_admin(); $_GET = $get_params; $this->mock_api_client From e115e39ac2df46ac5aefc8cb1daca23de9568128 Mon Sep 17 00:00:00 2001 From: leonardo lopes de albuquerque Date: Wed, 11 Oct 2023 10:27:35 -0300 Subject: [PATCH 06/60] Fixed the version check regex to allow major versions > 9 (#7352) --- .github/actions/version-check/action.yml | 4 ++-- changelog/fix-major-version-check | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 changelog/fix-major-version-check diff --git a/.github/actions/version-check/action.yml b/.github/actions/version-check/action.yml index 8eaa13705fb..0a2627ec047 100644 --- a/.github/actions/version-check/action.yml +++ b/.github/actions/version-check/action.yml @@ -29,9 +29,9 @@ runs: TRIMMED_VERSION=$(echo "$VERSION" | xargs) if ${{ env.IS_PRERELEASE == 'true' }}; then - VERSION_FORMAT="^[0-9]\.[0-9]\.[0-9]-test-[1-9]$" + VERSION_FORMAT="^[0-9]+\.[0-9]\.[0-9]+-test-[1-9]$" else - VERSION_FORMAT="^[0-9]\.[0-9]\.[0-9]$" + VERSION_FORMAT="^[0-9]+\.[0-9]\.[0-9]+$" fi if [[ $TRIMMED_VERSION =~ $VERSION_FORMAT ]]; then diff --git a/changelog/fix-major-version-check b/changelog/fix-major-version-check new file mode 100644 index 00000000000..205c41a7451 --- /dev/null +++ b/changelog/fix-major-version-check @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Fixed version check regex From 63333783683c07ec4a3775f0bf0e3be5e7371ed2 Mon Sep 17 00:00:00 2001 From: leonardo lopes de albuquerque Date: Wed, 11 Oct 2023 10:27:53 -0300 Subject: [PATCH 07/60] Now the setup intents ids are no more a link to the transaction page (#7359) --- changelog/fix-setup-intent-link | 4 ++++ includes/class-wc-payments-utils.php | 4 ++++ 2 files changed, 8 insertions(+) create mode 100644 changelog/fix-setup-intent-link diff --git a/changelog/fix-setup-intent-link b/changelog/fix-setup-intent-link new file mode 100644 index 00000000000..8248d64ac79 --- /dev/null +++ b/changelog/fix-setup-intent-link @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Removed link to setup intent diff --git a/includes/class-wc-payments-utils.php b/includes/class-wc-payments-utils.php index 171f11b4c55..3b91d13bc3d 100644 --- a/includes/class-wc-payments-utils.php +++ b/includes/class-wc-payments-utils.php @@ -660,6 +660,10 @@ public static function compose_transaction_url( $primary_id, $fallback_id, $quer return ''; } + if ( strpos( $primary_id, 'seti_' ) !== false ) { + return ''; + } + return add_query_arg( // nosemgrep: audit.php.wp.security.xss.query-arg -- server generated url is passed in. array_merge( [ From 163b12207971cd2ed674ee6da8af1bb4544145bf Mon Sep 17 00:00:00 2001 From: Brian Borman <68524302+bborman22@users.noreply.github.com> Date: Wed, 11 Oct 2023 15:08:40 -0400 Subject: [PATCH 08/60] Disable WooPay first party auth when using adapted extensions (#7455) --- changelog/fix-disable-woopay-first-party | 4 ++++ includes/woopay/class-woopay-scheduler.php | 6 ++++++ 2 files changed, 10 insertions(+) create mode 100644 changelog/fix-disable-woopay-first-party diff --git a/changelog/fix-disable-woopay-first-party b/changelog/fix-disable-woopay-first-party new file mode 100644 index 00000000000..3ba91212297 --- /dev/null +++ b/changelog/fix-disable-woopay-first-party @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Set WooPay first party feature flag to off when incompatible extensions are active. diff --git a/includes/woopay/class-woopay-scheduler.php b/includes/woopay/class-woopay-scheduler.php index f2044d3c49b..e57cce1345f 100644 --- a/includes/woopay/class-woopay-scheduler.php +++ b/includes/woopay/class-woopay-scheduler.php @@ -117,6 +117,12 @@ public function update_compatibility_and_maybe_show_incompatibility_warning() { public function update_enabled_adapted_extensions( $active_plugins, $adapted_extensions ) { $enabled_adapted_extensions = $this->get_extensions_in_list( $active_plugins, $adapted_extensions ); + if ( count( $enabled_adapted_extensions ) > 0 ) { + update_option( '_wcpay_feature_woopay_first_party_auth', 0 ); + } else { + update_option( '_wcpay_feature_woopay_first_party_auth', 1 ); + } + update_option( self::ENABLED_ADAPTED_EXTENSIONS_OPTION_NAME, $enabled_adapted_extensions ); } From 8f59c78ecc88a2b384f2dc297ec4d16609a33b78 Mon Sep 17 00:00:00 2001 From: Shendy <73803630+shendy-a8c@users.noreply.github.com> Date: Thu, 12 Oct 2023 13:10:49 +0700 Subject: [PATCH 09/60] Remove 'dispute' from possible value of ParentSegment in `details-link`. (#7460) --- changelog/remove-7363-parentsegment-dispute | 5 +++++ client/components/details-link/index.tsx | 2 +- .../details-link/test/__snapshots__/index.test.tsx.snap | 2 +- client/components/details-link/test/index.test.tsx | 4 ++-- 4 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 changelog/remove-7363-parentsegment-dispute diff --git a/changelog/remove-7363-parentsegment-dispute b/changelog/remove-7363-parentsegment-dispute new file mode 100644 index 00000000000..d60f08bbd34 --- /dev/null +++ b/changelog/remove-7363-parentsegment-dispute @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Removing possible value of an argument type that is no longer used. + + diff --git a/client/components/details-link/index.tsx b/client/components/details-link/index.tsx index 599247ce6c5..03226a5eb8e 100644 --- a/client/components/details-link/index.tsx +++ b/client/components/details-link/index.tsx @@ -15,7 +15,7 @@ import { getAdminUrl } from 'wcpay/utils'; /** * The parent segment is the first part of the URL after the /payments/ path. */ -type ParentSegment = 'deposits' | 'transactions' | 'disputes'; +type ParentSegment = 'deposits' | 'transactions'; export const getDetailsURL = ( /** diff --git a/client/components/details-link/test/__snapshots__/index.test.tsx.snap b/client/components/details-link/test/__snapshots__/index.test.tsx.snap index fee05f412bd..a2b861aebf1 100644 --- a/client/components/details-link/test/__snapshots__/index.test.tsx.snap +++ b/client/components/details-link/test/__snapshots__/index.test.tsx.snap @@ -6,7 +6,7 @@ exports[`Details link renders dispute details with ID 1`] = `
{ test( 'renders dispute details with ID', () => { const { container: link } = render( - + ); expect( link ).toMatchSnapshot(); } ); test( 'empty render with no ID', () => { const { container: link } = render( - + ); expect( link ).toMatchSnapshot(); } ); From 48888427ddafa8f102b35fc21781c542a0aabe64 Mon Sep 17 00:00:00 2001 From: Shendy <73803630+shendy-a8c@users.noreply.github.com> Date: Thu, 12 Oct 2023 13:11:50 +0700 Subject: [PATCH 10/60] Rename dispute action from `acceptTransactionDetailsDispute()` to `acceptDispute()` (#7456) Co-authored-by: Eric Jinks <3147296+Jinksi@users.noreply.github.com> --- changelog/remove-7363-legacy-dispute-details | 5 + .../remove-7363-unused-dispute-details-code | 5 + client/data/disputes/actions.js | 62 +- client/data/disputes/hooks.ts | 12 +- client/data/disputes/test/actions.js | 10 +- client/disputes/details/actions.tsx | 74 - client/disputes/details/index.tsx | 159 - .../test/__snapshots__/actions.tsx.snap | 35 - .../details/test/__snapshots__/index.tsx.snap | 5096 ----------------- client/disputes/details/test/actions.tsx | 80 - client/disputes/details/test/index.tsx | 148 - client/disputes/style.scss | 6 - 12 files changed, 22 insertions(+), 5670 deletions(-) create mode 100644 changelog/remove-7363-legacy-dispute-details create mode 100644 changelog/remove-7363-unused-dispute-details-code delete mode 100644 client/disputes/details/actions.tsx delete mode 100644 client/disputes/details/index.tsx delete mode 100644 client/disputes/details/test/__snapshots__/actions.tsx.snap delete mode 100644 client/disputes/details/test/__snapshots__/index.tsx.snap delete mode 100644 client/disputes/details/test/actions.tsx delete mode 100644 client/disputes/details/test/index.tsx diff --git a/changelog/remove-7363-legacy-dispute-details b/changelog/remove-7363-legacy-dispute-details new file mode 100644 index 00000000000..da3e0db1cf4 --- /dev/null +++ b/changelog/remove-7363-legacy-dispute-details @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Removing unused legacy dispute details code that does not affect user facing UX. + + diff --git a/changelog/remove-7363-unused-dispute-details-code b/changelog/remove-7363-unused-dispute-details-code new file mode 100644 index 00000000000..da3e0db1cf4 --- /dev/null +++ b/changelog/remove-7363-unused-dispute-details-code @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Removing unused legacy dispute details code that does not affect user facing UX. + + diff --git a/client/data/disputes/actions.js b/client/data/disputes/actions.js index 4a0f82197e1..3979390e3cf 100644 --- a/client/data/disputes/actions.js +++ b/client/data/disputes/actions.js @@ -13,7 +13,6 @@ import { __, sprintf } from '@wordpress/i18n'; import { NAMESPACE, STORE_NAME } from '../constants'; import TYPES from './action-types'; import wcpayTracks from 'tracks'; -import { getAdminUrl } from 'wcpay/utils'; import { getPaymentIntent } from '../payment-intents/resolvers'; export function updateDispute( data ) { @@ -48,66 +47,7 @@ export function updateDisputesSummary( query, data ) { }; } -export function* acceptDispute( id ) { - try { - yield controls.dispatch( STORE_NAME, 'startResolution', 'getDispute', [ - id, - ] ); - - const dispute = yield apiFetch( { - path: `${ NAMESPACE }/disputes/${ id }/close`, - method: 'post', - } ); - - yield updateDispute( dispute ); - yield controls.dispatch( STORE_NAME, 'finishResolution', 'getDispute', [ - id, - ] ); - - // Redirect to Disputes list. - window.location.replace( - getAdminUrl( { - page: 'wc-admin', - path: '/payments/disputes', - filter: 'awaiting_response', - } ) - ); - - wcpayTracks.recordEvent( 'wcpay_dispute_accept_success' ); - const message = dispute.order - ? sprintf( - /* translators: #%s is an order number, e.g. 15 */ - __( - 'You have accepted the dispute for order #%s.', - 'woocommerce-payments' - ), - dispute.order.number - ) - : __( 'You have accepted the dispute.', 'woocommerce-payments' ); - yield controls.dispatch( - 'core/notices', - 'createSuccessNotice', - message - ); - } catch ( e ) { - const message = __( - 'There has been an error accepting the dispute. Please try again later.', - 'woocommerce-payments' - ); - wcpayTracks.recordEvent( 'wcpay_dispute_accept_failed' ); - yield controls.dispatch( 'core/notices', 'createErrorNotice', message ); - } -} - -// This function handles the dispute acceptance flow from the Transaction Details screen. -// It differs from the `acceptDispute` function above in that it also fetches and updates -// the payment intent associated with the dispute to reflect changes to the dispute -// on the Transaction Details screen. -// -// Once the '_wcpay_feature_dispute_on_transaction_page' is enabled by default, -// the `acceptDispute` function above can be removed and this function can be renamed -// to `acceptDispute`. -export function* acceptTransactionDetailsDispute( dispute ) { +export function* acceptDispute( dispute ) { const { id, payment_intent: paymentIntent } = dispute; try { diff --git a/client/data/disputes/hooks.ts b/client/data/disputes/hooks.ts index e3797499537..b8a95b1e5e6 100644 --- a/client/data/disputes/hooks.ts +++ b/client/data/disputes/hooks.ts @@ -20,7 +20,7 @@ import { STORE_NAME } from '../constants'; import { disputeAwaitingResponseStatuses } from 'wcpay/disputes/filters/config'; /** - * Returns the dispute object, loading state, and accept function. + * Returns the dispute object, error object, and loading state. * Fetches the dispute object if it is not already cached. */ export const useDispute = ( @@ -29,7 +29,6 @@ export const useDispute = ( dispute?: Dispute; error?: ApiError; isLoading: boolean; - doAccept: () => void; } => { const { dispute, error, isLoading } = useSelect( ( select ) => { @@ -46,10 +45,7 @@ export const useDispute = ( [ id ] ); - const { acceptDispute } = useDispatch( STORE_NAME ); - const doAccept = () => acceptDispute( id ); - - return { dispute, isLoading, error, doAccept }; + return { dispute, isLoading, error }; }; /** @@ -72,8 +68,8 @@ export const useDisputeAccept = ( }, [ dispute.id ] ); - const { acceptTransactionDetailsDispute } = useDispatch( STORE_NAME ); - const doAccept = () => acceptTransactionDetailsDispute( dispute ); + const { acceptDispute } = useDispatch( STORE_NAME ); + const doAccept = () => acceptDispute( dispute ); return { doAccept, isLoading }; }; diff --git a/client/data/disputes/test/actions.js b/client/data/disputes/test/actions.js index f70a2d8da5e..68639fc1e20 100644 --- a/client/data/disputes/test/actions.js +++ b/client/data/disputes/test/actions.js @@ -10,12 +10,14 @@ import { controls } from '@wordpress/data'; * Internal dependencies */ import { acceptDispute, updateDispute } from '../actions'; +import { getPaymentIntent } from '../../payment-intents/resolvers'; describe( 'acceptDispute action', () => { const mockDispute = { id: 'dp_mock1', reason: 'product_unacceptable', status: 'lost', + payment_intent: 'payment_intent', }; beforeEach( () => { @@ -27,7 +29,7 @@ describe( 'acceptDispute action', () => { } ); test( 'should close dispute and update state with dispute data', () => { - const generator = acceptDispute( 'dp_mock1' ); + const generator = acceptDispute( mockDispute ); expect( generator.next().value ).toEqual( controls.dispatch( 'wc/payments', 'startResolution', 'getDispute', [ @@ -43,6 +45,9 @@ describe( 'acceptDispute action', () => { expect( generator.next( mockDispute ).value ).toEqual( updateDispute( mockDispute ) ); + expect( generator.next().value ).toEqual( + getPaymentIntent( mockDispute.payment_intent ) + ); expect( generator.next().value ).toEqual( controls.dispatch( 'wc/payments', @@ -53,7 +58,6 @@ describe( 'acceptDispute action', () => { ); const noticeAction = generator.next().value; - expect( window.location.replace ).toHaveBeenCalledTimes( 1 ); expect( noticeAction ).toEqual( controls.dispatch( 'core/notices', @@ -65,7 +69,7 @@ describe( 'acceptDispute action', () => { } ); test( 'should show notice on error', () => { - const generator = acceptDispute( 'dp_mock1' ); + const generator = acceptDispute( mockDispute ); generator.next(); expect( generator.throw( { code: 'error' } ).value ).toEqual( diff --git a/client/disputes/details/actions.tsx b/client/disputes/details/actions.tsx deleted file mode 100644 index d67fa5bee28..00000000000 --- a/client/disputes/details/actions.tsx +++ /dev/null @@ -1,74 +0,0 @@ -/** @format **/ - -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; -import { Button } from '@wordpress/components'; -import React from 'react'; - -/** - * Internal dependencies - */ -import wcpayTracks from 'tracks'; -import { getAdminUrl } from 'wcpay/utils'; - -const Actions = ( { - id, - needsResponse, - isSubmitted, - onAccept, -}: { - id: string; - needsResponse: boolean; - isSubmitted: boolean | undefined; - onAccept: () => void; -} ): JSX.Element => { - if ( ! needsResponse && ! isSubmitted ) { - return <>; - } - - const challengeUrl = getAdminUrl( { - page: 'wc-admin', - path: '/payments/disputes/challenge', - id, - } ); - - const acceptMessage = __( - "Are you sure you'd like to accept this dispute? This action can not be undone.", - 'woocommerce-payments' - ); - - return ( -
- - { needsResponse && ( - - ) } -
- ); -}; - -export default Actions; diff --git a/client/disputes/details/index.tsx b/client/disputes/details/index.tsx deleted file mode 100644 index e8af4bbcf97..00000000000 --- a/client/disputes/details/index.tsx +++ /dev/null @@ -1,159 +0,0 @@ -/** @format **/ - -/** - * External dependencies - */ -import React from 'react'; -import { __, sprintf } from '@wordpress/i18n'; -import { Card, CardBody, CardFooter, CardHeader } from '@wordpress/components'; - -/** - * Internal dependencies. - */ -import { useDispute } from 'data/index'; -import { reasons } from '../strings'; -import Actions from './actions'; -import Info from '../info'; -import Paragraphs from 'components/paragraphs'; -import Page from 'components/page'; -import ErrorBoundary from 'components/error-boundary'; -import DisputeStatusChip from 'components/dispute-status-chip'; -import Loadable, { LoadableBlock } from 'components/loadable'; -import { TestModeNotice, topics } from 'components/test-mode-notice'; -import '../style.scss'; -import { Dispute } from 'wcpay/types/disputes'; - -const LegacyDisputeDetails = ( { - query: { id: disputeId }, -}: { - query: { id: string }; -} ): JSX.Element => { - const { dispute, isLoading, doAccept } = useDispute( disputeId ); - const disputeObject = dispute || ( {} as Dispute ); - const disputeIsAvailable = ! isLoading && dispute && disputeObject.id; - - const actions = disputeIsAvailable && ( - - ); - - const mapping = reasons[ disputeObject.reason ] || {}; - const testModeNotice = ; - - if ( ! isLoading && ! disputeIsAvailable ) { - return ( - - { testModeNotice } - -
- { __( 'Dispute not loaded', 'woocommerce-payments' ) } -
-
-
- ); - } - - return ( - - { testModeNotice } - - - - - { __( 'Dispute overview', 'woocommerce-payments' ) } - - - - - - - { mapping.overview } - - - - - { actions || [] } - - - - - - - - - - - - { mapping.summary } - - - - { mapping.required && ( -

- { ' ' } - { __( - 'Required to overturn dispute', - 'woocommerce-payments' - ) }{ ' ' } -

- ) } - { mapping.required } -
- - - { mapping.respond && ( -

- { __( - 'How to respond', - 'woocommerce-payments' - ) } -

- ) } - { mapping.respond } -
-
- - - { actions || [] } - - -
-
-
- ); -}; - -export default LegacyDisputeDetails; diff --git a/client/disputes/details/test/__snapshots__/actions.tsx.snap b/client/disputes/details/test/__snapshots__/actions.tsx.snap deleted file mode 100644 index f479757dcd4..00000000000 --- a/client/disputes/details/test/__snapshots__/actions.tsx.snap +++ /dev/null @@ -1,35 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Dispute details actions renders correctly for closed dispute 1`] = `
`; - -exports[`Dispute details actions renders correctly for dispute needing response, confirmation requested on submit 1`] = ` -
-
- - Challenge dispute - - -
-
-`; - -exports[`Dispute details actions renders correctly for dispute with evidence submitted 1`] = ` - -`; diff --git a/client/disputes/details/test/__snapshots__/index.tsx.snap b/client/disputes/details/test/__snapshots__/index.tsx.snap deleted file mode 100644 index 8baf2f66990..00000000000 --- a/client/disputes/details/test/__snapshots__/index.tsx.snap +++ /dev/null @@ -1,5096 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Dispute details screen renders correctly for bank_cannot_process dispute 1`] = ` -
-
-
-
-
- Dispute overview - - Needs response - -
-
-
-
- - Dispute date: - - - Nov 1, 2019 - -
-
- - Disputed amount: - - - $10.00 - -
-
- - Respond by: - - - Nov 8, 2019 - 2:46AM - -
-
- - Reason: - - - Bank cannot process - -
-
- - Order: - - - - 1 - - -
-
- - Transaction ID: - - - - -
-
-
- -
-