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; } }