Merge pull request #2678 from woocommerce/PCP-3732-void-button

Add void button
This commit is contained in:
Emili Castells 2024-10-22 12:41:31 +02:00 committed by GitHub
commit 49b790fef8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 513 additions and 23 deletions

View file

@ -0,0 +1,53 @@
import {
hide,
show,
} from '../../../ppcp-button/resources/js/modules/Helper/Hiding';
document.addEventListener( 'DOMContentLoaded', function () {
const refundButton = document.querySelector( 'button.refund-items' );
if ( ! refundButton ) {
return;
}
refundButton.insertAdjacentHTML(
'afterend',
`<button class="button" type="button" id="pcpVoid">${ PcpVoidButton.button_text }</button>`
);
hide( refundButton );
const voidButton = document.querySelector( '#pcpVoid' );
voidButton.addEventListener( 'click', async () => {
if ( ! window.confirm( PcpVoidButton.popup_text ) ) {
return;
}
voidButton.setAttribute( 'disabled', 'disabled' );
const res = await fetch( PcpVoidButton.ajax.void.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'same-origin',
body: JSON.stringify( {
nonce: PcpVoidButton.ajax.void.nonce,
wc_order_id: PcpVoidButton.wc_order_id,
} ),
} );
const data = await res.json();
if ( ! data.success ) {
hide( voidButton );
show( refundButton );
alert( PcpVoidButton.error_text );
throw Error( data.data.message );
}
location.reload();
} );
} );

View file

@ -25,8 +25,10 @@ use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingOptionsRenderer;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Admin\RenderReauthorizeAction;
use WooCommerce\PayPalCommerce\WcGateway\Assets\VoidButtonAssets;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\CaptureCardPayment;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\RefreshFeatureStatusEndpoint;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\VoidOrderEndpoint;
use WooCommerce\PayPalCommerce\WcGateway\Helper\CartCheckoutDetector;
use WooCommerce\PayPalCommerce\WcGateway\Helper\FeesUpdater;
use WooCommerce\PayPalCommerce\WcGateway\Settings\WcTasks\Factory\SimpleRedirectTaskFactory;
@ -46,7 +48,6 @@ use WooCommerce\PayPalCommerce\WcGateway\Checkout\DisableGateways;
use WooCommerce\PayPalCommerce\WcGateway\Cli\SettingsCommand;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\ReturnUrlEndpoint;
use WooCommerce\PayPalCommerce\WcGateway\FraudNet\FraudNet;
use WooCommerce\PayPalCommerce\WcGateway\FraudNet\FraudNetSessionId;
use WooCommerce\PayPalCommerce\WcGateway\FraudNet\FraudNetSourceWebsiteId;
use WooCommerce\PayPalCommerce\WcGateway\FundingSource\FundingSourceRenderer;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
@ -515,12 +516,20 @@ return array(
);
},
'wcgateway.processor.refunds' => static function ( ContainerInterface $container ): RefundProcessor {
$order_endpoint = $container->get( 'api.endpoint.order' );
$payments_endpoint = $container->get( 'api.endpoint.payments' );
$refund_fees_updater = $container->get( 'wcgateway.helper.refund-fees-updater' );
$prefix = $container->get( 'api.prefix' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
return new RefundProcessor( $order_endpoint, $payments_endpoint, $refund_fees_updater, $prefix, $logger );
return new RefundProcessor(
$container->get( 'api.endpoint.order' ),
$container->get( 'api.endpoint.payments' ),
$container->get( 'wcgateway.helper.refund-fees-updater' ),
$container->get( 'wcgateway.allowed_refund_payment_methods' ),
$container->get( 'api.prefix' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'wcgateway.allowed_refund_payment_methods' => static function ( ContainerInterface $container ): array {
return apply_filters(
'woocommerce_paypal_payments_allowed_refund_payment_methods',
array( PayPalGateway::ID, CreditCardGateway::ID, CardButtonGateway::ID, PayUponInvoiceGateway::ID )
);
},
'wcgateway.processor.authorized-payments' => static function ( ContainerInterface $container ): AuthorizedPaymentsProcessor {
$order_endpoint = $container->get( 'api.endpoint.order' );
@ -1916,6 +1925,24 @@ return array(
return $simple_redirect_tasks;
},
'wcgateway.void-button.assets' => function( ContainerInterface $container ) : VoidButtonAssets {
return new VoidButtonAssets(
$container->get( 'wcgateway.url' ),
$container->get( 'ppcp.asset-version' ),
$container->get( 'api.endpoint.order' ),
$container->get( 'wcgateway.processor.refunds' ),
$container->get( 'wcgateway.allowed_refund_payment_methods' )
);
},
'wcgateway.void-button.endpoint' => function( ContainerInterface $container ) : VoidOrderEndpoint {
return new VoidOrderEndpoint(
$container->get( 'button.request-data' ),
$container->get( 'api.endpoint.order' ),
$container->get( 'wcgateway.processor.refunds' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'wcgateway.settings.admin-settings-enabled' => static function( ContainerInterface $container ): bool {
return $container->has( 'settings.url' );
},

View file

@ -0,0 +1,171 @@
<?php
/**
* Register and configure assets for the void button.
*
* @package WooCommerce\PayPalCommerce\WcGateway\Assets
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Assets;
use Exception;
use WC_AJAX;
use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\VoidOrderEndpoint;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
use WP_Screen;
/**
* Class VoidButtonAssets
*/
class VoidButtonAssets {
/**
* The URL of this module.
*
* @var string
*/
private $module_url;
/**
* The assets version.
*
* @var string
*/
private $version;
/**
* The order endpoint.
*
* @var OrderEndpoint
*/
private $order_endpoint;
/**
* The Refund Processor.
*
* @var RefundProcessor
*/
private $refund_processor;
/**
* The methods that can be refunded.
*
* @var array
*/
private $allowed_refund_payment_methods;
/**
* VoidButtonAssets constructor.
*
* @param string $module_url The url of this module.
* @param string $version The assets version.
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param RefundProcessor $refund_processor The Refund Processor.
* @param array $allowed_refund_payment_methods The methods that can be refunded.
*/
public function __construct(
string $module_url,
string $version,
OrderEndpoint $order_endpoint,
RefundProcessor $refund_processor,
array $allowed_refund_payment_methods
) {
$this->module_url = $module_url;
$this->version = $version;
$this->order_endpoint = $order_endpoint;
$this->refund_processor = $refund_processor;
$this->allowed_refund_payment_methods = $allowed_refund_payment_methods;
}
/**
* Checks if should register assets on the current page.
*/
public function should_register(): bool {
if ( ! is_admin() || wp_doing_ajax() ) {
return false;
}
global $theorder;
if ( ! $theorder instanceof WC_Order ) {
return false;
}
$current_screen = get_current_screen();
if ( ! $current_screen instanceof WP_Screen ) {
return false;
}
if ( $current_screen->post_type !== 'shop_order' ) {
return false;
}
if ( ! in_array( $theorder->get_payment_method(), $this->allowed_refund_payment_methods, true ) ) {
return false;
}
// Skip if there are refunds already, it is probably not voidable anymore + void cannot be partial.
if ( $theorder->get_remaining_refund_amount() !== $theorder->get_total() ) {
return false;
}
$order_id = $theorder->get_meta( PayPalGateway::ORDER_ID_META_KEY );
if ( ! $order_id ) {
return false;
}
try {
$order = $this->order_endpoint->order( $order_id );
if ( $this->refund_processor->determine_refund_mode( $order ) !== RefundProcessor::REFUND_MODE_VOID ) {
return false;
}
} catch ( Exception $exception ) {
return false;
}
return true;
}
/**
* Enqueues the assets.
*/
public function register(): void {
global $theorder;
assert( $theorder instanceof WC_Order );
wp_enqueue_script(
'ppcp-void-button',
trailingslashit( $this->module_url ) . 'assets/js/void-button.js',
array(),
$this->version,
true
);
wp_localize_script(
'ppcp-void-button',
'PcpVoidButton',
array(
'button_text' => __( 'Void authorization', 'woocommerce-paypal-payments' ),
'popup_text' => __(
'After voiding an authorized transaction, you cannot capture any funds associated with that transaction, and the funds are returned to the customer. Voiding an authorization cancels the entire open amount.',
'woocommerce-paypal-payments'
),
'error_text' => __(
'The operation failed. Use the Refund button if the funds were already captured.',
'woocommerce-paypal-payments'
),
'wc_order_id' => $theorder->get_id(),
'ajax' => array(
'void' => array(
'endpoint' => WC_AJAX::get_endpoint( VoidOrderEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( VoidOrderEndpoint::nonce() ),
),
),
),
);
}
}

View file

@ -0,0 +1,198 @@
<?php
/**
* The Void Order endpoint.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Endpoint;
use Exception;
use Psr\Log\LoggerInterface;
use WC_Order;
use WC_Order_Item_Fee;
use WC_Order_Item_Product;
use WC_Order_Item_Shipping;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
/**
* Class VoidOrderEndpoint
*/
class VoidOrderEndpoint {
const ENDPOINT = 'ppc-void-order';
/**
* The order endpoint.
*
* @var OrderEndpoint
*/
private $order_endpoint;
/**
* The Refund Processor.
*
* @var RefundProcessor
*/
private $refund_processor;
/**
* The Request Data Helper.
*
* @var RequestData
*/
private $request_data;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* VoidOrderEndpoint constructor.
*
* @param RequestData $request_data The Request Data Helper.
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param RefundProcessor $refund_processor The Refund Processor.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
RequestData $request_data,
OrderEndpoint $order_endpoint,
RefundProcessor $refund_processor,
LoggerInterface $logger
) {
$this->request_data = $request_data;
$this->order_endpoint = $order_endpoint;
$this->refund_processor = $refund_processor;
$this->logger = $logger;
}
/**
* Returns the nonce.
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the incoming request.
*/
public function handle_request(): void {
$request = $this->request_data->read_request( self::nonce() );
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_send_json_error(
array(
'message' => 'Invalid request.',
)
);
return;
}
$wc_order_id = (int) $request['wc_order_id'];
$wc_order = wc_get_order( $wc_order_id );
if ( ! $wc_order instanceof WC_Order ) {
wp_send_json_error(
array(
'message' => 'WC order not found.',
)
);
return;
}
$order_id = $wc_order->get_meta( PayPalGateway::ORDER_ID_META_KEY );
if ( ! $order_id ) {
wp_send_json_error(
array(
'message' => 'PayPal order ID not found in meta.',
)
);
return;
}
try {
$order = $this->order_endpoint->order( $order_id );
$this->refund_processor->void( $order );
$this->make_refunded( $wc_order );
} catch ( Exception $exception ) {
wp_send_json_error(
array(
'message' => 'Void failed. ' . $exception->getMessage(),
)
);
$this->logger->error( 'Void failed. ' . $exception->getMessage() );
return;
}
wp_send_json_success();
}
/**
* Returns the list of items for the wc_create_refund data,
* making all items refunded (max qty, total, taxes).
*
* @param WC_Order $wc_order The WC order.
*/
protected function refund_items( WC_Order $wc_order ): array {
$refunded_items = array();
foreach ( $wc_order->get_items( array( 'line_item', 'fee', 'shipping' ) ) as $item ) {
// Some methods like get_taxes() are not defined in WC_Order_Item.
if (
! $item instanceof WC_Order_Item_Product
&& ! $item instanceof WC_Order_Item_Fee
&& ! $item instanceof WC_Order_Item_Shipping
) {
continue;
}
$taxes = array();
$item_taxes = $item->get_taxes();
/**
* The type is not really guaranteed in the code.
*
* @psalm-suppress RedundantConditionGivenDocblockType
*/
if ( is_array( $item_taxes ) && isset( $item_taxes['total'] ) ) {
$taxes = $item_taxes['total'];
}
$refunded_items[ $item->get_id() ] = array(
'qty' => $item->get_type() === 'line_item' ? $item->get_quantity() : 0,
'refund_total' => $item->get_total(),
'refund_tax' => $taxes,
);
}
return $refunded_items;
}
/**
* Creates a full refund.
*
* @param WC_Order $wc_order The WC order.
*/
private function make_refunded( WC_Order $wc_order ): void {
wc_create_refund(
array(
'amount' => $wc_order->get_total(),
'reason' => __( 'Voided authorization', 'woocommerce-paypal-payments' ),
'order_id' => $wc_order->get_id(),
'line_items' => $this->refund_items( $wc_order ),
'refund_payment' => false,
'restock_items' => (bool) apply_filters( 'woocommerce_paypal_payments_void_restock_items', true ),
)
);
$wc_order->set_status( 'refunded' );
}
}

View file

@ -33,9 +33,9 @@ use WooCommerce\PayPalCommerce\WcGateway\Helper\RefundFeesUpdater;
class RefundProcessor {
use RefundMetaTrait;
private const REFUND_MODE_REFUND = 'refund';
private const REFUND_MODE_VOID = 'void';
private const REFUND_MODE_UNKNOWN = 'unknown';
public const REFUND_MODE_REFUND = 'refund';
public const REFUND_MODE_VOID = 'void';
public const REFUND_MODE_UNKNOWN = 'unknown';
/**
* The order endpoint.
@ -72,12 +72,20 @@ class RefundProcessor {
*/
private $refund_fees_updater;
/**
* The methods that can be refunded.
*
* @var array
*/
private $allowed_refund_payment_methods;
/**
* RefundProcessor constructor.
*
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param PaymentsEndpoint $payments_endpoint The payments endpoint.
* @param RefundFeesUpdater $refund_fees_updater The refund fees updater.
* @param array $allowed_refund_payment_methods The methods that can be refunded.
* @param string $prefix The prefix.
* @param LoggerInterface $logger The logger.
*/
@ -85,15 +93,17 @@ class RefundProcessor {
OrderEndpoint $order_endpoint,
PaymentsEndpoint $payments_endpoint,
RefundFeesUpdater $refund_fees_updater,
array $allowed_refund_payment_methods,
string $prefix,
LoggerInterface $logger
) {
$this->order_endpoint = $order_endpoint;
$this->payments_endpoint = $payments_endpoint;
$this->refund_fees_updater = $refund_fees_updater;
$this->prefix = $prefix;
$this->logger = $logger;
$this->order_endpoint = $order_endpoint;
$this->payments_endpoint = $payments_endpoint;
$this->refund_fees_updater = $refund_fees_updater;
$this->allowed_refund_payment_methods = $allowed_refund_payment_methods;
$this->prefix = $prefix;
$this->logger = $logger;
}
/**
@ -109,11 +119,7 @@ class RefundProcessor {
*/
public function process( WC_Order $wc_order, float $amount = null, string $reason = '' ) : bool {
try {
$allowed_refund_payment_methods = apply_filters(
'woocommerce_paypal_payments_allowed_refund_payment_methods',
array( PayPalGateway::ID, CreditCardGateway::ID, CardButtonGateway::ID, PayUponInvoiceGateway::ID )
);
if ( ! in_array( $wc_order->get_payment_method(), $allowed_refund_payment_methods, true ) ) {
if ( ! in_array( $wc_order->get_payment_method(), $this->allowed_refund_payment_methods, true ) ) {
return true;
}
@ -134,7 +140,7 @@ class RefundProcessor {
)
);
$mode = $this->determine_refund_mode( $payments );
$mode = $this->determine_refund_mode( $order );
switch ( $mode ) {
case self::REFUND_MODE_REFUND:
@ -226,11 +232,13 @@ class RefundProcessor {
/**
* Determines the refunding mode.
*
* @param Payments $payments The order payments state.
* @param Order $order The order.
*
* @return string One of the REFUND_MODE_ constants.
*/
private function determine_refund_mode( Payments $payments ): string {
public function determine_refund_mode( Order $order ): string {
$payments = $this->get_payments( $order );
$authorizations = $payments->authorizations();
if ( $authorizations ) {
foreach ( $authorizations as $authorization ) {

View file

@ -21,7 +21,9 @@ use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
use WooCommerce\PayPalCommerce\WcGateway\Assets\VoidButtonAssets;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\RefreshFeatureStatusEndpoint;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\VoidOrderEndpoint;
use WooCommerce\PayPalCommerce\WcGateway\Processor\CreditCardOrderInfoHandlingTrait;
use WC_Order;
use WooCommerce\PayPalCommerce\AdminNotices\Repository\Repository;
@ -90,6 +92,7 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul
$this->register_columns( $c );
$this->register_checkout_paypal_address_preset( $c );
$this->register_wc_tasks( $c );
$this->register_void_button( $c );
add_action(
'woocommerce_sections_checkout',
@ -870,4 +873,33 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul
},
);
}
/**
* Registers the assets and ajax endpoint for the void button.
*
* @param ContainerInterface $container The container.
*/
protected function register_void_button( ContainerInterface $container ): void {
add_action(
'admin_enqueue_scripts',
static function () use ( $container ) {
$assets = $container->get( 'wcgateway.void-button.assets' );
assert( $assets instanceof VoidButtonAssets );
if ( $assets->should_register() ) {
$assets->register();
}
}
);
add_action(
'wc_ajax_' . VoidOrderEndpoint::ENDPOINT,
static function () use ( $container ) {
$endpoint = $container->get( 'wcgateway.void-button.endpoint' );
assert( $endpoint instanceof VoidOrderEndpoint );
$endpoint->handle_request();
}
);
}
}

View file

@ -10,6 +10,7 @@ module.exports = {
'gateway-settings': path.resolve('./resources/js/gateway-settings.js'),
'fraudnet': path.resolve('./resources/js/fraudnet.js'),
'oxxo': path.resolve('./resources/js/oxxo.js'),
'void-button': path.resolve('./resources/js/void-button.js'),
'gateway-settings-style': path.resolve('./resources/css/gateway-settings.scss'),
'common-style': path.resolve('./resources/css/common.scss'),
},