mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-08-31 04:58:28 +08:00
Merge pull request #2062 from woocommerce/PCP-2143-reauthorize-authorized-payments
reauthorize authorized payments (2143)
This commit is contained in:
commit
0ce740ba62
11 changed files with 354 additions and 9 deletions
|
@ -87,6 +87,32 @@ function ppcp_capture_order( WC_Order $wc_order ): void {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reauthorizes the PayPal order.
|
||||
*
|
||||
* @param WC_Order $wc_order The WC order.
|
||||
* @throws InvalidArgumentException When the order cannot be captured.
|
||||
* @throws Exception When the operation fails.
|
||||
*/
|
||||
function ppcp_reauthorize_order( WC_Order $wc_order ): void {
|
||||
$intent = strtoupper( (string) $wc_order->get_meta( PayPalGateway::INTENT_META_KEY ) );
|
||||
|
||||
if ( $intent !== 'AUTHORIZE' ) {
|
||||
throw new InvalidArgumentException( 'Only orders with "authorize" intent can be reauthorized.' );
|
||||
}
|
||||
$captured = wc_string_to_bool( $wc_order->get_meta( AuthorizedPaymentsProcessor::CAPTURED_META_KEY ) );
|
||||
if ( $captured ) {
|
||||
throw new InvalidArgumentException( 'The order is already captured.' );
|
||||
}
|
||||
|
||||
$authorized_payment_processor = PPCP::container()->get( 'wcgateway.processor.authorized-payments' );
|
||||
assert( $authorized_payment_processor instanceof AuthorizedPaymentsProcessor );
|
||||
|
||||
if ( $authorized_payment_processor->reauthorize_payment( $wc_order ) !== AuthorizedPaymentsProcessor::SUCCESSFUL ) {
|
||||
throw new RuntimeException( $authorized_payment_processor->reauthorization_failure_reason() ?: 'Reauthorization failed.' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refunds the PayPal order.
|
||||
* Note that you can use wc_refund_payment() to trigger the refund in WC and PayPal.
|
||||
|
|
|
@ -9,6 +9,8 @@ declare(strict_types=1);
|
|||
|
||||
namespace WooCommerce\PayPalCommerce\AdminNotices;
|
||||
|
||||
use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message;
|
||||
use WooCommerce\PayPalCommerce\AdminNotices\Repository\Repository;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
|
||||
|
@ -40,6 +42,34 @@ class AdminNotices implements ModuleInterface {
|
|||
$renderer->render();
|
||||
}
|
||||
);
|
||||
|
||||
add_action(
|
||||
Repository::NOTICES_FILTER,
|
||||
/**
|
||||
* Adds persisted notices to the notices array.
|
||||
*
|
||||
* @param array $notices The notices.
|
||||
* @return array
|
||||
*
|
||||
* @psalm-suppress MissingClosureParamType
|
||||
*/
|
||||
function ( $notices ) use ( $c ) {
|
||||
if ( ! is_array( $notices ) ) {
|
||||
return $notices;
|
||||
}
|
||||
|
||||
$admin_notices = $c->get( 'admin-notices.repository' );
|
||||
assert( $admin_notices instanceof Repository );
|
||||
|
||||
$persisted_notices = $admin_notices->get_persisted_and_clear();
|
||||
|
||||
if ( $persisted_notices ) {
|
||||
$notices = array_merge( $notices, $persisted_notices );
|
||||
}
|
||||
|
||||
return $notices;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -92,4 +92,18 @@ class Message {
|
|||
public function wrapper(): string {
|
||||
return $this->wrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the object as array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function to_array(): array {
|
||||
return array(
|
||||
'type' => $this->type,
|
||||
'message' => $this->message,
|
||||
'dismissable' => $this->dismissable,
|
||||
'wrapper' => $this->wrapper,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,8 @@ use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message;
|
|||
*/
|
||||
class Repository implements RepositoryInterface {
|
||||
|
||||
const NOTICES_FILTER = 'ppcp.admin-notices.current-notices';
|
||||
const NOTICES_FILTER = 'ppcp.admin-notices.current-notices';
|
||||
const PERSISTED_NOTICES_OPTION = 'woocommerce_ppcp-admin-notices';
|
||||
|
||||
/**
|
||||
* Returns the current messages.
|
||||
|
@ -37,4 +38,40 @@ class Repository implements RepositoryInterface {
|
|||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a message to persist between page reloads.
|
||||
*
|
||||
* @param Message $message The message.
|
||||
* @return void
|
||||
*/
|
||||
public function persist( Message $message ): void {
|
||||
$persisted_notices = get_option( self::PERSISTED_NOTICES_OPTION ) ?: array();
|
||||
|
||||
$persisted_notices[] = $message->to_array();
|
||||
|
||||
update_option( self::PERSISTED_NOTICES_OPTION, $persisted_notices );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a message to persist between page reloads.
|
||||
*
|
||||
* @return array|Message[]
|
||||
*/
|
||||
public function get_persisted_and_clear(): array {
|
||||
$notices = array();
|
||||
|
||||
$persisted_data = get_option( self::PERSISTED_NOTICES_OPTION ) ?: array();
|
||||
foreach ( $persisted_data as $notice_data ) {
|
||||
$notices[] = new Message(
|
||||
(string) ( $notice_data['message'] ?? '' ),
|
||||
(string) ( $notice_data['type'] ?? '' ),
|
||||
(bool) ( $notice_data['dismissable'] ?? true ),
|
||||
(string) ( $notice_data['wrapper'] ?? '' )
|
||||
);
|
||||
}
|
||||
|
||||
update_option( self::PERSISTED_NOTICES_OPTION, array(), true );
|
||||
return $notices;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -193,6 +193,53 @@ class PaymentsEndpoint {
|
|||
return $this->capture_factory->from_paypal_response( $json );
|
||||
}
|
||||
|
||||
/**
|
||||
* Reauthorizes an order.
|
||||
*
|
||||
* @param string $authorization_id The id.
|
||||
* @param Money|null $amount The amount to capture. If not specified, the whole authorized amount is captured.
|
||||
*
|
||||
* @return string
|
||||
* @throws RuntimeException If the request fails.
|
||||
* @throws PayPalApiException If the request fails.
|
||||
*/
|
||||
public function reauthorize( string $authorization_id, ?Money $amount = null ) : string {
|
||||
$bearer = $this->bearer->bearer();
|
||||
$url = trailingslashit( $this->host ) . 'v2/payments/authorizations/' . $authorization_id . '/reauthorize';
|
||||
|
||||
$data = array();
|
||||
if ( $amount ) {
|
||||
$data['amount'] = $amount->to_array();
|
||||
}
|
||||
|
||||
$args = array(
|
||||
'method' => 'POST',
|
||||
'headers' => array(
|
||||
'Authorization' => 'Bearer ' . $bearer->token(),
|
||||
'Content-Type' => 'application/json',
|
||||
'Prefer' => 'return=representation',
|
||||
),
|
||||
'body' => wp_json_encode( $data, JSON_FORCE_OBJECT ),
|
||||
);
|
||||
|
||||
$response = $this->request( $url, $args );
|
||||
$json = json_decode( $response['body'] );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
throw new RuntimeException( 'Could not reauthorize authorized payment.' );
|
||||
}
|
||||
|
||||
$status_code = (int) wp_remote_retrieve_response_code( $response );
|
||||
if ( 201 !== $status_code || ! is_object( $json ) ) {
|
||||
throw new PayPalApiException(
|
||||
$json,
|
||||
$status_code
|
||||
);
|
||||
}
|
||||
|
||||
return $json->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refunds a payment.
|
||||
*
|
||||
|
|
|
@ -22,6 +22,7 @@ use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator;
|
|||
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\Endpoint\RefreshFeatureStatusEndpoint;
|
||||
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
||||
|
@ -384,19 +385,25 @@ return array(
|
|||
$notice = $container->get( 'wcgateway.notice.authorize-order-action' );
|
||||
$settings = $container->get( 'wcgateway.settings' );
|
||||
$subscription_helper = $container->get( 'wc-subscriptions.helper' );
|
||||
$amount_factory = $container->get( 'api.factory.amount' );
|
||||
return new AuthorizedPaymentsProcessor(
|
||||
$order_endpoint,
|
||||
$payments_endpoint,
|
||||
$logger,
|
||||
$notice,
|
||||
$settings,
|
||||
$subscription_helper
|
||||
$subscription_helper,
|
||||
$amount_factory
|
||||
);
|
||||
},
|
||||
'wcgateway.admin.render-authorize-action' => static function ( ContainerInterface $container ): RenderAuthorizeAction {
|
||||
$column = $container->get( 'wcgateway.admin.orders-payment-status-column' );
|
||||
return new RenderAuthorizeAction( $column );
|
||||
},
|
||||
'wcgateway.admin.render-reauthorize-action' => static function ( ContainerInterface $container ): RenderReauthorizeAction {
|
||||
$column = $container->get( 'wcgateway.admin.orders-payment-status-column' );
|
||||
return new RenderReauthorizeAction( $column );
|
||||
},
|
||||
'wcgateway.admin.order-payment-status' => static function ( ContainerInterface $container ): PaymentStatusOrderDetail {
|
||||
$column = $container->get( 'wcgateway.admin.orders-payment-status-column' );
|
||||
return new PaymentStatusOrderDetail( $column );
|
||||
|
|
|
@ -9,9 +9,6 @@ declare( strict_types=1 );
|
|||
|
||||
namespace WooCommerce\PayPalCommerce\WcGateway\Admin;
|
||||
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor;
|
||||
|
||||
/**
|
||||
* Class RenderAuthorizeAction
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
/**
|
||||
* Renders the order action "Reauthorize PayPal payment"
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\WcGateway\Admin
|
||||
*/
|
||||
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\WcGateway\Admin;
|
||||
|
||||
/**
|
||||
* Class RenderReauthorizeAction
|
||||
*/
|
||||
class RenderReauthorizeAction {
|
||||
/**
|
||||
* The capture info column.
|
||||
*
|
||||
* @var OrderTablePaymentStatusColumn
|
||||
*/
|
||||
private $column;
|
||||
|
||||
/**
|
||||
* PaymentStatusOrderDetail constructor.
|
||||
*
|
||||
* @param OrderTablePaymentStatusColumn $column The capture info column.
|
||||
*/
|
||||
public function __construct( OrderTablePaymentStatusColumn $column ) {
|
||||
$this->column = $column;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the action into the $order_actions array based on the WooCommerce order.
|
||||
*
|
||||
* @param array $order_actions The actions to render into.
|
||||
* @param \WC_Order $wc_order The order for which to render the action.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function render( array $order_actions, \WC_Order $wc_order ) : array {
|
||||
|
||||
if ( ! $this->should_render_for_order( $wc_order ) ) {
|
||||
return $order_actions;
|
||||
}
|
||||
|
||||
$order_actions['ppcp_reauthorize_order'] = esc_html__(
|
||||
'Reauthorize PayPal payment',
|
||||
'woocommerce-paypal-payments'
|
||||
);
|
||||
return $order_actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the action should be rendered for a certain WooCommerce order.
|
||||
*
|
||||
* @param \WC_Order $order The Woocommerce order.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function should_render_for_order( \WC_Order $order ) : bool {
|
||||
$status = $order->get_status();
|
||||
$not_allowed_statuses = array( 'refunded', 'cancelled', 'failed' );
|
||||
return $this->column->should_render_for_order( $order ) &&
|
||||
! $this->column->is_captured( $order ) &&
|
||||
! in_array( $status, $not_allowed_statuses, true );
|
||||
}
|
||||
}
|
|
@ -10,6 +10,8 @@ declare(strict_types=1);
|
|||
namespace WooCommerce\PayPalCommerce\WcGateway\Processor;
|
||||
|
||||
use Exception;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\AmountFactory;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use WC_Order;
|
||||
|
@ -91,6 +93,20 @@ class AuthorizedPaymentsProcessor {
|
|||
*/
|
||||
private $subscription_helper;
|
||||
|
||||
/**
|
||||
* The amount factory.
|
||||
*
|
||||
* @var AmountFactory
|
||||
*/
|
||||
private $amount_factory;
|
||||
|
||||
/**
|
||||
* The reauthorization failure reason.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $reauthorization_failure_reason = '';
|
||||
|
||||
/**
|
||||
* AuthorizedPaymentsProcessor constructor.
|
||||
*
|
||||
|
@ -100,6 +116,7 @@ class AuthorizedPaymentsProcessor {
|
|||
* @param AuthorizeOrderActionNotice $notice The notice.
|
||||
* @param ContainerInterface $config The settings.
|
||||
* @param SubscriptionHelper $subscription_helper The subscription helper.
|
||||
* @param AmountFactory $amount_factory The amount factory.
|
||||
*/
|
||||
public function __construct(
|
||||
OrderEndpoint $order_endpoint,
|
||||
|
@ -107,7 +124,8 @@ class AuthorizedPaymentsProcessor {
|
|||
LoggerInterface $logger,
|
||||
AuthorizeOrderActionNotice $notice,
|
||||
ContainerInterface $config,
|
||||
SubscriptionHelper $subscription_helper
|
||||
SubscriptionHelper $subscription_helper,
|
||||
AmountFactory $amount_factory
|
||||
) {
|
||||
|
||||
$this->order_endpoint = $order_endpoint;
|
||||
|
@ -116,6 +134,7 @@ class AuthorizedPaymentsProcessor {
|
|||
$this->notice = $notice;
|
||||
$this->config = $config;
|
||||
$this->subscription_helper = $subscription_helper;
|
||||
$this->amount_factory = $amount_factory;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -249,6 +268,67 @@ class AuthorizedPaymentsProcessor {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reauthorizes an authorized payment for an WooCommerce order.
|
||||
*
|
||||
* @param WC_Order $wc_order The WooCommerce order.
|
||||
*
|
||||
* @return string The status or reauthorization id.
|
||||
*/
|
||||
public function reauthorize_payment( WC_Order $wc_order ): string {
|
||||
$this->reauthorization_failure_reason = '';
|
||||
|
||||
try {
|
||||
$order = $this->paypal_order_from_wc_order( $wc_order );
|
||||
} catch ( Exception $exception ) {
|
||||
$this->logger->error( 'Could not get PayPal order from WC order: ' . $exception->getMessage() );
|
||||
if ( $exception->getCode() === 404 ) {
|
||||
return self::NOT_FOUND;
|
||||
}
|
||||
return self::INACCESSIBLE;
|
||||
}
|
||||
|
||||
$amount = $this->amount_factory->from_wc_order( $wc_order );
|
||||
|
||||
$authorizations = $this->all_authorizations( $order );
|
||||
$uncaptured_authorizations = $this->authorizations_to_capture( ...$authorizations );
|
||||
|
||||
if ( ! $uncaptured_authorizations ) {
|
||||
if ( $this->captured_authorizations( ...$authorizations ) ) {
|
||||
$this->logger->info( 'Authorizations already captured.' );
|
||||
return self::ALREADY_CAPTURED;
|
||||
}
|
||||
|
||||
$this->logger->info( 'Bad authorization.' );
|
||||
return self::BAD_AUTHORIZATION;
|
||||
}
|
||||
|
||||
$authorization = end( $uncaptured_authorizations );
|
||||
|
||||
try {
|
||||
$this->payments_endpoint->reauthorize( $authorization->id(), new Money( $amount->value(), $amount->currency_code() ) );
|
||||
} catch ( PayPalApiException $exception ) {
|
||||
$this->reauthorization_failure_reason = $exception->details()[0]->description ?? null;
|
||||
$this->logger->error( 'Reauthorization failed: ' . $exception->name() . ' | ' . $this->reauthorization_failure_reason );
|
||||
return self::FAILED;
|
||||
|
||||
} catch ( Exception $exception ) {
|
||||
$this->logger->error( 'Failed to capture authorization: ' . $exception->getMessage() );
|
||||
return self::FAILED;
|
||||
}
|
||||
|
||||
return self::SUCCESSFUL;
|
||||
}
|
||||
|
||||
/**
|
||||
* The reason for a failed reauthorization.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function reauthorization_failure_reason(): string {
|
||||
return $this->reauthorization_failure_reason;
|
||||
}
|
||||
|
||||
/**
|
||||
* Voids authorizations for the given PayPal order.
|
||||
*
|
||||
|
@ -392,4 +472,5 @@ class AuthorizedPaymentsProcessor {
|
|||
}
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\WcGateway;
|
|||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Throwable;
|
||||
use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
|
||||
|
@ -614,13 +615,18 @@ class WCGatewayModule implements ModuleInterface {
|
|||
return $order_actions;
|
||||
}
|
||||
|
||||
$render = $container->get( 'wcgateway.admin.render-authorize-action' );
|
||||
$render_reauthorize = $container->get( 'wcgateway.admin.render-reauthorize-action' );
|
||||
$render_authorize = $container->get( 'wcgateway.admin.render-authorize-action' );
|
||||
|
||||
/**
|
||||
* Renders the authorize action in the select field.
|
||||
*
|
||||
* @var RenderAuthorizeAction $render
|
||||
*/
|
||||
return $render->render( $order_actions, $theorder );
|
||||
return $render_reauthorize->render(
|
||||
$render_authorize->render( $order_actions, $theorder ),
|
||||
$theorder
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -637,6 +643,36 @@ class WCGatewayModule implements ModuleInterface {
|
|||
$authorized_payments_processor->capture_authorized_payment( $wc_order );
|
||||
}
|
||||
);
|
||||
|
||||
add_action(
|
||||
'woocommerce_order_action_ppcp_reauthorize_order',
|
||||
static function ( WC_Order $wc_order ) use ( $container ) {
|
||||
$admin_notices = $container->get( 'admin-notices.repository' );
|
||||
assert( $admin_notices instanceof Repository );
|
||||
|
||||
/**
|
||||
* The authorized payments processor.
|
||||
*
|
||||
* @var AuthorizedPaymentsProcessor $authorized_payments_processor
|
||||
*/
|
||||
$authorized_payments_processor = $container->get( 'wcgateway.processor.authorized-payments' );
|
||||
|
||||
if ( $authorized_payments_processor->reauthorize_payment( $wc_order ) !== AuthorizedPaymentsProcessor::SUCCESSFUL ) {
|
||||
$message = sprintf(
|
||||
'%1$s %2$s',
|
||||
esc_html__( 'Reauthorization with PayPal failed: ', 'woocommerce-paypal-payments' ),
|
||||
$authorized_payments_processor->reauthorization_failure_reason() ?: ''
|
||||
);
|
||||
$admin_notices->persist( new Message( $message, 'error' ) );
|
||||
} else {
|
||||
$admin_notices->persist( new Message( 'Payment reauthorized.', 'info' ) );
|
||||
|
||||
$wc_order->add_order_note(
|
||||
__( 'Payment reauthorized.', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,6 +5,7 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Processor;
|
|||
|
||||
|
||||
use Mockery\MockInterface;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\AmountFactory;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use WC_Order;
|
||||
|
@ -69,6 +70,7 @@ class AuthorizedPaymentsProcessorTest extends TestCase
|
|||
|
||||
$this->config = Mockery::mock(ContainerInterface::class);
|
||||
$this->subscription_helper = Mockery::mock(SubscriptionHelper::class);
|
||||
$this->amount_factory = Mockery::mock(AmountFactory::class);
|
||||
|
||||
$this->testee = new AuthorizedPaymentsProcessor(
|
||||
$this->orderEndpoint,
|
||||
|
@ -76,7 +78,8 @@ class AuthorizedPaymentsProcessorTest extends TestCase
|
|||
new NullLogger(),
|
||||
$this->notice,
|
||||
$this->config,
|
||||
$this->subscription_helper
|
||||
$this->subscription_helper,
|
||||
$this->amount_factory
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue