Skip to content

Commit

Permalink
Merge branch 'develop' into cleanup/upe-checkout-class
Browse files Browse the repository at this point in the history
  • Loading branch information
timur27 authored Dec 19, 2023
2 parents 7ceff11 + 21ee31d commit 3cf2d34
Show file tree
Hide file tree
Showing 14 changed files with 437 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: update

Confirmation when cancelling order with pending authorization. Automatic order changes submission if confirmed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: fix

Fixed a Level 3 error occurring during the capture of an authorization for amounts lower than the initial authorization amount.
134 changes: 134 additions & 0 deletions client/order/cancel-authorization-confirm-modal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/** @format */
/**
* External dependencies
*/
import * as React from 'react';
import { __ } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import interpolateComponents from '@automattic/interpolate-components';
import { useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import ConfirmationModal from 'wcpay/components/confirmation-modal';

interface CancelAuthorizationConfirmationModalProps {
originalOrderStatus: string;
}

const CancelAuthorizationConfirmationModal: React.FunctionComponent< CancelAuthorizationConfirmationModalProps > = ( {
originalOrderStatus,
} ) => {
const [
isCancelAuthorizationConfirmationModalOpen,
setIsCancelAuthorizationConfirmationModalOpen,
] = useState( true );

const closeModal = (): void => {
setIsCancelAuthorizationConfirmationModalOpen( false );
};

const handleCancelOrder = (): void => {
const orderEditForm: HTMLFormElement | null =
document.querySelector( '#order_status' )?.closest( 'form' ) ||
null;
if ( null !== orderEditForm ) {
orderEditForm.submit();
}
};

const doNotCancel = (): void => {
const orderStatusElement: HTMLInputElement | null = document.querySelector(
'#order_status'
);
if ( null !== orderStatusElement ) {
orderStatusElement.value = originalOrderStatus;
orderStatusElement.dispatchEvent( new Event( 'change' ) );
}
closeModal();
};

const cancelOrder = (): void => {
handleCancelOrder();
closeModal();
};

const buttonContent = (
<>
<Button isSecondary onClick={ doNotCancel }>
{ __( 'Do Nothing', 'woocommerce-payments' ) }
</Button>
<Button isPrimary onClick={ cancelOrder }>
{ __(
'Cancel order and authorization',
'woocommerce-payments'
) }
</Button>
</>
);

const confirmationMessage = interpolateComponents( {
mixedString: __(
'This order has been {{authorizedNotCaptured/}} yet. Changing the status to ' +
'Cancelled will also {{cancelAuthorization/}}. Do you want to continue?',
'woocommerce-payments'
),
components: {
authorizedNotCaptured: (
<a
target="_blank"
href="https://woo.com/document/woopayments/settings-guide/authorize-and-capture/#authorize-vs-capture"
rel="noopener noreferrer"
>
{ __(
'authorized but not captured',
'woocommerce-payments'
) }
</a>
),
cancelAuthorization: (
<a
target="_blank"
href="https://woo.com/document/woopayments/settings-guide/authorize-and-capture/#cancelling-authorizations"
rel="noopener noreferrer"
>
{ __( 'cancel the authorization', 'woocommerce-payments' ) }
</a>
),
doNothingBold: (
<b>{ __( 'Do Nothing', 'woocommerce-payments' ) }</b>
),
cancelOrderBold: (
<b>
{ __(
'Cancel order and authorization',
'woocommerce-payments'
) }
</b>
),
},
} );

return (
<>
{ isCancelAuthorizationConfirmationModalOpen && (
<ConfirmationModal
title={ __(
'Cancel authorization',
'woocommerce-payments'
) }
isDismissible={ false }
className="cancel-authorization-confirmation-modal"
actions={ buttonContent }
onRequestClose={ () => {
return false;
} }
>
<p>{ confirmationMessage }</p>
</ConfirmationModal>
) }
</>
);
};

export default CancelAuthorizationConfirmationModal;
27 changes: 21 additions & 6 deletions client/order/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { getConfig } from 'utils/order';
import { isAwaitingResponse, isUnderReview } from 'wcpay/disputes/utils';
import RefundConfirmationModal from './refund-confirm-modal';
import CancelConfirmationModal from './cancel-confirm-modal';
import CancelAuthorizationConfirmationModal from './cancel-authorization-confirm-modal';
import DisputedOrderNoticeHandler from 'wcpay/components/disputed-order-notice';

function disableWooOrderRefundButton( disputeStatus ) {
Expand Down Expand Up @@ -95,6 +96,7 @@ jQuery( function ( $ ) {

const canRefund = getConfig( 'canRefund' );
const refundAmount = getConfig( 'refundAmount' );
const hasOpenAuthorization = getConfig( 'hasOpenAuthorization' );
if (
this.value === 'wc-refunded' &&
originalStatus !== 'wc-refunded'
Expand All @@ -108,14 +110,27 @@ jQuery( function ( $ ) {
this.value === 'wc-cancelled' &&
originalStatus !== 'wc-cancelled'
) {
if ( ! canRefund || refundAmount <= 0 ) {
// If order has an uncaptured authorization, confirm
// that merchant indeed wants to cancel both the order
// and the authorization.
if ( hasOpenAuthorization ) {
renderModal(
<CancelAuthorizationConfirmationModal
originalOrderStatus={ originalStatus }
/>
);
return;
}
renderModal(
<CancelConfirmationModal
originalOrderStatus={ originalStatus }
/>
);
// If it is possible to refund an order, double check that
// merchants indeed wants to cancel, or if they just want to
// refund.
if ( canRefund && refundAmount > 0 ) {
renderModal(
<CancelConfirmationModal
originalOrderStatus={ originalStatus }
/>
);
}
}
} );

Expand Down
1 change: 0 additions & 1 deletion client/payment-methods-map.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/**
* External dependencies
*/
import React from 'react';
import { __ } from '@wordpress/i18n';

/**
Expand Down
1 change: 0 additions & 1 deletion client/payment-methods/capability-request/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
* External dependencies
*/
import React from 'react';
import { __ } from '@wordpress/i18n';

import CapabilityRequestList from './capability-request-map';
import CapabilityNotice from './capability-request-notice';
Expand Down
4 changes: 1 addition & 3 deletions client/transactions/list/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -658,9 +658,7 @@ export const TransactionsList = (
window.confirm( confirmMessage )
) {
try {
const {
exported_transactions: exportedTransactions,
} = await apiFetch( {
await apiFetch( {
path: getTransactionsCSV( {
userEmail,
dateAfter,
Expand Down
1 change: 1 addition & 0 deletions includes/admin/class-wc-payments-admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,7 @@ public function enqueue_payments_scripts() {
'refundedAmount' => $order->get_total_refunded(),
'canRefund' => $this->wcpay_gateway->can_refund_order( $order ),
'chargeId' => $this->order_service->get_charge_id_for_order( $order ),
'hasOpenAuthorization' => $this->order_service->has_open_authorization( $order ),
]
);
wp_localize_script(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ public function capture_authorization( WP_REST_Request $request ) {

$this->add_fraud_outcome_manual_entry( $order, 'approve' );

$result = $this->gateway->capture_charge( $order, false, $intent_metadata );
$result = $this->gateway->capture_charge( $order, true, $intent_metadata );

if ( Intent_Status::SUCCEEDED !== $result['status'] ) {
return new WP_Error(
Expand Down
47 changes: 47 additions & 0 deletions includes/class-wc-payment-gateway-wcpay.php
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,7 @@ public function init_hooks() {
add_action( 'woocommerce_order_actions', [ $this, 'add_order_actions' ] );
add_action( 'woocommerce_order_action_capture_charge', [ $this, 'capture_charge' ] );
add_action( 'woocommerce_order_action_cancel_authorization', [ $this, 'cancel_authorization' ] );
add_action( 'woocommerce_order_status_cancelled', [ $this, 'cancel_authorizations_on_order_cancel' ] );

add_action( 'wp_ajax_update_order_status', [ $this, 'update_order_status' ] );
add_action( 'wp_ajax_nopriv_update_order_status', [ $this, 'update_order_status' ] );
Expand Down Expand Up @@ -2863,6 +2864,52 @@ public function cancel_authorization( $order ) {
];
}

/**
* Cancels uncaptured authorizations on order cancel.
*
* @param int $order_id - Order ID.
*/
public function cancel_authorizations_on_order_cancel( $order_id ) {
$order = new WC_Order( $order_id );
if ( null !== $order ) {
$intent_id = $this->order_service->get_intent_id_for_order( $order );
if ( null !== $intent_id && '' !== $intent_id ) {
try {
$request = Get_Intention::create( $intent_id );
$request->set_hook_args( $order );
$intent = $request->send();
$charge = $intent->get_charge();

/**
* Successful but not captured Charge is an authorization
* that needs to be cancelled.
*/
if ( null !== $charge
&& false === $charge->is_captured()
&& Intent_Status::SUCCEEDED === $charge->get_status()
&& Intent_Status::REQUIRES_CAPTURE === $intent->get_status()
) {
$request = Cancel_Intention::create( $intent_id );
$request->set_hook_args( $order );
$intent = $request->send();

$this->order_service->post_unique_capture_cancelled_note( $order );
}

$this->order_service->set_intention_status_for_order( $order, $intent->get_status() );
$order->save();
} catch ( \Exception $e ) {
$order->add_order_note(
WC_Payments_Utils::esc_interpolated_html(
__( 'Canceling authorization <strong>failed</strong> to complete.', 'woocommerce-payments' ),
[ 'strong' => '<strong>' ]
)
);
}
}
}
}

/**
* Create the level 3 data array to send to Stripe when making a purchase.
*
Expand Down
34 changes: 31 additions & 3 deletions includes/class-wc-payments-order-service.php
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,21 @@ public function get_intention_status_for_order( $order ) : string {
return $order->get_meta( self::INTENTION_STATUS_META_KEY, true );
}

/**
* Checks if order has an open (uncaptured) authorization.
*
* @param mixed $order The order Id or order object.
*
* @return bool
*
* @throws Order_Not_Found_Exception
*/
public function has_open_authorization( $order ) : bool {
$order = $this->get_order( $order );
return Intent_Status::REQUIRES_CAPTURE === $order->get_meta( self::INTENTION_STATUS_META_KEY, true );
}


/**
* Set the payment metadata for customer id.
*
Expand Down Expand Up @@ -806,6 +821,21 @@ public function get_billing_data_from_order( WC_Order $order ): array {
];
}

/**
* Creates an "authorization cancelled" order note if not already present.
*
* @param WC_Order $order The order.
* @return boolean True if the note was added, false otherwise.
*/
public function post_unique_capture_cancelled_note( $order ) {
$note = $this->generate_capture_cancelled_note();
if ( ! $this->order_note_exists( $order, $note ) ) {
$order->add_order_note( $note );
return true;
}
return false;
}

/**
* Updates an order to cancelled status, while adding a note with a link to the transaction.
*
Expand All @@ -815,8 +845,7 @@ public function get_billing_data_from_order( WC_Order $order ): array {
* @return void
*/
private function mark_payment_capture_cancelled( $order, $intent_data ) {
$note = $this->generate_capture_cancelled_note();
if ( $this->order_note_exists( $order, $note ) ) {
if ( false === $this->post_unique_capture_cancelled_note( $order ) ) {
$this->complete_order_processing( $order );
return;
}
Expand All @@ -832,7 +861,6 @@ private function mark_payment_capture_cancelled( $order, $intent_data ) {
}

$this->update_order_status( $order, Order_Status::CANCELLED );
$order->add_order_note( $note );
$this->complete_order_processing( $order, $intent_data['intent_status'] );
}

Expand Down
Loading

0 comments on commit 3cf2d34

Please sign in to comment.