=> 'other', 'location' => 'contact', 'param' => 'additional_fields', ], ]; foreach ( $additional_field_contexts as $context => $context_data ) { $document_object = $this->get_document_object_from_rest_request( $request ); $document_object->set_context( $context ); $additional_fields = $this->additional_fields_controller->get_contextual_fields_for_location( $context_data['location'], $document_object ); if ( 'shipping_address' === $context_data['param'] ) { $field_values = WC()->cart->needs_shipping() ? (array) ( $request['shipping_address'] ?? $request['billing_address'] ?? [] ) : (array) ( $request['billing_address'] ?? [] ); } else { $field_values = (array) $request[ $context_data['param'] ] ?? []; } if ( 'address' === $context_data['location'] ) { $persist_keys = array_merge( $this->additional_fields_controller->get_address_fields_keys(), [ 'email' ], array_keys( $additional_fields ) ); } else { $persist_keys = array_keys( $additional_fields ); } foreach ( $field_values as $key => $value ) { if ( in_array( $key, $persist_keys, true ) ) { $this->update_customer_address_field( $customer, $key, $value, $context_data['group'] ); } } wc_log_order_step( '[Store API #3::update_customer_from_request] Persisted ' . $context . ' fields' ); } /** * 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(); } /** * 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 ) { $request_payment_method = wc_clean( wp_unslash( $request['payment_method'] ?? '' ) ); if ( empty( $request_payment_method ) ) { return null; } $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); if ( ! isset( $available_gateways[ $request_payment_method ] ) ) { $all_payment_gateways = WC()->payment_gateways->payment_gateways(); $gateway_title = isset( $all_payment_gateways[ $request_payment_method ] ) ? $all_payment_gateways[ $request_payment_method ]->get_title() : $request_payment_method; throw new RouteException( 'woocommerce_rest_checkout_payment_method_disabled', sprintf( // Translators: %s Payment method ID. esc_html__( '%s is not available for this order—please choose a different payment method', 'woocommerce' ), esc_html( $gateway_title ) ), 400 ); } return $available_gateways[ $request_payment_method ]; } /** * Order processing relating to customer account. * * Creates a customer account as needed (based on request & store settings) and updates the order with the new customer ID. * 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 ) { $order = $this->get_order_or_throw(); if ( $this->should_create_customer_account( $request ) ) { $customer_id = wc_create_new_customer( $request['billing_address']['email'], '', $request['customer_password'], [ 'first_name' => $request['billing_address']['first_name'], 'last_name' => $request['billing_address']['last_name'], 'source' => 'store-api', ] ); if ( is_wp_error( $customer_id ) ) { throw new RouteException( esc_html( $customer_id->get_error_code() ), esc_html( $customer_id->get_error_message() ), 400 ); } // Associate customer with the order. $order->set_customer_id( $customer_id ); // Set the customer auth cookie. wc_set_customer_auth_cookie( $customer_id ); wc_log_order_step( '[Store API #6::process_customer] Created new customer', array( 'customer_id' => $customer_id ) ); } // Persist customer address data to account. $this->order_controller->sync_customer_data_with_order( $order ); wc_log_order_step( '[Store API #6::process_customer] Synced customer data from order', array( 'customer_id' => $order->get_customer_id() ) ); } /** * Check request options and store (shop) config to determine if a user account should be created as part of order * processing. * * @param \WP_REST_Request $request The current request object being handled. * @return boolean True if a new user account should be created. */ private function should_create_customer_account( \WP_REST_Request $request ) { if ( is_user_logged_in() ) { return false; } // Return false if registration is not enabled for the store. if ( false === filter_var( WC()->checkout()->is_registration_enabled(), FILTER_VALIDATE_BOOLEAN ) ) { return false; } // Return true if the store requires an account for all purchases. Note - checkbox is not displayed to shopper in this case. if ( true === filter_var( WC()->checkout()->is_registration_required(), FILTER_VALIDATE_BOOLEAN ) ) { return true; } // Create an account if requested via the endpoint. if ( true === filter_var( $request['create_account'], FILTER_VALIDATE_BOOLEAN ) ) { // User has requested an account as part of checkout processing. return true; } return false; } /** * Reject the order if what the customer is going to be charged at is higher from what they saw. * * Runs on POST /checkout only, before the draft order is materialised, so a mismatch leaves * no order behind. The check only runs when the client sends the total it displayed; flows * that cannot know the final total up front (e.g. some express payment methods) omit it and * are unaffected. * * @phpstan-param \WP_REST_Request> $request * * @param \WP_REST_Request $request Request object. * @throws RouteException When the recalculated total is higher. Returns HTTP 409 with the refreshed cart so the client can display the updated total. */ private function validate_order_totals( \WP_REST_Request $request ): void { // The route schema constrains this to minor-unit digits, so the integer comparison below is exact. $expected_total = (string) ( $request['expected_total'] ?? '' ); if ( '' === $expected_total ) { return; } // CartSchema::prepare_money_response() is not exported, so we use the money formatter directly here to match the total_price format. $decimals = wc_get_price_decimals(); $actual_total = woocommerce_store_api_get_formatter( 'money' )->format( $this->cart_controller->get_cart_instance()->get_total( 'edit' ), [ 'decimals' => $decimals ] ); if ( (int) $actual_total <= (int) $expected_total ) { return; } $expected_amount = wc_remove_number_precision( absint( $expected_total ) ); $actual_amount = wc_remove_number_precision( absint( $actual_total ) ); throw new RouteException( 'woocommerce_rest_checkout_total_mismatch', sprintf( /* translators: %1$s: total the shopper confirmed, %2$s: updated, higher total */ esc_html__( 'The order total changed from %1$s to %2$s while you were checking out. Please review the updated total and place your order again.', 'woocommerce' ), esc_html( wc_price( $expected_amount, [ 'in_span' => false ] ) ), esc_html( wc_price( $actual_amount, [ 'in_span' => false ] ) ) ), 409, [ 'expected_total' => absint( $expected_total ), 'actual_total' => absint( $actual_total ), ] ); } /** * This validates if the order can be placed regarding settings in WooCommerce > Settings > Accounts & Privacy * If registration during checkout is disabled, guest checkout is disabled and the user is not logged in, prevent checkout. * * @throws RouteException If user cannot place order. */ private function validate_user_can_place_order() { if ( // "woocommerce_enable_signup_and_login_from_checkout" === no. false === filter_var( WC()->checkout()->is_registration_enabled(), FILTER_VALIDATE_BOOLEAN ) && // "woocommerce_enable_guest_checkout" === no. true === filter_var( WC()->checkout()->is_registration_required(), FILTER_VALIDATE_BOOLEAN ) && ! is_user_logged_in() ) { throw new RouteException( 'woocommerce_rest_guest_checkout_disabled', esc_html( /** * Filter to customize the checkout message when a user must be logged in. * * @since 9.4.3 * * @param string $message Message to display when a user must be logged in to check out. */ apply_filters( 'woocommerce_checkout_must_be_logged_in_message', __( 'You must be logged in to checkout.', 'woocommerce' ) ) ), 403 ); } } }