From 1cf631f3a3ddeb136352913eb1b7bf3673618486 Mon Sep 17 00:00:00 2001 From: hsingyuc Date: Fri, 31 Mar 2023 13:46:12 -0700 Subject: [PATCH 01/26] Register order route --- src/StoreApi/Routes/V1/Order.php | 36 ------------------------------- src/StoreApi/RoutesController.php | 1 + 2 files changed, 1 insertion(+), 36 deletions(-) diff --git a/src/StoreApi/Routes/V1/Order.php b/src/StoreApi/Routes/V1/Order.php index 3a6d04e7850..740c233d91d 100644 --- a/src/StoreApi/Routes/V1/Order.php +++ b/src/StoreApi/Routes/V1/Order.php @@ -71,42 +71,6 @@ public function get_args() { ]; } - /** - * Check if authorized to get the order. - * - * @throws RouteException If the order is not found or the order key is invalid. - * - * @param \WP_REST_Request $request Request object. - * @return boolean|WP_Error - */ - public function is_authorized( \WP_REST_Request $request ) { - $order_id = absint( $request['id'] ); - $order_key = wc_clean( wp_unslash( $request->get_param( 'key' ) ) ); - $billing_email = wc_clean( wp_unslash( $request->get_param( 'billing_email' ) ) ); - - try { - // In this context, pay_for_order capability checks that the current user ID matches the customer ID stored - // within the order, or if the order was placed by a guest. - // See https://github.com/woocommerce/woocommerce/blob/abcedbefe02f9e89122771100c42ff588da3e8e0/plugins/woocommerce/includes/wc-user-functions.php#L458. - if ( ! current_user_can( 'pay_for_order', $order_id ) ) { - throw new RouteException( 'woocommerce_rest_invalid_user', __( 'This order belongs to a different customer. Please log in to the correct account.', 'woo-gutenberg-products-block' ), 403 ); - } - - if ( get_current_user_id() === 0 ) { - $this->order_controller->validate_order_key( $order_id, $order_key ); - $this->order_controller->validate_billing_email( $order_id, $billing_email ); - } - } catch ( RouteException $error ) { - return new \WP_Error( - $error->getErrorCode(), - $error->getMessage(), - array( 'status' => $error->getCode() ) - ); - } - - return true; - } - /** * Handle the request and return a valid response for this endpoint. * diff --git a/src/StoreApi/RoutesController.php b/src/StoreApi/RoutesController.php index bf94a73c417..d0028145caa 100644 --- a/src/StoreApi/RoutesController.php +++ b/src/StoreApi/RoutesController.php @@ -48,6 +48,7 @@ public function __construct( SchemaController $schema_controller ) { Routes\V1\CartUpdateItem::IDENTIFIER => Routes\V1\CartUpdateItem::class, Routes\V1\CartUpdateCustomer::IDENTIFIER => Routes\V1\CartUpdateCustomer::class, Routes\V1\Checkout::IDENTIFIER => Routes\V1\Checkout::class, + Routes\V1\Order::IDENTIFIER => Routes\V1\Order::class, Routes\V1\ProductAttributes::IDENTIFIER => Routes\V1\ProductAttributes::class, Routes\V1\ProductAttributesById::IDENTIFIER => Routes\V1\ProductAttributesById::class, Routes\V1\ProductAttributeTerms::IDENTIFIER => Routes\V1\ProductAttributeTerms::class, From 82e21ea5b8d688e1a725ea3aceaf243f754c603f Mon Sep 17 00:00:00 2001 From: hsingyuc Date: Tue, 4 Apr 2023 18:11:32 -0700 Subject: [PATCH 02/26] Check authorization for getting the order --- src/StoreApi/Routes/V1/Order.php | 101 +++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/src/StoreApi/Routes/V1/Order.php b/src/StoreApi/Routes/V1/Order.php index 740c233d91d..373f4929df4 100644 --- a/src/StoreApi/Routes/V1/Order.php +++ b/src/StoreApi/Routes/V1/Order.php @@ -71,6 +71,107 @@ public function get_args() { ]; } + /** + * Check if authorized to get the order. + * + * @param \WP_REST_Request $request Request object. + * @return boolean|WP_Error + */ + public function is_authorized( \WP_REST_Request $request ) { + $order_id = absint( $request['id'] ); + $order_key = wc_clean( wp_unslash( $request->get_param( 'key' ) ) ); + $order = $this->order_controller->get_order( $order_id ); + + if ( ! $order || $order->get_id() !== $order_id || ! hash_equals( $order->get_order_key(), $order_key ) ) { + return new \WP_Error( 'invalid_order', __( 'Sorry, this order is invalid and cannot be paid for.', 'woo-gutenberg-products-block' ), array( 'status' => 401 ) ); + } + + // Logged out customer does not have permission to pay for this order. + if ( ! current_user_can( 'pay_for_order', $order_id ) && ! is_user_logged_in() ) { + return new \WP_Error( 'invalid_user', __( 'Please log in to your account below to continue to the payment form.', 'woo-gutenberg-products-block' ), array( 'status' => 403 ) ); + } + + // Logged in customer trying to pay for someone else's order. + if ( ! current_user_can( 'pay_for_order', $order_id ) ) { + return new \WP_Error( 'invalid_user', __( 'This order cannot be paid for. Please contact us if you need assistance.', 'woo-gutenberg-products-block' ), array( 'status' => 403 ) ); + } + + // Does not need payment. + if ( ! $order->needs_payment() ) { + /* translators: %s: order status */ + return new \WP_Error( '@Todo', sprintf( __( 'This order’s status is “%s”—it cannot be paid for. Please contact us if you need assistance.', 'woo-gutenberg-products-block' ), wc_get_order_status_name( $order->get_status() ) ), array( 'status' => 403 ) ); + } + + // Ensure order items are still stocked if paying for a failed order. Pending orders do not need this check because stock is held. + if ( ! $order->has_status( wc_get_is_pending_statuses() ) ) { + $quantities = array(); + + foreach ( $order->get_items() as $item_key => $item ) { + if ( $item && is_callable( array( $item, 'get_product' ) ) ) { + $product = $item->get_product(); + + if ( ! $product ) { + continue; + } + + $quantities[ $product->get_stock_managed_by_id() ] = isset( $quantities[ $product->get_stock_managed_by_id() ] ) ? $quantities[ $product->get_stock_managed_by_id() ] + $item->get_quantity() : $item->get_quantity(); + } + } + + // Stock levels may already have been adjusted for this order (in which case we don't need to worry about checking for low stock). + if ( ! $order->get_data_store()->get_stock_reduced( $order->get_id() ) ) { + foreach ( $order->get_items() as $item_key => $item ) { + if ( $item && is_callable( array( $item, 'get_product' ) ) ) { + $product = $item->get_product(); + + if ( ! $product ) { + continue; + } + + /** + * Filters whether or not the product is in stock for this pay for order. + * + * @param boolean True if in stock. + * @param \WC_Product $product Product. + * @param \WC_Order $order Order. + * + * @since 9.8.0-dev + */ + if ( ! apply_filters( 'woocommerce_pay_order_product_in_stock', $product->is_in_stock(), $product, $order ) ) { + /* translators: %s: product name */ + return new \WP_Error( '@Todo', sprintf( __( 'Sorry, "%s" is no longer in stock so this order cannot be paid for. We apologize for any inconvenience caused.', 'woo-gutenberg-products-block' ), $product->get_name() ), array( 'status' => 403 ) ); + } + + // We only need to check products managing stock, with a limited stock qty. + if ( ! $product->managing_stock() || $product->backorders_allowed() ) { + continue; + } + + // Check stock based on all items in the cart and consider any held stock within pending orders. + $held_stock = wc_get_held_stock_quantity( $product, $order->get_id() ); + $required_stock = $quantities[ $product->get_stock_managed_by_id() ]; + + /** + * Filters whether or not the product has enough stock. + * + * @param boolean True if has enough stock. + * @param \WC_Product $product Product. + * @param \WC_Order $order Order. + * + * @since 9.8.0-dev + */ + if ( ! apply_filters( 'woocommerce_pay_order_product_has_enough_stock', ( $product->get_stock_quantity() >= ( $held_stock + $required_stock ) ), $product, $order ) ) { + /* translators: 1: product name 2: quantity in stock */ + return new \WP_Error( '@Todo', sprintf( __( 'Sorry, we do not have enough "%1$s" in stock to fulfill your order (%2$s available). We apologize for any inconvenience caused.', 'woo-gutenberg-products-block' ), $product->get_name(), wc_format_stock_quantity_for_display( $product->get_stock_quantity() - $held_stock, $product ) ), array( 'status' => 403 ) ); + } + } + } + } + } + + return true; + } + /** * Handle the request and return a valid response for this endpoint. * From 9ae503a1041fed45000f10b4628f2f4a81ff47d1 Mon Sep 17 00:00:00 2001 From: hsingyuc Date: Tue, 4 Apr 2023 18:21:57 -0700 Subject: [PATCH 03/26] Add order data to the response --- src/StoreApi/Schemas/V1/OrderSchema.php | 51 +++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/StoreApi/Schemas/V1/OrderSchema.php b/src/StoreApi/Schemas/V1/OrderSchema.php index a10a7a5587a..a85ca2fbb7a 100644 --- a/src/StoreApi/Schemas/V1/OrderSchema.php +++ b/src/StoreApi/Schemas/V1/OrderSchema.php @@ -339,4 +339,55 @@ function( $item ) { ), ]; } + + /** + * Get items data. + * + * @param \WC_Order $order Order instance. + * @return array + */ + private function get_item_data( $order ) { + $items = $order->get_items(); + $data = []; + + foreach ( $items as $item ) { + $data[ $item->get_id() ]['id'] = $item->get_id(); + $data[ $item->get_id() ]['name'] = $item->get_name(); + $data[ $item->get_id() ]['meta_data'] = $item->get_all_formatted_meta_data(); + $data[ $item->get_id() ]['quantity'] = $item->get_quantity(); + $data[ $item->get_id() ]['subtotal'] = $order->get_line_subtotal( $item ); + } + + return array_values( $data ); + } + + /** + * Get fee. + * + * @param \WC_Order $order Order instance. + * @return array + */ + private function get_fees_total( $order ) { + $fees = $order->get_fees(); + $total_fees = 0; + + if ( $fees ) { + foreach ( $fees as $id => $fee ) { + /** + * Filters whether or not free fees should be excluded. + * + * @param boolean True to skip the fee, false to include the fee. + * @param integer $id Fee ID. + * + * @since 9.8.0-dev + */ + if ( apply_filters( 'woocommerce_get_order_item_totals_excl_free_fees', empty( $fee['line_total'] ) && empty( $fee['line_tax'] ), $id ) ) { + continue; + } + $total_fees += $fee->get_total(); + } + } + + return $total_fees; + } } From df8c55b5a562cc8c147bdbad770540fe837e4c6d Mon Sep 17 00:00:00 2001 From: hsingyuc Date: Tue, 4 Apr 2023 18:36:09 -0700 Subject: [PATCH 04/26] Add order schema for the endpoint --- src/StoreApi/Schemas/V1/OrderSchema.php | 30 ------------------------- 1 file changed, 30 deletions(-) diff --git a/src/StoreApi/Schemas/V1/OrderSchema.php b/src/StoreApi/Schemas/V1/OrderSchema.php index a85ca2fbb7a..8bd7205ab5f 100644 --- a/src/StoreApi/Schemas/V1/OrderSchema.php +++ b/src/StoreApi/Schemas/V1/OrderSchema.php @@ -360,34 +360,4 @@ private function get_item_data( $order ) { return array_values( $data ); } - - /** - * Get fee. - * - * @param \WC_Order $order Order instance. - * @return array - */ - private function get_fees_total( $order ) { - $fees = $order->get_fees(); - $total_fees = 0; - - if ( $fees ) { - foreach ( $fees as $id => $fee ) { - /** - * Filters whether or not free fees should be excluded. - * - * @param boolean True to skip the fee, false to include the fee. - * @param integer $id Fee ID. - * - * @since 9.8.0-dev - */ - if ( apply_filters( 'woocommerce_get_order_item_totals_excl_free_fees', empty( $fee['line_total'] ) && empty( $fee['line_tax'] ), $id ) ) { - continue; - } - $total_fees += $fee->get_total(); - } - } - - return $total_fees; - } } From 31506f348d19905f265a318c98634f048fc9f8c1 Mon Sep 17 00:00:00 2001 From: hsingyuc Date: Tue, 4 Apr 2023 23:07:20 -0700 Subject: [PATCH 05/26] Move validation check to order controller --- src/StoreApi/Routes/V1/Order.php | 94 +++----------------------------- 1 file changed, 8 insertions(+), 86 deletions(-) diff --git a/src/StoreApi/Routes/V1/Order.php b/src/StoreApi/Routes/V1/Order.php index 373f4929df4..7f74d45b823 100644 --- a/src/StoreApi/Routes/V1/Order.php +++ b/src/StoreApi/Routes/V1/Order.php @@ -80,93 +80,15 @@ public function get_args() { public function is_authorized( \WP_REST_Request $request ) { $order_id = absint( $request['id'] ); $order_key = wc_clean( wp_unslash( $request->get_param( 'key' ) ) ); - $order = $this->order_controller->get_order( $order_id ); - if ( ! $order || $order->get_id() !== $order_id || ! hash_equals( $order->get_order_key(), $order_key ) ) { - return new \WP_Error( 'invalid_order', __( 'Sorry, this order is invalid and cannot be paid for.', 'woo-gutenberg-products-block' ), array( 'status' => 401 ) ); - } - - // Logged out customer does not have permission to pay for this order. - if ( ! current_user_can( 'pay_for_order', $order_id ) && ! is_user_logged_in() ) { - return new \WP_Error( 'invalid_user', __( 'Please log in to your account below to continue to the payment form.', 'woo-gutenberg-products-block' ), array( 'status' => 403 ) ); - } - - // Logged in customer trying to pay for someone else's order. - if ( ! current_user_can( 'pay_for_order', $order_id ) ) { - return new \WP_Error( 'invalid_user', __( 'This order cannot be paid for. Please contact us if you need assistance.', 'woo-gutenberg-products-block' ), array( 'status' => 403 ) ); - } - - // Does not need payment. - if ( ! $order->needs_payment() ) { - /* translators: %s: order status */ - return new \WP_Error( '@Todo', sprintf( __( 'This order’s status is “%s”—it cannot be paid for. Please contact us if you need assistance.', 'woo-gutenberg-products-block' ), wc_get_order_status_name( $order->get_status() ) ), array( 'status' => 403 ) ); - } - - // Ensure order items are still stocked if paying for a failed order. Pending orders do not need this check because stock is held. - if ( ! $order->has_status( wc_get_is_pending_statuses() ) ) { - $quantities = array(); - - foreach ( $order->get_items() as $item_key => $item ) { - if ( $item && is_callable( array( $item, 'get_product' ) ) ) { - $product = $item->get_product(); - - if ( ! $product ) { - continue; - } - - $quantities[ $product->get_stock_managed_by_id() ] = isset( $quantities[ $product->get_stock_managed_by_id() ] ) ? $quantities[ $product->get_stock_managed_by_id() ] + $item->get_quantity() : $item->get_quantity(); - } - } - - // Stock levels may already have been adjusted for this order (in which case we don't need to worry about checking for low stock). - if ( ! $order->get_data_store()->get_stock_reduced( $order->get_id() ) ) { - foreach ( $order->get_items() as $item_key => $item ) { - if ( $item && is_callable( array( $item, 'get_product' ) ) ) { - $product = $item->get_product(); - - if ( ! $product ) { - continue; - } - - /** - * Filters whether or not the product is in stock for this pay for order. - * - * @param boolean True if in stock. - * @param \WC_Product $product Product. - * @param \WC_Order $order Order. - * - * @since 9.8.0-dev - */ - if ( ! apply_filters( 'woocommerce_pay_order_product_in_stock', $product->is_in_stock(), $product, $order ) ) { - /* translators: %s: product name */ - return new \WP_Error( '@Todo', sprintf( __( 'Sorry, "%s" is no longer in stock so this order cannot be paid for. We apologize for any inconvenience caused.', 'woo-gutenberg-products-block' ), $product->get_name() ), array( 'status' => 403 ) ); - } - - // We only need to check products managing stock, with a limited stock qty. - if ( ! $product->managing_stock() || $product->backorders_allowed() ) { - continue; - } - - // Check stock based on all items in the cart and consider any held stock within pending orders. - $held_stock = wc_get_held_stock_quantity( $product, $order->get_id() ); - $required_stock = $quantities[ $product->get_stock_managed_by_id() ]; - - /** - * Filters whether or not the product has enough stock. - * - * @param boolean True if has enough stock. - * @param \WC_Product $product Product. - * @param \WC_Order $order Order. - * - * @since 9.8.0-dev - */ - if ( ! apply_filters( 'woocommerce_pay_order_product_has_enough_stock', ( $product->get_stock_quantity() >= ( $held_stock + $required_stock ) ), $product, $order ) ) { - /* translators: 1: product name 2: quantity in stock */ - return new \WP_Error( '@Todo', sprintf( __( 'Sorry, we do not have enough "%1$s" in stock to fulfill your order (%2$s available). We apologize for any inconvenience caused.', 'woo-gutenberg-products-block' ), $product->get_name(), wc_format_stock_quantity_for_display( $product->get_stock_quantity() - $held_stock, $product ) ), array( 'status' => 403 ) ); - } - } - } - } + try { + $this->order_controller->validate_order_key( $order_id, $order_key ); + } catch ( RouteException $error ) { + return new \WP_Error( + $error->getErrorCode(), + $error->getMessage(), + array( 'status' => $error->getCode() ) + ); } return true; From c356b2a30c3700c7e1b7255c227fde61b7c1b527 Mon Sep 17 00:00:00 2001 From: hsingyuc Date: Mon, 24 Apr 2023 23:00:07 -0700 Subject: [PATCH 06/26] Add order item schema --- src/StoreApi/Schemas/V1/OrderSchema.php | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/src/StoreApi/Schemas/V1/OrderSchema.php b/src/StoreApi/Schemas/V1/OrderSchema.php index 8bd7205ab5f..a10a7a5587a 100644 --- a/src/StoreApi/Schemas/V1/OrderSchema.php +++ b/src/StoreApi/Schemas/V1/OrderSchema.php @@ -339,25 +339,4 @@ function( $item ) { ), ]; } - - /** - * Get items data. - * - * @param \WC_Order $order Order instance. - * @return array - */ - private function get_item_data( $order ) { - $items = $order->get_items(); - $data = []; - - foreach ( $items as $item ) { - $data[ $item->get_id() ]['id'] = $item->get_id(); - $data[ $item->get_id() ]['name'] = $item->get_name(); - $data[ $item->get_id() ]['meta_data'] = $item->get_all_formatted_meta_data(); - $data[ $item->get_id() ]['quantity'] = $item->get_quantity(); - $data[ $item->get_id() ]['subtotal'] = $order->get_line_subtotal( $item ); - } - - return array_values( $data ); - } } From 4454195fc6ccbd5de81cf9c1229375d4e21eab2e Mon Sep 17 00:00:00 2001 From: hsingyuc Date: Thu, 27 Apr 2023 10:53:19 -0700 Subject: [PATCH 07/26] Check if the order is associated with current user --- src/StoreApi/Routes/V1/Order.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/StoreApi/Routes/V1/Order.php b/src/StoreApi/Routes/V1/Order.php index 7f74d45b823..fe160e4b458 100644 --- a/src/StoreApi/Routes/V1/Order.php +++ b/src/StoreApi/Routes/V1/Order.php @@ -80,6 +80,12 @@ public function get_args() { public function is_authorized( \WP_REST_Request $request ) { $order_id = absint( $request['id'] ); $order_key = wc_clean( wp_unslash( $request->get_param( 'key' ) ) ); + $user_id = get_current_user_id(); + $order = wc_get_order( $order_id ); + + if ( $user_id !== $order->get_user_id() ) { + return false; + } try { $this->order_controller->validate_order_key( $order_id, $order_key ); From 9311cfc75eb304dd1b574ced420bde538878a16b Mon Sep 17 00:00:00 2001 From: hsingyuc Date: Mon, 1 May 2023 13:29:05 -0700 Subject: [PATCH 08/26] Fix after rebase --- src/StoreApi/RoutesController.php | 1 + src/StoreApi/SchemaController.php | 1 + 2 files changed, 2 insertions(+) diff --git a/src/StoreApi/RoutesController.php b/src/StoreApi/RoutesController.php index d0028145caa..6658b84a4cb 100644 --- a/src/StoreApi/RoutesController.php +++ b/src/StoreApi/RoutesController.php @@ -48,6 +48,7 @@ public function __construct( SchemaController $schema_controller ) { Routes\V1\CartUpdateItem::IDENTIFIER => Routes\V1\CartUpdateItem::class, Routes\V1\CartUpdateCustomer::IDENTIFIER => Routes\V1\CartUpdateCustomer::class, Routes\V1\Checkout::IDENTIFIER => Routes\V1\Checkout::class, + Routes\V1\CheckoutOrder::IDENTIFIER => Routes\V1\CheckoutOrder::class, Routes\V1\Order::IDENTIFIER => Routes\V1\Order::class, Routes\V1\ProductAttributes::IDENTIFIER => Routes\V1\ProductAttributes::class, Routes\V1\ProductAttributesById::IDENTIFIER => Routes\V1\ProductAttributesById::class, diff --git a/src/StoreApi/SchemaController.php b/src/StoreApi/SchemaController.php index 478b99e3599..0454c4d416f 100644 --- a/src/StoreApi/SchemaController.php +++ b/src/StoreApi/SchemaController.php @@ -43,6 +43,7 @@ public function __construct( ExtendSchema $extend ) { Schemas\V1\CartItemSchema::IDENTIFIER => Schemas\V1\CartItemSchema::class, Schemas\V1\CartSchema::IDENTIFIER => Schemas\V1\CartSchema::class, Schemas\V1\CartExtensionsSchema::IDENTIFIER => Schemas\V1\CartExtensionsSchema::class, + Schemas\V1\CheckoutOrderSchema::IDENTIFIER => Schemas\V1\CheckoutOrderSchema::class, Schemas\V1\CheckoutSchema::IDENTIFIER => Schemas\V1\CheckoutSchema::class, Schemas\V1\OrderItemSchema::IDENTIFIER => Schemas\V1\OrderItemSchema::class, Schemas\V1\OrderCouponSchema::IDENTIFIER => Schemas\V1\OrderCouponSchema::class, From e2d1fd7dfd9d76c425a35dcab325d05f5c2a38fc Mon Sep 17 00:00:00 2001 From: hsingyuc Date: Mon, 1 May 2023 13:35:02 -0700 Subject: [PATCH 09/26] Add checkout order endpoint --- src/StoreApi/Routes/V1/CheckoutOrder.php | 442 ++++++++++++++++++ .../Schemas/V1/CheckoutOrderSchema.php | 211 +++++++++ 2 files changed, 653 insertions(+) create mode 100644 src/StoreApi/Routes/V1/CheckoutOrder.php create mode 100644 src/StoreApi/Schemas/V1/CheckoutOrderSchema.php diff --git a/src/StoreApi/Routes/V1/CheckoutOrder.php b/src/StoreApi/Routes/V1/CheckoutOrder.php new file mode 100644 index 00000000000..97108108285 --- /dev/null +++ b/src/StoreApi/Routes/V1/CheckoutOrder.php @@ -0,0 +1,442 @@ +[\d]+)'; + } + + /** + * Checks if a nonce is required for the route. + * + * @param \WP_REST_Request $request Request. + * @return bool + */ + protected function requires_nonce( \WP_REST_Request $request ) { + return true; + } + + /** + * Check if authorized to get the order. + * + * @param \WP_REST_Request $request Request object. + * @return boolean|WP_Error + */ + public function is_authorized( \WP_REST_Request $request ) { + $order_id = absint( $request['id'] ); + $order_key = wc_clean( wp_unslash( $request->get_param( 'key' ) ) ); + $user_id = get_current_user_id(); + $this->order = $this->order_controller->get_order( $order_id ); + + if ( $user_id !== $this->order->get_user_id() ) { + return false; + } + + try { + $this->order_controller->validate_order_key( $order_id, $order_key ); + } catch ( RouteException $error ) { + return new \WP_Error( + $error->getErrorCode(), + $error->getMessage(), + array( 'status' => $error->getCode() ) + ); + } + + return true; + } + + /** + * Get method arguments for this REST route. + * + * @return array An array of endpoints. + */ + public function get_args() { + return [ + [ + 'methods' => \WP_REST_Server::CREATABLE, + 'callback' => [ $this, 'get_response' ], + 'permission_callback' => [ $this, 'is_authorized' ], + 'args' => array_merge( + [ + 'payment_data' => [ + 'description' => __( 'Data to pass through to the payment method when processing payment.', 'woo-gutenberg-products-block' ), + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'key' => [ + 'type' => 'string', + ], + 'value' => [ + 'type' => [ 'string', 'boolean' ], + ], + ], + ], + ], + ], + $this->schema->get_endpoint_args_for_item_schema( \WP_REST_Server::CREATABLE ) + ), + ], + 'schema' => [ $this->schema, 'get_public_item_schema' ], + 'allow_batch' => [ 'v1' => true ], + ]; + } + + /** + * Prepare a single item for response. Handles setting the status based on the payment result. + * + * @param mixed $item Item to format to schema. + * @param \WP_REST_Request $request Request object. + * @return \WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $item, \WP_REST_Request $request ) { + $response = parent::prepare_item_for_response( $item, $request ); + $status_codes = [ + 'success' => 200, + 'pending' => 202, + 'failure' => 400, + 'error' => 500, + ]; + + if ( isset( $item->payment_result ) && $item->payment_result instanceof PaymentResult ) { + $response->set_status( $status_codes[ $item->payment_result->status ] ?? 200 ); + } + + return $response; + } + + /** + * Process an order. + * + * 1. Process Request + * 2. Process Customer + * 3. Validate Order + * 4. Process Payment + * + * @throws RouteException On error. + * @throws InvalidStockLevelsInCartException On error. + * + * @param \WP_REST_Request $request Request object. + * + * @return \WP_REST_Response + */ + protected function get_route_post_response( \WP_REST_Request $request ) { + /** + * Process request data. + * + * Note: Customer data is persisted from the request first so that OrderController::update_addresses_from_cart + * uses the up to date customer address. + */ + $this->update_customer_from_request( $request ); + $this->update_order_from_request( $request ); + + /** + * Process customer data. + * + * Update order with customer details, and sign up a user account as necessary. + */ + $this->process_customer( $request ); + + /** + * Validate order. + * + * This logic ensures the order is valid before payment is attempted. + */ + $this->order_controller->validate_order_before_payment( $this->order ); + + /** + * Fires before an order is processed by the Checkout Block/Store API. + * + * This hook informs extensions that $order has completed processing and is ready for payment. + * + * This is similar to existing core hook woocommerce_checkout_order_processed. We're using a new action: + * - To keep the interface focused (only pass $order, not passing request data). + * - This also explicitly indicates these orders are from checkout block/StoreAPI. + * + * @since 7.2.0 + * + * @see https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3238 + * @example See docs/examples/checkout-order-processed.md + + * @param \WC_Order $order Order object. + */ + do_action( 'woocommerce_store_api_checkout_order_processed', $this->order ); + + /** + * Process the payment and return the results. + */ + $payment_result = new PaymentResult(); + + if ( $this->order->needs_payment() ) { + $this->process_payment( $request, $payment_result ); + } else { + $this->process_without_payment( $request, $payment_result ); + } + + return $this->prepare_item_for_response( + (object) [ + 'order' => wc_get_order( $this->order ), + 'payment_result' => $payment_result, + ], + $request + ); + } + + /** + * Updates the current customer session using data from the request (e.g. address data). + * + * Address session data is synced to the order itself later on by OrderController::update_order_from_cart() + * + * @param \WP_REST_Request $request Full details about the request. + */ + private function update_customer_from_request( \WP_REST_Request $request ) { + $customer = wc()->customer; + + // Billing address is a required field. + foreach ( $request['billing_address'] as $key => $value ) { + if ( is_callable( [ $customer, "set_billing_$key" ] ) ) { + $customer->{"set_billing_$key"}( $value ); + } + } + + // If shipping address (optional field) was not provided, set it to the given billing address (required field). + $shipping_address_values = $request['shipping_address'] ?? $request['billing_address']; + + foreach ( $shipping_address_values as $key => $value ) { + if ( is_callable( [ $customer, "set_shipping_$key" ] ) ) { + $customer->{"set_shipping_$key"}( $value ); + } elseif ( 'phone' === $key ) { + $customer->update_meta_data( 'shipping_phone', $value ); + } + } + + /** + * Fires when the Checkout Block/Store API updates a customer from the API request data. + * + * @since 8.2.0 + * + * @param \WC_Customer $customer Customer object. + * @param \WP_REST_Request $request Full details about the request. + */ + do_action( 'woocommerce_store_api_checkout_update_customer_from_request', $customer, $request ); + + $customer->save(); + } + + /** + * Update the current order using the posted values from the request. + * + * @param \WP_REST_Request $request Full details about the request. + */ + private function update_order_from_request( \WP_REST_Request $request ) { + $this->order->set_customer_note( $request['customer_note'] ?? '' ); + $this->order->set_payment_method( $this->get_request_payment_method_id( $request ) ); + + wc_do_deprecated_action( + '__experimental_woocommerce_blocks_checkout_update_order_from_request', + array( + $this->order, + $request, + ), + '6.3.0', + 'woocommerce_store_api_checkout_update_order_from_request', + 'This action was deprecated in WooCommerce Blocks version 6.3.0. Please use woocommerce_store_api_checkout_update_order_from_request instead.' + ); + + wc_do_deprecated_action( + 'woocommerce_blocks_checkout_update_order_from_request', + array( + $this->order, + $request, + ), + '7.2.0', + 'woocommerce_store_api_checkout_update_order_from_request', + 'This action was deprecated in WooCommerce Blocks version 7.2.0. Please use woocommerce_store_api_checkout_update_order_from_request instead.' + ); + + /** + * Fires when the Checkout Block/Store API updates an order's from the API request data. + * + * This hook gives extensions the chance to update orders based on the data in the request. This can be used in + * conjunction with the ExtendSchema class to post custom data and then process it. + * + * @since 7.2.0 + * + * @param \WC_Order $order Order object. + * @param \WP_REST_Request $request Full details about the request. + */ + do_action( 'woocommerce_store_api_checkout_update_order_from_request', $this->order, $request ); + + $this->order->save(); + } + + /** + * For orders which do not require payment, just update status. + * + * @param \WP_REST_Request $request Request object. + * @param PaymentResult $payment_result Payment result object. + */ + private function process_without_payment( \WP_REST_Request $request, PaymentResult $payment_result ) { + // Transition the order to pending, and then completed. This ensures transactional emails fire for pending_to_complete events. + $this->order->update_status( 'pending' ); + $this->order->payment_complete(); + + // Mark the payment as successful. + $payment_result->set_status( 'success' ); + $payment_result->set_redirect_url( $this->order->get_checkout_order_received_url() ); + } + + /** + * Fires an action hook instructing active payment gateways to process the payment for an order and provide a result. + * + * @throws RouteException On error. + * + * @param \WP_REST_Request $request Request object. + * @param PaymentResult $payment_result Payment result object. + */ + private function process_payment( \WP_REST_Request $request, PaymentResult $payment_result ) { + try { + // Transition the order to pending before making payment. + $this->order->update_status( 'pending' ); + + // Prepare the payment context object to pass through payment hooks. + $context = new PaymentContext(); + $context->set_payment_method( $this->get_request_payment_method_id( $request ) ); + $context->set_payment_data( $this->get_request_payment_data( $request ) ); + $context->set_order( $this->order ); + + /** + * Process payment with context. + * + * @hook woocommerce_rest_checkout_process_payment_with_context + * + * @throws \Exception If there is an error taking payment, an \Exception object can be thrown with an error message. + * + * @param PaymentContext $context Holds context for the payment, including order ID and payment method. + * @param PaymentResult $payment_result Result object for the transaction. + */ + do_action_ref_array( 'woocommerce_rest_checkout_process_payment_with_context', [ $context, &$payment_result ] ); + + if ( ! $payment_result instanceof PaymentResult ) { + throw new RouteException( 'woocommerce_rest_checkout_invalid_payment_result', __( 'Invalid payment result received from payment method.', 'woo-gutenberg-products-block' ), 500 ); + } + } catch ( \Exception $e ) { + throw new RouteException( 'woocommerce_rest_checkout_process_payment_error', $e->getMessage(), 402 ); + } + } + + /** + * Gets the chosen payment method ID from the request. + * + * @throws RouteException On error. + * @param \WP_REST_Request $request Request object. + * @return string + */ + private function get_request_payment_method_id( \WP_REST_Request $request ) { + $payment_method = $this->get_request_payment_method( $request ); + return is_null( $payment_method ) ? '' : $payment_method->id; + } + + /** + * Gets the chosen payment method from the request. + * + * @throws RouteException On error. + * @param \WP_REST_Request $request Request object. + * @return \WC_Payment_Gateway|null + */ + private function get_request_payment_method( \WP_REST_Request $request ) { + $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); + $request_payment_method = wc_clean( wp_unslash( $request['payment_method'] ?? '' ) ); + $requires_payment_method = $this->order->needs_payment(); + + if ( empty( $request_payment_method ) ) { + if ( $requires_payment_method ) { + throw new RouteException( + 'woocommerce_rest_checkout_missing_payment_method', + __( 'No payment method provided.', 'woo-gutenberg-products-block' ), + 400 + ); + } + return null; + } + + if ( ! isset( $available_gateways[ $request_payment_method ] ) ) { + throw new RouteException( + 'woocommerce_rest_checkout_payment_method_disabled', + sprintf( + // Translators: %s Payment method ID. + __( 'The %s payment gateway is not available.', 'woo-gutenberg-products-block' ), + esc_html( $request_payment_method ) + ), + 400 + ); + } + + return $available_gateways[ $request_payment_method ]; + } + + /** + * Gets and formats payment request data. + * + * @param \WP_REST_Request $request Request object. + * @return array + */ + private function get_request_payment_data( \WP_REST_Request $request ) { + static $payment_data = []; + if ( ! empty( $payment_data ) ) { + return $payment_data; + } + if ( ! empty( $request['payment_data'] ) ) { + foreach ( $request['payment_data'] as $data ) { + $payment_data[ sanitize_key( $data['key'] ) ] = wc_clean( $data['value'] ); + } + } + + return $payment_data; + } + + /** + * Updates the order with user details (e.g. address). + * + * @throws RouteException API error object with error details. + * @param \WP_REST_Request $request Request object. + */ + private function process_customer( \WP_REST_Request $request ) { + $this->order_controller->sync_customer_data_with_order( $this->order ); + } +} diff --git a/src/StoreApi/Schemas/V1/CheckoutOrderSchema.php b/src/StoreApi/Schemas/V1/CheckoutOrderSchema.php new file mode 100644 index 00000000000..fd8eb0b4f34 --- /dev/null +++ b/src/StoreApi/Schemas/V1/CheckoutOrderSchema.php @@ -0,0 +1,211 @@ +billing_address_schema = $this->controller->get( BillingAddressSchema::IDENTIFIER ); + $this->shipping_address_schema = $this->controller->get( ShippingAddressSchema::IDENTIFIER ); + } + + /** + * Checkout schema properties. + * + * @return array + */ + public function get_properties() { + return [ + 'order_id' => [ + 'description' => __( 'The order ID to process during checkout.', 'woo-gutenberg-products-block' ), + 'type' => 'integer', + 'context' => [ 'view', 'edit' ], + 'readonly' => true, + ], + 'status' => [ + 'description' => __( 'Order status. Payment providers will update this value after payment.', 'woo-gutenberg-products-block' ), + 'type' => 'string', + 'context' => [ 'view', 'edit' ], + 'readonly' => true, + ], + 'order_key' => [ + 'description' => __( 'Order key used to check validity or protect access to certain order data.', 'woo-gutenberg-products-block' ), + 'type' => 'string', + 'context' => [ 'view', 'edit' ], + 'readonly' => true, + ], + 'customer_note' => [ + 'description' => __( 'Note added to the order by the customer during checkout.', 'woo-gutenberg-products-block' ), + 'type' => 'string', + 'context' => [ 'view', 'edit' ], + ], + 'customer_id' => [ + 'description' => __( 'Customer ID if registered. Will return 0 for guests.', 'woo-gutenberg-products-block' ), + 'type' => 'integer', + 'context' => [ 'view', 'edit' ], + 'readonly' => true, + ], + 'billing_address' => [ + 'description' => __( 'Billing address.', 'woo-gutenberg-products-block' ), + 'type' => 'object', + 'context' => [ 'view', 'edit' ], + 'properties' => $this->billing_address_schema->get_properties(), + 'arg_options' => [ + 'sanitize_callback' => [ $this->billing_address_schema, 'sanitize_callback' ], + 'validate_callback' => [ $this->billing_address_schema, 'validate_callback' ], + ], + 'required' => true, + ], + 'shipping_address' => [ + 'description' => __( 'Shipping address.', 'woo-gutenberg-products-block' ), + 'type' => 'object', + 'context' => [ 'view', 'edit' ], + 'properties' => $this->shipping_address_schema->get_properties(), + 'arg_options' => [ + 'sanitize_callback' => [ $this->shipping_address_schema, 'sanitize_callback' ], + 'validate_callback' => [ $this->shipping_address_schema, 'validate_callback' ], + ], + ], + 'payment_method' => [ + 'description' => __( 'The ID of the payment method being used to process the payment.', 'woo-gutenberg-products-block' ), + 'type' => 'string', + 'context' => [ 'view', 'edit' ], + 'enum' => wc()->payment_gateways->get_payment_gateway_ids(), + ], + 'payment_result' => [ + 'description' => __( 'Result of payment processing, or false if not yet processed.', 'woo-gutenberg-products-block' ), + 'type' => 'object', + 'context' => [ 'view', 'edit' ], + 'readonly' => true, + 'properties' => [ + 'payment_status' => [ + 'description' => __( 'Status of the payment returned by the gateway. One of success, pending, failure, error.', 'woo-gutenberg-products-block' ), + 'readonly' => true, + 'type' => 'string', + ], + 'payment_details' => [ + 'description' => __( 'An array of data being returned from the payment gateway.', 'woo-gutenberg-products-block' ), + 'readonly' => true, + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'key' => [ + 'type' => 'string', + ], + 'value' => [ + 'type' => 'string', + ], + ], + ], + ], + 'redirect_url' => [ + 'description' => __( 'A URL to redirect the customer after checkout. This could be, for example, a link to the payment processors website.', 'woo-gutenberg-products-block' ), + 'readonly' => true, + 'type' => 'string', + ], + ], + ], + self::EXTENDING_KEY => $this->get_extended_schema( self::IDENTIFIER ), + ]; + } + + /** + * Return the response for checkout. + * + * @param object $item Results from checkout action. + * @return array + */ + public function get_item_response( $item ) { + return $this->get_checkout_response( $item->order, $item->payment_result ); + } + + /** + * Get the checkout response based on the current order and any payments. + * + * @param \WC_Order $order Order object. + * @param PaymentResult $payment_result Payment result object. + * @return array + */ + protected function get_checkout_response( \WC_Order $order, PaymentResult $payment_result = null ) { + return [ + 'order_id' => $order->get_id(), + 'status' => $order->get_status(), + 'order_key' => $order->get_order_key(), + 'customer_note' => $order->get_customer_note(), + 'customer_id' => $order->get_customer_id(), + 'billing_address' => $this->billing_address_schema->get_item_response( $order ), + 'shipping_address' => $this->shipping_address_schema->get_item_response( $order ), + 'payment_method' => $order->get_payment_method(), + 'payment_result' => [ + 'payment_status' => $payment_result->status, + 'payment_details' => $this->prepare_payment_details_for_response( $payment_result->payment_details ), + 'redirect_url' => $payment_result->redirect_url, + ], + self::EXTENDING_KEY => $this->get_extended_data( self::IDENTIFIER ), + ]; + } + + /** + * This prepares the payment details for the response so it's following the + * schema where it's an array of objects. + * + * @param array $payment_details An array of payment details from the processed payment. + * + * @return array An array of objects where each object has the key and value + * as distinct properties. + */ + protected function prepare_payment_details_for_response( array $payment_details ) { + return array_map( + function( $key, $value ) { + return (object) [ + 'key' => $key, + 'value' => $value, + ]; + }, + array_keys( $payment_details ), + $payment_details + ); + } +} From c389bd4bd6d736fc3a768d3f53e42af2ddcace57 Mon Sep 17 00:00:00 2001 From: hsingyuc Date: Wed, 19 Jul 2023 14:28:41 -0700 Subject: [PATCH 10/26] Add order authorization trait --- src/StoreApi/Routes/V1/Order.php | 34 ++-------- .../Utilities/OrderAuthorizationTrait.php | 63 +++++++++++++++++++ src/StoreApi/Utilities/OrderController.php | 16 +---- 3 files changed, 69 insertions(+), 44 deletions(-) create mode 100644 src/StoreApi/Utilities/OrderAuthorizationTrait.php diff --git a/src/StoreApi/Routes/V1/Order.php b/src/StoreApi/Routes/V1/Order.php index fe160e4b458..3fc42d32395 100644 --- a/src/StoreApi/Routes/V1/Order.php +++ b/src/StoreApi/Routes/V1/Order.php @@ -5,11 +5,14 @@ use Automattic\WooCommerce\StoreApi\Schemas\v1\AbstractSchema; use Automattic\WooCommerce\StoreApi\Utilities\OrderController; use Automattic\WooCommerce\StoreApi\Exceptions\RouteException; +use Automattic\WooCommerce\StoreApi\Utilities\OrderAuthorizationTrait; /** * Order class. */ class Order extends AbstractRoute { + use OrderAuthorizationTrait; + /** * The route identifier. * @@ -71,35 +74,6 @@ public function get_args() { ]; } - /** - * Check if authorized to get the order. - * - * @param \WP_REST_Request $request Request object. - * @return boolean|WP_Error - */ - public function is_authorized( \WP_REST_Request $request ) { - $order_id = absint( $request['id'] ); - $order_key = wc_clean( wp_unslash( $request->get_param( 'key' ) ) ); - $user_id = get_current_user_id(); - $order = wc_get_order( $order_id ); - - if ( $user_id !== $order->get_user_id() ) { - return false; - } - - try { - $this->order_controller->validate_order_key( $order_id, $order_key ); - } catch ( RouteException $error ) { - return new \WP_Error( - $error->getErrorCode(), - $error->getMessage(), - array( 'status' => $error->getCode() ) - ); - } - - return true; - } - /** * Handle the request and return a valid response for this endpoint. * @@ -108,6 +82,6 @@ public function is_authorized( \WP_REST_Request $request ) { */ protected function get_route_response( \WP_REST_Request $request ) { $order_id = absint( $request['id'] ); - return rest_ensure_response( $this->schema->get_item_response( $this->order_controller->get_order( $order_id ) ) ); + return rest_ensure_response( $this->schema->get_item_response( wc_get_order( $order_id ) ) ); } } diff --git a/src/StoreApi/Utilities/OrderAuthorizationTrait.php b/src/StoreApi/Utilities/OrderAuthorizationTrait.php new file mode 100644 index 00000000000..9ef93575498 --- /dev/null +++ b/src/StoreApi/Utilities/OrderAuthorizationTrait.php @@ -0,0 +1,63 @@ +get_param( 'key' ) ) ); + $billing_email = wc_clean( wp_unslash( $request->get_param( 'billing_email' ) ) ); + + try { + // In this context, pay_for_order capability checks that the current user ID matches the customer ID stored + // within the order, or if the order was placed by a guest. + // See https://github.com/woocommerce/woocommerce/blob/abcedbefe02f9e89122771100c42ff588da3e8e0/plugins/woocommerce/includes/wc-user-functions.php#L458. + if ( ! current_user_can( 'pay_for_order', $order_id ) ) { + throw new RouteException( 'woocommerce_rest_invalid_user', __( 'This order belongs to a different customer. Please log in to the correct account.', 'woo-gutenberg-products-block' ), 403 ); + } + if ( get_current_user_id() === 0 ) { + $this->order_controller->validate_order_key( $order_id, $order_key ); + $this->validate_billing_email_matches_order( $order_id, $billing_email ); + } + } catch ( RouteException $error ) { + return new \WP_Error( + $error->getErrorCode(), + $error->getMessage(), + array( 'status' => $error->getCode() ) + ); + } + + return true; + } + + /** + * Validate a given billing email against an existing order. + * + * @throws RouteException Exception if invalid data is detected. + * @param integer $order_id Order ID. + * @param string $billing_email Billing email. + */ + public function validate_billing_email_matches_order( $order_id, $billing_email ) { + $order = wc_get_order( $order_id ); + + if ( ! $order || ! $billing_email || $order->get_billing_email() !== $billing_email ) { + throw new RouteException( 'woocommerce_rest_invalid_billing_email', __( 'Invalid billing email provided.', 'woo-gutenberg-products-block' ), 401 ); + } + } + +} diff --git a/src/StoreApi/Utilities/OrderController.php b/src/StoreApi/Utilities/OrderController.php index 64afc22f8ca..dfcc6d7ff5e 100644 --- a/src/StoreApi/Utilities/OrderController.php +++ b/src/StoreApi/Utilities/OrderController.php @@ -38,18 +38,6 @@ public function create_order_from_cart() { return $order; } - /** - * Get order. - * - * @throws RouteException Exception if invalid data is detected. - * - * @param integer $order_id Order ID. - * @return \WC_Order A new order object. - */ - public function get_order( $order_id ) { - return wc_get_order( $order_id ); - } - /** * Update an order using data from the current cart. * @@ -481,7 +469,7 @@ public function validate_selected_shipping_methods( $needs_shipping, $chosen_shi * @param string $order_key Order key. */ public function validate_order_key( $order_id, $order_key ) { - $order = $this->get_order( $order_id ); + $order = wc_get_order( $order_id ); if ( ! $order || ! $order_key || $order->get_id() !== $order_id || ! hash_equals( $order->get_order_key(), $order_key ) ) { throw new RouteException( 'woocommerce_rest_invalid_order', __( 'Invalid order ID or key provided.', 'woo-gutenberg-products-block' ), 401 ); @@ -510,7 +498,7 @@ public function validate_billing_email( $order_id, $billing_email ) { * @param integer $order_id Order ID. */ public function get_failed_order_stock_error( $order_id ) { - $order = $this->get_order( $order_id ); + $order = wc_get_order( $order_id ); // Ensure order items are still stocked if paying for a failed order. Pending orders do not need this check because stock is held. if ( ! $order->has_status( wc_get_is_pending_statuses() ) ) { From 1297d194a81f914423f7f1cc1bfb274dd0ee2b3a Mon Sep 17 00:00:00 2001 From: hsingyuc Date: Wed, 19 Jul 2023 14:40:36 -0700 Subject: [PATCH 11/26] Allow to use the order update customer endpoint in dev build only --- src/StoreApi/RoutesController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/StoreApi/RoutesController.php b/src/StoreApi/RoutesController.php index 6658b84a4cb..9152a6c58be 100644 --- a/src/StoreApi/RoutesController.php +++ b/src/StoreApi/RoutesController.php @@ -48,7 +48,6 @@ public function __construct( SchemaController $schema_controller ) { Routes\V1\CartUpdateItem::IDENTIFIER => Routes\V1\CartUpdateItem::class, Routes\V1\CartUpdateCustomer::IDENTIFIER => Routes\V1\CartUpdateCustomer::class, Routes\V1\Checkout::IDENTIFIER => Routes\V1\Checkout::class, - Routes\V1\CheckoutOrder::IDENTIFIER => Routes\V1\CheckoutOrder::class, Routes\V1\Order::IDENTIFIER => Routes\V1\Order::class, Routes\V1\ProductAttributes::IDENTIFIER => Routes\V1\ProductAttributes::class, Routes\V1\ProductAttributesById::IDENTIFIER => Routes\V1\ProductAttributesById::class, @@ -65,7 +64,8 @@ public function __construct( SchemaController $schema_controller ) { ]; if ( Package::is_experimental_build() ) { - $this->routes['v1'][ Routes\V1\Order::IDENTIFIER ] = Routes\V1\Order::class; + $this->routes['v1'][ Routes\V1\Order::IDENTIFIER ] = Routes\V1\Order::class; + $this->routes['v1'][ Routes\V1\CheckoutOrder::IDENTIFIER ] = Routes\V1\CheckoutOrder::class; } } From 842f8f686cd3f2c5fae53e94bde76d289b2ef650 Mon Sep 17 00:00:00 2001 From: hsingyuc Date: Wed, 19 Jul 2023 14:44:15 -0700 Subject: [PATCH 12/26] Get both customer and guest details --- src/StoreApi/Schemas/V1/OrderSchema.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/StoreApi/Schemas/V1/OrderSchema.php b/src/StoreApi/Schemas/V1/OrderSchema.php index a10a7a5587a..7b785d2282b 100644 --- a/src/StoreApi/Schemas/V1/OrderSchema.php +++ b/src/StoreApi/Schemas/V1/OrderSchema.php @@ -271,8 +271,8 @@ public function get_item_response( $order ) { 'coupons' => $this->get_item_responses_from_schema( $this->coupon_schema, $order->get_items( 'coupon' ) ), 'fees' => $this->get_item_responses_from_schema( $this->fee_schema, $order->get_items( 'fee' ) ), 'totals' => (object) $this->prepare_currency_response( $this->get_totals( $order ) ), - 'shipping_address' => (object) $this->shipping_address_schema->get_item_response( new \WC_Customer( $order->get_customer_id() ) ), - 'billing_address' => (object) $this->billing_address_schema->get_item_response( new \WC_Customer( $order->get_customer_id() ) ), + 'shipping_address' => (object) $this->shipping_address_schema->get_item_response( $order ), + 'billing_address' => (object) $this->billing_address_schema->get_item_response( $order ), 'needs_payment' => $order->needs_payment(), 'needs_shipping' => $order->needs_shipping_address(), 'payment_requirements' => $this->extend->get_payment_requirements(), From cac08cccc8b1134dd6eaf7d1da3530bd8ec35b77 Mon Sep 17 00:00:00 2001 From: hsingyuc Date: Wed, 19 Jul 2023 14:46:54 -0700 Subject: [PATCH 13/26] Remove duplicate function --- src/StoreApi/Utilities/OrderController.php | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/StoreApi/Utilities/OrderController.php b/src/StoreApi/Utilities/OrderController.php index dfcc6d7ff5e..7f07e2a56f6 100644 --- a/src/StoreApi/Utilities/OrderController.php +++ b/src/StoreApi/Utilities/OrderController.php @@ -476,21 +476,6 @@ public function validate_order_key( $order_id, $order_key ) { } } - /** - * Validate a given billing email against an existing order. - * - * @throws RouteException Exception if invalid data is detected. - * @param integer $order_id Order ID. - * @param string $billing_email Billing email. - */ - public function validate_billing_email( $order_id, $billing_email ) { - $order = $this->get_order( $order_id ); - - if ( ! $order || ! $billing_email || $order->get_billing_email() !== $billing_email ) { - throw new RouteException( 'woocommerce_rest_invalid_billing_email', __( 'Invalid billing email provided.', 'woo-gutenberg-products-block' ), 401 ); - } - } - /** * Get errors for order stock on failed orders. * From 3c23d3c4c3e927ecf58ceb080372161b2800aa3c Mon Sep 17 00:00:00 2001 From: hsingyuc Date: Wed, 19 Jul 2023 14:49:06 -0700 Subject: [PATCH 14/26] Update the cart update customer class doc block --- src/StoreApi/Routes/V1/CartUpdateCustomer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StoreApi/Routes/V1/CartUpdateCustomer.php b/src/StoreApi/Routes/V1/CartUpdateCustomer.php index bceeaff7e32..a9201b33320 100644 --- a/src/StoreApi/Routes/V1/CartUpdateCustomer.php +++ b/src/StoreApi/Routes/V1/CartUpdateCustomer.php @@ -7,7 +7,7 @@ /** * CartUpdateCustomer class. * - * Updates the customer billing and shipping address and returns an updated cart--things such as taxes may be recalculated. + * Updates the customer billing and shipping addresses, recalculates the cart totals, and returns an updated cart. */ class CartUpdateCustomer extends AbstractCartRoute { use DraftOrderTrait; From 8fd423ea63aedf80716e28a9f80a8a2c44f6977e Mon Sep 17 00:00:00 2001 From: hsingyuc Date: Wed, 19 Jul 2023 15:17:00 -0700 Subject: [PATCH 15/26] Remove duplicate order route --- src/StoreApi/RoutesController.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/StoreApi/RoutesController.php b/src/StoreApi/RoutesController.php index 9152a6c58be..4e7c01346bf 100644 --- a/src/StoreApi/RoutesController.php +++ b/src/StoreApi/RoutesController.php @@ -48,7 +48,6 @@ public function __construct( SchemaController $schema_controller ) { Routes\V1\CartUpdateItem::IDENTIFIER => Routes\V1\CartUpdateItem::class, Routes\V1\CartUpdateCustomer::IDENTIFIER => Routes\V1\CartUpdateCustomer::class, Routes\V1\Checkout::IDENTIFIER => Routes\V1\Checkout::class, - Routes\V1\Order::IDENTIFIER => Routes\V1\Order::class, Routes\V1\ProductAttributes::IDENTIFIER => Routes\V1\ProductAttributes::class, Routes\V1\ProductAttributesById::IDENTIFIER => Routes\V1\ProductAttributesById::class, Routes\V1\ProductAttributeTerms::IDENTIFIER => Routes\V1\ProductAttributeTerms::class, From e4fb165207c02f6b67ea5e2ca8c788e42b454fbf Mon Sep 17 00:00:00 2001 From: hsingyuc Date: Wed, 19 Jul 2023 17:05:31 -0700 Subject: [PATCH 16/26] Update documentation for feature flags --- .../blocks/feature-flags-and-experimental-interfaces.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md b/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md index 08d66036a7c..9aa12b425b3 100644 --- a/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md +++ b/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md @@ -43,6 +43,7 @@ The majority of our feature flagging is blocks, this is a list of them: - Product Rating Counter ([PHP flag](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/src/BlockTypesController.php#L229) | [webpack flag](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/bin/webpack-entries.js#L71-L73)) - ⚛️ Add to cart ([JS flag](https://github.com/woocommerce/woocommerce-blocks/blob/dfd2902bd8a247b5d048577db6753c5e901fc60f/assets/js/atomic/blocks/product-elements/add-to-cart/index.ts#L26-L29)). - Order Route ([PHP flag](https://github.com/woocommerce/woocommerce-blocks/blob/b4a9dc9334f82c09f533b0f88c947b5c34e4e546/src/StoreApi/RoutesController.php#L65-L67)) +- Checkout Order Route ([PHP flag](https://github.com/woocommerce/woocommerce-blocks/blob/add/an-endpoint-for-process-pay-for-order-orders/src/StoreApi/RoutesController.php#L67)) ## Features behind flags From f8ce88888bd7bc80629c0849cdd5b22b73d843b2 Mon Sep 17 00:00:00 2001 From: hsingyuc Date: Wed, 19 Jul 2023 22:45:31 -0700 Subject: [PATCH 17/26] Add checkout trait --- src/StoreApi/Routes/V1/CheckoutOrder.php | 72 +++++------------------- src/StoreApi/Utilities/CheckoutTrait.php | 44 +++++++++++++++ 2 files changed, 58 insertions(+), 58 deletions(-) create mode 100644 src/StoreApi/Utilities/CheckoutTrait.php diff --git a/src/StoreApi/Routes/V1/CheckoutOrder.php b/src/StoreApi/Routes/V1/CheckoutOrder.php index 97108108285..8e3e6ce1899 100644 --- a/src/StoreApi/Routes/V1/CheckoutOrder.php +++ b/src/StoreApi/Routes/V1/CheckoutOrder.php @@ -5,11 +5,17 @@ use Automattic\WooCommerce\StoreApi\Payments\PaymentResult; use Automattic\WooCommerce\StoreApi\Exceptions\InvalidStockLevelsInCartException; use Automattic\WooCommerce\StoreApi\Exceptions\RouteException; +use Automattic\WooCommerce\StoreApi\Utilities\OrderAuthorizationTrait; +use Automattic\WooCommerce\StoreApi\Utilities\CheckoutTrait; /** * CheckoutOrder class. */ class CheckoutOrder extends AbstractCartRoute { + use OrderAuthorizationTrait; + use CheckoutTrait { + get_args as protected get_checkout_order_args; + } /** * The route identifier. @@ -51,70 +57,20 @@ protected function requires_nonce( \WP_REST_Request $request ) { return true; } - /** - * Check if authorized to get the order. - * - * @param \WP_REST_Request $request Request object. - * @return boolean|WP_Error - */ - public function is_authorized( \WP_REST_Request $request ) { - $order_id = absint( $request['id'] ); - $order_key = wc_clean( wp_unslash( $request->get_param( 'key' ) ) ); - $user_id = get_current_user_id(); - $this->order = $this->order_controller->get_order( $order_id ); - - if ( $user_id !== $this->order->get_user_id() ) { - return false; - } - - try { - $this->order_controller->validate_order_key( $order_id, $order_key ); - } catch ( RouteException $error ) { - return new \WP_Error( - $error->getErrorCode(), - $error->getMessage(), - array( 'status' => $error->getCode() ) - ); - } - - return true; - } - /** * Get method arguments for this REST route. * * @return array An array of endpoints. */ public function get_args() { - return [ - [ - 'methods' => \WP_REST_Server::CREATABLE, - 'callback' => [ $this, 'get_response' ], - 'permission_callback' => [ $this, 'is_authorized' ], - 'args' => array_merge( - [ - 'payment_data' => [ - 'description' => __( 'Data to pass through to the payment method when processing payment.', 'woo-gutenberg-products-block' ), - 'type' => 'array', - 'items' => [ - 'type' => 'object', - 'properties' => [ - 'key' => [ - 'type' => 'string', - ], - 'value' => [ - 'type' => [ 'string', 'boolean' ], - ], - ], - ], - ], - ], - $this->schema->get_endpoint_args_for_item_schema( \WP_REST_Server::CREATABLE ) - ), - ], - 'schema' => [ $this->schema, 'get_public_item_schema' ], - 'allow_batch' => [ 'v1' => true ], - ]; + $args = $this->get_checkout_order_args(); + + // Add authorization check to all checkout order endpoints. + foreach ( $args as $index => $arg ) { + $args[ $index ]['permission_callback'] = [ $this, 'is_authorized' ]; + } + + return $args; } /** diff --git a/src/StoreApi/Utilities/CheckoutTrait.php b/src/StoreApi/Utilities/CheckoutTrait.php new file mode 100644 index 00000000000..8b4f5041947 --- /dev/null +++ b/src/StoreApi/Utilities/CheckoutTrait.php @@ -0,0 +1,44 @@ + \WP_REST_Server::CREATABLE, + 'callback' => [ $this, 'get_response' ], + 'permission_callback' => '__return_true', + 'args' => array_merge( + [ + 'payment_data' => [ + 'description' => __( 'Data to pass through to the payment method when processing payment.', 'woo-gutenberg-products-block' ), + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'key' => [ + 'type' => 'string', + ], + 'value' => [ + 'type' => [ 'string', 'boolean' ], + ], + ], + ], + ], + ], + $this->schema->get_endpoint_args_for_item_schema( \WP_REST_Server::CREATABLE ) + ), + ], + 'schema' => [ $this->schema, 'get_public_item_schema' ], + 'allow_batch' => [ 'v1' => true ], + ]; + } +} From 0d8cdf20ac2ea12f477fa69e5055a8bff1d13eac Mon Sep 17 00:00:00 2001 From: hsingyuc Date: Thu, 20 Jul 2023 08:23:19 -0700 Subject: [PATCH 18/26] Remove checkout trait --- src/StoreApi/Utilities/CheckoutTrait.php | 44 ------------------------ 1 file changed, 44 deletions(-) delete mode 100644 src/StoreApi/Utilities/CheckoutTrait.php diff --git a/src/StoreApi/Utilities/CheckoutTrait.php b/src/StoreApi/Utilities/CheckoutTrait.php deleted file mode 100644 index 8b4f5041947..00000000000 --- a/src/StoreApi/Utilities/CheckoutTrait.php +++ /dev/null @@ -1,44 +0,0 @@ - \WP_REST_Server::CREATABLE, - 'callback' => [ $this, 'get_response' ], - 'permission_callback' => '__return_true', - 'args' => array_merge( - [ - 'payment_data' => [ - 'description' => __( 'Data to pass through to the payment method when processing payment.', 'woo-gutenberg-products-block' ), - 'type' => 'array', - 'items' => [ - 'type' => 'object', - 'properties' => [ - 'key' => [ - 'type' => 'string', - ], - 'value' => [ - 'type' => [ 'string', 'boolean' ], - ], - ], - ], - ], - ], - $this->schema->get_endpoint_args_for_item_schema( \WP_REST_Server::CREATABLE ) - ), - ], - 'schema' => [ $this->schema, 'get_public_item_schema' ], - 'allow_batch' => [ 'v1' => true ], - ]; - } -} From f5dd3d94ecceaccd792d71a0090bfe8e35857ff5 Mon Sep 17 00:00:00 2001 From: hsingyuc Date: Thu, 20 Jul 2023 09:09:51 -0700 Subject: [PATCH 19/26] Update billing address and order --- src/StoreApi/Routes/V1/CheckoutOrder.php | 59 +++++++++++++++++------- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/src/StoreApi/Routes/V1/CheckoutOrder.php b/src/StoreApi/Routes/V1/CheckoutOrder.php index 8e3e6ce1899..7b155ea7312 100644 --- a/src/StoreApi/Routes/V1/CheckoutOrder.php +++ b/src/StoreApi/Routes/V1/CheckoutOrder.php @@ -6,16 +6,12 @@ use Automattic\WooCommerce\StoreApi\Exceptions\InvalidStockLevelsInCartException; use Automattic\WooCommerce\StoreApi\Exceptions\RouteException; use Automattic\WooCommerce\StoreApi\Utilities\OrderAuthorizationTrait; -use Automattic\WooCommerce\StoreApi\Utilities\CheckoutTrait; /** * CheckoutOrder class. */ class CheckoutOrder extends AbstractCartRoute { use OrderAuthorizationTrait; - use CheckoutTrait { - get_args as protected get_checkout_order_args; - } /** * The route identifier. @@ -63,14 +59,35 @@ protected function requires_nonce( \WP_REST_Request $request ) { * @return array An array of endpoints. */ public function get_args() { - $args = $this->get_checkout_order_args(); - - // Add authorization check to all checkout order endpoints. - foreach ( $args as $index => $arg ) { - $args[ $index ]['permission_callback'] = [ $this, 'is_authorized' ]; - } - - return $args; + return [ + [ + 'methods' => \WP_REST_Server::CREATABLE, + 'callback' => [ $this, 'get_response' ], + 'permission_callback' => [ $this, 'is_authorized' ], + 'args' => array_merge( + [ + 'payment_data' => [ + 'description' => __( 'Data to pass through to the payment method when processing payment.', 'woo-gutenberg-products-block' ), + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'key' => [ + 'type' => 'string', + ], + 'value' => [ + 'type' => [ 'string', 'boolean' ], + ], + ], + ], + ], + ], + $this->schema->get_endpoint_args_for_item_schema( \WP_REST_Server::CREATABLE ) + ), + ], + 'schema' => [ $this->schema, 'get_public_item_schema' ], + 'allow_batch' => [ 'v1' => true ], + ]; } /** @@ -112,13 +129,16 @@ public function prepare_item_for_response( $item, \WP_REST_Request $request ) { * @return \WP_REST_Response */ protected function get_route_post_response( \WP_REST_Request $request ) { + $order_id = absint( $request['id'] ); + $this->order = wc_get_order( $order_id ); + /** * Process request data. * * Note: Customer data is persisted from the request first so that OrderController::update_addresses_from_cart * uses the up to date customer address. */ - $this->update_customer_from_request( $request ); + $this->update_billing_address( $request ); $this->update_order_from_request( $request ); /** @@ -180,18 +200,20 @@ protected function get_route_post_response( \WP_REST_Request $request ) { * * @param \WP_REST_Request $request Full details about the request. */ - private function update_customer_from_request( \WP_REST_Request $request ) { + private function update_billing_address( \WP_REST_Request $request ) { $customer = wc()->customer; + $billing = $request['billing_address']; + $shipping = $request['shipping_address']; // Billing address is a required field. - foreach ( $request['billing_address'] as $key => $value ) { + foreach ( $billing as $key => $value ) { if ( is_callable( [ $customer, "set_billing_$key" ] ) ) { $customer->{"set_billing_$key"}( $value ); } } // If shipping address (optional field) was not provided, set it to the given billing address (required field). - $shipping_address_values = $request['shipping_address'] ?? $request['billing_address']; + $shipping_address_values = $shipping ?? $billing; foreach ( $shipping_address_values as $key => $value ) { if ( is_callable( [ $customer, "set_shipping_$key" ] ) ) { @@ -212,6 +234,11 @@ private function update_customer_from_request( \WP_REST_Request $request ) { do_action( 'woocommerce_store_api_checkout_update_customer_from_request', $customer, $request ); $customer->save(); + + $this->order->set_billing_address( $billing ); + $this->order->set_shipping_address( $shipping ); + $this->order->save(); + $this->order->calculate_totals(); } /** From e0c7440ddc3fa897f3fc984845459aa472c4c42b Mon Sep 17 00:00:00 2001 From: hsingyuc Date: Thu, 20 Jul 2023 11:22:06 -0700 Subject: [PATCH 20/26] Only allow checkout pending orders --- src/StoreApi/Routes/V1/CheckoutOrder.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/StoreApi/Routes/V1/CheckoutOrder.php b/src/StoreApi/Routes/V1/CheckoutOrder.php index 7b155ea7312..fef32e67542 100644 --- a/src/StoreApi/Routes/V1/CheckoutOrder.php +++ b/src/StoreApi/Routes/V1/CheckoutOrder.php @@ -132,6 +132,13 @@ protected function get_route_post_response( \WP_REST_Request $request ) { $order_id = absint( $request['id'] ); $this->order = wc_get_order( $order_id ); + if ( $this->order->get_status() !== 'pending' ) { + return new \WP_Error( + 'invalid_order_update_status', + __( 'This order is not pending and cannot be paid for.', 'woo-gutenberg-products-block' ) + ); + } + /** * Process request data. * From c8b12c7d86253c636ed8ee0b7249a57df1cf9c57 Mon Sep 17 00:00:00 2001 From: hsingyuc Date: Mon, 24 Jul 2023 12:00:45 -0500 Subject: [PATCH 21/26] Create checout trait --- src/StoreApi/Routes/V1/Checkout.php | 173 +-------------------- src/StoreApi/Routes/V1/CheckoutOrder.php | 170 +-------------------- src/StoreApi/Utilities/CheckoutTrait.php | 182 +++++++++++++++++++++++ 3 files changed, 186 insertions(+), 339 deletions(-) create mode 100644 src/StoreApi/Utilities/CheckoutTrait.php diff --git a/src/StoreApi/Routes/V1/Checkout.php b/src/StoreApi/Routes/V1/Checkout.php index 6251b9c707d..3c7942367ed 100644 --- a/src/StoreApi/Routes/V1/Checkout.php +++ b/src/StoreApi/Routes/V1/Checkout.php @@ -1,7 +1,6 @@ add_response_headers( $response ); } - /** - * Prepare a single item for response. Handles setting the status based on the payment result. - * - * @param mixed $item Item to format to schema. - * @param \WP_REST_Request $request Request object. - * @return \WP_REST_Response $response Response data. - */ - public function prepare_item_for_response( $item, \WP_REST_Request $request ) { - $response = parent::prepare_item_for_response( $item, $request ); - $status_codes = [ - 'success' => 200, - 'pending' => 202, - 'failure' => 400, - 'error' => 500, - ]; - - if ( isset( $item->payment_result ) && $item->payment_result instanceof PaymentResult ) { - $response->set_status( $status_codes[ $item->payment_result->status ] ?? 200 ); - } - - return $response; - } - /** * Convert the cart into a new draft order, or update an existing draft order, and return an updated cart response. * @@ -466,133 +444,6 @@ private function update_customer_from_request( \WP_REST_Request $request ) { $customer->save(); } - /** - * Update the current order using the posted values from the request. - * - * @param \WP_REST_Request $request Full details about the request. - */ - private function update_order_from_request( \WP_REST_Request $request ) { - $this->order->set_customer_note( $request['customer_note'] ?? '' ); - $this->order->set_payment_method( $this->get_request_payment_method_id( $request ) ); - $this->order->set_payment_method_title( $this->get_request_payment_method_title( $request ) ); - - wc_do_deprecated_action( - '__experimental_woocommerce_blocks_checkout_update_order_from_request', - array( - $this->order, - $request, - ), - '6.3.0', - 'woocommerce_store_api_checkout_update_order_from_request', - 'This action was deprecated in WooCommerce Blocks version 6.3.0. Please use woocommerce_store_api_checkout_update_order_from_request instead.' - ); - - wc_do_deprecated_action( - 'woocommerce_blocks_checkout_update_order_from_request', - array( - $this->order, - $request, - ), - '7.2.0', - 'woocommerce_store_api_checkout_update_order_from_request', - 'This action was deprecated in WooCommerce Blocks version 7.2.0. Please use woocommerce_store_api_checkout_update_order_from_request instead.' - ); - - /** - * Fires when the Checkout Block/Store API updates an order's from the API request data. - * - * This hook gives extensions the chance to update orders based on the data in the request. This can be used in - * conjunction with the ExtendSchema class to post custom data and then process it. - * - * @since 7.2.0 - * - * @param \WC_Order $order Order object. - * @param \WP_REST_Request $request Full details about the request. - */ - do_action( 'woocommerce_store_api_checkout_update_order_from_request', $this->order, $request ); - - $this->order->save(); - } - - /** - * For orders which do not require payment, just update status. - * - * @param \WP_REST_Request $request Request object. - * @param PaymentResult $payment_result Payment result object. - */ - private function process_without_payment( \WP_REST_Request $request, PaymentResult $payment_result ) { - // Transition the order to pending, and then completed. This ensures transactional emails fire for pending_to_complete events. - $this->order->update_status( 'pending' ); - $this->order->payment_complete(); - - // Mark the payment as successful. - $payment_result->set_status( 'success' ); - $payment_result->set_redirect_url( $this->order->get_checkout_order_received_url() ); - } - - /** - * Fires an action hook instructing active payment gateways to process the payment for an order and provide a result. - * - * @throws RouteException On error. - * - * @param \WP_REST_Request $request Request object. - * @param PaymentResult $payment_result Payment result object. - */ - private function process_payment( \WP_REST_Request $request, PaymentResult $payment_result ) { - try { - // Transition the order to pending before making payment. - $this->order->update_status( 'pending' ); - - // Prepare the payment context object to pass through payment hooks. - $context = new PaymentContext(); - $context->set_payment_method( $this->get_request_payment_method_id( $request ) ); - $context->set_payment_data( $this->get_request_payment_data( $request ) ); - $context->set_order( $this->order ); - - /** - * Process payment with context. - * - * @hook woocommerce_rest_checkout_process_payment_with_context - * - * @throws \Exception If there is an error taking payment, an \Exception object can be thrown with an error message. - * - * @param PaymentContext $context Holds context for the payment, including order ID and payment method. - * @param PaymentResult $payment_result Result object for the transaction. - */ - do_action_ref_array( 'woocommerce_rest_checkout_process_payment_with_context', [ $context, &$payment_result ] ); - - if ( ! $payment_result instanceof PaymentResult ) { - throw new RouteException( 'woocommerce_rest_checkout_invalid_payment_result', __( 'Invalid payment result received from payment method.', 'woo-gutenberg-products-block' ), 500 ); - } - } catch ( \Exception $e ) { - throw new RouteException( 'woocommerce_rest_checkout_process_payment_error', $e->getMessage(), 402 ); - } - } - - /** - * Gets the chosen payment method ID from the request. - * - * @throws RouteException On error. - * @param \WP_REST_Request $request Request object. - * @return string - */ - private function get_request_payment_method_id( \WP_REST_Request $request ) { - $payment_method = $this->get_request_payment_method( $request ); - return is_null( $payment_method ) ? '' : $payment_method->id; - } - - /** - * Gets the chosen payment method title from the request. - * - * @throws RouteException On error. - * @param \WP_REST_Request $request Request object. - * @return string - */ - private function get_request_payment_method_title( \WP_REST_Request $request ) { - $payment_method = $this->get_request_payment_method( $request ); - return is_null( $payment_method ) ? '' : $payment_method->get_title(); - } - /** * Gets the chosen payment method from the request. * @@ -633,26 +484,6 @@ private function get_request_payment_method( \WP_REST_Request $request ) { return $available_gateways[ $request_payment_method ]; } - /** - * Gets and formats payment request data. - * - * @param \WP_REST_Request $request Request object. - * @return array - */ - private function get_request_payment_data( \WP_REST_Request $request ) { - static $payment_data = []; - if ( ! empty( $payment_data ) ) { - return $payment_data; - } - if ( ! empty( $request['payment_data'] ) ) { - foreach ( $request['payment_data'] as $data ) { - $payment_data[ sanitize_key( $data['key'] ) ] = wc_clean( $data['value'] ); - } - } - - return $payment_data; - } - /** * Order processing relating to customer account. * diff --git a/src/StoreApi/Routes/V1/CheckoutOrder.php b/src/StoreApi/Routes/V1/CheckoutOrder.php index fef32e67542..33fd1e6833f 100644 --- a/src/StoreApi/Routes/V1/CheckoutOrder.php +++ b/src/StoreApi/Routes/V1/CheckoutOrder.php @@ -1,17 +1,18 @@ [\d]+)'; } - /** - * Checks if a nonce is required for the route. - * - * @param \WP_REST_Request $request Request. - * @return bool - */ - protected function requires_nonce( \WP_REST_Request $request ) { - return true; - } - /** * Get method arguments for this REST route. * @@ -90,29 +81,6 @@ public function get_args() { ]; } - /** - * Prepare a single item for response. Handles setting the status based on the payment result. - * - * @param mixed $item Item to format to schema. - * @param \WP_REST_Request $request Request object. - * @return \WP_REST_Response $response Response data. - */ - public function prepare_item_for_response( $item, \WP_REST_Request $request ) { - $response = parent::prepare_item_for_response( $item, $request ); - $status_codes = [ - 'success' => 200, - 'pending' => 202, - 'failure' => 400, - 'error' => 500, - ]; - - if ( isset( $item->payment_result ) && $item->payment_result instanceof PaymentResult ) { - $response->set_status( $status_codes[ $item->payment_result->status ] ?? 200 ); - } - - return $response; - } - /** * Process an order. * @@ -248,120 +216,6 @@ private function update_billing_address( \WP_REST_Request $request ) { $this->order->calculate_totals(); } - /** - * Update the current order using the posted values from the request. - * - * @param \WP_REST_Request $request Full details about the request. - */ - private function update_order_from_request( \WP_REST_Request $request ) { - $this->order->set_customer_note( $request['customer_note'] ?? '' ); - $this->order->set_payment_method( $this->get_request_payment_method_id( $request ) ); - - wc_do_deprecated_action( - '__experimental_woocommerce_blocks_checkout_update_order_from_request', - array( - $this->order, - $request, - ), - '6.3.0', - 'woocommerce_store_api_checkout_update_order_from_request', - 'This action was deprecated in WooCommerce Blocks version 6.3.0. Please use woocommerce_store_api_checkout_update_order_from_request instead.' - ); - - wc_do_deprecated_action( - 'woocommerce_blocks_checkout_update_order_from_request', - array( - $this->order, - $request, - ), - '7.2.0', - 'woocommerce_store_api_checkout_update_order_from_request', - 'This action was deprecated in WooCommerce Blocks version 7.2.0. Please use woocommerce_store_api_checkout_update_order_from_request instead.' - ); - - /** - * Fires when the Checkout Block/Store API updates an order's from the API request data. - * - * This hook gives extensions the chance to update orders based on the data in the request. This can be used in - * conjunction with the ExtendSchema class to post custom data and then process it. - * - * @since 7.2.0 - * - * @param \WC_Order $order Order object. - * @param \WP_REST_Request $request Full details about the request. - */ - do_action( 'woocommerce_store_api_checkout_update_order_from_request', $this->order, $request ); - - $this->order->save(); - } - - /** - * For orders which do not require payment, just update status. - * - * @param \WP_REST_Request $request Request object. - * @param PaymentResult $payment_result Payment result object. - */ - private function process_without_payment( \WP_REST_Request $request, PaymentResult $payment_result ) { - // Transition the order to pending, and then completed. This ensures transactional emails fire for pending_to_complete events. - $this->order->update_status( 'pending' ); - $this->order->payment_complete(); - - // Mark the payment as successful. - $payment_result->set_status( 'success' ); - $payment_result->set_redirect_url( $this->order->get_checkout_order_received_url() ); - } - - /** - * Fires an action hook instructing active payment gateways to process the payment for an order and provide a result. - * - * @throws RouteException On error. - * - * @param \WP_REST_Request $request Request object. - * @param PaymentResult $payment_result Payment result object. - */ - private function process_payment( \WP_REST_Request $request, PaymentResult $payment_result ) { - try { - // Transition the order to pending before making payment. - $this->order->update_status( 'pending' ); - - // Prepare the payment context object to pass through payment hooks. - $context = new PaymentContext(); - $context->set_payment_method( $this->get_request_payment_method_id( $request ) ); - $context->set_payment_data( $this->get_request_payment_data( $request ) ); - $context->set_order( $this->order ); - - /** - * Process payment with context. - * - * @hook woocommerce_rest_checkout_process_payment_with_context - * - * @throws \Exception If there is an error taking payment, an \Exception object can be thrown with an error message. - * - * @param PaymentContext $context Holds context for the payment, including order ID and payment method. - * @param PaymentResult $payment_result Result object for the transaction. - */ - do_action_ref_array( 'woocommerce_rest_checkout_process_payment_with_context', [ $context, &$payment_result ] ); - - if ( ! $payment_result instanceof PaymentResult ) { - throw new RouteException( 'woocommerce_rest_checkout_invalid_payment_result', __( 'Invalid payment result received from payment method.', 'woo-gutenberg-products-block' ), 500 ); - } - } catch ( \Exception $e ) { - throw new RouteException( 'woocommerce_rest_checkout_process_payment_error', $e->getMessage(), 402 ); - } - } - - /** - * Gets the chosen payment method ID from the request. - * - * @throws RouteException On error. - * @param \WP_REST_Request $request Request object. - * @return string - */ - private function get_request_payment_method_id( \WP_REST_Request $request ) { - $payment_method = $this->get_request_payment_method( $request ); - return is_null( $payment_method ) ? '' : $payment_method->id; - } - /** * Gets the chosen payment method from the request. * @@ -400,26 +254,6 @@ private function get_request_payment_method( \WP_REST_Request $request ) { return $available_gateways[ $request_payment_method ]; } - /** - * Gets and formats payment request data. - * - * @param \WP_REST_Request $request Request object. - * @return array - */ - private function get_request_payment_data( \WP_REST_Request $request ) { - static $payment_data = []; - if ( ! empty( $payment_data ) ) { - return $payment_data; - } - if ( ! empty( $request['payment_data'] ) ) { - foreach ( $request['payment_data'] as $data ) { - $payment_data[ sanitize_key( $data['key'] ) ] = wc_clean( $data['value'] ); - } - } - - return $payment_data; - } - /** * Updates the order with user details (e.g. address). * diff --git a/src/StoreApi/Utilities/CheckoutTrait.php b/src/StoreApi/Utilities/CheckoutTrait.php new file mode 100644 index 00000000000..24e41fc5b3f --- /dev/null +++ b/src/StoreApi/Utilities/CheckoutTrait.php @@ -0,0 +1,182 @@ + 200, + 'pending' => 202, + 'failure' => 400, + 'error' => 500, + ]; + + if ( isset( $item->payment_result ) && $item->payment_result instanceof PaymentResult ) { + $response->set_status( $status_codes[ $item->payment_result->status ] ?? 200 ); + } + + return $response; + } + + /** + * For orders which do not require payment, just update status. + * + * @param \WP_REST_Request $request Request object. + * @param PaymentResult $payment_result Payment result object. + */ + private function process_without_payment( \WP_REST_Request $request, PaymentResult $payment_result ) { + // Transition the order to pending, and then completed. This ensures transactional emails fire for pending_to_complete events. + $this->order->update_status( 'pending' ); + $this->order->payment_complete(); + + // Mark the payment as successful. + $payment_result->set_status( 'success' ); + $payment_result->set_redirect_url( $this->order->get_checkout_order_received_url() ); + } + + /** + * Fires an action hook instructing active payment gateways to process the payment for an order and provide a result. + * + * @throws RouteException On error. + * + * @param \WP_REST_Request $request Request object. + * @param PaymentResult $payment_result Payment result object. + */ + private function process_payment( \WP_REST_Request $request, PaymentResult $payment_result ) { + try { + // Transition the order to pending before making payment. + $this->order->update_status( 'pending' ); + + // Prepare the payment context object to pass through payment hooks. + $context = new PaymentContext(); + $context->set_payment_method( $this->get_request_payment_method_id( $request ) ); + $context->set_payment_data( $this->get_request_payment_data( $request ) ); + $context->set_order( $this->order ); + + /** + * Process payment with context. + * + * @hook woocommerce_rest_checkout_process_payment_with_context + * + * @throws \Exception If there is an error taking payment, an \Exception object can be thrown with an error message. + * + * @param PaymentContext $context Holds context for the payment, including order ID and payment method. + * @param PaymentResult $payment_result Result object for the transaction. + */ + do_action_ref_array( 'woocommerce_rest_checkout_process_payment_with_context', [ $context, &$payment_result ] ); + + if ( ! $payment_result instanceof PaymentResult ) { + throw new RouteException( 'woocommerce_rest_checkout_invalid_payment_result', __( 'Invalid payment result received from payment method.', 'woo-gutenberg-products-block' ), 500 ); + } + } catch ( \Exception $e ) { + throw new RouteException( 'woocommerce_rest_checkout_process_payment_error', $e->getMessage(), 402 ); + } + } + + /** + * Gets the chosen payment method ID from the request. + * + * @throws RouteException On error. + * @param \WP_REST_Request $request Request object. + * @return string + */ + private function get_request_payment_method_id( \WP_REST_Request $request ) { + $payment_method = $this->get_request_payment_method( $request ); + return is_null( $payment_method ) ? '' : $payment_method->id; + } + + /** + * Gets and formats payment request data. + * + * @param \WP_REST_Request $request Request object. + * @return array + */ + private function get_request_payment_data( \WP_REST_Request $request ) { + static $payment_data = []; + if ( ! empty( $payment_data ) ) { + return $payment_data; + } + if ( ! empty( $request['payment_data'] ) ) { + foreach ( $request['payment_data'] as $data ) { + $payment_data[ sanitize_key( $data['key'] ) ] = wc_clean( $data['value'] ); + } + } + + return $payment_data; + } + + /** + * Update the current order using the posted values from the request. + * + * @param \WP_REST_Request $request Full details about the request. + */ + private function update_order_from_request( \WP_REST_Request $request ) { + $this->order->set_customer_note( $request['customer_note'] ?? '' ); + $this->order->set_payment_method( $this->get_request_payment_method_id( $request ) ); + $this->order->set_payment_method_title( $this->get_request_payment_method_title( $request ) ); + + wc_do_deprecated_action( + '__experimental_woocommerce_blocks_checkout_update_order_from_request', + array( + $this->order, + $request, + ), + '6.3.0', + 'woocommerce_store_api_checkout_update_order_from_request', + 'This action was deprecated in WooCommerce Blocks version 6.3.0. Please use woocommerce_store_api_checkout_update_order_from_request instead.' + ); + + wc_do_deprecated_action( + 'woocommerce_blocks_checkout_update_order_from_request', + array( + $this->order, + $request, + ), + '7.2.0', + 'woocommerce_store_api_checkout_update_order_from_request', + 'This action was deprecated in WooCommerce Blocks version 7.2.0. Please use woocommerce_store_api_checkout_update_order_from_request instead.' + ); + + /** + * Fires when the Checkout Block/Store API updates an order's from the API request data. + * + * This hook gives extensions the chance to update orders based on the data in the request. This can be used in + * conjunction with the ExtendSchema class to post custom data and then process it. + * + * @since 7.2.0 + * + * @param \WC_Order $order Order object. + * @param \WP_REST_Request $request Full details about the request. + */ + do_action( 'woocommerce_store_api_checkout_update_order_from_request', $this->order, $request ); + + $this->order->save(); + } + + /** + * Gets the chosen payment method title from the request. + * + * @throws RouteException On error. + * @param \WP_REST_Request $request Request object. + * @return string + */ + private function get_request_payment_method_title( \WP_REST_Request $request ) { + $payment_method = $this->get_request_payment_method( $request ); + return is_null( $payment_method ) ? '' : $payment_method->get_title(); + } +} From 7ab08783a9e7d8d60ea53cde65bb00e22d9b2e40 Mon Sep 17 00:00:00 2001 From: hsingyuc Date: Mon, 24 Jul 2023 12:05:31 -0500 Subject: [PATCH 22/26] Use sanitize text field --- src/StoreApi/Utilities/OrderAuthorizationTrait.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/StoreApi/Utilities/OrderAuthorizationTrait.php b/src/StoreApi/Utilities/OrderAuthorizationTrait.php index 9ef93575498..dedd9770f27 100644 --- a/src/StoreApi/Utilities/OrderAuthorizationTrait.php +++ b/src/StoreApi/Utilities/OrderAuthorizationTrait.php @@ -20,8 +20,8 @@ trait OrderAuthorizationTrait { */ public function is_authorized( \WP_REST_Request $request ) { $order_id = absint( $request['id'] ); - $order_key = wc_clean( wp_unslash( $request->get_param( 'key' ) ) ); - $billing_email = wc_clean( wp_unslash( $request->get_param( 'billing_email' ) ) ); + $order_key = sanitize_text_field( wp_unslash( $request->get_param( 'key' ) ) ); + $billing_email = sanitize_text_field( wp_unslash( $request->get_param( 'billing_email' ) ) ); try { // In this context, pay_for_order capability checks that the current user ID matches the customer ID stored From d3ec6f345dd16085a3bce1ec3b5b76f70cc9cacc Mon Sep 17 00:00:00 2001 From: hsingyuc Date: Mon, 24 Jul 2023 12:24:24 -0500 Subject: [PATCH 23/26] Extend from checkout schema --- .../Schemas/V1/CheckoutOrderSchema.php | 182 +----------------- 1 file changed, 4 insertions(+), 178 deletions(-) diff --git a/src/StoreApi/Schemas/V1/CheckoutOrderSchema.php b/src/StoreApi/Schemas/V1/CheckoutOrderSchema.php index fd8eb0b4f34..6c362ed6664 100644 --- a/src/StoreApi/Schemas/V1/CheckoutOrderSchema.php +++ b/src/StoreApi/Schemas/V1/CheckoutOrderSchema.php @@ -9,7 +9,7 @@ /** * CheckoutOrderSchema class. */ -class CheckoutOrderSchema extends AbstractSchema { +class CheckoutOrderSchema extends CheckoutSchema { /** * The schema item name. * @@ -24,188 +24,14 @@ class CheckoutOrderSchema extends AbstractSchema { */ const IDENTIFIER = 'checkout-order'; - /** - * Billing address schema instance. - * - * @var BillingAddressSchema - */ - protected $billing_address_schema; - - /** - * Shipping address schema instance. - * - * @var ShippingAddressSchema - */ - protected $shipping_address_schema; - - /** - * Constructor. - * - * @param ExtendSchema $extend Rest Extending instance. - * @param SchemaController $controller Schema Controller instance. - */ - public function __construct( ExtendSchema $extend, SchemaController $controller ) { - parent::__construct( $extend, $controller ); - $this->billing_address_schema = $this->controller->get( BillingAddressSchema::IDENTIFIER ); - $this->shipping_address_schema = $this->controller->get( ShippingAddressSchema::IDENTIFIER ); - } - /** * Checkout schema properties. * * @return array */ public function get_properties() { - return [ - 'order_id' => [ - 'description' => __( 'The order ID to process during checkout.', 'woo-gutenberg-products-block' ), - 'type' => 'integer', - 'context' => [ 'view', 'edit' ], - 'readonly' => true, - ], - 'status' => [ - 'description' => __( 'Order status. Payment providers will update this value after payment.', 'woo-gutenberg-products-block' ), - 'type' => 'string', - 'context' => [ 'view', 'edit' ], - 'readonly' => true, - ], - 'order_key' => [ - 'description' => __( 'Order key used to check validity or protect access to certain order data.', 'woo-gutenberg-products-block' ), - 'type' => 'string', - 'context' => [ 'view', 'edit' ], - 'readonly' => true, - ], - 'customer_note' => [ - 'description' => __( 'Note added to the order by the customer during checkout.', 'woo-gutenberg-products-block' ), - 'type' => 'string', - 'context' => [ 'view', 'edit' ], - ], - 'customer_id' => [ - 'description' => __( 'Customer ID if registered. Will return 0 for guests.', 'woo-gutenberg-products-block' ), - 'type' => 'integer', - 'context' => [ 'view', 'edit' ], - 'readonly' => true, - ], - 'billing_address' => [ - 'description' => __( 'Billing address.', 'woo-gutenberg-products-block' ), - 'type' => 'object', - 'context' => [ 'view', 'edit' ], - 'properties' => $this->billing_address_schema->get_properties(), - 'arg_options' => [ - 'sanitize_callback' => [ $this->billing_address_schema, 'sanitize_callback' ], - 'validate_callback' => [ $this->billing_address_schema, 'validate_callback' ], - ], - 'required' => true, - ], - 'shipping_address' => [ - 'description' => __( 'Shipping address.', 'woo-gutenberg-products-block' ), - 'type' => 'object', - 'context' => [ 'view', 'edit' ], - 'properties' => $this->shipping_address_schema->get_properties(), - 'arg_options' => [ - 'sanitize_callback' => [ $this->shipping_address_schema, 'sanitize_callback' ], - 'validate_callback' => [ $this->shipping_address_schema, 'validate_callback' ], - ], - ], - 'payment_method' => [ - 'description' => __( 'The ID of the payment method being used to process the payment.', 'woo-gutenberg-products-block' ), - 'type' => 'string', - 'context' => [ 'view', 'edit' ], - 'enum' => wc()->payment_gateways->get_payment_gateway_ids(), - ], - 'payment_result' => [ - 'description' => __( 'Result of payment processing, or false if not yet processed.', 'woo-gutenberg-products-block' ), - 'type' => 'object', - 'context' => [ 'view', 'edit' ], - 'readonly' => true, - 'properties' => [ - 'payment_status' => [ - 'description' => __( 'Status of the payment returned by the gateway. One of success, pending, failure, error.', 'woo-gutenberg-products-block' ), - 'readonly' => true, - 'type' => 'string', - ], - 'payment_details' => [ - 'description' => __( 'An array of data being returned from the payment gateway.', 'woo-gutenberg-products-block' ), - 'readonly' => true, - 'type' => 'array', - 'items' => [ - 'type' => 'object', - 'properties' => [ - 'key' => [ - 'type' => 'string', - ], - 'value' => [ - 'type' => 'string', - ], - ], - ], - ], - 'redirect_url' => [ - 'description' => __( 'A URL to redirect the customer after checkout. This could be, for example, a link to the payment processors website.', 'woo-gutenberg-products-block' ), - 'readonly' => true, - 'type' => 'string', - ], - ], - ], - self::EXTENDING_KEY => $this->get_extended_schema( self::IDENTIFIER ), - ]; - } - - /** - * Return the response for checkout. - * - * @param object $item Results from checkout action. - * @return array - */ - public function get_item_response( $item ) { - return $this->get_checkout_response( $item->order, $item->payment_result ); - } - - /** - * Get the checkout response based on the current order and any payments. - * - * @param \WC_Order $order Order object. - * @param PaymentResult $payment_result Payment result object. - * @return array - */ - protected function get_checkout_response( \WC_Order $order, PaymentResult $payment_result = null ) { - return [ - 'order_id' => $order->get_id(), - 'status' => $order->get_status(), - 'order_key' => $order->get_order_key(), - 'customer_note' => $order->get_customer_note(), - 'customer_id' => $order->get_customer_id(), - 'billing_address' => $this->billing_address_schema->get_item_response( $order ), - 'shipping_address' => $this->shipping_address_schema->get_item_response( $order ), - 'payment_method' => $order->get_payment_method(), - 'payment_result' => [ - 'payment_status' => $payment_result->status, - 'payment_details' => $this->prepare_payment_details_for_response( $payment_result->payment_details ), - 'redirect_url' => $payment_result->redirect_url, - ], - self::EXTENDING_KEY => $this->get_extended_data( self::IDENTIFIER ), - ]; - } - - /** - * This prepares the payment details for the response so it's following the - * schema where it's an array of objects. - * - * @param array $payment_details An array of payment details from the processed payment. - * - * @return array An array of objects where each object has the key and value - * as distinct properties. - */ - protected function prepare_payment_details_for_response( array $payment_details ) { - return array_map( - function( $key, $value ) { - return (object) [ - 'key' => $key, - 'value' => $value, - ]; - }, - array_keys( $payment_details ), - $payment_details - ); + $parent_properties = parent::get_properties(); + unset( $parent_properties['create_account'] ); + return $parent_properties; } } From 138fcbf9ebe6c4bc89fcb416df101f4330c81a82 Mon Sep 17 00:00:00 2001 From: hsingyuc Date: Mon, 24 Jul 2023 12:26:23 -0500 Subject: [PATCH 24/26] Update response message --- src/StoreApi/Routes/V1/CheckoutOrder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StoreApi/Routes/V1/CheckoutOrder.php b/src/StoreApi/Routes/V1/CheckoutOrder.php index 33fd1e6833f..53e473743cd 100644 --- a/src/StoreApi/Routes/V1/CheckoutOrder.php +++ b/src/StoreApi/Routes/V1/CheckoutOrder.php @@ -103,7 +103,7 @@ protected function get_route_post_response( \WP_REST_Request $request ) { if ( $this->order->get_status() !== 'pending' ) { return new \WP_Error( 'invalid_order_update_status', - __( 'This order is not pending and cannot be paid for.', 'woo-gutenberg-products-block' ) + __( 'This order cannot be paid for.', 'woo-gutenberg-products-block' ) ); } From 461be202f248093451506021e4c2846fa81f05d6 Mon Sep 17 00:00:00 2001 From: hsingyuc Date: Mon, 24 Jul 2023 14:59:32 -0500 Subject: [PATCH 25/26] Allow failed orders to be paid for --- src/StoreApi/Routes/V1/CheckoutOrder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StoreApi/Routes/V1/CheckoutOrder.php b/src/StoreApi/Routes/V1/CheckoutOrder.php index 53e473743cd..be08217b16b 100644 --- a/src/StoreApi/Routes/V1/CheckoutOrder.php +++ b/src/StoreApi/Routes/V1/CheckoutOrder.php @@ -100,7 +100,7 @@ protected function get_route_post_response( \WP_REST_Request $request ) { $order_id = absint( $request['id'] ); $this->order = wc_get_order( $order_id ); - if ( $this->order->get_status() !== 'pending' ) { + if ( $this->order->get_status() !== 'pending' && $this->order->get_status() !== 'failed' ) { return new \WP_Error( 'invalid_order_update_status', __( 'This order cannot be paid for.', 'woo-gutenberg-products-block' ) From b88fe537e09bec16574eb983c955f071b4a50b65 Mon Sep 17 00:00:00 2001 From: hsingyuc Date: Tue, 25 Jul 2023 08:24:44 -0700 Subject: [PATCH 26/26] Update authorization error message --- src/StoreApi/Utilities/OrderAuthorizationTrait.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StoreApi/Utilities/OrderAuthorizationTrait.php b/src/StoreApi/Utilities/OrderAuthorizationTrait.php index dedd9770f27..94040603f7f 100644 --- a/src/StoreApi/Utilities/OrderAuthorizationTrait.php +++ b/src/StoreApi/Utilities/OrderAuthorizationTrait.php @@ -28,7 +28,7 @@ public function is_authorized( \WP_REST_Request $request ) { // within the order, or if the order was placed by a guest. // See https://github.com/woocommerce/woocommerce/blob/abcedbefe02f9e89122771100c42ff588da3e8e0/plugins/woocommerce/includes/wc-user-functions.php#L458. if ( ! current_user_can( 'pay_for_order', $order_id ) ) { - throw new RouteException( 'woocommerce_rest_invalid_user', __( 'This order belongs to a different customer. Please log in to the correct account.', 'woo-gutenberg-products-block' ), 403 ); + throw new RouteException( 'woocommerce_rest_invalid_user', __( 'This order belongs to a different customer.', 'woo-gutenberg-products-block' ), 403 ); } if ( get_current_user_id() === 0 ) { $this->order_controller->validate_order_key( $order_id, $order_key );