diff --git a/composer.lock b/composer.lock index 70d06917..69228781 100644 --- a/composer.lock +++ b/composer.lock @@ -1129,20 +1129,20 @@ }, { "name": "symfony/polyfill-php73", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "ec444d3f3f6505bb28d11afa41e75faadebc10a1" + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/ec444d3f3f6505bb28d11afa41e75faadebc10a1", - "reference": "ec444d3f3f6505bb28d11afa41e75faadebc10a1", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { @@ -1185,7 +1185,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.31.0" }, "funding": [ { @@ -1201,7 +1201,7 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "szepeviktor/phpstan-wordpress", diff --git a/includes/class-woocommerce-filters.php b/includes/class-woocommerce-filters.php index 2a26c878..60ff7682 100644 --- a/includes/class-woocommerce-filters.php +++ b/includes/class-woocommerce-filters.php @@ -157,7 +157,14 @@ public static function add_session_header_to_allow_headers( array $allowed_heade * @return array */ public static function woographql_stripe_gateway_args( $gateway_args, $payment_method ) { - if ( 'stripe' === $payment_method ) { + /** @var false|\WC_Order|\WC_Order_Refund $order */ + $order = wc_get_order( $gateway_args[0] ); + if ( false === $order ) { + return $gateway_args; + } + + $stripe_source_id = $order->get_meta( '_stripe_source_id' ); + if ( 'stripe' === $payment_method && ! empty( $stripe_source_id ) ) { $gateway_args = [ $gateway_args[0], true, diff --git a/includes/data/mutation/class-checkout-mutation.php b/includes/data/mutation/class-checkout-mutation.php index 5acd01d4..cb98184f 100644 --- a/includes/data/mutation/class-checkout-mutation.php +++ b/includes/data/mutation/class-checkout-mutation.php @@ -493,6 +493,7 @@ protected static function validate_checkout( &$data ) { if ( WC()->cart->needs_payment() ) { $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); + \codecept_debug( $available_gateways ); if ( ! isset( $available_gateways[ $data['payment_method'] ] ) ) { throw new UserError( __( 'Invalid payment method.', 'wp-graphql-woocommerce' ) ); } else { diff --git a/includes/data/mutation/class-order-mutation.php b/includes/data/mutation/class-order-mutation.php index e7ed3574..525e62ac 100644 --- a/includes/data/mutation/class-order-mutation.php +++ b/includes/data/mutation/class-order-mutation.php @@ -10,6 +10,7 @@ use GraphQL\Error\UserError; + /** * Class - Order_Mutation */ @@ -86,8 +87,8 @@ public static function create_order( $input, $context, $info ) { /** * Action called before order is created. * - * @param array $input Input data describing order. - * @param \WPGraphQL\AppContext $context Request AppContext instance. + * @param array $input Input data describing order. + * @param \WPGraphQL\AppContext $context Request AppContext instance. * @param \GraphQL\Type\Definition\ResolveInfo $info Request ResolveInfo instance. */ do_action( 'graphql_woocommerce_before_order_create', $input, $context, $info ); @@ -118,78 +119,186 @@ public static function create_order( $input, $context, $info ) { * @param \WPGraphQL\AppContext $context AppContext instance. * @param \GraphQL\Type\Definition\ResolveInfo $info ResolveInfo instance. * + * @throws \Exception Failed to retrieve order. + * * @return void */ public static function add_items( $input, $order_id, $context, $info ) { + /** @var \WC_Order|false $order */ + $order = \WC_Order_Factory::get_order( $order_id ); + if ( false === $order ) { + throw new \Exception( __( 'Failed to retrieve order.', 'wp-graphql-woocommerce' ) ); + } + $item_group_keys = [ 'lineItems' => 'line_item', 'shippingLines' => 'shipping', 'feeLines' => 'fee', ]; - $item_groups = []; - foreach ( $input as $key => $items ) { + $order_items = []; + foreach ( $input as $key => $group_items ) { if ( array_key_exists( $key, $item_group_keys ) ) { - $type = $item_group_keys[ $key ]; + $type = $item_group_keys[ $key ]; + $order_items[ $type ] = []; /** * Action called before an item group is added to an order. * - * @param array $items Item data being added. - * @param integer $order_id ID of target order. - * @param \WPGraphQL\AppContext $context Request AppContext instance. - * @param \GraphQL\Type\Definition\ResolveInfo $info Request ResolveInfo instance. + * @param array $group_items Items data being added. + * @param \WC_Order $order Order object. + * @param \WPGraphQL\AppContext $context Request AppContext instance. + * @param \GraphQL\Type\Definition\ResolveInfo $info Request ResolveInfo instance. */ - do_action( "graphql_woocommerce_before_{$type}s_added_to_order", $items, $order_id, $context, $info ); - - foreach ( $items as $item_data ) { - // Create Order item. - $item_id = ( ! empty( $item_data['id'] ) && \WC_Order_Factory::get_order_item( $item_data['id'] ) ) - ? $item_data['id'] - : \wc_add_order_item( $order_id, [ 'order_item_type' => $type ] ); - - // Continue if order item creation failed. - if ( ! $item_id ) { - continue; + do_action( "graphql_woocommerce_before_{$type}s_added_to_order", $group_items, $order, $context, $info ); + + foreach ( $group_items as $item_data ) { + $item = self::set_item( + $item_data, + $type, + $order, + $context, + $info + ); + + /** + * Action called before an item group is added to an order. + * + * @param \WC_Order_Item $item Order item object. + * @param array $item_data Item data being added. + * @param \WC_Order $order Order object. + * @param \WPGraphQL\AppContext $context Request AppContext instance. + * @param \GraphQL\Type\Definition\ResolveInfo $info Request ResolveInfo instance. + */ + do_action( "graphql_woocommerce_before_{$type}_added_to_order", $item, $item_data, $order, $context, $info ); + + if ( 0 === $item->get_id() ) { + $order->add_item( $item ); + $order_items[ $type ][] = $item; + } else { + $item->save(); + $order_items[ $type ][] = $item; } - - // Add input item data to order item. - $item_keys = self::get_order_item_keys( $type ); - self::map_input_to_item( $item_id, $item_data, $item_keys, $context, $info ); } /** - * Action called after an item group is added to an order. + * Action called after an item group is added to an order, and before the order has been saved with the new items. * - * @param array $items Item data being added. - * @param integer $order_id ID of target order. - * @param \WPGraphQL\AppContext $context Request AppContext instance. - * @param \GraphQL\Type\Definition\ResolveInfo $info Request ResolveInfo instance. + * @param array $group_items Item data being added. + * @param \WC_Order $order Order object. + * @param \WPGraphQL\AppContext $context Request AppContext instance. + * @param \GraphQL\Type\Definition\ResolveInfo $info Request ResolveInfo instance. */ - do_action( "graphql_woocommerce_after_{$type}s_added_to_order", $items, $order_id, $context, $info ); + do_action( "graphql_woocommerce_after_{$type}s_added_to_order", $group_items, $order, $context, $info ); }//end if }//end foreach + + /** + * Action called after all items have been added and right before the new items have been saved. + * + * @param array> $order_items Order items. + * @param \WC_Order $order WC_Order instance. + * @param array $input Input data describing order. + * @param \WPGraphQL\AppContext $context Request AppContext instance. + * @param \GraphQL\Type\Definition\ResolveInfo $info Request ResolveInfo instance. + */ + do_action( 'graphql_woocommerce_before_new_order_items_save', $order_items, $order, $input, $context, $info ); + + $order->save(); } /** - * Return array of item mapped with the provided $item_keys and extracts $meta_data * - * @param integer $item_id Order item ID. - * @param array $input Item input data. - * @param array $item_keys Item key map. + * @param array $item_data Item data. + * @param string $type Item type. + * @param \WC_Order $order Order object. * @param \WPGraphQL\AppContext $context AppContext instance. * @param \GraphQL\Type\Definition\ResolveInfo $info ResolveInfo instance. * - * @throws \Exception Failed to retrieve order item | Failed to retrieve connected product. + * @return \WC_Order_Item + */ + public static function set_item( $item_data, $type, $order, $context, $info ) { + $item_id = ! empty( $item_data['id'] ) ? $item_data['id'] : 0; + $item_class = self::get_order_item_classname( $type, $item_id ); + + /** @var \WC_Order_Item $item */ + $item = new $item_class( $item_id ); + + /** + * Filter the order item object before it is created. + * + * @param \WC_Order_Item $item Order item object. + * @param array $item_data Item data. + * @param \WC_Order $order Order object. + * @param \WPGraphQL\AppContext $context AppContext instance. + * @param \GraphQL\Type\Definition\ResolveInfo $info ResolveInfo instance. + */ + $item = apply_filters( "graphql_create_order_{$type}_object", $item, $item_data, $order, $context, $info ); + + self::map_input_to_item( $item, $item_data, $type ); + + /** + * Action called after an order item is created. + * + * @param \WC_Order_Item $item Order item object. + * @param array $item_data Item data. + * @param \WC_Order $order Order object. + * @param \WPGraphQL\AppContext $context AppContext instance. + * @param \GraphQL\Type\Definition\ResolveInfo $info ResolveInfo instance. + */ + do_action( "graphql_create_order_{$type}", $item, $item_data, $order, $context, $info ); + + return $item; + } + + /** + * Get order item class name. * - * @return int + * @param string $type Order item type. + * @param int $id Order item ID. + * + * @return string */ - protected static function map_input_to_item( $item_id, $input, $item_keys, $context, $info ) { - $order_item = \WC_Order_Factory::get_order_item( $item_id ); - if ( ! is_object( $order_item ) ) { - throw new \Exception( __( 'Failed to retrieve order item.', 'wp-graphql-woocommerce' ) ); + public static function get_order_item_classname( $type, $id = 0 ) { + $classname = false; + switch ( $type ) { + case 'line_item': + case 'product': + $classname = 'WC_Order_Item_Product'; + break; + case 'coupon': + $classname = 'WC_Order_Item_Coupon'; + break; + case 'fee': + $classname = 'WC_Order_Item_Fee'; + break; + case 'shipping': + $classname = 'WC_Order_Item_Shipping'; + break; + case 'tax': + $classname = 'WC_Order_Item_Tax'; + break; } + $classname = apply_filters( 'woocommerce_get_order_item_classname', $classname, $type, $id ); // phpcs:ignore WordPress.NamingConventions + + return $classname; + } + + /** + * Return array of item mapped with the provided $item_keys and extracts $meta_data + * + * @param \WC_Order_Item &$item Order item. + * @param array $input Item input data. + * @param string $type Item type. + * + * @throws \Exception Failed to retrieve connected product. + * + * @return void + */ + protected static function map_input_to_item( &$item, $input, $type ) { + $item_keys = self::get_order_item_keys( $type ); + $args = []; $meta_data = null; foreach ( $input as $key => $value ) { @@ -203,10 +312,9 @@ protected static function map_input_to_item( $item_id, $input, $item_keys, $cont } // Calculate to subtotal/total for line items. - if ( isset( $args['quantity'] ) ) { - $product = ( ! empty( $order_item['product_id'] ) ) - ? wc_get_product( $order_item['product_id'] ) + $product = ( ! empty( $item['product_id'] ) ) + ? wc_get_product( $item['product_id'] ) : wc_get_product( self::get_product_id( $args ) ); if ( ! is_object( $product ) ) { throw new \Exception( __( 'Failed to retrieve product connected to order item.', 'wp-graphql-woocommerce' ) ); @@ -219,18 +327,24 @@ protected static function map_input_to_item( $item_id, $input, $item_keys, $cont // Set item props. foreach ( $args as $key => $value ) { - if ( is_callable( [ $order_item, "set_{$key}" ] ) ) { - $order_item->{"set_{$key}"}( $value ); + if ( is_callable( [ $item, "set_{$key}" ] ) ) { + $item->{"set_{$key}"}( $value ); } } // Update item meta data if any is found. - if ( 0 !== $item_id && ! empty( $meta_data ) ) { - // Update item meta data. - self::update_item_meta_data( $item_id, $meta_data, $context, $info ); + if ( empty( $meta_data ) ) { + return; } - return $order_item->save(); + foreach ( $meta_data as $entry ) { + $exists = $item->get_meta( $entry['key'], true, 'edit' ); + if ( '' !== $exists && $exists !== $entry['value'] ) { + $item->update_meta_data( $entry['key'], $entry['value'] ); + } else { + $item->add_meta_data( $entry['key'], $entry['value'] ); + } + } } /** @@ -285,10 +399,10 @@ protected static function get_order_item_keys( $type ) { protected static function get_product_id( $data ) { if ( ! empty( $data['sku'] ) ) { $product_id = (int) wc_get_product_id_by_sku( $data['sku'] ); - } elseif ( ! empty( $data['product_id'] ) && empty( $data['variation_id'] ) ) { - $product_id = (int) $data['product_id']; } elseif ( ! empty( $data['variation_id'] ) ) { $product_id = (int) $data['variation_id']; + } elseif ( ! empty( $data['product_id'] ) ) { + $product_id = (int) $data['product_id']; } else { throw new UserError( __( 'Product ID or SKU is required.', 'wp-graphql-woocommerce' ) ); } diff --git a/includes/model/class-order.php b/includes/model/class-order.php index ae0279b4..3b95a786 100644 --- a/includes/model/class-order.php +++ b/includes/model/class-order.php @@ -497,7 +497,7 @@ protected function order_fields() { return ! empty( $this->data->get_date_paid() ) ? $this->data->get_date_paid() : null; }, 'subtotal' => function () { - return ! empty( $this->data->get_subtotal() ) + return ! is_null( $this->data->get_subtotal() ) ? wc_graphql_price( $this->data->get_subtotal(), [ 'currency' => $this->data->get_currency() ] ) : null; }, diff --git a/tests/_support/Factory/OrderFactory.php b/tests/_support/Factory/OrderFactory.php index fe0de924..f9901cad 100644 --- a/tests/_support/Factory/OrderFactory.php +++ b/tests/_support/Factory/OrderFactory.php @@ -143,10 +143,14 @@ public function createNew( $args = [], $items = [] ) { public function add_line_item( $order, $args = [], $save = true ) { $order = $save ? \wc_get_order( $order ) : $order; - if ( empty( $args['product'] ) ) { - $product = \wc_get_product( $this->factory->product->createSimple() ); - } else { + if ( ! empty( $args['variation_id'] ) ) { + $product = \wc_get_product( $args['variation_id'] ); + } elseif ( ! empty( $args['product_id'] ) ) { + $product = \wc_get_product( $args['product_id'] ); + } elseif ( ! empty( $args['product'] ) ) { $product = \wc_get_product( $args['product'] ); + } else { + $product = \wc_get_product( $this->factory->product->createSimple() ); } if ( empty( $args['qty'] ) ) { diff --git a/tests/wpunit/IntrospectionQueryTest.php b/tests/wpunit/IntrospectionQueryTest.php index 2d4c053a..34ec34d7 100644 --- a/tests/wpunit/IntrospectionQueryTest.php +++ b/tests/wpunit/IntrospectionQueryTest.php @@ -1,6 +1,6 @@ schema->assertValid(); + new \WPGraphQL\Request(); + + $schema = WPGraphQL::get_schema(); + $schema->assertValid(); // Assert true upon success. - $this->assertTrue( true ); + $this->assertTrue( true, 'Schema is valid.' ); } catch ( \GraphQL\Error\InvariantViolation $e ) { // use --debug flag to view. - codecept_debug( $e->getMessage() ); + $this->logData( $e->getMessage() ); // Fail upon throwing - $this->assertTrue( false ); + $this->assertTrue( false, $e->getMessage() ); } }