Add an endpoint for processing pay for order orders (https://github.com/woocommerce/woocommerce-blocks/pull/10287)
* Register order route * Check authorization for getting the order * Add order data to the response * Add order schema for the endpoint * Move validation check to order controller * Add order item schema * Check if the order is associated with current user * Fix after rebase * Add checkout order endpoint * Add order authorization trait * Allow to use the order update customer endpoint in dev build only * Get both customer and guest details * Remove duplicate function * Update the cart update customer class doc block * Remove duplicate order route * Update documentation for feature flags * Add checkout trait * Remove checkout trait * Update billing address and order * Only allow checkout pending orders * Create checout trait * Use sanitize text field * Extend from checkout schema * Update response message * Allow failed orders to be paid for * Update authorization error message
This commit is contained in:
parent
e29360642d
commit
35760814d6
|
@ -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
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ use Automattic\WooCommerce\StoreApi\Utilities\ValidationUtils;
|
|||
/**
|
||||
* 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;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<?php
|
||||
namespace Automattic\WooCommerce\StoreApi\Routes\V1;
|
||||
|
||||
use Automattic\WooCommerce\StoreApi\Payments\PaymentContext;
|
||||
use Automattic\WooCommerce\StoreApi\Payments\PaymentResult;
|
||||
use Automattic\WooCommerce\StoreApi\Exceptions\InvalidStockLevelsInCartException;
|
||||
use Automattic\WooCommerce\StoreApi\Exceptions\InvalidCartException;
|
||||
|
@ -9,12 +8,14 @@ use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
|
|||
use Automattic\WooCommerce\StoreApi\Utilities\DraftOrderTrait;
|
||||
use Automattic\WooCommerce\Checkout\Helpers\ReserveStock;
|
||||
use Automattic\WooCommerce\Checkout\Helpers\ReserveStockException;
|
||||
use Automattic\WooCommerce\StoreApi\Utilities\CheckoutTrait;
|
||||
|
||||
/**
|
||||
* Checkout class.
|
||||
*/
|
||||
class Checkout extends AbstractCartRoute {
|
||||
use DraftOrderTrait;
|
||||
use CheckoutTrait;
|
||||
|
||||
/**
|
||||
* The route identifier.
|
||||
|
@ -138,29 +139,6 @@ class Checkout extends AbstractCartRoute {
|
|||
return $this->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 @@ class Checkout extends AbstractCartRoute {
|
|||
$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 @@ class Checkout extends AbstractCartRoute {
|
|||
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.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,266 @@
|
|||
<?php
|
||||
namespace Automattic\WooCommerce\StoreApi\Routes\V1;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* The route identifier.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const IDENTIFIER = 'checkout-order';
|
||||
|
||||
/**
|
||||
* The routes schema.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const SCHEMA_TYPE = 'checkout-order';
|
||||
|
||||
/**
|
||||
* Holds the current order being processed.
|
||||
*
|
||||
* @var \WC_Order
|
||||
*/
|
||||
private $order = null;
|
||||
|
||||
/**
|
||||
* Get the path of this REST route.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_path() {
|
||||
return '/checkout/(?P<id>[\d]+)';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ) {
|
||||
$order_id = absint( $request['id'] );
|
||||
$this->order = wc_get_order( $order_id );
|
||||
|
||||
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' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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_billing_address( $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_billing_address( \WP_REST_Request $request ) {
|
||||
$customer = wc()->customer;
|
||||
$billing = $request['billing_address'];
|
||||
$shipping = $request['shipping_address'];
|
||||
|
||||
// Billing address is a required field.
|
||||
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 = $shipping ?? $billing;
|
||||
|
||||
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();
|
||||
|
||||
$this->order->set_billing_address( $billing );
|
||||
$this->order->set_shipping_address( $shipping );
|
||||
$this->order->save();
|
||||
$this->order->calculate_totals();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 );
|
||||
}
|
||||
}
|
|
@ -5,11 +5,14 @@ use Automattic\WooCommerce\StoreApi\SchemaController;
|
|||
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,42 +74,6 @@ class Order extends AbstractRoute {
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
@ -115,6 +82,6 @@ class Order extends AbstractRoute {
|
|||
*/
|
||||
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 ) ) );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,7 +63,8 @@ class RoutesController {
|
|||
];
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@ class SchemaController {
|
|||
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,
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
namespace Automattic\WooCommerce\StoreApi\Schemas\V1;
|
||||
|
||||
use Automattic\WooCommerce\StoreApi\SchemaController;
|
||||
use Automattic\WooCommerce\StoreApi\Payments\PaymentResult;
|
||||
use Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema;
|
||||
|
||||
|
||||
/**
|
||||
* CheckoutOrderSchema class.
|
||||
*/
|
||||
class CheckoutOrderSchema extends CheckoutSchema {
|
||||
/**
|
||||
* The schema item name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $title = 'checkout-order';
|
||||
|
||||
/**
|
||||
* The schema item identifier.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const IDENTIFIER = 'checkout-order';
|
||||
|
||||
/**
|
||||
* Checkout schema properties.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_properties() {
|
||||
$parent_properties = parent::get_properties();
|
||||
unset( $parent_properties['create_account'] );
|
||||
return $parent_properties;
|
||||
}
|
||||
}
|
|
@ -271,8 +271,8 @@ class OrderSchema extends AbstractSchema {
|
|||
'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(),
|
||||
|
|
|
@ -0,0 +1,182 @@
|
|||
<?php
|
||||
namespace Automattic\WooCommerce\StoreApi\Utilities;
|
||||
|
||||
use Automattic\WooCommerce\StoreApi\Payments\PaymentContext;
|
||||
use Automattic\WooCommerce\StoreApi\Payments\PaymentResult;
|
||||
|
||||
/**
|
||||
* CheckoutTrait
|
||||
*
|
||||
* Shared functionality for checkout route.
|
||||
*/
|
||||
trait CheckoutTrait {
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
namespace Automattic\WooCommerce\StoreApi\Utilities;
|
||||
|
||||
use Automattic\WooCommerce\StoreApi\Utilities\OrderController;
|
||||
use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
|
||||
|
||||
/**
|
||||
* OrderAuthorizationTrait
|
||||
*
|
||||
* Shared functionality for getting order authorization.
|
||||
*/
|
||||
trait OrderAuthorizationTrait {
|
||||
/**
|
||||
* 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 = 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
|
||||
// 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.', '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 );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -38,18 +38,6 @@ class OrderController {
|
|||
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,28 +469,13 @@ class OrderController {
|
|||
* @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 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
@ -510,7 +483,7 @@ class OrderController {
|
|||
* @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() ) ) {
|
||||
|
|
Loading…
Reference in New Issue