// Validate additional fields.
$result = $this->additional_fields_controller->validate_fields_for_location( $address, 'address', $address_type );
if ( $result->has_errors() ) {
// Add errors to main error object but ensure they maintain the billing/shipping error code.
foreach ( $result->get_error_codes() as $code ) {
$errors->add( $address_type, $result->get_error_message( $code ), $code );
* Check email restrictions of a coupon against the order.
* @throws Exception Exception if invalid data is detected.
* @param \WC_Coupon $coupon Coupon object applied to the cart.
* @param \WC_Order $order Order object.
protected function validate_coupon_email_restriction( \WC_Coupon $coupon, \WC_Order $order ) {
$restrictions = $coupon->get_email_restrictions();
if ( empty( $restrictions ) ) {
// Check the logged-in user's email.
$current_user = wp_get_current_user();
if ( $current_user->exists() ) {
$user_email = trim( sanitize_email( $current_user->user_email ) );
if ( ! empty( $user_email ) ) {
$check_emails[] = strtolower( $user_email );
// Also check the billing email from the order.
$billing_email = $order->get_billing_email();
if ( ! empty( $billing_email ) ) {
$billing_email = trim( sanitize_email( $billing_email ) );
if ( ! empty( $billing_email ) ) {
$check_emails[] = strtolower( $billing_email );
// Remove duplicates and empty values.
$check_emails = array_unique( array_filter( $check_emails ) );
if ( ! empty( $check_emails ) && ! DiscountsUtil::is_coupon_emails_allowed( $check_emails, $restrictions ) ) {
// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
throw new Exception( $coupon->get_coupon_error( \WC_Coupon::E_WC_COUPON_NOT_YOURS_REMOVED ) );
* Check usage restrictions of a coupon against the order.
* @throws Exception Exception if invalid data is detected.
* @param \WC_Coupon $coupon Coupon object applied to the cart.
* @param \WC_Order $order Order object.
protected function validate_coupon_usage_limit( \WC_Coupon $coupon, \WC_Order $order ) {
$coupon_usage_limit = $coupon->get_usage_limit_per_user();
if ( 0 === $coupon_usage_limit ) {
// First, we check a logged in customer usage count, which happens against their user id, billing email, and account email.
if ( $order->get_customer_id() ) {
// We get usage per user id and associated emails.
$usage_count = $this->get_usage_per_aliases(
$order->get_billing_email(),
$order->get_customer_id(),
$this->get_email_from_user_id( $order->get_customer_id() ),
// Otherwise we check if the email doesn't belong to an existing user.
// This will get us any user ids for the given billing email.
$user_ids = wc_get_container()->get( CustomerSearchService::class )->find_user_ids_by_billing_email_for_coupons_usage_lookup( array( $order->get_billing_email() ) );
// Convert all found user ids to a list of email addresses.
$user_emails = array_map( array( $this, 'get_email_from_user_id' ), $user_ids );
// This matches a user against the given billing email and gets their ID/email/billing email.
$found_user = get_user_by( 'email', $order->get_billing_email() );
$user_ids[] = $found_user->ID;
$user_emails[] = $found_user->user_email;
$user_emails[] = get_user_meta( $found_user->ID, 'billing_email', true );
// Finally, grab usage count for all found IDs and emails.
$usage_count = $this->get_usage_per_aliases(
array( $order->get_billing_email() )
if ( $usage_count >= $coupon_usage_limit ) {
throw new Exception( $coupon->get_coupon_error( \WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED ) );
* Get user email from user id.
* @param integer $user_id User ID.
* @return string Email or empty string.
private function get_email_from_user_id( $user_id ) {
$user_data = get_userdata( $user_id );
return $user_data ? $user_data->user_email : '';
* Get the usage count for a coupon based on a list of aliases (ids, emails).
* @param \WC_Coupon $coupon Coupon object applied to the cart.
* @param array $aliases List of aliases to check.
private function get_usage_per_aliases( $coupon, $aliases ) {
$aliases = array_unique( array_filter( $aliases ) );
$aliases_string = "('" . implode( "','", array_map( 'esc_sql', $aliases ) ) . "')";
$usage_count = $wpdb->get_var(
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
"SELECT COUNT( meta_id ) FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_used_by' AND meta_value IN {$aliases_string};",
$data_store = $coupon->get_data_store();
// Coupons can be held for an x amount of time before being applied to an order, so we need to check if it's already being held in (maybe via another flow).
$tentative_usage_count = $data_store->get_tentative_usages_for_user( $coupon->get_id(), $aliases );
return $tentative_usage_count + $usage_count;
* Check there is a shipping method if it requires shipping.
* @throws RouteException Exception if invalid data is detected.
* @param boolean $needs_shipping Current order needs shipping.
* @param array $chosen_shipping_methods Array of shipping methods.
public function validate_selected_shipping_methods( $needs_shipping, $chosen_shipping_methods = array() ) {
if ( ! $needs_shipping ) {
$exception = new RouteException(
'woocommerce_rest_invalid_shipping_option',
__( 'Sorry, this order requires a shipping option.', 'woocommerce' ),
if ( ! is_array( $chosen_shipping_methods ) || empty( $chosen_shipping_methods ) ) {
// Validate that the chosen shipping methods are valid according to the returned package rates.
$packages = WC()->shipping()->get_packages();
foreach ( $packages as $package_id => $package ) {
$chosen_rate_for_package = $chosen_shipping_methods[ $package_id ];
$valid_rate_ids_for_package = wp_list_pluck( $package['rates'], 'id' );
if ( ! is_string( $chosen_rate_for_package ) || ! ArrayUtils::string_contains_array( $chosen_rate_for_package, $valid_rate_ids_for_package ) ) {
* Validate a given order key against an existing order.
* @throws RouteException Exception if invalid data is detected.
* @param integer $order_id Order ID.
* @param string $order_key Order key.
public function validate_order_key( $order_id, $order_key ) {
$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.', 'woocommerce' ), 401 );
* Get errors for order stock on failed orders.
* @throws RouteException Exception if invalid data is detected.
* @param integer $order_id Order ID.
public function get_failed_order_stock_error( $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() ) ) {
foreach ( $order->get_items() as $item_key => $item ) {
if ( $item && is_callable( array( $item, 'get_product' ) ) ) {
$product = $item->get_product();
$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();
* 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.
if ( ! apply_filters( 'woocommerce_pay_order_product_in_stock', $product->is_in_stock(), $product, $order ) ) {
'code' => 'woocommerce_rest_out_of_stock',
/* translators: %s: product name */
'message' => sprintf( __( 'Sorry, "%s" is no longer in stock so this order cannot be paid for. We apologize for any inconvenience caused.', 'woocommerce' ), $product->get_name() ),
// We only need to check products managing stock, with a limited stock qty.
if ( ! $product->managing_stock() || $product->backorders_allowed() ) {
// 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.
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 */
'code' => 'woocommerce_rest_out_of_stock',
/* translators: %s: product name */
'message' => sprintf( __( 'Sorry, we do not have enough "%1$s" in stock to fulfill your order (%2$s available). We apologize for any inconvenience caused.', 'woocommerce' ), $product->get_name(), wc_format_stock_quantity_for_display( $product->get_stock_quantity() - $held_stock, $product ) ),
* Changes default order status to draft for orders created via this API.
public function default_order_status() {
* Create order line items.
* @param \WC_Order $order The order object to update.
protected function update_line_items_from_cart( \WC_Order $order ) {
$cart_controller = new CartController();
$cart = $cart_controller->get_cart_instance();
$cart_hashes = $cart_controller->get_cart_hashes();
if ( $order->get_cart_hash() !== $cart_hashes['line_items'] ) {
$order->set_cart_hash( $cart_hashes['line_items'] );
$order->remove_order_items( 'line_item' );
wc()->checkout->create_order_line_items( $order, $cart );
if ( $order->get_meta( '_shipping_hash' ) !== $cart_hashes['shipping'] ) {
$order->update_meta_data( '_shipping_hash', $cart_hashes['shipping'] );
$order->remove_order_items( 'shipping' );
wc()->checkout->create_order_shipping_lines( $order, wc()->session->get( 'chosen_shipping_methods' ), wc()->shipping()->get_packages() );
if ( $order->get_meta( '_coupons_hash' ) !== $cart_hashes['coupons'] ) {
$order->remove_order_items( 'coupon' );
$order->update_meta_data( '_coupons_hash', $cart_hashes['coupons'] );
wc()->checkout->create_order_coupon_lines( $order, $cart );
if ( $order->get_meta( '_fees_hash' ) !== $cart_hashes['fees'] ) {
$order->update_meta_data( '_fees_hash', $cart_hashes['fees'] );
$order->remove_order_items( 'fee' );
wc()->checkout->create_order_fee_lines( $order, $cart );
if ( $order->get_meta( '_taxes_hash' ) !== $cart_hashes['taxes'] ) {
$order->update_meta_data( '_taxes_hash', $cart_hashes['taxes'] );
$order->remove_order_items( 'tax' );
wc()->checkout->create_order_tax_lines( $order, $cart );
* Update address data from cart and/or customer session data.
* @param \WC_Order $order The order object to update.
protected function update_addresses_from_cart( \WC_Order $order ) {
'billing_first_name' => wc()->customer->get_billing_first_name(),
'billing_last_name' => wc()->customer->get_billing_last_name(),
'billing_company' => wc()->customer->get_billing_company(),
'billing_address_1' => wc()->customer->get_billing_address_1(),
'billing_address_2' => wc()->customer->get_billing_address_2(),
'billing_city' => wc()->customer->get_billing_city(),
'billing_state' => wc()->customer->get_billing_state(),
'billing_postcode' => wc()->customer->get_billing_postcode(),
'billing_country' => wc()->customer->get_billing_country(),
'billing_email' => wc()->customer->get_billing_email(),
'billing_phone' => wc()->customer->get_billing_phone(),
'shipping_first_name' => wc()->customer->get_shipping_first_name(),
'shipping_last_name' => wc()->customer->get_shipping_last_name(),
'shipping_company' => wc()->customer->get_shipping_company(),
'shipping_address_1' => wc()->customer->get_shipping_address_1(),
'shipping_address_2' => wc()->customer->get_shipping_address_2(),
'shipping_city' => wc()->customer->get_shipping_city(),
'shipping_state' => wc()->customer->get_shipping_state(),
'shipping_postcode' => wc()->customer->get_shipping_postcode(),
'shipping_country' => wc()->customer->get_shipping_country(),
'shipping_phone' => wc()->customer->get_shipping_phone(),
$this->additional_fields_controller->sync_order_additional_fields_with_customer( $order, wc()->customer );