Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions includes/class-type-registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public function init() {
Type\WPEnum\Currency_Enum::register();
Type\WPEnum\Shipping_Location_Type_Enum::register();
Type\WPEnum\WC_Setting_Type_Enum::register();
Type\WPEnum\Cart_Notice_Type::register();
Type\WPEnum\Product_Attributes_Connection_Orderby_Enum::register();

/**
Expand Down Expand Up @@ -121,6 +122,7 @@ public function init() {
Type\WPObject\Shipping_Location_Type::register();
Type\WPObject\Tax_Class_Type::register();
Type\WPObject\WC_Setting_Type::register();
Type\WPObject\Cart_Notice::register();

/**
* Object fields.
Expand Down
2 changes: 2 additions & 0 deletions includes/class-wp-graphql-woocommerce.php
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ private function includes() {
require $include_directory_path . 'type/enum/class-currency-enum.php';
require $include_directory_path . 'type/enum/class-shipping-location-type-enum.php';
require $include_directory_path . 'type/enum/class-wc-setting-type-enum.php';
require $include_directory_path . 'type/enum/class-cart-notice-type.php';
require $include_directory_path . 'type/enum/class-product-attributes-connection-orderby-enum.php';

// Include interface type class files.
Expand All @@ -266,6 +267,7 @@ private function includes() {

// Include object type class files.
require $include_directory_path . 'type/object/class-cart-error-types.php';
require $include_directory_path . 'type/object/class-cart-notice.php';
require $include_directory_path . 'type/object/class-cart-type.php';
require $include_directory_path . 'type/object/class-coupon-type.php';
require $include_directory_path . 'type/object/class-customer-address-type.php';
Expand Down
44 changes: 32 additions & 12 deletions includes/data/mutation/class-checkout-mutation.php
Original file line number Diff line number Diff line change
Expand Up @@ -448,31 +448,37 @@ protected static function validate_data( &$data ) {
* Validates that the checkout has enough info to proceed.
*
* @param array $data An array of posted data.
* @param WP_Error $errors Validation errors.
*
* @throws \GraphQL\Error\UserError Invalid input.
*
* @return void
*/
protected static function validate_checkout( &$data ) {
protected static function validate_checkout( &$data, &$errors ) {
self::validate_data( $data );
WC()->checkout()->check_cart_items();

// Throw cart validation errors stored in the session.
$cart_item_errors = wc_get_notices( 'error' );
// $cart_item_errors = wc_get_notices( 'error' );

if ( ! empty( $cart_item_errors ) ) {
$cart_item_error_msgs = implode( ' ', array_column( $cart_item_errors, 'notice' ) );
\wc_clear_notices();
throw new UserError( $cart_item_error_msgs );
// if ( ! empty( $cart_item_errors ) ) {
// $cart_item_error_msgs = implode( ' ', array_column( $cart_item_errors, 'notice' ) );
// \wc_clear_notices();
// throw new UserError( $cart_item_error_msgs );
// }

if ( empty( $data['woocommerce_checkout_update_totals'] ) && empty( $data['terms'] ) && ! empty( $data['terms-field'] ) ) {
$errors->add( 'terms', __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce' ) );
}

if ( WC()->cart->needs_shipping() ) {
$shipping_country = WC()->customer->get_shipping_country();

if ( empty( $shipping_country ) ) {
throw new UserError( __( 'Please enter an address to continue.', 'wp-graphql-woocommerce' ) );
$errors->add( 'shipping', __( 'Please enter an address to continue.', 'wp-graphql-woocommerce' ) );
} elseif ( ! in_array( WC()->customer->get_shipping_country(), array_keys( WC()->countries->get_shipping_countries() ), true ) ) {
throw new UserError(
$errors->add(
'shipping',
sprintf(
/* translators: %s: shipping location */
__( 'Unfortunately, we do not ship %s. Please enter an alternative shipping address.', 'wp-graphql-woocommerce' ),
Expand All @@ -484,7 +490,7 @@ protected static function validate_checkout( &$data ) {

foreach ( WC()->shipping()->get_packages() as $i => $package ) {
if ( ! isset( $chosen_shipping_methods[ $i ], $package['rates'][ $chosen_shipping_methods[ $i ] ] ) ) {
throw new UserError( __( 'No shipping method has been selected. Please double check your address, or contact us if you need any help.', 'wp-graphql-woocommerce' ) );
$errors->add( 'shipping', __( 'No shipping method has been selected. Please double check your address, or contact us if you need any help.', 'wp-graphql-woocommerce' ) );
}
}
}
Expand All @@ -493,14 +499,15 @@ protected static function validate_checkout( &$data ) {
if ( WC()->cart->needs_payment() ) {
$available_gateways = WC()->payment_gateways->get_available_payment_gateways();
if ( ! isset( $available_gateways[ $data['payment_method'] ] ) ) {
throw new UserError( __( 'Invalid payment method.', 'wp-graphql-woocommerce' ) );
$errors->add( 'payment', __( 'Invalid payment method.', 'wp-graphql-woocommerce' ) );
} else {
$available_gateways[ $data['payment_method'] ]->validate_fields();
}
}

// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
do_action( 'woocommerce_after_checkout_validation', $data, new WP_Error() );
do_action( 'woocommerce_after_checkout_validation', $data, $errors );
do_action( 'graphql_woocommerce_after_checkout_validation', $data, $errors );
}

/**
Expand Down Expand Up @@ -584,6 +591,7 @@ public static function process_checkout( $data, $input, $context, $info, &$resul

do_action( 'woocommerce_checkout_process', $data, $context, $info ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound


if ( ! empty( $input['billing']['overwrite'] ) && true === $input['billing']['overwrite'] ) {
self::clear_customer_address( 'billing' );
}
Expand All @@ -597,7 +605,19 @@ public static function process_checkout( $data, $input, $context, $info, &$resul
self::update_session( $data );

// Validate posted data and cart items before proceeding.
self::validate_checkout( $data );
$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' ) ) {
throw new UserError( __('Failed to validate checkout', 'wp-graphql-woocommerce') );
}

self::process_customer( $data );
$order_id = WC()->checkout->create_order( $data );
Expand Down
90 changes: 87 additions & 3 deletions includes/mutation/class-checkout.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@ public static function get_output_fields() {
return $payload['redirect'];
},
],
'notices' => [
'type' => [ 'list_of' => 'CartNotice' ],
'description' => __( 'WooCommerce notices generated during checkout', 'wp-graphql-woocommerce' ),
'resolve' => static function ( $payload ) {
return $payload['notices'] ?? [];
},
],
];
}

Expand Down Expand Up @@ -163,17 +170,94 @@ public static function mutate_and_get_payload() {
* @param \WPGraphQL\AppContext $context Request AppContext instance.
* @param \GraphQL\Type\Definition\ResolveInfo $info Request ResolveInfo instance.
*/
// Capture any non-error notices for successful checkouts
$notices = wc_get_notices();
$formatted_notices = self::format_notices_for_response( $notices );

// Clear notices to prevent persistence
wc_clear_notices();

do_action( 'graphql_woocommerce_after_checkout', $order, $input, $context, $info );

return array_merge( [ 'id' => $order_id ], $results );
return array_merge( [ 'id' => $order_id ], $results, [ 'notices' => $formatted_notices ] );
} catch ( \Throwable $e ) {
// Delete order if it was created.
if ( is_object( $order ) ) {
Order_Mutation::purge( $order );
}
// Throw error.
throw new UserError( $e->getMessage() );

// Capture any WC notices that were added during checkout process
$notices = wc_get_notices();
$error_message = $e->getMessage();

// If there are notices, use them instead of the original error
if ( ! empty( $notices ) ) {
$formatted_notices = self::format_notices_for_error( $notices );
if ( ! empty( $formatted_notices ) ) {
$error_message = $formatted_notices;
}
}

// Clear notices to prevent them from persisting to next request
wc_clear_notices();

// Throw error with enhanced message
throw new UserError( $error_message );
}//end try
};
}

/**
* Format WC notices for GraphQL response
*
* @param array $notices WC notices array
* @return array Formatted notices for GraphQL
*/
private static function format_notices_for_response( $notices ) {
$formatted_notices = [];

// Include non-error notices (success, notice)
foreach ( [ 'success', 'notice' ] as $type ) {
if ( ! empty( $notices[ $type ] ) ) {
foreach ( $notices[ $type ] as $notice ) {
$formatted_notices[] = [
'type' => $type,
'message' => $notice['notice'] ?? $notice,
];
}
}
}

return $formatted_notices;
}

/**
* Format WC notices for error reporting
*
* @param array $notices WC notices array
* @return string Formatted error message
*/
private static function format_notices_for_error( $notices ) {
$error_messages = [];

// Prioritize error notices
if ( ! empty( $notices['error'] ) ) {
foreach ( $notices['error'] as $notice ) {
$error_messages[] = $notice['notice'] ?? $notice;
}
}

// Include other notice types if no errors
if ( empty( $error_messages ) ) {
foreach ( [ 'notice', 'success' ] as $type ) {
if ( ! empty( $notices[ $type ] ) ) {
foreach ( $notices[ $type ] as $notice ) {
$error_messages[] = $notice['notice'] ?? $notice;
}
}
}
}

return implode( ' ', $error_messages );
}
}
42 changes: 42 additions & 0 deletions includes/type/enum/class-cart-notice-type.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php
/**
* WPEnum Type - Cart_Notice_Type
*
* @package WPGraphQL\WooCommerce\Type\WPEnum
* @since TBD
*/

namespace WPGraphQL\WooCommerce\Type\WPEnum;

/**
* Class Cart_Notice_Type
*/
class Cart_Notice_Type {
/**
* Register Cart Notice Type enum
*
* @return void
*/
public static function register() {
register_graphql_enum_type(
'CartNoticeTypeEnum',
[
'description' => __( 'WooCommerce notice types', 'wp-graphql-woocommerce' ),
'values' => [
'ERROR' => [
'value' => 'error',
'description' => __( 'Error notice', 'wp-graphql-woocommerce' ),
],
'SUCCESS' => [
'value' => 'success',
'description' => __( 'Success notice', 'wp-graphql-woocommerce' ),
],
'NOTICE' => [
'value' => 'notice',
'description' => __( 'General notice', 'wp-graphql-woocommerce' ),
],
],
]
);
}
}
44 changes: 44 additions & 0 deletions includes/type/object/class-cart-notice.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php
/**
* WPObject Type - Cart_Notice
*
* @package WPGraphQL\WooCommerce\Type\WPObject
* @since TBD
*/

namespace WPGraphQL\WooCommerce\Type\WPObject;

/**
* Class Cart_Notice
*/
class Cart_Notice {
/**
* Register Cart Notice type
*
* @return void
*/
public static function register() {
register_graphql_object_type(
'CartNotice',
[
'description' => __( 'A WooCommerce notice', 'wp-graphql-woocommerce' ),
'fields' => [
'type' => [
'type' => 'CartNoticeTypeEnum',
'description' => __( 'Notice type', 'wp-graphql-woocommerce' ),
'resolve' => static function ( $notice ) {
return $notice['type'] ?? null;
},
],
'message' => [
'type' => 'String',
'description' => __( 'Notice message', 'wp-graphql-woocommerce' ),
'resolve' => static function ( $notice ) {
return $notice['message'] ?? null;
},
],
],
]
);
}
}
2 changes: 1 addition & 1 deletion tests/wpunit/CartMutationsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1260,7 +1260,7 @@ public function testFillCartMutationAndErrors() {
'cartErrors',
[
$this->expectedField( 'type', 'INVALID_COUPON' ),
$this->expectedField( 'reasons', [ "Coupon \"{$invalid_coupon}\" does not exist!" ] ),
$this->expectedField( 'reasons', [ "Coupon &quot;{$invalid_coupon}&quot; cannot be applied because it does not exist." ] ),
$this->expectedField( 'code', $invalid_coupon ),
]
),
Expand Down
Loading
Loading