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 all 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
33 changes: 33 additions & 0 deletions client/order/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/* global jQuery */

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

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

$( '#woocommerce-order-items' ).on(
'click',
'button.refund-items',
function () {
const $manualRefundButton = $( '.do-manual-refund' );

if ( disableManualRefunds ) {
$manualRefundButton.hide();
} else {
// Adjust the messaging on the manual refund button.
$manualRefundButton
.attr( {
// Tips are readable through $.data(), but jQuery.tipTip use the title attribute to generate
// the tooltip.
title: manualRefundsTip,
} )
// 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 @@ -302,6 +302,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 @@ -414,6 +422,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' => __( 'Refunding manually requires reimbursing your customer offline via cash, check, etc. The refund amounts entered here will only be used to balance your analytics.', '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
24 changes: 19 additions & 5 deletions includes/class-wc-payment-gateway-wcpay.php
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,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 @@ -504,11 +504,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 @@ -596,7 +596,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 @@ -605,7 +605,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 @@ -1166,6 +1166,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 @@ -1187,10 +1189,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
60 changes: 60 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,66 @@ 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,
'receipt_number' => null,
'source_transfer_reversal' => null,
'status' => 'succeeded',
'transfer_reversal' => null,
'currency' => 'usd',
]
)
);

$this->wcpay_gateway->process_refund( $order->get_id(), 19.99 );

// Reload the order information to get the new meta.
$order = wc_get_order( $order->get_id() );
$this->assertFalse( $this->wcpay_gateway->has_refund_failed( $order ) );
}

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' ) );

$this->wcpay_gateway->process_refund( $order_id, 19.99 );

// Reload the order information to get the new meta.
$order = wc_get_order( $order_id );
$this->assertTrue( $this->wcpay_gateway->has_refund_failed( $order ) );
}

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 @@ -20,6 +20,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