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

wp-graphql / wp-graphql-woocommerce / 27452430870

13 Jun 2026 01:26AM UTC coverage: 91.8%. Remained the same
27452430870

Pull #1019

github

web-flow
Merge f03617ca3 into 2ce9424e1
Pull Request #1019: fix: address WordPress.org plugin review (rename + prefixing + headers)

1330 of 1587 new or added lines in 201 files covered. (83.81%)

1 existing line in 1 file now uncovered.

18528 of 20183 relevant lines covered (91.8%)

152.68 hits per line

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

91.75
/includes/data/mutation/class-order-mutation.php
1
<?php
2
/**
3
 * Defines helper functions for executing mutations related to the orders.
4
 *
5
 * @package WPGraphQL\WooCommerce\Data\Mutation
6
 * @since 0.2.0
7
 */
8

9
namespace WPGraphQL\WooCommerce\Data\Mutation;
10

11
use GraphQL\Error\UserError;
12
use WPGraphQL\Utils\Utils;
13

14

15
/**
16
 * Class - Order_Mutation
17
 */
18
class Order_Mutation {
19
        /**
20
         * Filterable authentication function.
21
         *
22
         * @param array                                $input     Input data describing order.
23
         * @param \WPGraphQL\AppContext                $context   AppContext instance.
24
         * @param \GraphQL\Type\Definition\ResolveInfo $info      ResolveInfo instance.
25
         * @param string                               $mutation  Mutation being executed.
26
         * @param integer|null|false                   $order_id  Order ID.
27
         * @throws \GraphQL\Error\UserError  Error locating order.
28
         *
29
         * @return boolean
30
         */
31
        public static function authorized( $input, $context, $info, $mutation = 'create', $order_id = null ) {
32
                /**
33
                 * Get order post type.
34
                 *
35
                 * @var \WP_Post_Type $post_type_object
36
                 */
37
                $post_type_object = get_post_type_object( 'shop_order' );
11✔
38

39
                if ( ! $order_id ) {
11✔
40
                        return apply_filters(
7✔
41
                                "graphql_woocommerce_authorized_to_{$mutation}_orders",
7✔
42
                                current_user_can( $post_type_object->cap->edit_posts ),
7✔
43
                                $order_id,
7✔
44
                                $input,
7✔
45
                                $context,
7✔
46
                                $info
7✔
47
                        );
7✔
48
                }
49

50
                /** @var false|\WC_Order $order */
51
                $order = \wc_get_order( $order_id );
7✔
52
                if ( false === $order ) {
7✔
53
                        throw new UserError(
1✔
54
                                sprintf(
1✔
55
                                        /* translators: %d: Order ID */
56
                                        __( 'Failed to find order with ID of %d.', 'graphql-for-ecommerce' ),
1✔
57
                                        $order_id
1✔
58
                                )
1✔
59
                        );
1✔
60
                }
61

62
                $post_type = get_post_type( $order_id );
7✔
63
                if ( false === $post_type ) {
7✔
NEW
64
                        throw new UserError( __( 'Failed to identify the post type of the order.', 'graphql-for-ecommerce' ) );
×
65
                }
66

67
                // Return true if user is owner or admin.
68
                $is_owner = 0 !== get_current_user_id() && $order->get_customer_id() === get_current_user_id();
7✔
69
                $is_admin = \wc_rest_check_post_permissions( $post_type, 'edit', $order_id );
7✔
70
                return $is_owner || $is_admin;
7✔
71
        }
72

73
        /**
74
         * Create an order.
75
         *
76
         * @param array                                $input    Input data describing order.
77
         * @param \WPGraphQL\AppContext                $context  AppContext instance.
78
         * @param \GraphQL\Type\Definition\ResolveInfo $info     ResolveInfo instance.
79
         *
80
         * @return integer
81
         *
82
         * @throws \GraphQL\Error\UserError  Error creating order.
83
         */
84
        public static function create_order( $input, $context, $info ) {
85
                $order = new \WC_Order();
7✔
86

87
                $order->set_currency( ! empty( $input['currency'] ) ? $input['currency'] : get_woocommerce_currency() );
7✔
88
                $order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) );
7✔
89
                $order->set_customer_ip_address( \WC_Geolocation::get_ip_address() );
7✔
90
                $order->set_customer_user_agent( wc_get_user_agent() );
7✔
91

92
                $order_id = $order->save();
7✔
93

94
                $order_keys = [
7✔
95
                        'status'       => 'status',
7✔
96
                        'customerId'   => 'customer_id',
7✔
97
                        'customerNote' => 'customer_note',
7✔
98
                        'parent'       => 'parent',
7✔
99
                        'createdVia'   => 'created_via',
7✔
100
                ];
7✔
101

102
                $args = [ 'order_id' => $order_id ];
7✔
103
                foreach ( $input as $key => $value ) {
7✔
104
                        if ( array_key_exists( $key, $order_keys ) ) {
7✔
105
                                $args[ $order_keys[ $key ] ] = $value;
6✔
106
                        }
107
                }
108

109
                /**
110
                 * Action called before order is created.
111
                 *
112
                 * @param array                                $input   Input data describing order.
113
                 * @param \WPGraphQL\AppContext                $context Request AppContext instance.
114
                 * @param \GraphQL\Type\Definition\ResolveInfo $info    Request ResolveInfo instance.
115
                 */
116
                do_action( 'graphql_woocommerce_before_order_create', $input, $context, $info );
7✔
117

118
                $order = \wc_create_order( $args );
7✔
119
                if ( is_wp_error( $order ) ) {
7✔
120
                        throw new UserError( $order->get_error_code() . $order->get_error_message() );
×
121
                }
122

123
                /**
124
                 * Action called after order is created.
125
                 *
126
                 * @param \WC_Order    $order   WC_Order instance.
127
                 * @param array       $input   Input data describing order.
128
                 * @param \WPGraphQL\AppContext  $context Request AppContext instance.
129
                 * @param \GraphQL\Type\Definition\ResolveInfo $info    Request ResolveInfo instance.
130
                 */
131
                do_action( 'graphql_woocommerce_after_order_create', $order, $input, $context, $info );
7✔
132

133
                return $order->get_id();
7✔
134
        }
135

136
        /**
137
         *
138
         * @param array<string, mixed>                 $item_data  Item data.
139
         * @param string                               $type       Item type.
140
         * @param \WC_Order                            $order      Order object.
141
         * @param \WPGraphQL\AppContext                $context    AppContext instance.
142
         * @param \GraphQL\Type\Definition\ResolveInfo $info       ResolveInfo instance.
143
         *
144
         * @return \WC_Order_Item
145
         */
146
        public static function set_item( $item_data, $type, $order, $context, $info ) {
147
                $item_id    = ! empty( $item_data['id'] ) ? $item_data['id'] : 0;
7✔
148
                $item_class = self::get_order_item_classname( $type, $item_id );
7✔
149

150
                /** @var \WC_Order_Item $item */
151
                $item = new $item_class( $item_id );
7✔
152

153
                /**
154
                 * Filter the order item object before it is created.
155
                 *
156
                 * @param \WC_Order_Item                       $item       Order item object.
157
                 * @param array                                $item_data  Item data.
158
                 * @param \WC_Order                            $order      Order object.
159
                 * @param \WPGraphQL\AppContext                $context    AppContext instance.
160
                 * @param \GraphQL\Type\Definition\ResolveInfo $info       ResolveInfo instance.
161
                 */
162
                $item = apply_filters( "graphql_create_order_{$type}_object", $item, $item_data, $order, $context, $info );
7✔
163

164
                self::map_input_to_item( $item, $item_data, $type );
7✔
165

166
                /**
167
                 * Action called after an order item is created.
168
                 *
169
                 * @param \WC_Order_Item                       $item       Order item object.
170
                 * @param array                                $item_data  Item data.
171
                 * @param \WC_Order                            $order      Order object.
172
                 * @param \WPGraphQL\AppContext                $context    AppContext instance.
173
                 * @param \GraphQL\Type\Definition\ResolveInfo $info       ResolveInfo instance.
174
                 */
175
                do_action( "graphql_create_order_{$type}", $item, $item_data, $order, $context, $info );
7✔
176

177
                return $item;
7✔
178
        }
179

180
        /**
181
         * Get order item class name.
182
         *
183
         * @param string $type Order item type.
184
         * @param int    $id  Order item ID.
185
         *
186
         * @return string
187
         */
188
        public static function get_order_item_classname( $type, $id = 0 ) {
189
                $classname = false;
7✔
190
                switch ( $type ) {
191
                        case 'line_item':
7✔
192
                        case 'product':
4✔
193
                                $classname = 'WC_Order_Item_Product';
7✔
194
                                break;
7✔
195
                        case 'coupon':
4✔
196
                                $classname = 'WC_Order_Item_Coupon';
×
197
                                break;
×
198
                        case 'fee':
4✔
199
                                $classname = 'WC_Order_Item_Fee';
4✔
200
                                break;
4✔
201
                        case 'shipping':
4✔
202
                                $classname = 'WC_Order_Item_Shipping';
4✔
203
                                break;
4✔
204
                        case 'tax':
×
205
                                $classname = 'WC_Order_Item_Tax';
×
206
                                break;
×
207
                }
208

209
                $classname = apply_filters( 'woocommerce_get_order_item_classname', $classname, $type, $id ); // phpcs:ignore WordPress.NamingConventions
7✔
210

211
                return $classname;
7✔
212
        }
213

214
        /**
215
         * Return array of item mapped with the provided $item_keys and extracts $meta_data
216
         *
217
         * @param \WC_Order_Item &$item      Order item.
218
         * @param array          $input      Item input data.
219
         * @param string         $type       Item type.
220
         *
221
         * @throws \Exception Failed to retrieve connected product.
222
         *
223
         * @return void
224
         */
225
        protected static function map_input_to_item( &$item, $input, $type ) {
226
                $item_keys = self::get_order_item_keys( $type );
7✔
227

228
                $args      = [];
7✔
229
                $meta_data = null;
7✔
230
                foreach ( $input as $key => $value ) {
7✔
231
                        if ( array_key_exists( $key, $item_keys ) ) {
7✔
232
                                $args[ $item_keys[ $key ] ] = $value;
7✔
233
                        } elseif ( 'metaData' === $key ) {
6✔
234
                                $meta_data = $value;
4✔
235
                        } else {
236
                                $args[ $key ] = $value;
6✔
237
                        }
238
                }
239

240
                // Auto-fill line item name, subtotal, and total from the product when not provided.
241
                $has_product_id   = ! empty( $args['product_id'] ) || ! empty( $args['variation_id'] );
7✔
242
                $missing_defaults = ! isset( $args['subtotal'] ) || ! isset( $args['total'] ) || ! isset( $args['name'] );
7✔
243
                if ( 'line_item' === $type && $has_product_id && $missing_defaults ) {
7✔
244
                        $product_id = self::get_product_id( $args );
7✔
245
                        $product    = ! empty( $product_id ) ? wc_get_product( $product_id ) : null;
7✔
246
                        if ( ! is_object( $product ) ) {
7✔
NEW
247
                                throw new \Exception( __( 'Failed to retrieve product connected to order item.', 'graphql-for-ecommerce' ) );
×
248
                        }
249

250
                        $total            = wc_get_price_excluding_tax( $product, [ 'qty' => $args['quantity'] ?? 1 ] );
7✔
251
                        $args['subtotal'] = $args['subtotal'] ?? $total;
7✔
252
                        $args['total']    = $args['total'] ?? $total;
7✔
253
                        $args['name']     = $args['name'] ?? $product->get_name();
7✔
254
                }
255

256
                // Set item props.
257
                foreach ( $args as $key => $value ) {
7✔
258
                        if ( is_callable( [ $item, "set_{$key}" ] ) ) {
7✔
259
                                $item->{"set_{$key}"}( $value );
7✔
260
                        }
261
                }
262

263
                // Update item meta data if any is found.
264
                if ( empty( $meta_data ) ) {
7✔
265
                        return;
7✔
266
                }
267

268
                foreach ( $meta_data as $entry ) {
4✔
269
                        $exists = $item->get_meta( $entry['key'], true, 'edit' );
4✔
270
                        if ( '' !== $exists && $exists !== $entry['value'] ) {
4✔
271
                                $item->update_meta_data( $entry['key'], $entry['value'] );
1✔
272
                        } else {
273
                                $item->add_meta_data( $entry['key'], $entry['value'] );
4✔
274
                        }
275
                }
276
        }
277

278
        /**
279
         * Returns array of item keys by item type.
280
         *
281
         * @param string $type  Order item type.
282
         *
283
         * @return array
284
         */
285
        protected static function get_order_item_keys( $type ) {
286
                switch ( $type ) {
287
                        case 'line_item':
7✔
288
                                return [
7✔
289
                                        'productId'   => 'product_id',
7✔
290
                                        'variationId' => 'variation_id',
7✔
291
                                        'taxClass'    => 'tax_class',
7✔
292
                                ];
7✔
293

294
                        case 'shipping':
4✔
295
                                return [
4✔
296
                                        'name'        => 'order_item_name',
4✔
297
                                        'methodTitle' => 'method_title',
4✔
298
                                        'methodId'    => 'method_id',
4✔
299
                                        'instanceId'  => 'instance_id',
4✔
300
                                ];
4✔
301

302
                        case 'fee':
4✔
303
                                return [
4✔
304
                                        'name'      => 'name',
4✔
305
                                        'taxClass'  => 'tax_class',
4✔
306
                                        'taxStatus' => 'tax_status',
4✔
307
                                ];
4✔
308
                        default:
309
                                /**
310
                                 * Allow filtering of order item keys for unknown item types.
311
                                 *
312
                                 * @param array  $item_keys  Order item keys.
313
                                 * @param string $type       Order item type slug.
314
                                 */
315
                                return apply_filters( 'woographql_get_order_item_keys', [], $type );
×
316
                }//end switch
317
        }
318

319
        /**
320
         * Gets the product ID from the SKU or line item data ID.
321
         *
322
         * @param array $data  Line item data.
323
         *
324
         * @return integer
325
         * @throws \GraphQL\Error\UserError When SKU or ID is not valid.
326
         */
327
        protected static function get_product_id( $data ) {
328
                if ( ! empty( $data['sku'] ) ) {
7✔
329
                        $product_id = (int) wc_get_product_id_by_sku( $data['sku'] );
×
330
                } elseif ( ! empty( $data['variation_id'] ) ) {
7✔
331
                        $product_id = (int) $data['variation_id'];
4✔
332
                } elseif ( ! empty( $data['product_id'] ) ) {
7✔
333
                        $product_id = (int) $data['product_id'];
7✔
334
                } else {
NEW
335
                        throw new UserError( __( 'Product ID or SKU is required.', 'graphql-for-ecommerce' ) );
×
336
                }
337

338
                return $product_id;
7✔
339
        }
340

341
        /**
342
         * Sets all order props, address fields, items, and meta on the provided order object
343
         * and saves once, mirroring the WC REST API pattern to avoid HPOS data loss from
344
         * multiple save() calls across different object instances.
345
         *
346
         * @param \WC_Order                            $order   WC_Order instance.
347
         * @param array                                $input   Input data describing order.
348
         * @param \WPGraphQL\AppContext                $context AppContext instance.
349
         * @param \GraphQL\Type\Definition\ResolveInfo $info    ResolveInfo instance.
350
         *
351
         * @return void
352
         */
353
        public static function prepare_order( $order, $input, $context, $info ) {
354

355
                foreach ( $input as $key => $value ) {
8✔
356
                        switch ( $key ) {
357
                                case 'clientMutationId':
8✔
358
                                case 'id':
8✔
359
                                case 'orderId':
8✔
360
                                case 'coupons':
8✔
361
                                case 'status':
8✔
362
                                case 'isPaid':
8✔
363
                                        break;
7✔
364
                                case 'billing':
8✔
365
                                case 'shipping':
8✔
366
                                        $formatted_address = Customer_Mutation::address_input_mapping( $value, $key );
4✔
367
                                        foreach ( $formatted_address as $field => $field_value ) {
4✔
368
                                                if ( is_callable( [ $order, "set_{$key}_{$field}" ] ) ) {
4✔
369
                                                        $order->{"set_{$key}_{$field}"}( $field_value );
4✔
370
                                                }
371
                                        }
372
                                        break;
4✔
373
                                case 'lineItems':
8✔
374
                                case 'shippingLines':
8✔
375
                                case 'feeLines':
8✔
376
                                        $item_group_keys = [
7✔
377
                                                'lineItems'     => 'line_item',
7✔
378
                                                'shippingLines' => 'shipping',
7✔
379
                                                'feeLines'      => 'fee',
7✔
380
                                        ];
7✔
381
                                        $type            = $item_group_keys[ $key ];
7✔
382

383
                                        /**
384
                                         * Action called before an item group is added to an order.
385
                                         *
386
                                         * @param array                                $value    Items data being added.
387
                                         * @param \WC_Order                            $order    Order object.
388
                                         * @param \WPGraphQL\AppContext                $context  Request AppContext instance.
389
                                         * @param \GraphQL\Type\Definition\ResolveInfo $info     Request ResolveInfo instance.
390
                                         */
391
                                        do_action( "graphql_woocommerce_before_{$type}s_added_to_order", $value, $order, $context, $info );
7✔
392

393
                                        foreach ( $value as $item_data ) {
7✔
394
                                                $item = self::set_item( $item_data, $type, $order, $context, $info );
7✔
395

396
                                                /**
397
                                                 * Action called before an item is added to an order.
398
                                                 *
399
                                                 * @param \WC_Order_Item                       $item      Order item object.
400
                                                 * @param array                                $item_data Item data being added.
401
                                                 * @param \WC_Order                            $order     Order object.
402
                                                 * @param \WPGraphQL\AppContext                $context   Request AppContext instance.
403
                                                 * @param \GraphQL\Type\Definition\ResolveInfo $info      Request ResolveInfo instance.
404
                                                 */
405
                                                do_action( "graphql_woocommerce_before_{$type}_added_to_order", $item, $item_data, $order, $context, $info );
7✔
406

407
                                                if ( 0 === $item->get_id() ) {
7✔
408
                                                        $order->add_item( $item );
7✔
409
                                                } else {
410
                                                        $item->save();
1✔
411
                                                }
412
                                        }
413

414
                                        /**
415
                                         * Action called after an item group is added to an order.
416
                                         *
417
                                         * @param array                                $value    Item data being added.
418
                                         * @param \WC_Order                            $order    Order object.
419
                                         * @param \WPGraphQL\AppContext                $context  Request AppContext instance.
420
                                         * @param \GraphQL\Type\Definition\ResolveInfo $info     Request ResolveInfo instance.
421
                                         */
422
                                        do_action( "graphql_woocommerce_after_{$type}s_added_to_order", $value, $order, $context, $info );
7✔
423
                                        break;
7✔
424
                                case 'metaData':
8✔
425
                                        if ( is_array( $value ) ) {
5✔
426
                                                foreach ( $value as $meta ) {
5✔
427
                                                        $order->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
5✔
428
                                                }
429
                                        }
430
                                        break;
5✔
431
                                default:
432
                                        $prop = \wc_graphql_camel_case_to_underscore( $key );
7✔
433
                                        if ( is_callable( [ $order, "set_{$prop}" ] ) ) {
7✔
434
                                                $order->{"set_{$prop}"}( $value );
7✔
435
                                        }
436
                                        break;
7✔
437
                        }//end switch
438
                }//end foreach
439

440
                /**
441
                 * Action called before changes to order meta are saved.
442
                 *
443
                 * @param \WC_Order                            $order   WC_Order instance.
444
                 * @param array                                $input   Order props array.
445
                 * @param \WPGraphQL\AppContext                $context Request AppContext instance.
446
                 * @param \GraphQL\Type\Definition\ResolveInfo $info    Request ResolveInfo instance.
447
                 */
448
                do_action( 'graphql_woocommerce_before_order_meta_save', $order, $input, $context, $info );
8✔
449

450
                $order->save_meta_data();
8✔
451
                $order->save();
8✔
452
        }
453

454
        /**
455
         * Applies coupons to WC_Order instance.
456
         *
457
         * @param \WC_Order $order   WC_Order instance.
458
         * @param array     $coupons Coupon codes to be applied to order.
459
         *
460
         * @return void
461
         */
462
        public static function apply_coupons( $order, $coupons ) {
463
                // Remove all coupons first to ensure calculation is correct.
464
                foreach ( $order->get_items( 'coupon' ) as $coupon ) {
4✔
465
                        /**
466
                         * Order item coupon.
467
                         *
468
                         * @var \WC_Order_Item_Coupon $coupon
469
                         */
470

471
                        $order->remove_coupon( $coupon->get_code() );
1✔
472
                }
473

474
                foreach ( $coupons as $code ) {
4✔
475
                        $results = $order->apply_coupon( sanitize_text_field( $code ) );
4✔
476
                        if ( is_wp_error( $results ) ) {
4✔
477
                                do_action( 'graphql_woocommerce_' . $results->get_error_code(), $results, $code, $coupons, $order );
×
478
                        }
479
                }
480

481
                $order->save();
4✔
482
        }
483

484
        /**
485
         * Validates order customer
486
         *
487
         * @param string $customer_id  ID of customer for order.
488
         *
489
         * @return bool
490
         */
491
        public static function validate_customer( $customer_id ) {
492
                $id = Utils::get_database_id_from_id( $customer_id );
4✔
493
                if ( ! $id ) {
4✔
494
                        return false;
×
495
                }
496

497
                if ( false === get_user_by( 'id', $id ) ) {
4✔
498
                        return false;
×
499
                }
500

501
                // Make sure customer is part of blog.
502
                if ( is_multisite() && ! is_user_member_of_blog( $id ) ) {
4✔
503
                        add_user_to_blog( get_current_blog_id(), $id, 'customer' );
×
504
                }
505

506
                return true;
4✔
507
        }
508

509
        /**
510
         * Purge object when creating.
511
         *
512
         * @param null|\WC_Order|\WPGraphQL\WooCommerce\Model\Order $order         Object data.
513
         * @param boolean                                           $force_delete  Delete or put in trash.
514
         *
515
         * @return bool
516
         * @throws \GraphQL\Error\UserError  Failed to delete order.
517
         */
518
        public static function purge( $order, $force_delete = true ) {
519
                if ( ! empty( $order ) ) {
1✔
520
                        return $order->delete( $force_delete );
1✔
521
                }
522

523
                return false;
×
524
        }
525
}
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