2019-07-09 00:49:11 -04:00
<? php
/**
* Defines helper functions for user checkout.
*
2019-10-25 19:13:36 -04:00
* @package WPGraphQL\WooCommerce\Data\Mutation
2019-07-09 00:49:11 -04:00
* @since 0.2.0
*/
2019-10-25 19:13:36 -04:00
namespace WPGraphQL\WooCommerce\Data\Mutation ;
2019-07-09 00:49:11 -04:00
use GraphQL\Error\UserError ;
2021-09-01 16:08:33 -07:00
use WP_Error ;
2019-07-09 00:49:11 -04:00
use function WC ;
/**
* Class - Checkout_Mutation
*/
class Checkout_Mutation {
2019-08-27 13:47:34 -04:00
/**
* Caches customer object. @see get_value.
*
2023-06-13 23:17:02 +03:00
* @var null|\WC_Customer
2019-08-27 13:47:34 -04:00
*/
2020-06-05 00:29:35 +08:00
private static $logged_in_customer = null ;
2019-08-27 13:47:34 -04:00
2019-07-09 00:49:11 -04:00
/**
* Is registration required to checkout?
*
* @since 3.0.0
* @return boolean
*/
public static function is_registration_required () {
2020-02-16 00:04:40 -05:00
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
2019-07-09 00:49:11 -04:00
return apply_filters ( 'woocommerce_checkout_registration_required' , 'yes' !== get_option ( 'woocommerce_enable_guest_checkout' ) );
}
/**
* See if a fieldset should be skipped.
*
* @since 3.0.0
* @param string $fieldset_key Fieldset key.
* @param array $data Posted data.
* @return bool
*/
protected static function maybe_skip_fieldset ( $fieldset_key , $data ) {
2023-07-19 17:16:00 -04:00
if ( 'shipping' === $fieldset_key && ( ! $data [ 'ship_to_different_address' ] && ! \WC () -> cart -> needs_shipping_address () ) ) {
2019-07-09 00:49:11 -04:00
return true ;
}
if ( 'account' === $fieldset_key && ( is_user_logged_in () || ( ! self :: is_registration_required () && empty ( $data [ 'createaccount' ] ) ) ) ) {
return true ;
}
return false ;
}
/**
* Returns order data for use when user checking out.
*
2023-07-19 23:01:50 +03:00
* @param array $input Input data describing order.
* @param \WPGraphQL\AppContext $context AppContext instance.
* @param \GraphQL\Type\Definition\ResolveInfo $info ResolveInfo instance.
2019-07-09 00:49:11 -04:00
*
* @return array
*/
public static function prepare_checkout_args ( $input , $context , $info ) {
2023-06-13 23:17:02 +03:00
$data = [
2019-08-26 17:39:46 -04:00
'terms' => ( int ) isset ( $input [ 'terms' ] ),
'createaccount' => ( int ) ! empty ( $input [ 'account' ] ),
2026-03-30 21:42:00 -04:00
'authenticate_account' => ! empty ( $input [ 'account' ][ 'authenticate' ] ),
2019-08-26 17:39:46 -04:00
'payment_method' => isset ( $input [ 'paymentMethod' ] ) ? $input [ 'paymentMethod' ] : '' ,
'shipping_method' => isset ( $input [ 'shippingMethod' ] ) ? $input [ 'shippingMethod' ] : '' ,
'ship_to_different_address' => ! empty ( $input [ 'shipToDifferentAddress' ] ) && ! wc_ship_to_billing_address_only (),
2022-08-26 14:53:36 -04:00
];
2019-07-09 00:49:11 -04:00
2026-03-18 16:59:08 -04:00
$skipped = [ 'fees' ];
2019-07-09 00:49:11 -04:00
foreach ( self :: get_checkout_fields () as $fieldset_key => $fieldset ) {
if ( self :: maybe_skip_fieldset ( $fieldset_key , $data ) ) {
$skipped [] = $fieldset_key ;
continue ;
}
foreach ( $fieldset as $field => $input_key ) {
2021-01-21 22:41:43 -05:00
$key = " { $fieldset_key } _ { $field } " ;
2020-04-30 18:19:38 -04:00
if ( 'order' === $fieldset_key ) {
2021-01-21 22:41:43 -05:00
$value = ! empty ( $input [ $input_key ] ) ? $input [ $input_key ] : null ;
2020-04-30 18:19:38 -04:00
} else {
$value = ! empty ( $input [ $fieldset_key ][ $input_key ] ) ? $input [ $fieldset_key ][ $input_key ] : null ;
}
2020-07-06 09:10:27 -04:00
2019-07-09 00:49:11 -04:00
if ( $value ) {
2019-08-27 13:47:34 -04:00
$data [ $key ] = $value ;
} elseif ( 'billing_country' === $key || 'shipping_country' === $key ) {
$data [ $key ] = self :: get_value ( $key );
2019-07-09 00:49:11 -04:00
}
}
2022-02-04 15:00:44 -05:00
} //end foreach
2019-07-09 00:49:11 -04:00
2026-03-18 16:59:08 -04:00
if ( ! empty ( $input [ 'fees' ] ) ) {
$fees = $input [ 'fees' ];
add_action (
'woocommerce_cart_calculate_fees' ,
static function () use ( $fees ) {
foreach ( $fees as $fee_input ) {
if ( empty ( $fee_input [ 'name' ] ) || empty ( $fee_input [ 'amount' ] ) ) {
// TODO: Log invalid fee input.
continue ;
}
$fee_args = [
$fee_input [ 'name' ],
$fee_input [ 'amount' ],
isset ( $fee_input [ 'taxable' ] ) ? $fee_input [ 'taxable' ] : false ,
isset ( $fee_input [ 'taxClass' ] ) ? $fee_input [ 'taxClass' ] : '' ,
];
\WC () -> cart -> add_fee ( ... $fee_args );
}
}
);
}
2019-07-09 00:49:11 -04:00
if ( in_array ( 'shipping' , $skipped , true ) && ( \WC () -> cart -> needs_shipping_address () || \wc_ship_to_billing_address_only () ) ) {
2019-07-09 03:55:55 -04:00
foreach ( self :: get_checkout_fields ( 'shipping' ) as $field => $input_key ) {
2019-07-09 00:49:11 -04:00
$data [ "shipping_ { $field } " ] = isset ( $data [ "billing_ { $field } " ] ) ? $data [ "billing_ { $field } " ] : '' ;
}
}
2020-02-16 00:04:40 -05:00
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
2019-07-09 00:49:11 -04:00
return apply_filters ( 'woocommerce_checkout_posted_data' , $data , $input , $context , $info );
}
/**
* Get an array of checkout fields.
*
* @param string $fieldset Target fieldset.
* @param boolean $prefixed Prefixed field keys with fieldset name.
*
2023-06-13 23:17:02 +03:00
* @return array
2019-07-09 00:49:11 -04:00
*/
public static function get_checkout_fields ( $fieldset = '' , $prefixed = false ) {
2022-08-26 14:53:36 -04:00
$fields = [
'billing' => [
2019-07-09 00:49:11 -04:00
'first_name' => 'firstName' ,
'last_name' => 'lastName' ,
2020-04-30 18:19:38 -04:00
'company' => 'company' ,
2019-07-09 00:49:11 -04:00
'address_1' => 'address1' ,
'address_2' => 'address2' ,
'city' => 'city' ,
'postcode' => 'postcode' ,
'state' => 'state' ,
'country' => 'country' ,
'phone' => 'phone' ,
'email' => 'email' ,
2022-08-26 14:53:36 -04:00
],
'shipping' => [
2019-07-09 00:49:11 -04:00
'first_name' => 'firstName' ,
'last_name' => 'lastName' ,
2020-04-30 18:19:38 -04:00
'company' => 'company' ,
2019-07-09 00:49:11 -04:00
'address_1' => 'address1' ,
'address_2' => 'address2' ,
'city' => 'city' ,
'postcode' => 'postcode' ,
'state' => 'state' ,
'country' => 'country' ,
2026-06-11 20:02:26 -04:00
'phone' => 'phone' ,
2022-08-26 14:53:36 -04:00
],
'account' => [
2019-07-09 00:49:11 -04:00
'username' => 'username' ,
'password' => 'password' ,
2022-08-26 14:53:36 -04:00
],
'order' => [
2021-01-21 22:41:43 -05:00
'comments' => 'customerNote' ,
2022-08-26 14:53:36 -04:00
],
];
2019-07-09 00:49:11 -04:00
if ( $prefixed ) {
foreach ( $fields as $prefix => $values ) {
foreach ( $values as $index => $value ) {
$fields [ $prefix ][ $index ] = " { $prefix } _ { $value } " ;
}
}
}
if ( ! empty ( $fieldset ) ) {
2023-06-13 23:17:02 +03:00
return ! empty ( $fields [ $fieldset ] ) ? $fields [ $fieldset ] : [];
2019-07-09 00:49:11 -04:00
}
return $fields ;
}
/**
* Update customer and session data from the posted checkout data.
*
* @param array $data Order data.
2023-06-13 23:17:02 +03:00
*
* @return void
2019-07-09 00:49:11 -04:00
*/
2021-06-08 15:41:11 +02:00
protected static function update_session ( $data ) {
2019-07-09 00:49:11 -04:00
// Update both shipping and billing to the passed billing address first if set.
2022-08-26 14:53:36 -04:00
$address_fields = [
2019-07-09 00:49:11 -04:00
'first_name' ,
'last_name' ,
'company' ,
'email' ,
'phone' ,
'address_1' ,
'address_2' ,
'city' ,
'postcode' ,
'state' ,
'country' ,
2022-08-26 14:53:36 -04:00
];
2019-07-09 00:49:11 -04:00
foreach ( $address_fields as $field ) {
self :: set_customer_address_fields ( $field , $data );
}
WC () -> customer -> save ();
// Update customer shipping and payment method to posted method.
$chosen_shipping_methods = WC () -> session -> get ( 'chosen_shipping_methods' );
if ( is_array ( $data [ 'shipping_method' ] ) ) {
foreach ( $data [ 'shipping_method' ] as $i => $value ) {
$chosen_shipping_methods [ $i ] = $value ;
}
}
WC () -> session -> set ( 'chosen_shipping_methods' , $chosen_shipping_methods );
WC () -> session -> set ( 'chosen_payment_method' , $data [ 'payment_method' ] );
// Update cart totals now we have customer address.
WC () -> cart -> calculate_totals ();
}
2020-04-30 18:19:38 -04:00
/**
* Clears customer address
*
* @param string $type Address type.
*
* @return bool
*/
protected static function clear_customer_address ( $type = 'billing' ) {
2020-04-30 20:06:21 -04:00
if ( 'billing' !== $type && 'shipping' !== $type ) {
2020-04-30 18:19:38 -04:00
return false ;
}
2022-08-26 14:53:36 -04:00
$address = [
2020-04-30 18:19:38 -04:00
'first_name' => '' ,
'last_name' => '' ,
'company' => '' ,
'address_1' => '' ,
'address_2' => '' ,
'city' => '' ,
'state' => '' ,
'postcode' => '' ,
'country' => '' ,
2022-08-26 14:53:36 -04:00
];
2020-04-30 18:19:38 -04:00
2021-01-21 22:41:43 -05:00
if ( 'billing' === $type ) {
2020-04-30 18:19:38 -04:00
$address = array_merge (
$address ,
2022-08-26 14:53:36 -04:00
[
2020-04-30 18:19:38 -04:00
'email' => '' ,
'phone' => '' ,
2022-08-26 14:53:36 -04:00
]
2020-04-30 18:19:38 -04:00
);
}
foreach ( $address as $prop => $value ) {
$setter = "set_ { $type } _ { $prop } " ;
WC () -> customer -> { $setter }( $value );
}
return true ;
}
2019-07-09 00:49:11 -04:00
/**
* Create a new customer account if needed.
*
* @param array $data Checkout data.
*
2023-07-19 23:01:50 +03:00
* @throws \GraphQL\Error\UserError When not able to create customer.
2023-06-13 23:17:02 +03:00
*
* @return void
2019-07-09 00:49:11 -04:00
*/
protected static function process_customer ( $data ) {
2020-02-16 00:04:40 -05:00
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
2019-07-09 00:49:11 -04:00
$customer_id = apply_filters ( 'woocommerce_checkout_customer_id' , get_current_user_id () );
if ( ! is_user_logged_in () && ( self :: is_registration_required () || ! empty ( $data [ 'createaccount' ] ) ) ) {
$username = ! empty ( $data [ 'account_username' ] ) ? $data [ 'account_username' ] : '' ;
$password = ! empty ( $data [ 'account_password' ] ) ? $data [ 'account_password' ] : '' ;
$customer_id = wc_create_new_customer (
$data [ 'billing_email' ],
$username ,
$password ,
2022-08-26 14:53:36 -04:00
[
2019-07-09 00:49:11 -04:00
'first_name' => ! empty ( $data [ 'billing_first_name' ] ) ? $data [ 'billing_first_name' ] : '' ,
'last_name' => ! empty ( $data [ 'billing_last_name' ] ) ? $data [ 'billing_last_name' ] : '' ,
2022-08-26 14:53:36 -04:00
]
2019-07-09 00:49:11 -04:00
);
if ( is_wp_error ( $customer_id ) ) {
throw new UserError ( $customer_id -> get_error_message () );
}
2026-03-30 21:42:00 -04:00
if ( ! empty ( $data [ 'authenticate_account' ] ) ) {
wc_set_customer_auth_cookie ( $customer_id );
2019-07-09 00:49:11 -04:00
2026-03-30 21:42:00 -04:00
// As we are now logged in, checkout will need to refresh to show logged in data.
WC () -> session -> set ( 'reload_checkout' , true );
}
2019-07-09 00:49:11 -04:00
// Also, recalculate cart totals to reveal any role-based discounts that were unavailable before registering.
WC () -> cart -> calculate_totals ();
2022-02-04 15:00:44 -05:00
} //end if
2019-07-09 00:49:11 -04:00
// On multisite, ensure user exists on current site, if not add them before allowing login.
if ( $customer_id && is_multisite () && is_user_logged_in () && ! is_user_member_of_blog () ) {
add_user_to_blog ( get_current_blog_id (), $customer_id , 'customer' );
}
// Add customer info from other fields.
2020-02-16 00:04:40 -05:00
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
2019-07-09 00:49:11 -04:00
if ( $customer_id && apply_filters ( 'woocommerce_checkout_update_customer_data' , true , WC () -> checkout () ) ) {
$customer = new \WC_Customer ( $customer_id );
if ( ! empty ( $data [ 'billing_first_name' ] ) && '' === $customer -> get_first_name () ) {
$customer -> set_first_name ( $data [ 'billing_first_name' ] );
}
if ( ! empty ( $data [ 'billing_last_name' ] ) && '' === $customer -> get_last_name () ) {
$customer -> set_last_name ( $data [ 'billing_last_name' ] );
}
// If the display name is an email, update to the user's full name.
if ( is_email ( $customer -> get_display_name () ) ) {
$customer -> set_display_name ( $customer -> get_first_name () . ' ' . $customer -> get_last_name () );
}
foreach ( $data as $key => $value ) {
// Use setters where available.
2022-08-26 14:53:36 -04:00
if ( is_callable ( [ $customer , "set_ { $key } " ] ) ) {
2019-07-09 00:49:11 -04:00
$customer -> { "set_ { $key } " }( $value );
// Store custom fields prefixed with wither shipping_ or billing_.
} elseif ( 0 === stripos ( $key , 'billing_' ) || 0 === stripos ( $key , 'shipping_' ) ) {
$customer -> update_meta_data ( $key , $value );
}
}
2020-02-16 00:04:40 -05:00
// Action hook to adjust customer before save.
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
2019-07-09 00:49:11 -04:00
do_action ( 'woocommerce_checkout_update_customer' , $customer , $data );
$customer -> save ();
2022-02-04 15:00:44 -05:00
} //end if
2019-07-09 00:49:11 -04:00
2020-02-16 00:04:40 -05:00
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
2019-07-09 00:49:11 -04:00
do_action ( 'woocommerce_checkout_update_user_meta' , $customer_id , $data );
}
/**
* Set address field for customer.
*
* @param string $field String to update.
* @param array $data Array of data to get the value from.
2023-06-13 23:17:02 +03:00
*
* @return void
2019-07-09 00:49:11 -04:00
*/
protected static function set_customer_address_fields ( $field , $data ) {
$billing_value = null ;
$shipping_value = null ;
2022-08-26 14:53:36 -04:00
if ( isset ( $data [ "billing_ { $field } " ] ) && is_callable ( [ WC () -> customer , "set_billing_ { $field } " ] ) ) {
2019-07-09 00:49:11 -04:00
$billing_value = $data [ "billing_ { $field } " ];
$shipping_value = $data [ "billing_ { $field } " ];
}
2022-08-26 14:53:36 -04:00
if ( isset ( $data [ "shipping_ { $field } " ] ) && is_callable ( [ WC () -> customer , "set_shipping_ { $field } " ] ) ) {
2019-07-09 00:49:11 -04:00
$shipping_value = $data [ "shipping_ { $field } " ];
}
2022-08-26 14:53:36 -04:00
if ( ! is_null ( $billing_value ) && is_callable ( [ WC () -> customer , "set_billing_ { $field } " ] ) ) {
2019-07-09 00:49:11 -04:00
WC () -> customer -> { "set_billing_ { $field } " }( $billing_value );
}
2022-08-26 14:53:36 -04:00
if ( ! is_null ( $shipping_value ) && is_callable ( [ WC () -> customer , "set_shipping_ { $field } " ] ) ) {
2019-07-09 00:49:11 -04:00
WC () -> customer -> { "set_shipping_ { $field } " }( $shipping_value );
}
}
/**
* Validates the posted checkout data based on field properties.
*
* @param array $data Checkout data.
*
2023-07-19 23:01:50 +03:00
* @throws \GraphQL\Error\UserError Invalid input.
2023-06-13 23:17:02 +03:00
*
* @return void
2019-07-09 00:49:11 -04:00
*/
protected static function validate_data ( & $data ) {
foreach ( self :: get_checkout_fields ( '' , true ) as $fieldset_key => $fieldset ) {
$validate_fieldset = true ;
if ( self :: maybe_skip_fieldset ( $fieldset_key , $data ) ) {
$validate_fieldset = false ;
}
foreach ( $fieldset as $key => $field_label ) {
if ( ! isset ( $data [ $key ] ) ) {
continue ;
}
2023-09-11 17:42:26 -04:00
if ( \str_ends_with ( $key , 'postcode' ) ) {
2019-07-09 00:49:11 -04:00
$country = isset ( $data [ $fieldset_key . '_country' ] ) ? $data [ $fieldset_key . '_country' ] : WC () -> customer -> { "get_ { $fieldset_key } _country" }();
$data [ $key ] = \wc_format_postcode ( $data [ $key ], $country );
if ( $validate_fieldset && '' !== $data [ $key ] && ! \WC_Validation :: is_postcode ( $data [ $key ], $country ) ) {
switch ( $country ) {
case 'IE' :
/* translators: %1$s: field name, %2$s finder.eircode.ie URL */
2026-06-30 10:16:21 -04:00
$postcode_validation_notice = sprintf ( __ ( '%1$s is not valid. You can look up the correct Eircode. %2$s' , 'graphql-for-ecommerce' ), $field_label , 'https://finder.eircode.ie' );
2019-07-09 00:49:11 -04:00
break ;
default :
/* translators: %s: field name */
2026-06-30 10:16:21 -04:00
$postcode_validation_notice = sprintf ( __ ( '%s is not a valid postcode / ZIP.' , 'graphql-for-ecommerce' ), $field_label );
2019-07-09 00:49:11 -04:00
}
2020-02-16 00:04:40 -05:00
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
2019-07-09 00:49:11 -04:00
throw new UserError ( apply_filters ( 'woocommerce_checkout_postcode_validation_notice' , $postcode_validation_notice , $country , $data [ $key ] ) );
}
}
2023-09-11 17:42:26 -04:00
if ( \str_ends_with ( $key , 'phone' ) ) {
2023-06-13 23:17:02 +03:00
if ( $validate_fieldset && '' !== $data [ $key ] && ! \WC_Validation :: is_phone ( $data [ $key ] ) ) {
2019-07-09 00:49:11 -04:00
/* translators: %s: phone number */
2026-06-30 10:16:21 -04:00
throw new UserError ( sprintf ( __ ( '%s is not a valid phone number.' , 'graphql-for-ecommerce' ), $field_label ) );
2019-07-09 00:49:11 -04:00
}
}
2023-09-11 17:42:26 -04:00
if ( \str_ends_with ( $key , 'email' ) && '' !== $data [ $key ] ) {
2019-07-09 00:49:11 -04:00
$email_is_valid = is_email ( $data [ $key ] );
$data [ $key ] = sanitize_email ( $data [ $key ] );
if ( $validate_fieldset && ! $email_is_valid ) {
/* translators: %s: email address */
2026-06-30 10:16:21 -04:00
throw new UserError ( sprintf ( __ ( '%s is not a valid email address.' , 'graphql-for-ecommerce' ), $field_label ) );
2019-07-09 00:49:11 -04:00
}
}
2023-09-11 17:42:26 -04:00
if ( \str_ends_with ( $key , 'state' ) && '' !== $data [ $key ] ) {
2019-07-09 00:49:11 -04:00
$country = isset ( $data [ $fieldset_key . '_country' ] ) ? $data [ $fieldset_key . '_country' ] : WC () -> customer -> { "get_ { $fieldset_key } _country" }();
$valid_states = WC () -> countries -> get_states ( $country );
2023-06-13 23:17:02 +03:00
if ( ! empty ( $valid_states ) && is_array ( $valid_states ) ) {
2019-07-09 00:49:11 -04:00
$valid_state_values = array_map ( 'wc_strtoupper' , array_flip ( array_map ( 'wc_strtoupper' , $valid_states ) ) );
$data [ $key ] = wc_strtoupper ( $data [ $key ] );
if ( isset ( $valid_state_values [ $data [ $key ] ] ) ) {
// With this part we consider state value to be valid as well, convert it to the state key for the valid_states check below.
$data [ $key ] = $valid_state_values [ $data [ $key ] ];
}
if ( $validate_fieldset && ! in_array ( $data [ $key ], $valid_state_values , true ) ) {
/* translators: 1: state field 2: valid states */
2026-06-30 10:16:21 -04:00
throw new UserError ( sprintf ( __ ( '%1$s is not valid. Please enter one of the following: %2$s' , 'graphql-for-ecommerce' ), $field_label , implode ( ', ' , $valid_states ) ) );
2019-07-09 00:49:11 -04:00
}
}
}
2022-02-04 15:00:44 -05:00
} //end foreach
} //end foreach
2019-07-09 00:49:11 -04:00
}
/**
* Validates that the checkout has enough info to proceed.
*
2026-03-18 17:51:27 -04:00
* @param array $data An array of posted data.
* @param \WP_Error $errors Validation errors.
2019-07-09 00:49:11 -04:00
*
2023-07-19 23:01:50 +03:00
* @throws \GraphQL\Error\UserError Invalid input.
2023-06-13 23:17:02 +03:00
*
* @return void
2019-07-09 00:49:11 -04:00
*/
2026-03-18 17:51:27 -04:00
protected static function validate_checkout ( & $data , & $errors ) {
2019-07-09 00:49:11 -04:00
self :: validate_data ( $data );
WC () -> checkout () -> check_cart_items ();
2026-03-18 17:51:27 -04:00
if ( empty ( $data [ 'woocommerce_checkout_update_totals' ] ) && empty ( $data [ 'terms' ] ) && ! empty ( $data [ 'terms-field' ] ) ) {
2026-06-30 10:16:21 -04:00
$errors -> add ( 'terms' , __ ( 'Please read and accept the terms and conditions to proceed with your order.' , 'graphql-for-ecommerce' ) );
2020-07-06 09:10:27 -04:00
}
2019-07-09 00:49:11 -04:00
if ( WC () -> cart -> needs_shipping () ) {
$shipping_country = WC () -> customer -> get_shipping_country ();
if ( empty ( $shipping_country ) ) {
2026-06-30 10:16:21 -04:00
$errors -> add ( 'shipping' , __ ( 'Please enter an address to continue.' , 'graphql-for-ecommerce' ) );
2019-07-09 00:49:11 -04:00
} elseif ( ! in_array ( WC () -> customer -> get_shipping_country (), array_keys ( WC () -> countries -> get_shipping_countries () ), true ) ) {
2026-03-18 17:51:27 -04:00
$errors -> add (
'shipping' ,
2019-07-09 00:49:11 -04:00
sprintf (
/* translators: %s: shipping location */
2026-06-30 10:16:21 -04:00
__ ( 'Unfortunately, we do not ship %s. Please enter an alternative shipping address.' , 'graphql-for-ecommerce' ),
2019-07-09 00:49:11 -04:00
WC () -> countries -> shipping_to_prefix () . ' ' . WC () -> customer -> get_shipping_country ()
)
);
} else {
$chosen_shipping_methods = WC () -> session -> get ( 'chosen_shipping_methods' );
foreach ( WC () -> shipping () -> get_packages () as $i => $package ) {
if ( ! isset ( $chosen_shipping_methods [ $i ], $package [ 'rates' ][ $chosen_shipping_methods [ $i ] ] ) ) {
2026-06-30 10:16:21 -04:00
$errors -> add ( 'shipping' , __ ( 'No shipping method has been selected. Please double check your address, or contact us if you need any help.' , 'graphql-for-ecommerce' ) );
2019-07-09 00:49:11 -04:00
}
}
}
2022-02-04 15:00:44 -05:00
} //end if
2019-07-09 00:49:11 -04:00
if ( WC () -> cart -> needs_payment () ) {
$available_gateways = WC () -> payment_gateways -> get_available_payment_gateways ();
if ( ! isset ( $available_gateways [ $data [ 'payment_method' ] ] ) ) {
2026-06-30 10:16:21 -04:00
$errors -> add ( 'payment' , __ ( 'Invalid payment method.' , 'graphql-for-ecommerce' ) );
2019-07-09 00:49:11 -04:00
} else {
$available_gateways [ $data [ 'payment_method' ] ] -> validate_fields ();
}
}
2020-02-16 00:04:40 -05:00
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
2026-03-18 17:51:27 -04:00
do_action ( 'woocommerce_after_checkout_validation' , $data , $errors );
do_action ( 'graphql_woocommerce_after_checkout_validation' , $data , $errors );
2019-07-09 00:49:11 -04:00
}
/**
* Process an order that does require payment.
*
* @param int $order_id Order ID.
* @param string $payment_method Payment method.
*
2023-07-19 23:01:50 +03:00
* @throws \GraphQL\Error\UserError When payment method is invalid.
2023-06-13 23:17:02 +03:00
*
* @return array Processed payment results.
2019-07-09 00:49:11 -04:00
*/
2021-10-15 21:04:10 +02:00
protected static function process_order_payment ( $order_id , $payment_method ) {
2019-07-09 00:49:11 -04:00
$available_gateways = WC () -> payment_gateways -> get_available_payment_gateways ();
if ( ! isset ( $available_gateways [ $payment_method ] ) ) {
2026-06-30 10:16:21 -04:00
throw new UserError ( __ ( 'Cannot process invalid payment method.' , 'graphql-for-ecommerce' ) );
2019-07-09 00:49:11 -04:00
}
// Store Order ID in session so it can be re-used after payment failure.
WC () -> session -> set ( 'order_awaiting_payment' , $order_id );
2020-04-03 20:42:05 -04:00
$process_payment_args = apply_filters (
"graphql_ { $payment_method } _process_payment_args" ,
2022-08-26 14:53:36 -04:00
[ $order_id ],
2020-04-03 20:42:05 -04:00
$payment_method
);
2019-07-09 00:49:11 -04:00
// Process Payment.
2020-04-03 20:42:05 -04:00
return $available_gateways [ $payment_method ] -> process_payment ( ... $process_payment_args );
2019-07-09 00:49:11 -04:00
}
/**
* Process an order that doesn't require payment.
*
* @since 3.0.0
2020-02-03 19:30:43 -05:00
* @param int $order_id Order ID.
* @param string $transaction_id Payment transaction ID.
*
2023-06-13 23:17:02 +03:00
* @throws \Exception Order cannot be retrieved.
*
2020-02-03 19:30:43 -05:00
* @return array
2019-07-09 00:49:11 -04:00
*/
2021-10-15 21:04:10 +02:00
protected static function process_order_without_payment ( $order_id , $transaction_id = '' ) {
2019-07-09 00:49:11 -04:00
$order = wc_get_order ( $order_id );
2023-06-13 23:17:02 +03:00
if ( ! is_object ( $order ) || ! is_a ( $order , \WC_Order :: class ) ) {
2026-06-30 10:16:21 -04:00
throw new \Exception ( __ ( 'Failed to retrieve order.' , 'graphql-for-ecommerce' ) );
2023-06-13 23:17:02 +03:00
}
2020-02-03 19:30:43 -05:00
$order -> payment_complete ( $transaction_id );
2019-08-26 17:39:46 -04:00
2022-08-26 14:53:36 -04:00
return [
2019-08-26 17:39:46 -04:00
'result' => 'success' ,
2020-02-16 00:04:40 -05:00
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
2019-08-26 17:39:46 -04:00
'redirect' => apply_filters ( 'woocommerce_checkout_no_payment_needed_redirect' , $order -> get_checkout_order_received_url (), $order ),
2022-08-26 14:53:36 -04:00
];
2019-07-09 00:49:11 -04:00
}
/**
* Process the checkout.
*
2023-07-19 23:01:50 +03:00
* @param array $data Order data.
* @param array $input Input data describing order.
* @param \WPGraphQL\AppContext $context AppContext instance.
* @param \GraphQL\Type\Definition\ResolveInfo $info ResolveInfo instance.
* @param array $results Order status.
2019-07-09 00:49:11 -04:00
*
2023-07-19 23:01:50 +03:00
* @throws \GraphQL\Error\UserError When validation fails.
2023-06-13 23:17:02 +03:00
*
* @return int Order ID.
2019-07-09 00:49:11 -04:00
*/
2020-02-03 19:30:43 -05:00
public static function process_checkout ( $data , $input , $context , $info , & $results = null ) {
2019-07-09 00:49:11 -04:00
wc_maybe_define_constant ( 'WOOCOMMERCE_CHECKOUT' , true );
wc_set_time_limit ( 0 );
2020-02-16 00:04:40 -05:00
do_action ( 'woocommerce_before_checkout_process' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
2019-07-09 00:49:11 -04:00
if ( WC () -> cart -> is_empty () ) {
2026-06-30 10:16:21 -04:00
throw new UserError ( __ ( 'Sorry, no session found.' , 'graphql-for-ecommerce' ) );
2019-07-09 00:49:11 -04:00
}
2020-02-16 00:04:40 -05:00
do_action ( 'woocommerce_checkout_process' , $data , $context , $info ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
2019-07-09 00:49:11 -04:00
2020-04-30 18:19:38 -04:00
if ( ! empty ( $input [ 'billing' ][ 'overwrite' ] ) && true === $input [ 'billing' ][ 'overwrite' ] ) {
self :: clear_customer_address ( 'billing' );
}
if ( ! empty ( $input [ 'shipping' ] ) && ! empty ( $input [ 'shipping' ][ 'overwrite' ] )
&& true === $input [ 'shipping' ][ 'overwrite' ] ) {
self :: clear_customer_address ( 'shipping' );
}
2019-07-09 00:49:11 -04:00
// Update session for customer and totals.
self :: update_session ( $data );
// Validate posted data and cart items before proceeding.
2026-03-18 17:51:27 -04:00
$errors = new WP_Error ();
self :: validate_checkout ( $data , $errors );
foreach ( $errors -> errors as $code => $messages ) {
$data = $errors -> get_error_data ( $code );
foreach ( $messages as $message ) {
wc_add_notice ( $message , 'error' , $data );
}
}
if ( 0 < wc_notice_count ( 'error' ) ) {
2026-06-30 10:16:21 -04:00
throw new UserError ( __ ( 'Failed to validate checkout' , 'graphql-for-ecommerce' ) );
2026-03-18 17:51:27 -04:00
}
2019-07-09 00:49:11 -04:00
2019-08-26 17:39:46 -04:00
self :: process_customer ( $data );
$order_id = WC () -> checkout -> create_order ( $data );
$order = wc_get_order ( $order_id );
2019-07-09 00:49:11 -04:00
2019-08-26 17:39:46 -04:00
if ( is_wp_error ( $order_id ) ) {
throw new UserError ( $order_id -> get_error_message () );
}
2019-07-09 00:49:11 -04:00
2023-06-13 23:17:02 +03:00
if ( ! is_object ( $order ) || ! is_a ( $order , \WC_Order :: class ) ) {
2026-06-30 10:16:21 -04:00
throw new UserError ( __ ( 'Unable to create order.' , 'graphql-for-ecommerce' ) );
2019-08-26 17:39:46 -04:00
}
2019-07-09 00:49:11 -04:00
2026-06-12 13:16:53 -04:00
// Override the "created via" source when provided. WC_Checkout::create_order() hardcodes it to "checkout".
if ( ! empty ( $input [ 'createdVia' ] ) ) {
$order -> set_created_via ( $input [ 'createdVia' ] );
$order -> add_meta_data ( '_wc_order_attribution_source_type' , $input [ 'createdVia' ], true );
$order -> save ();
}
2020-04-07 15:15:08 -04:00
// Add meta data.
if ( ! empty ( $input [ 'metaData' ] ) ) {
2021-06-14 19:56:43 +01:00
self :: update_order_meta ( $order_id , $input [ 'metaData' ], $input , $context , $info );
2026-03-20 19:22:31 -04:00
// Refresh the order object so the hook below receives the updated meta.
$order = wc_get_order ( $order_id );
if ( ! is_object ( $order ) || ! is_a ( $order , \WC_Order :: class ) ) {
2026-06-30 10:16:21 -04:00
throw new UserError ( __ ( 'Failed to get order with updated meta.' , 'graphql-for-ecommerce' ) );
2026-03-20 19:22:31 -04:00
}
2020-04-07 15:15:08 -04:00
}
2020-02-16 00:04:40 -05:00
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
2019-08-26 17:39:46 -04:00
do_action ( 'woocommerce_checkout_order_processed' , $order_id , $data , $order );
2019-07-09 00:49:11 -04:00
2023-06-13 23:17:02 +03:00
if ( WC () -> cart -> needs_payment () && ( empty ( $input [ 'isPaid' ] ) ) ) {
2019-08-26 17:39:46 -04:00
$results = self :: process_order_payment ( $order_id , $data [ 'payment_method' ] );
} else {
2020-02-03 19:30:43 -05:00
$transaction_id = ! empty ( $input [ 'transactionId' ] ) ? $input [ 'transactionId' ] : '' ;
/**
* Use this to do some last minute transaction ID validation.
*
* @param bool $is_valid Is transaction ID valid.
2023-06-13 23:17:02 +03:00
* @param \WC_Order $order Order being processed.
2020-02-03 19:30:43 -05:00
* @param String|null $transaction_id Order payment transaction ID.
* @param array $data Order data.
* @param array $input Order raw input data.
2023-07-19 23:01:50 +03:00
* @param \WPGraphQL\AppContext $context Request's AppContext instance.
* @param \GraphQL\Type\Definition\ResolveInfo $info Request's ResolveInfo instance.
2020-02-03 19:30:43 -05:00
*/
$valid = apply_filters (
'graphql_checkout_prepaid_order_validation' ,
true ,
$order ,
$transaction_id ,
$data ,
$input ,
$context ,
$info
);
if ( $valid ) {
$results = self :: process_order_without_payment ( $order_id , $transaction_id );
} else {
2022-08-26 14:53:36 -04:00
$results = [
2020-02-03 19:30:43 -05:00
'result' => 'failed' ,
'redirect' => apply_filters (
'graphql_woocommerce_checkout_payment_failed_redirect' ,
$order -> get_checkout_payment_url (),
$order ,
$order_id ,
$transaction_id
),
2022-08-26 14:53:36 -04:00
];
2020-02-03 19:30:43 -05:00
}
2022-02-04 15:00:44 -05:00
} //end if
2019-07-09 00:49:11 -04:00
2022-08-26 20:32:37 -04:00
if ( 'success' === $results [ 'result' ] ) {
wc_empty_cart ();
}
2019-07-09 00:49:11 -04:00
return $order_id ;
}
2019-08-27 13:47:34 -04:00
/**
* Gets the value either from 3rd party logic or the customer object. Sets the default values in checkout fields.
*
* @param string $input Name of the input we want to grab data for. e.g. billing_country.
* @return string The default value.
*/
public static function get_value ( $input ) {
// Allow 3rd parties to short circuit the logic and return their own default value.
2020-02-16 00:04:40 -05:00
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
2019-08-27 13:47:34 -04:00
$value = apply_filters ( 'woocommerce_checkout_get_value' , null , $input );
if ( ! is_null ( $value ) ) {
return $value ;
}
/**
* For logged in customers, pull data from their account rather than the session which may contain incomplete data.
* Another reason is that WC sets shipping address to the billing address on the checkout updates unless the
* "shipToDifferentAddress" is set.
*/
$customer_object = false ;
if ( is_user_logged_in () ) {
// Load customer object, but keep it cached to avoid reloading it multiple times.
if ( is_null ( self :: $logged_in_customer ) ) {
2020-06-05 16:47:37 +08:00
self :: $logged_in_customer = new \WC_Customer ( get_current_user_id (), true );
2019-08-27 13:47:34 -04:00
}
2020-06-05 16:47:37 +08:00
$customer_object = new \WC_Customer ( get_current_user_id (), true );
2019-08-27 13:47:34 -04:00
}
if ( ! $customer_object ) {
$customer_object = WC () -> customer ;
}
2022-08-26 14:53:36 -04:00
if ( is_callable ( [ $customer_object , "get_ $input " ] ) ) {
2019-08-27 13:47:34 -04:00
$value = $customer_object -> { "get_ $input " }();
} elseif ( $customer_object -> meta_exists ( $input ) ) {
$value = $customer_object -> get_meta ( $input , true );
}
if ( '' === $value ) {
$value = null ;
}
2020-02-16 00:04:40 -05:00
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
2019-08-27 13:47:34 -04:00
return apply_filters ( 'default_checkout_' . $input , $value , $input );
}
2020-01-13 18:36:31 -05:00
/**
2021-06-14 19:56:43 +01:00
* Add or update meta data not set in WC_Checkout::create_order().
2020-01-13 18:36:31 -05:00
*
2023-07-19 23:01:50 +03:00
* @param int $order_id Order ID.
* @param array $meta_data Order meta data.
* @param array $input Order properties.
* @param \WPGraphQL\AppContext $context AppContext instance.
* @param \GraphQL\Type\Definition\ResolveInfo $info ResolveInfo instance.
2023-06-13 23:17:02 +03:00
*
* @throws \Exception Order cannot be retrieved.
*
* @return void
2020-01-13 18:36:31 -05:00
*/
2021-06-14 19:56:43 +01:00
public static function update_order_meta ( $order_id , $meta_data , $input , $context , $info ) {
2020-01-13 18:36:31 -05:00
$order = \WC_Order_Factory :: get_order ( $order_id );
2023-06-13 23:17:02 +03:00
if ( ! is_object ( $order ) ) {
2026-06-30 10:16:21 -04:00
throw new \Exception ( __ ( 'Failed to retrieve order.' , 'graphql-for-ecommerce' ) );
2023-06-13 23:17:02 +03:00
}
2020-01-13 18:36:31 -05:00
if ( $meta_data ) {
foreach ( $meta_data as $meta ) {
2021-06-14 19:56:43 +01:00
$order -> update_meta_data ( $meta [ 'key' ], $meta [ 'value' ] );
2020-01-13 18:36:31 -05:00
}
}
/**
* Action called before changes to order meta are saved.
*
2023-06-13 23:17:02 +03:00
* @param \WC_Order $order WC_Order instance.
2020-01-13 18:36:31 -05:00
* @param array $meta_data Order meta data.
* @param array $props Order props array.
2023-07-19 23:01:50 +03:00
* @param \WPGraphQL\AppContext $context Request AppContext instance.
* @param \GraphQL\Type\Definition\ResolveInfo $info Request ResolveInfo instance.
2020-01-13 18:36:31 -05:00
*/
2020-02-16 00:04:40 -05:00
do_action ( 'graphql_woocommerce_before_checkout_meta_save' , $order , $meta_data , $input , $context , $info );
2020-01-13 18:36:31 -05:00
$order -> save ();
}
2019-07-09 00:49:11 -04:00
}