woocommerce-paypal-payments/modules/ppcp-wc-gateway/src/Processor/RefundProcessor.php

269 lines
6.9 KiB
PHP

<?php
/**
* Processes refunds started in the WooCommerce environment.
*
* @package WooCommerce\PayPalCommerce\WcGateway\Processor
*/
declare( strict_types=1 );
namespace WooCommerce\PayPalCommerce\WcGateway\Processor;
use Exception;
use Psr\Log\LoggerInterface;
use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Amount;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payments;
use WooCommerce\PayPalCommerce\ApiClient\Entity\RefundCapture;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoiceGateway;
use WooCommerce\PayPalCommerce\WcGateway\Helper\RefundFeesUpdater;
/**
* Class RefundProcessor
*/
class RefundProcessor {
use RefundMetaTrait;
private const REFUND_MODE_REFUND = 'refund';
private const REFUND_MODE_VOID = 'void';
private const REFUND_MODE_UNKNOWN = 'unknown';
/**
* The order endpoint.
*
* @var OrderEndpoint
*/
private $order_endpoint;
/**
* The payments endpoint.
*
* @var PaymentsEndpoint
*/
private $payments_endpoint;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* The prefix.
*
* @var string
*/
private $prefix;
/**
* The refund fees updater.
*
* @var RefundFeesUpdater
*/
private $refund_fees_updater;
/**
* 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 string $prefix The prefix.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
OrderEndpoint $order_endpoint,
PaymentsEndpoint $payments_endpoint,
RefundFeesUpdater $refund_fees_updater,
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;
}
/**
* Processes a refund.
*
* @param WC_Order $wc_order The WooCommerce order.
* @param float|null $amount The refund amount.
* @param string $reason The reason for the refund.
*
* @return bool
*
* @phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.Missing
*/
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 ) ) {
return true;
}
$order_id = $wc_order->get_meta( PayPalGateway::ORDER_ID_META_KEY );
if ( ! $order_id ) {
throw new RuntimeException( 'PayPal order ID not found in meta.' );
}
$order = $this->order_endpoint->order( $order_id );
$payments = $this->get_payments( $order );
$this->logger->debug(
sprintf(
'Trying to refund/void order %1$s, payments: %2$s.',
$order->id(),
wp_json_encode( $payments->to_array() )
)
);
$mode = $this->determine_refund_mode( $payments );
switch ( $mode ) {
case self::REFUND_MODE_REFUND:
$refund_id = $this->refund( $order, $wc_order, $amount, $reason );
$this->add_refund_to_meta( $wc_order, $refund_id );
$this->refund_fees_updater->update( $wc_order );
break;
case self::REFUND_MODE_VOID:
$this->void( $order );
$wc_order->set_status( 'refunded' );
$wc_order->save();
break;
default:
throw new RuntimeException( 'Nothing to refund/void.' );
}
return true;
} catch ( Exception $error ) {
$this->logger->error( 'Refund failed: ' . $error->getMessage() );
return false;
}
}
/**
* Adds a refund to the PayPal order.
*
* @param Order $order The PayPal order.
* @param WC_Order $wc_order The WooCommerce order.
* @param float $amount The refund amount.
* @param string $reason The reason for the refund.
*
* @throws RuntimeException When operation fails.
* @return string The PayPal refund ID.
*/
public function refund(
Order $order,
WC_Order $wc_order,
float $amount,
string $reason = ''
): string {
$payments = $this->get_payments( $order );
$captures = $payments->captures();
if ( ! $captures ) {
throw new RuntimeException( 'No capture.' );
}
$capture = $captures[0];
$refund = new RefundCapture(
$capture,
$capture->invoice_id() ?: $this->prefix . $wc_order->get_order_number(),
$reason,
new Amount(
new Money( $amount, $wc_order->get_currency() )
)
);
return $this->payments_endpoint->refund( $refund );
}
/**
* Voids the authorization.
*
* @param Order $order The PayPal order.
* @throws RuntimeException When operation fails.
*/
public function void( Order $order ): void {
$payments = $this->get_payments( $order );
$voidable_authorizations = array_filter(
$payments->authorizations(),
function ( Authorization $authorization ): bool {
return $authorization->is_voidable();
}
);
if ( ! $voidable_authorizations ) {
throw new RuntimeException( 'No voidable authorizations.' );
}
foreach ( $voidable_authorizations as $authorization ) {
$this->payments_endpoint->void( $authorization );
}
}
/**
* Determines the refunding mode.
*
* @param Payments $payments The order payments state.
*
* @return string One of the REFUND_MODE_ constants.
*/
private function determine_refund_mode( Payments $payments ): string {
$authorizations = $payments->authorizations();
if ( $authorizations ) {
foreach ( $authorizations as $authorization ) {
if ( $authorization->is_voidable() ) {
return self::REFUND_MODE_VOID;
}
}
}
if ( $payments->captures() ) {
return self::REFUND_MODE_REFUND;
}
return self::REFUND_MODE_UNKNOWN;
}
/**
* Returns the payments object or throws.
*
* @param Order $order The order.
* @throws RuntimeException When payment not available.
*/
protected function get_payments( Order $order ): Payments {
$purchase_units = $order->purchase_units();
if ( ! $purchase_units ) {
throw new RuntimeException( 'No purchase units.' );
}
$payments = $purchase_units[0]->payments();
if ( ! $payments ) {
throw new RuntimeException( 'No payments.' );
}
return $payments;
}
}