diff --git a/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php b/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php index 2446fbf17..0ff094e3c 100644 --- a/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php @@ -204,6 +204,10 @@ class OrderEndpoint { : ApplicationContext::SHIPPING_PREFERENCE_GET_FROM_FILE : ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING; + if ( $this->has_items_without_shipping( $items ) ) { + $shipping_preferences = ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING; + } + $bearer = $this->bearer->bearer(); $data = array( 'intent' => ( $this->subscription_helper->cart_contains_subscription() || $this->subscription_helper->current_product_is_subscription() ) ? 'AUTHORIZE' : $this->intent, @@ -583,4 +587,20 @@ class OrderEndpoint { $new_order = $this->order( $order_to_update->id() ); return $new_order; } + + /** + * Checks if there is at least one item without shipping. + * + * @param array $items The items. + * @return bool Whether items contains shipping or not. + */ + private function has_items_without_shipping( array $items ): bool { + foreach ( $items as $item ) { + if ( ! $item->shipping() ) { + return true; + } + } + + return false; + } } diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index edfdd43d2..1c2675fa6 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -58,6 +58,7 @@ return array( $payments_endpoint = $container->get( 'api.endpoint.payments' ); $order_endpoint = $container->get( 'api.endpoint.order' ); $environment = $container->get( 'onboarding.environment' ); + $logger = $container->get( 'woocommerce.logger.woocommerce' ); return new PayPalGateway( $settings_renderer, $order_processor, @@ -236,8 +237,8 @@ return array( return new AuthorizedPaymentsProcessor( $order_endpoint, $payments_endpoint, $logger, $notice ); }, 'wcgateway.admin.render-authorize-action' => static function ( ContainerInterface $container ): RenderAuthorizeAction { - - return new RenderAuthorizeAction(); + $column = $container->get( 'wcgateway.admin.orders-payment-status-column' ); + return new RenderAuthorizeAction( $column ); }, 'wcgateway.admin.order-payment-status' => static function ( ContainerInterface $container ): PaymentStatusOrderDetail { $column = $container->get( 'wcgateway.admin.orders-payment-status-column' ); diff --git a/modules/ppcp-wc-gateway/src/Admin/RenderAuthorizeAction.php b/modules/ppcp-wc-gateway/src/Admin/RenderAuthorizeAction.php index 19408d75b..1d38e64a6 100644 --- a/modules/ppcp-wc-gateway/src/Admin/RenderAuthorizeAction.php +++ b/modules/ppcp-wc-gateway/src/Admin/RenderAuthorizeAction.php @@ -16,6 +16,21 @@ use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; * Class RenderAuthorizeAction */ class RenderAuthorizeAction { + /** + * 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. @@ -46,7 +61,10 @@ class RenderAuthorizeAction { * @return bool */ private function should_render_for_order( \WC_Order $order ) : bool { - $data = $order->get_meta( AuthorizedPaymentsProcessor::CAPTURED_META_KEY ); - return in_array( $data, array( 'true', 'false' ), true ); + $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 ); } } diff --git a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php index 8ea49b653..3bedd11b5 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php @@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Gateway; use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatus; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Session\SessionHandler; @@ -106,13 +107,6 @@ class PayPalGateway extends \WC_Payment_Gateway { */ protected $payment_token_repository; - /** - * The logger. - * - * @var LoggerInterface - */ - protected $logger; - /** * The payments endpoint * @@ -148,6 +142,13 @@ class PayPalGateway extends \WC_Payment_Gateway { */ protected $environment; + /** + * The logger. + * + * @var LoggerInterface + */ + private $logger; + /** * PayPalGateway constructor. * @@ -166,6 +167,7 @@ class PayPalGateway extends \WC_Payment_Gateway { * @param LoggerInterface $logger The logger. * @param PaymentsEndpoint $payments_endpoint The payments endpoint. * @param OrderEndpoint $order_endpoint The order endpoint. + * @param LoggerInterface $logger The logger. */ public function __construct( SettingsRenderer $settings_renderer, @@ -196,6 +198,18 @@ class PayPalGateway extends \WC_Payment_Gateway { $this->page_id = $page_id; $this->environment = $environment; $this->onboarded = $state->current_state() === State::STATE_ONBOARDED; + $this->id = self::ID; + $this->order_processor = $order_processor; + $this->authorized_payments = $authorized_payments_processor; + $this->settings_renderer = $settings_renderer; + $this->config = $config; + $this->session_handler = $session_handler; + $this->refund_processor = $refund_processor; + $this->transaction_url_provider = $transaction_url_provider; + $this->page_id = $page_id; + $this->environment = $environment; + $this->logger = $logger; + $this->onboarded = $state->current_state() === State::STATE_ONBOARDED; if ( $this->onboarded ) { $this->supports = array( 'refunds' ); diff --git a/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php b/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php index c93ec58be..de1e0a70c 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php +++ b/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php @@ -19,13 +19,14 @@ use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderMetaTrait; use WooCommerce\PayPalCommerce\WcGateway\Processor\PaymentsStatusHandlingTrait; +use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait; /** * Trait ProcessPaymentTrait */ trait ProcessPaymentTrait { - use OrderMetaTrait, PaymentsStatusHandlingTrait; + use OrderMetaTrait, PaymentsStatusHandlingTrait, TransactionIdHandlingTrait; /** * Process a payment for an WooCommerce order. @@ -107,6 +108,11 @@ trait ProcessPaymentTrait { $wc_order->update_meta_data( AuthorizedPaymentsProcessor::CAPTURED_META_KEY, 'false' ); } + $transaction_id = $this->get_paypal_order_transaction_id( $order ); + if ( $transaction_id ) { + $this->update_transaction_id( $transaction_id, $wc_order ); + } + $this->handle_new_order_status( $order, $wc_order ); if ( $this->config->has( 'intent' ) && strtoupper( (string) $this->config->get( 'intent' ) ) === 'CAPTURE' ) { diff --git a/modules/ppcp-wc-gateway/src/Processor/OrderProcessor.php b/modules/ppcp-wc-gateway/src/Processor/OrderProcessor.php index 6bb512eda..dc4796f42 100644 --- a/modules/ppcp-wc-gateway/src/Processor/OrderProcessor.php +++ b/modules/ppcp-wc-gateway/src/Processor/OrderProcessor.php @@ -26,7 +26,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; */ class OrderProcessor { - use OrderMetaTrait, PaymentsStatusHandlingTrait; + use OrderMetaTrait, PaymentsStatusHandlingTrait, TransactionIdHandlingTrait; /** * The environment. @@ -176,8 +176,8 @@ class OrderProcessor { $transaction_id = $this->get_paypal_order_transaction_id( $order ); - if ( '' !== $transaction_id ) { - $this->set_order_transaction_id( $transaction_id, $wc_order ); + if ( $transaction_id ) { + $this->update_transaction_id( $transaction_id, $wc_order ); } $this->handle_new_order_status( $order, $wc_order ); @@ -195,55 +195,6 @@ class OrderProcessor { return true; } - /** - * Set transaction id to WC order meta data. - * - * @param string $transaction_id Transaction id to set. - * @param \WC_Order $wc_order Order to set transaction ID to. - */ - public function set_order_transaction_id( string $transaction_id, \WC_Order $wc_order ) { - try { - $wc_order->set_transaction_id( $transaction_id ); - } catch ( \WC_Data_Exception $exception ) { - $this->logger->log( - 'warning', - sprintf( - 'Failed to set transaction ID. Exception caught when tried: %1$s', - $exception->getMessage() - ) - ); - } - } - - /** - * Retrieve transaction id from PayPal order. - * - * @param Order $order Order to get transaction id from. - * - * @return string - */ - private function get_paypal_order_transaction_id( Order $order ): string { - $purchase_units = $order->purchase_units(); - - if ( ! isset( $purchase_units[0] ) ) { - return ''; - } - - $payments = $purchase_units[0]->payments(); - - if ( null === $payments ) { - return ''; - } - - $captures = $payments->captures(); - - if ( isset( $captures[0] ) ) { - return $captures[0]->id(); - } - - return ''; - } - /** * Returns if an order should be captured immediately. * diff --git a/modules/ppcp-wc-gateway/src/Processor/TransactionIdHandlingTrait.php b/modules/ppcp-wc-gateway/src/Processor/TransactionIdHandlingTrait.php new file mode 100644 index 000000000..76c16a977 --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Processor/TransactionIdHandlingTrait.php @@ -0,0 +1,85 @@ +set_transaction_id( $transaction_id ); + $wc_order->save(); + return true; + } catch ( Exception $exception ) { + if ( $logger ) { + $logger->warning( + sprintf( + 'Failed to set transaction ID %1$s. %2$s', + $transaction_id, + $exception->getMessage() + ) + ); + } + return false; + } + } + + /** + * Retrieves transaction id from PayPal order. + * + * @param Order $order The order to get transaction id from. + * + * @return string|null + */ + protected function get_paypal_order_transaction_id( Order $order ): ?string { + $purchase_unit = $order->purchase_units()[0] ?? null; + if ( ! $purchase_unit ) { + return null; + } + + $payments = $purchase_unit->payments(); + if ( null === $payments ) { + return null; + } + + $capture = $payments->captures()[0] ?? null; + if ( $capture ) { + return $capture->id(); + } + + $authorization = $payments->authorizations()[0] ?? null; + if ( $authorization ) { + return $authorization->id(); + } + + return null; + } + +} diff --git a/modules/ppcp-webhooks/services.php b/modules/ppcp-webhooks/services.php index f9fdb09e0..8282ef3f6 100644 --- a/modules/ppcp-webhooks/services.php +++ b/modules/ppcp-webhooks/services.php @@ -68,7 +68,7 @@ return array( new CheckoutOrderCompleted( $logger, $prefix ), new PaymentCaptureRefunded( $logger, $prefix ), new PaymentCaptureReversed( $logger, $prefix ), - new PaymentCaptureCompleted( $logger, $prefix ), + new PaymentCaptureCompleted( $logger, $prefix, $order_endpoint ), ); }, diff --git a/modules/ppcp-webhooks/src/Handler/PaymentCaptureCompleted.php b/modules/ppcp-webhooks/src/Handler/PaymentCaptureCompleted.php index cac71434b..aecaaddaf 100644 --- a/modules/ppcp-webhooks/src/Handler/PaymentCaptureCompleted.php +++ b/modules/ppcp-webhooks/src/Handler/PaymentCaptureCompleted.php @@ -9,16 +9,20 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Webhooks\Handler; +use Exception; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; +use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait; +use WP_REST_Response; /** * Class PaymentCaptureCompleted */ class PaymentCaptureCompleted implements RequestHandler { - use PrefixTrait; + use PrefixTrait, TransactionIdHandlingTrait; /** * The logger. @@ -27,15 +31,28 @@ class PaymentCaptureCompleted implements RequestHandler { */ private $logger; + /** + * The order endpoint. + * + * @var OrderEndpoint + */ + private $order_endpoint; + /** * PaymentCaptureCompleted constructor. * * @param LoggerInterface $logger The logger. * @param string $prefix The prefix. + * @param OrderEndpoint $order_endpoint The order endpoint. */ - public function __construct( LoggerInterface $logger, string $prefix ) { - $this->logger = $logger; - $this->prefix = $prefix; + public function __construct( + LoggerInterface $logger, + string $prefix, + OrderEndpoint $order_endpoint + ) { + $this->logger = $logger; + $this->prefix = $prefix; + $this->order_endpoint = $order_endpoint; } /** @@ -63,56 +80,41 @@ class PaymentCaptureCompleted implements RequestHandler { * * @param \WP_REST_Request $request The request. * - * @return \WP_REST_Response + * @return WP_REST_Response */ - public function handle_request( \WP_REST_Request $request ): \WP_REST_Response { + public function handle_request( \WP_REST_Request $request ): WP_REST_Response { $response = array( 'success' => false ); - $order_id = isset( $request['resource']['custom_id'] ) ? - $this->sanitize_custom_id( $request['resource']['custom_id'] ) : 0; - if ( ! $order_id ) { - $message = sprintf( - // translators: %s is the PayPal webhook Id. - __( - 'No order for webhook event %s was found.', - 'woocommerce-paypal-payments' - ), - isset( $request['id'] ) ? $request['id'] : '' - ); - $this->logger->log( - 'warning', - $message, - array( - 'request' => $request, - ) - ); - $response['message'] = $message; - return rest_ensure_response( $response ); - } - $wc_order = wc_get_order( $order_id ); - if ( ! is_a( $wc_order, \WC_Order::class ) ) { - $message = sprintf( - // translators: %s is the PayPal webhook Id. - __( - 'No order for webhook event %s was found.', - 'woocommerce-paypal-payments' - ), - isset( $request['id'] ) ? $request['id'] : '' - ); - $this->logger->log( - 'warning', - $message, - array( - 'request' => $request, - ) - ); + $webhook_id = (string) ( $request['id'] ?? '' ); + + $resource = $request['resource']; + if ( ! is_array( $resource ) ) { + $message = 'Resource data not found in webhook request.'; + $this->logger->warning( $message, array( 'request' => $request ) ); $response['message'] = $message; - return rest_ensure_response( $response ); + return new WP_REST_Response( $response ); + } + + $wc_order_id = isset( $resource['custom_id'] ) ? + $this->sanitize_custom_id( (string) $resource['custom_id'] ) : 0; + if ( ! $wc_order_id ) { + $message = sprintf( 'No order for webhook event %s was found.', $webhook_id ); + $this->logger->warning( $message, array( 'request' => $request ) ); + $response['message'] = $message; + return new WP_REST_Response( $response ); + } + + $wc_order = wc_get_order( $wc_order_id ); + if ( ! is_a( $wc_order, \WC_Order::class ) ) { + $message = sprintf( 'No order for webhook event %s was found.', $webhook_id ); + $this->logger->warning( $message, array( 'request' => $request ) ); + $response['message'] = $message; + return new WP_REST_Response( $response ); } if ( $wc_order->get_status() !== 'on-hold' ) { $response['success'] = true; - return rest_ensure_response( $response ); + return new WP_REST_Response( $response ); } $wc_order->add_order_note( __( 'Payment successfully captured.', 'woocommerce-paypal-payments' ) @@ -136,7 +138,25 @@ class PaymentCaptureCompleted implements RequestHandler { 'order' => $wc_order, ) ); + + $order_id = $resource['supplementary_data']['related_ids']['order_id'] ?? null; + + if ( $order_id ) { + try { + $this->logger->emergency( (string) $order_id ); + + $order = $this->order_endpoint->order( $order_id ); + + $transaction_id = $this->get_paypal_order_transaction_id( $order ); + if ( $transaction_id ) { + $this->update_transaction_id( $transaction_id, $wc_order, $this->logger ); + } + } catch ( Exception $exception ) { + $this->logger->warning( 'Failed to get transaction ID: ' . $exception->getMessage() ); + } + } + $response['success'] = true; - return rest_ensure_response( $response ); + return new WP_REST_Response( $response ); } } diff --git a/tests/PHPUnit/ApiClient/Endpoint/OrderEndpointTest.php b/tests/PHPUnit/ApiClient/Endpoint/OrderEndpointTest.php index 8ead360c6..9a777c3fa 100644 --- a/tests/PHPUnit/ApiClient/Endpoint/OrderEndpointTest.php +++ b/tests/PHPUnit/ApiClient/Endpoint/OrderEndpointTest.php @@ -912,6 +912,7 @@ class OrderEndpointTest extends TestCase $purchaseUnit ->expects('to_array') ->andReturn(['singlePurchaseUnit']); + $purchaseUnit->expects('shipping')->andReturn(true); expect('wp_remote_get') ->andReturnUsing( @@ -1014,6 +1015,7 @@ class OrderEndpointTest extends TestCase $purchaseUnit ->expects('to_array') ->andReturn(['singlePurchaseUnit']); + $purchaseUnit->expects('shipping')->andReturn(true); expect('wp_remote_get') ->andReturnUsing( @@ -1090,6 +1092,7 @@ class OrderEndpointTest extends TestCase $purchaseUnit ->expects('to_array') ->andReturn(['singlePurchaseUnit']); + $purchaseUnit->expects('shipping')->andReturn(true); expect('wp_remote_get') ->andReturnUsing( @@ -1174,6 +1177,7 @@ class OrderEndpointTest extends TestCase $purchaseUnit ->expects('to_array') ->andReturn(['singlePurchaseUnit']); + $purchaseUnit->expects('shipping')->andReturn(true); expect('wp_remote_get') ->andReturnUsing( diff --git a/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php b/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php index 603cca9da..c94ce2420 100644 --- a/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php +++ b/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php @@ -8,6 +8,9 @@ use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint; +use Psr\Log\NullLogger; +use WooCommerce\PayPalCommerce\ApiClient\Entity\Capture; +use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatus; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Session\SessionHandler; diff --git a/tests/PHPUnit/WcGateway/Processor/OrderProcessorTest.php b/tests/PHPUnit/WcGateway/Processor/OrderProcessorTest.php index 8d89e39ff..addf60602 100644 --- a/tests/PHPUnit/WcGateway/Processor/OrderProcessorTest.php +++ b/tests/PHPUnit/WcGateway/Processor/OrderProcessorTest.php @@ -159,7 +159,8 @@ class OrderProcessorTest extends TestCase $wcOrder ->expects('update_status') ->with('on-hold', 'Awaiting payment.'); - + $wcOrder->expects('set_transaction_id') + ->with($transactionId); $this->assertTrue($testee->process($wcOrder)); }