• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

wp-graphql / wp-graphql-woocommerce / 23268851131

18 Mar 2026 09:51PM UTC coverage: 83.244% (+0.1%) from 83.127%
23268851131

push

github

web-flow
fix: Checkout notices further implemented (#951)

* fix: Checkout notices further implemented

* fix: resolve PHPCS lint errors and remove commented-out code

* test: add stale notice leak regression test

Verifies that error notices from a failed checkout do not leak into
subsequent checkout attempts, reproducing the exact scenario from #666.

86 of 101 new or added lines in 6 files covered. (85.15%)

2 existing lines in 2 files now uncovered.

12743 of 15308 relevant lines covered (83.24%)

79.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.45
/includes/mutation/class-checkout.php
1
<?php
2
/**
3
 * Mutation - checkout
4
 *
5
 * Registers mutation for checking out.
6
 *
7
 * @package WPGraphQL\WooCommerce\Mutation
8
 * @since 0.2.0
9
 */
10

11
namespace WPGraphQL\WooCommerce\Mutation;
12

13
use GraphQL\Error\UserError;
14
use GraphQL\Type\Definition\ResolveInfo;
15
use WPGraphQL\AppContext;
16
use WPGraphQL\WooCommerce\Data\Mutation\Checkout_Mutation;
17
use WPGraphQL\WooCommerce\Data\Mutation\Order_Mutation;
18
use WPGraphQL\WooCommerce\Model\Customer;
19
use WPGraphQL\WooCommerce\Model\Order;
20

21
/**
22
 * Class Checkout
23
 */
24
class Checkout {
25
        /**
26
         * Registers mutation
27
         *
28
         * @return void
29
         */
30
        public static function register_mutation() {
31
                register_graphql_mutation(
161✔
32
                        'checkout',
161✔
33
                        [
161✔
34
                                'inputFields'         => self::get_input_fields(),
161✔
35
                                'outputFields'        => self::get_output_fields(),
161✔
36
                                'mutateAndGetPayload' => self::mutate_and_get_payload(),
161✔
37
                        ]
161✔
38
                );
161✔
39
        }
40

41
        /**
42
         * Defines the mutation input field configuration
43
         *
44
         * @return array
45
         */
46
        public static function get_input_fields() {
47
                return [
161✔
48
                        'paymentMethod'          => [
161✔
49
                                'type'        => 'String',
161✔
50
                                'description' => __( 'Payment method ID.', 'wp-graphql-woocommerce' ),
161✔
51
                        ],
161✔
52
                        'shippingMethod'         => [
161✔
53
                                'type'        => [ 'list_of' => 'String' ],
161✔
54
                                'description' => __( 'Order shipping method', 'wp-graphql-woocommerce' ),
161✔
55
                        ],
161✔
56
                        'shipToDifferentAddress' => [
161✔
57
                                'type'        => 'Boolean',
161✔
58
                                'description' => __( 'Ship to a separate address', 'wp-graphql-woocommerce' ),
161✔
59
                        ],
161✔
60
                        'billing'                => [
161✔
61
                                'type'        => 'CustomerAddressInput',
161✔
62
                                'description' => __( 'Order billing address', 'wp-graphql-woocommerce' ),
161✔
63
                        ],
161✔
64
                        'shipping'               => [
161✔
65
                                'type'        => 'CustomerAddressInput',
161✔
66
                                'description' => __( 'Order shipping address', 'wp-graphql-woocommerce' ),
161✔
67
                        ],
161✔
68
                        'account'                => [
161✔
69
                                'type'        => 'CreateAccountInput',
161✔
70
                                'description' => __( 'Create new customer account', 'wp-graphql-woocommerce' ),
161✔
71
                        ],
161✔
72
                        'transactionId'          => [
161✔
73
                                'type'        => 'String',
161✔
74
                                'description' => __( 'Order transaction ID', 'wp-graphql-woocommerce' ),
161✔
75
                        ],
161✔
76
                        'isPaid'                 => [
161✔
77
                                'type'        => 'Boolean',
161✔
78
                                'description' => __( 'Define if the order is paid. It will set the status to processing and reduce stock items.', 'wp-graphql-woocommerce' ),
161✔
79
                        ],
161✔
80
                        'metaData'               => [
161✔
81
                                'type'        => [ 'list_of' => 'MetaDataInput' ],
161✔
82
                                'description' => __( 'Order meta data', 'wp-graphql-woocommerce' ),
161✔
83
                        ],
161✔
84
                        'customerNote'           => [
161✔
85
                                'type'        => 'String',
161✔
86
                                'description' => __( 'Order customer note', 'wp-graphql-woocommerce' ),
161✔
87
                        ],
161✔
88
                        'fees'                   => [
161✔
89
                                'type'        => [ 'list_of' => 'FeeInput' ],
161✔
90
                                'description' => __( 'Fees to add to the order.', 'wp-graphql-woocommerce' ),
161✔
91
                        ],
161✔
92
                ];
161✔
93
        }
94

95
        /**
96
         * Defines the mutation output field configuration
97
         *
98
         * @return array
99
         */
100
        public static function get_output_fields() {
101
                return [
161✔
102
                        'order'    => [
161✔
103
                                'type'    => 'Order',
161✔
104
                                'resolve' => static function ( $payload ) {
161✔
105
                                        return new Order( $payload['id'] );
6✔
106
                                },
161✔
107
                        ],
161✔
108
                        'customer' => [
161✔
109
                                'type'    => 'Customer',
161✔
110
                                'resolve' => static function () {
161✔
111
                                        return is_user_logged_in() ? new Customer( get_current_user_id() ) : new Customer();
5✔
112
                                },
161✔
113
                        ],
161✔
114
                        'result'   => [
161✔
115
                                'type'    => 'String',
161✔
116
                                'resolve' => static function ( $payload ) {
161✔
117
                                        return $payload['result'];
6✔
118
                                },
161✔
119
                        ],
161✔
120
                        'redirect' => [
161✔
121
                                'type'    => 'String',
161✔
122
                                'resolve' => static function ( $payload ) {
161✔
123
                                        return $payload['redirect'];
5✔
124
                                },
161✔
125
                        ],
161✔
126
                        'notices'  => [
161✔
127
                                'type'        => [ 'list_of' => 'CartNotice' ],
161✔
128
                                'description' => __( 'WooCommerce notices generated during checkout', 'wp-graphql-woocommerce' ),
161✔
129
                                'resolve'     => static function ( $payload ) {
161✔
130
                                        return $payload['notices'] ?? [];
1✔
131
                                },
161✔
132
                        ],
161✔
133
                ];
161✔
134
        }
135

136
        /**
137
         * Defines the mutation data modification closure.
138
         *
139
         * @return callable
140
         */
141
        public static function mutate_and_get_payload() {
142
                return static function ( $input, AppContext $context, ResolveInfo $info ) {
161✔
143
                        // Create order.
144
                        $order = null;
11✔
145
                        try {
146
                                $args = Checkout_Mutation::prepare_checkout_args( $input, $context, $info );
11✔
147

148
                                /**
149
                                 * Action called before checking out.
150
                                 *
151
                                 * @param array       $args    Order data.
152
                                 * @param array       $input   Raw input data .
153
                                 * @param \WPGraphQL\AppContext  $context Request AppContext instance.
154
                                 * @param \GraphQL\Type\Definition\ResolveInfo $info    Request ResolveInfo instance.
155
                                 */
156
                                do_action( 'graphql_woocommerce_before_checkout', $args, $input, $context, $info );
11✔
157

158
                                // We define this now and pass it as a reference.
159
                                $results = [];
11✔
160

161
                                $order_id = Checkout_Mutation::process_checkout( $args, $input, $context, $info, $results );
11✔
162

163
                                $order = \WC_Order_Factory::get_order( $order_id );
6✔
164

165
                                if ( ! is_object( $order ) ) {
6✔
166
                                        throw new UserError( __( 'Failed to retrieve order after checkout', 'wp-graphql-woocommerce' ) );
×
167
                                }//end if
168

169
                                // Capture any non-error notices for successful checkouts.
170
                                $notices           = wc_get_notices();
6✔
171
                                $formatted_notices = self::format_notices_for_response( $notices );
6✔
172

173
                                // Clear notices to prevent persistence.
174
                                wc_clear_notices();
6✔
175

176
                                /**
177
                                 * Action called after checking out.
178
                                 *
179
                                 * @param \WC_Order   $order   WC_Order instance.
180
                                 * @param array       $input   Input data describing order.
181
                                 * @param \WPGraphQL\AppContext  $context Request AppContext instance.
182
                                 * @param \GraphQL\Type\Definition\ResolveInfo $info    Request ResolveInfo instance.
183
                                 */
184
                                do_action( 'graphql_woocommerce_after_checkout', $order, $input, $context, $info );
6✔
185

186
                                return array_merge( [ 'id' => $order_id ], $results, [ 'notices' => $formatted_notices ] );
6✔
187
                        } catch ( \Throwable $e ) {
5✔
188
                                // Delete order if it was created.
189
                                if ( is_object( $order ) ) {
5✔
190
                                        Order_Mutation::purge( $order );
×
191
                                }
192

193
                                // Capture any WC notices that were added during checkout process.
194
                                $notices       = wc_get_notices();
5✔
195
                                $error_message = $e->getMessage();
5✔
196

197
                                // If there are notices, use them instead of the original error.
198
                                if ( ! empty( $notices ) ) {
5✔
199
                                        $formatted_notices = self::format_notices_for_error( $notices );
5✔
200
                                        if ( ! empty( $formatted_notices ) ) {
5✔
201
                                                $error_message = $formatted_notices;
5✔
202
                                        }
203
                                }
204

205
                                // Clear notices to prevent them from persisting to next request.
206
                                wc_clear_notices();
5✔
207

208
                                // Throw error with enhanced message.
209
                                throw new UserError( $error_message );
5✔
210
                        }//end try
211
                };
161✔
212
        }
213

214
        /**
215
         * Format WC notices for GraphQL response.
216
         *
217
         * @param array $notices WC notices array.
218
         * @return array Formatted notices for GraphQL
219
         */
220
        private static function format_notices_for_response( $notices ) {
221
                $formatted_notices = [];
6✔
222

223
                // Include non-error notices (success, notice).
224
                foreach ( [ 'success', 'notice' ] as $type ) {
6✔
225
                        if ( ! empty( $notices[ $type ] ) ) {
6✔
226
                                foreach ( $notices[ $type ] as $notice ) {
4✔
227
                                        $formatted_notices[] = [
4✔
228
                                                'type'    => $type,
4✔
229
                                                'message' => $notice['notice'] ?? $notice,
4✔
230
                                        ];
4✔
231
                                }
232
                        }
233
                }
234

235
                return $formatted_notices;
6✔
236
        }
237

238
        /**
239
         * Format WC notices for error reporting
240
         *
241
         * @param array $notices WC notices array
242
         * @return string Formatted error message
243
         */
244
        private static function format_notices_for_error( $notices ) {
245
                $error_messages = [];
5✔
246

247
                // Prioritize error notices.
248
                if ( ! empty( $notices['error'] ) ) {
5✔
249
                        foreach ( $notices['error'] as $notice ) {
5✔
250
                                $error_messages[] = $notice['notice'] ?? $notice;
5✔
251
                        }
252
                }
253

254
                // Include other notice types if no errors.
255
                if ( empty( $error_messages ) ) {
5✔
NEW
256
                        foreach ( [ 'notice', 'success' ] as $type ) {
×
NEW
257
                                if ( ! empty( $notices[ $type ] ) ) {
×
NEW
258
                                        foreach ( $notices[ $type ] as $notice ) {
×
NEW
259
                                                $error_messages[] = $notice['notice'] ?? $notice;
×
260
                                        }
261
                                }
262
                        }
263
                }
264

265
                return implode( ' ', $error_messages );
5✔
266
        }
267
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc