host = $host; $this->bearer = $bearer; $this->order_factory = $order_factory; $this->patch_collection_factory = $patch_collection_factory; $this->intent = $intent; $this->logger = $logger; $this->application_context_repository = $application_context_repository; $this->bn_code = $bn_code; $this->paypal_request_id_repository = $paypal_request_id_repository; $this->subscription_helper = $subscription_helper; } /** * Changes the used BN Code. * * @param string $bn_code The new BN Code to use. * * @return OrderEndpoint * @throws RuntimeException If the request fails. */ public function with_bn_code( string $bn_code ): OrderEndpoint { $this->bn_code = $bn_code; return $this; } /** * Creates an order. * * @param PurchaseUnit[] $items The purchase unit items for the order. * @param Payer|null $payer The payer off the order. * @param PaymentToken|null $payment_token The payment token. * @param PaymentMethod|null $payment_method The payment method. * @param string $paypal_request_id The paypal request id. * @param bool $shipping_address_is_fixed Whether the shipping address is changeable or not. * * @return Order * @throws RuntimeException If the request fails. */ public function create( array $items, Payer $payer = null, PaymentToken $payment_token = null, PaymentMethod $payment_method = null, string $paypal_request_id = '', bool $shipping_address_is_fixed = false ): Order { $contains_physical_goods = false; $items = array_filter( $items, static function ( $item ) use ( &$contains_physical_goods ): bool { $is_purchase_unit = is_a( $item, PurchaseUnit::class ); /** * A purchase unit. * * @var PurchaseUnit $item */ if ( $is_purchase_unit && $item->contains_physical_goods() ) { $contains_physical_goods = true; } return $is_purchase_unit; } ); $shipping_preferences = $contains_physical_goods ? $shipping_address_is_fixed ? ApplicationContext::SHIPPING_PREFERENCE_SET_PROVIDED_ADDRESS : 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, 'purchase_units' => array_map( static function ( PurchaseUnit $item ): array { return $item->to_array(); }, $items ), 'application_context' => $this->application_context_repository ->current_context( $shipping_preferences )->to_array(), ); if ( $payer ) { $data['payer'] = $payer->to_array(); } if ( $payment_token ) { $data['payment_source']['token'] = $payment_token->to_array(); } if ( $payment_method ) { $data['payment_method'] = $payment_method->to_array(); } $url = trailingslashit( $this->host ) . 'v2/checkout/orders'; $args = array( 'method' => 'POST', 'headers' => array( 'Authorization' => 'Bearer ' . $bearer->token(), 'Content-Type' => 'application/json', 'Prefer' => 'return=representation', ), 'body' => wp_json_encode( $data ), ); $paypal_request_id = $paypal_request_id ? $paypal_request_id : uniqid( 'ppcp-', true ); $args['headers']['PayPal-Request-Id'] = $paypal_request_id; if ( $this->bn_code ) { $args['headers']['PayPal-Partner-Attribution-Id'] = $this->bn_code; } $response = $this->request( $url, $args ); if ( is_wp_error( $response ) ) { $error = new RuntimeException( __( 'Could not create order.', 'woocommerce-paypal-payments' ) ); $this->logger->log( 'warning', $error->getMessage(), array( 'args' => $args, 'response' => $response, ) ); throw $error; } $json = json_decode( $response['body'] ); $status_code = (int) wp_remote_retrieve_response_code( $response ); if ( 201 !== $status_code ) { $error = new PayPalApiException( $json, $status_code ); $this->logger->log( 'warning', sprintf( 'Failed to create order. PayPal API response: %1$s', $error->getMessage() ), array( 'args' => $args, 'response' => $response, ) ); throw $error; } $order = $this->order_factory->from_paypal_response( $json ); $this->paypal_request_id_repository->set_for_order( $order, $paypal_request_id ); return $order; } /** * Captures an order. * * @param Order $order The order. * * @return Order * @throws RuntimeException If the request fails. */ public function capture( Order $order ): Order { if ( $order->status()->is( OrderStatus::COMPLETED ) ) { return $order; } $bearer = $this->bearer->bearer(); $url = trailingslashit( $this->host ) . 'v2/checkout/orders/' . $order->id() . '/capture'; $args = array( 'method' => 'POST', 'headers' => array( 'Authorization' => 'Bearer ' . $bearer->token(), 'Content-Type' => 'application/json', 'Prefer' => 'return=representation', 'PayPal-Request-Id' => $this->paypal_request_id_repository->get_for_order( $order ), ), ); if ( $this->bn_code ) { $args['headers']['PayPal-Partner-Attribution-Id'] = $this->bn_code; } $response = $this->request( $url, $args ); if ( is_wp_error( $response ) ) { $error = new RuntimeException( __( 'Could not capture order.', 'woocommerce-paypal-payments' ) ); $this->logger->log( 'warning', $error->getMessage(), array( 'args' => $args, 'response' => $response, ) ); throw $error; } $json = json_decode( $response['body'] ); $status_code = (int) wp_remote_retrieve_response_code( $response ); if ( 201 !== $status_code ) { $error = new PayPalApiException( $json, $status_code ); // If the order has already been captured, we return the updated order. if ( strpos( $response['body'], ErrorResponse::ORDER_ALREADY_CAPTURED ) !== false ) { return $this->order( $order->id() ); } $this->logger->log( 'warning', sprintf( 'Failed to capture order. PayPal API response: %1$s', $error->getMessage() ), array( 'args' => $args, 'response' => $response, ) ); throw $error; } $order = $this->order_factory->from_paypal_response( $json ); $capture_status = $order->purchase_units()[0]->payments()->captures()[0]->status() ?? null; if ( $capture_status && $capture_status->is( CaptureStatus::DECLINED ) ) { throw new RuntimeException( __( 'Payment provider declined the payment, please use a different payment method.', 'woocommerce-paypal-payments' ) ); } return $order; } /** * Authorize an order. * * @param Order $order The order. * * @return Order * @throws RuntimeException If the request fails. */ public function authorize( Order $order ): Order { $bearer = $this->bearer->bearer(); $url = trailingslashit( $this->host ) . 'v2/checkout/orders/' . $order->id() . '/authorize'; $args = array( 'method' => 'POST', 'headers' => array( 'Authorization' => 'Bearer ' . $bearer->token(), 'Content-Type' => 'application/json', 'Prefer' => 'return=representation', 'PayPal-Request-Id' => $this->paypal_request_id_repository->get_for_order( $order ), ), ); if ( $this->bn_code ) { $args['headers']['PayPal-Partner-Attribution-Id'] = $this->bn_code; } $response = $this->request( $url, $args ); if ( is_wp_error( $response ) ) { $error = new RuntimeException( __( 'Could not authorize order.', 'woocommerce-paypal-payments' ) ); $this->logger->log( 'warning', $error->getMessage(), array( 'args' => $args, 'response' => $response, ) ); throw $error; } $json = json_decode( $response['body'] ); $status_code = (int) wp_remote_retrieve_response_code( $response ); if ( ! in_array( $status_code, array( 200, 201 ), true ) ) { if ( false !== strpos( $response['body'], ErrorResponse::ORDER_ALREADY_AUTHORIZED ) ) { return $this->order( $order->id() ); } $error = new PayPalApiException( $json, $status_code ); $this->logger->log( 'warning', sprintf( 'Failed to authorize order. PayPal API response: %1$s', $error->getMessage() ), array( 'args' => $args, 'response' => $response, ) ); throw $error; } $order = $this->order_factory->from_paypal_response( $json ); $authorization_status = $order->purchase_units()[0]->payments()->authorizations()[0]->status() ?? null; if ( $authorization_status && $authorization_status->is( AuthorizationStatus::DENIED ) ) { throw new RuntimeException( __( 'Payment provider declined the payment, please use a different payment method.', 'woocommerce-paypal-payments' ) ); } return $order; } /** * Fetches an order for a given ID. * * @param string $id The ID. * * @return Order * @throws RuntimeException If the request fails. */ public function order( string $id ): Order { $bearer = $this->bearer->bearer(); $url = trailingslashit( $this->host ) . 'v2/checkout/orders/' . $id; $args = array( 'headers' => array( 'Authorization' => 'Bearer ' . $bearer->token(), 'Content-Type' => 'application/json', 'PayPal-Request-Id' => $this->paypal_request_id_repository->get_for_order_id( $id ), ), ); if ( $this->bn_code ) { $args['headers']['PayPal-Partner-Attribution-Id'] = $this->bn_code; } $response = $this->request( $url, $args ); if ( is_wp_error( $response ) ) { $error = new RuntimeException( __( 'Could not retrieve order.', 'woocommerce-paypal-payments' ) ); $this->logger->log( 'warning', $error->getMessage(), array( 'args' => $args, 'response' => $response, ) ); throw $error; } $json = json_decode( $response['body'] ); $status_code = (int) wp_remote_retrieve_response_code( $response ); if ( 404 === $status_code || empty( $response['body'] ) ) { $error = new RuntimeException( __( 'Could not retrieve order.', 'woocommerce-paypal-payments' ), 404 ); $this->logger->log( 'warning', $error->getMessage(), array( 'args' => $args, 'response' => $response, ) ); throw $error; } if ( 200 !== $status_code ) { $error = new PayPalApiException( $json, $status_code ); $this->logger->log( 'warning', $error->getMessage(), array( 'args' => $args, 'response' => $response, ) ); throw $error; } $order = $this->order_factory->from_paypal_response( $json ); return $order; } /** * Patches an order. * * @param Order $order_to_update The order to patch. * @param Order $order_to_compare The target order. * * @return Order * @throws RuntimeException If the request fails. */ public function patch_order_with( Order $order_to_update, Order $order_to_compare ): Order { $patches = $this->patch_collection_factory->from_orders( $order_to_update, $order_to_compare ); if ( ! count( $patches->patches() ) ) { return $order_to_update; } $patches_array = $patches->to_array(); if ( ! isset( $patches_array[0]['value']['shipping'] ) ) { $shipping = isset( $order_to_update->purchase_units()[0] ) && null !== $order_to_update->purchase_units()[0]->shipping() ? $order_to_update->purchase_units()[0]->shipping() : null; if ( $shipping ) { $patches_array[0]['value']['shipping'] = $shipping->to_array(); } } $bearer = $this->bearer->bearer(); $url = trailingslashit( $this->host ) . 'v2/checkout/orders/' . $order_to_update->id(); $args = array( 'method' => 'PATCH', 'headers' => array( 'Authorization' => 'Bearer ' . $bearer->token(), 'Content-Type' => 'application/json', 'Prefer' => 'return=representation', 'PayPal-Request-Id' => $this->paypal_request_id_repository->get_for_order( $order_to_update ), ), 'body' => wp_json_encode( $patches_array ), ); if ( $this->bn_code ) { $args['headers']['PayPal-Partner-Attribution-Id'] = $this->bn_code; } $response = $this->request( $url, $args ); if ( is_wp_error( $response ) ) { $error = new RuntimeException( __( 'Could not retrieve order.', 'woocommerce-paypal-payments' ) ); $this->logger->log( 'warning', $error->getMessage(), array( 'args' => $args, 'response' => $response, ) ); throw $error; } $json = json_decode( $response['body'] ); $status_code = (int) wp_remote_retrieve_response_code( $response ); if ( 204 !== $status_code ) { $error = new PayPalApiException( $json, $status_code ); $this->logger->log( 'warning', $error->getMessage(), array( 'args' => $args, 'response' => $response, ) ); throw $error; } $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; } }