Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disable the manual refunds button unless a WCPay refund has failed #2167

Merged
merged 14 commits into from
Jun 21, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions client/order/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/* global jQuery */

/**
* Internal dependencies
*/
import { getConfig } from 'utils/order';

jQuery( function ( $ ) {
const disableManualRefunds = getConfig( 'disableManualRefunds' ) ?? false;
const manualRefundsTip = getConfig( 'manualRefundsTip' ) ?? '';

if ( disableManualRefunds ) {
/**
* The script is included in the footer, so all the DOM must already be in place.
* This allows us to modify the tip before it gets used on document.ready.
*/
$( '.do-manual-refund' ).each( function () {
const $refundButton = $( this );

// Disable the manual refund button.
$refundButton
.addClass( 'disabled' )
.attr( 'readonly', 'readonly' )
// Add the right label to indicate why the button is disabled.
.attr( {
// Tips are readable through $.data(), but jQuery.tipTip use the title attribute to generate
// the tooltip.
title: manualRefundsTip,
} )
.on( 'click', function () {
return false;
} )
// Regenerate the tipTip tooltip.
.tipTip();
} );
}
} );
16 changes: 16 additions & 0 deletions client/utils/order.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/* global wcpay_order_config, wc */

/**
* Retrieves a configuration value.
*
* @param {string} name The name of the config parameter.
* @return {*} The value of the parameter of null.
*/
export const getConfig = ( name ) => {
// Config for the Edit Order screen.
const config =
wcpay_order_config ?? // eslint-disable-line camelcase
wc.wcSettings.getSetting( 'woocommerce_payments_data' );

return config?.[ name ];
};
25 changes: 25 additions & 0 deletions includes/admin/class-wc-payments-admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,14 @@ public function register_payments_scripts() {
WC_Payments::get_file_version( 'dist/tos.css' )
);

wp_register_script(
'WCPAY_ADMIN_ORDER_ACTIONS',
plugins_url( 'dist/order.js', WCPAY_PLUGIN_FILE ),
[ 'jquery-tiptip' ],
WC_Payments::get_file_version( 'dist/order.js' ),
true
);

$settings_script_src_url = plugins_url( 'dist/settings.js', WCPAY_PLUGIN_FILE );
$settings_script_asset_path = WCPAY_ABSPATH . 'dist/settings.asset.php';
$settings_script_asset = file_exists( $settings_script_asset_path ) ? require_once $settings_script_asset_path : [ 'dependencies' => [] ];
Expand Down Expand Up @@ -403,6 +411,23 @@ public function enqueue_payments_scripts() {
wp_enqueue_script( 'WCPAY_PAYMENT_GATEWAYS_PAGE' );
wp_enqueue_style( 'WCPAY_PAYMENT_GATEWAYS_PAGE' );
}

$screen = get_current_screen();
if ( 'shop_order' === $screen->id ) {
$order = wc_get_order();

if ( WC_Payment_Gateway_WCPay::GATEWAY_ID === $order->get_payment_method() ) {
wp_localize_script(
'WCPAY_ADMIN_ORDER_ACTIONS',
'wcpay_order_config',
[
'disableManualRefunds' => ! $this->wcpay_gateway->has_refund_failed( $order ),
'manualRefundsTip' => __( 'Refunds are available only through WooCommerce Payments.', 'woocommerce-payments' ),
]
);
wp_enqueue_script( 'WCPAY_ADMIN_ORDER_ACTIONS' );
}
}
}

/**
Expand Down
2 changes: 2 additions & 0 deletions includes/admin/class-wc-rest-payments-webhook-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ private function process_webhook_refund_updated( $event_body ) {
$refund_id
);
$order->add_order_note( $note );
$order->update_meta_data( '_wcpay_refund_status', 'failed' );
$order->save();
}

/**
Expand Down
28 changes: 22 additions & 6 deletions includes/class-wc-payment-gateway-wcpay.php
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ public function needs_setup() {
* @return bool
*/
public static function is_current_page_settings() {
return count( self::$settings_url_params ) === count( array_intersect_assoc( $_GET, self::$settings_url_params ) ); // phpcs:disable WordPress.Security.NonceVerification.Recommended
return count( self::$settings_url_params ) === count( array_intersect_assoc( $_GET, self::$settings_url_params ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
}

/**
Expand Down Expand Up @@ -488,11 +488,11 @@ public function output_payments_settings_screen() {
global $hide_save_button;
$hide_save_button = true;

if ( ! empty( $_GET['method'] ) ) :
if ( ! empty( $_GET['method'] ) ) : // phpcs:ignore WordPress.Security.NonceVerification.Recommended
?>
<div
id="wcpay-payment-method-settings-container"
data-method-id="<?php echo esc_attr( sanitize_text_field( wp_unslash( $_GET['method'] ) ) ); ?>"
data-method-id="<?php echo esc_attr( sanitize_text_field( wp_unslash( $_GET['method'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended ?>"
></div>
<?php else : ?>
<div id="wcpay-account-settings-container"></div>
Expand Down Expand Up @@ -580,7 +580,7 @@ public function save_payment_method_checkbox( $force_checked = false ) {
* @return array|null An array with customer data or nothing.
*/
public function get_prepared_customer_data() {
if ( ! isset( $_GET['pay_for_order'] ) && ! is_add_payment_method_page() ) {
if ( ! isset( $_GET['pay_for_order'] ) && ! is_add_payment_method_page() ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return null;
}

Expand All @@ -589,7 +589,7 @@ public function get_prepared_customer_data() {
$firstname = '';
$lastname = '';

if ( isset( $_GET['pay_for_order'] ) && 'true' === $_GET['pay_for_order'] ) {
if ( isset( $_GET['pay_for_order'] ) && 'true' === $_GET['pay_for_order'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$order_id = absint( $wp->query_vars['order-pay'] );
$order = wc_get_order( $order_id );

Expand Down Expand Up @@ -1093,7 +1093,9 @@ protected function get_payment_token( $order ) {
* @return bool
*/
public function can_refund_order( $order ) {
return $order && $order->get_meta( '_charge_id', true );
return $order
&& $order->get_meta( '_charge_id', true )
&& 'failed' !== $order->get_meta( '_wcpay_refund_status', true );
dechov marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down Expand Up @@ -1143,6 +1145,8 @@ public function process_refund( $order_id, $amount = null, $reason = '' ) {

Logger::log( $note );
$order->add_order_note( $note );
$order->update_meta_data( '_wcpay_refund_status', 'failed' );
$order->save();

Tracker::track_admin( 'wcpay_edit_order_refund_failure', [ 'reason' => $note ] );
return new WP_Error( 'wcpay_edit_order_refund_failure', $e->getMessage() );
Expand All @@ -1164,10 +1168,22 @@ public function process_refund( $order_id, $amount = null, $reason = '' ) {
}

$order->add_order_note( $note );
$order->update_meta_data( '_wcpay_refund_status', 'successful' );
$order->save();

return true;
}

/**
* Checks whether a refund through the gateway has already failed.
*
* @param WC_Order $order The order to check.
* @return boolean
*/
public function has_refund_failed( $order ) {
return 'failed' === $order->get_meta( '_wcpay_refund_status', true );
}

/**
* Overrides the original method in woo's WC_Settings_API in order to conditionally render the enabled checkbox.
*
Expand Down
73 changes: 73 additions & 0 deletions tests/unit/admin/test-class-wc-rest-payments-webhook.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,79 @@ public function test_webhook_with_no_data_property() {
$this->assertEquals( [ 'result' => 'bad_request' ], $response_data );
}

public function test_valid_failed_refund_webhook_sets_failed_meta() {
// Setup test request data.
$this->request_body['type'] = 'charge.refund.updated';
$this->request_body['data']['object'] = [
'status' => 'failed',
'charge' => 'test_charge_id',
'id' => 'test_refund_id',
'amount' => 999,
'currency' => 'gbp',
];

$this->request->set_body( wp_json_encode( $this->request_body ) );

$mock_order = $this->getMockBuilder( WC_Order::class )
->disableOriginalConstructor()
->setMethods( [ 'add_order_note', 'update_meta_data' ] )
->getMock();

$mock_order
->expects( $this->once() )
->method( 'add_order_note' )
->with(
$this->matchesRegularExpression(
'~^A refund of <span class="woocommerce-Price-amount amount">(<bdi>)?<span class="woocommerce-Price-currencySymbol">&pound;</span>9.99(</bdi>)?</span> was <strong>unsuccessful</strong> using WooCommerce Payments \(<code>test_refund_id</code>\).$~'
)
);

// The expects condition here is the real test; we expect that the 'update_meta_data' function
// is called with the appropriate values.
$mock_order
->expects( $this->once() )
->method( 'update_meta_data' )
->with( '_wcpay_refund_status', 'failed' );

$this->mock_db_wrapper
->expects( $this->once() )
->method( 'order_from_charge_id' )
->with( 'test_charge_id' )
->willReturn( $mock_order );

// Run the test.
$this->controller->handle_webhook( $this->request );
}

public function test_non_failed_refund_update_webhook_does_not_set_failed_meta() {
// Setup test request data.
$this->request_body['type'] = 'charge.refund.updated';
$this->request_body['data']['object'] = [
'status' => 'success',
];

$this->request->set_body( wp_json_encode( $this->request_body ) );

$mock_order = $this->getMockBuilder( WC_Order::class )
->disableOriginalConstructor()
->setMethods( [ 'update_meta_data' ] )
->getMock();

$this->mock_db_wrapper
->expects( $this->never() )
->method( 'order_from_charge_id' );

// The expects condition here is the real test; we expect that the 'update_meta_data' function
// is never called to update the meta data.
$mock_order
->expects( $this->never() )
->method( 'update_meta_data' );

// Run the test.
$this->controller->handle_webhook( $this->request );

}

/**
* Test a valid failed refund update webhook.
*/
Expand Down
54 changes: 54 additions & 0 deletions tests/unit/test-class-wc-payment-gateway-wcpay.php
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,60 @@ public function test_process_refund_on_uncaptured_payment() {
$this->assertEquals( 'uncaptured-payment', $result->get_error_code() );
}

public function test_process_refund_success_does_not_set_refund_failed_meta() {
$intent_id = 'pi_xxxxxxxxxxxxx';
$charge_id = 'ch_yyyyyyyyyyyyy';

$order = WC_Helper_Order::create_order();
$order->update_meta_data( '_intent_id', $intent_id );
$order->update_meta_data( '_charge_id', $charge_id );
$order->save();

$this->mock_api_client->expects( $this->once() )->method( 'refund_charge' )->will(
$this->returnValue(
[
'id' => 're_123456789',
'object' => 'refund',
'amount' => 19.99,
'balance_transaction' => 'txn_987654321',
'charge' => 'ch_121212121212',
'created' => 1610123467,
'payment_intent' => 'pi_1234567890',
'reason' => null,
'reciept_number' => null,
reykjalin marked this conversation as resolved.
Show resolved Hide resolved
'source_transfer_reversal' => null,
'status' => 'succeeded',
'transfer_reversal' => null,
'currency' => 'usd',
]
)
);

$this->wcpay_gateway->process_refund( $order->get_id(), 19.99 );
$this->assertEquals( '', $order->get_meta( '_wcpay_refund_status', true ) );
}

public function test_process_refund_failure_sets_refund_failed_meta() {
$intent_id = 'pi_xxxxxxxxxxxxx';
$charge_id = 'ch_yyyyyyyyyyyyy';

$order = WC_Helper_Order::create_order();
$order->update_meta_data( '_intent_id', $intent_id );
$order->update_meta_data( '_charge_id', $charge_id );
$order->update_status( 'processing' );
$order->save();

$order_id = $order->get_id();

$this->mock_api_client
->expects( $this->once() )
->method( 'refund_charge' )
->willThrowException( new \Exception( 'Test message' ) );

$result = $this->wcpay_gateway->process_refund( $order_id, 19.99 );
$this->assertEquals( 'failed', $order->get_meta( '_wcpay_refund_status', true ) );
}
reykjalin marked this conversation as resolved.
Show resolved Hide resolved

public function test_process_refund_on_api_error() {
$intent_id = 'pi_xxxxxxxxxxxxx';
$charge_id = 'ch_yyyyyyyyyyyyy';
Expand Down
1 change: 1 addition & 0 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const webpackConfig = {
'./client/additional-methods-setup/index.js',
'payment-gateways': './client/payment-gateways/index.js',
'multi-currency': './client/multi-currency/index.js',
order: './client/order/index.js',
},
output: {
filename: '[name].js',
Expand Down