cap->edit_posts ), $order_id, $input, $context, $info ); } /** @var false|\WC_Order $order */ $order = \wc_get_order( $order_id ); if ( false === $order ) { throw new UserError( sprintf( /* translators: %d: Order ID */ __( 'Failed to find order with ID of %d.', 'graphql-for-ecommerce' ), $order_id ) ); } $post_type = get_post_type( $order_id ); if ( false === $post_type ) { throw new UserError( __( 'Failed to identify the post type of the order.', 'graphql-for-ecommerce' ) ); } // Return true if user is owner or admin. $is_owner = 0 !== get_current_user_id() && $order->get_customer_id() === get_current_user_id(); $is_admin = \wc_rest_check_post_permissions( $post_type, 'edit', $order_id ); return $is_owner || $is_admin; } /** * Create an order. * * @param array $input Input data describing order. * @param \WPGraphQL\AppContext $context AppContext instance. * @param \GraphQL\Type\Definition\ResolveInfo $info ResolveInfo instance. * * @return integer * * @throws \GraphQL\Error\UserError Error creating order. */ public static function create_order( $input, $context, $info ) { $order = new \WC_Order(); $order->set_currency( ! empty( $input['currency'] ) ? $input['currency'] : get_woocommerce_currency() ); $order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ); $order->set_customer_ip_address( \WC_Geolocation::get_ip_address() ); $order->set_customer_user_agent( wc_get_user_agent() ); $order_id = $order->save(); $order_keys = [ 'status' => 'status', 'customerId' => 'customer_id', 'customerNote' => 'customer_note', 'parent' => 'parent', 'createdVia' => 'created_via', ]; $args = [ 'order_id' => $order_id ]; foreach ( $input as $key => $value ) { if ( array_key_exists( $key, $order_keys ) ) { $args[ $order_keys[ $key ] ] = $value; } } /** * Action called before order is created. * * @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 ); $order = \wc_create_order( $args ); if ( is_wp_error( $order ) ) { throw new UserError( $order->get_error_code() . $order->get_error_message() ); } /** * Action called after order is created. * * @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_after_order_create', $order, $input, $context, $info ); return $order->get_id(); } /** * * @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. * * @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. * * @param string $type Order item type. * @param int $id Order item ID. * * @return string */ 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 ) { if ( array_key_exists( $key, $item_keys ) ) { $args[ $item_keys[ $key ] ] = $value; } elseif ( 'metaData' === $key ) { $meta_data = $value; } else { $args[ $key ] = $value; } } // Auto-fill line item name, subtotal, and total from the product when not provided. $has_product_id = ! empty( $args['product_id'] ) || ! empty( $args['variation_id'] ); $missing_defaults = ! isset( $args['subtotal'] ) || ! isset( $args['total'] ) || ! isset( $args['name'] ); if ( 'line_item' === $type && $has_product_id && $missing_defaults ) { $product_id = self::get_product_id( $args ); $product = ! empty( $product_id ) ? wc_get_product( $product_id ) : null; if ( ! is_object( $product ) ) { throw new \Exception( __( 'Failed to retrieve product connected to order item.', 'graphql-for-ecommerce' ) ); } $total = wc_get_price_excluding_tax( $product, [ 'qty' => $args['quantity'] ?? 1 ] ); $args['subtotal'] = $args['subtotal'] ?? $total; $args['total'] = $args['total'] ?? $total; $args['name'] = $args['name'] ?? $product->get_name(); } // Set item props. foreach ( $args as $key => $value ) { if ( is_callable( [ $item, "set_{$key}" ] ) ) { $item->{"set_{$key}"}( $value ); } } // Update item meta data if any is found. if ( empty( $meta_data ) ) { return; } 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'] ); } } } /** * Returns array of item keys by item type. * * @param string $type Order item type. * * @return array */ protected static function get_order_item_keys( $type ) { switch ( $type ) { case 'line_item': return [ 'productId' => 'product_id', 'variationId' => 'variation_id', 'taxClass' => 'tax_class', ]; case 'shipping': return [ 'name' => 'order_item_name', 'methodTitle' => 'method_title', 'methodId' => 'method_id', 'instanceId' => 'instance_id', ]; case 'fee': return [ 'name' => 'name', 'taxClass' => 'tax_class', 'taxStatus' => 'tax_status', ]; default: /** * Allow filtering of order item keys for unknown item types. * * @param array $item_keys Order item keys. * @param string $type Order item type slug. */ return apply_filters( 'woographql_get_order_item_keys', [], $type ); }//end switch } /** * Gets the product ID from the SKU or line item data ID. * * @param array $data Line item data. * * @return integer * @throws \GraphQL\Error\UserError When SKU or ID is not valid. */ 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['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.', 'graphql-for-ecommerce' ) ); } return $product_id; } /** * Sets all order props, address fields, items, and meta on the provided order object * and saves once, mirroring the WC REST API pattern to avoid HPOS data loss from * multiple save() calls across different object instances. * * @param \WC_Order $order WC_Order instance. * @param array $input Input data describing order. * @param \WPGraphQL\AppContext $context AppContext instance. * @param \GraphQL\Type\Definition\ResolveInfo $info ResolveInfo instance. * * @return void */ public static function prepare_order( $order, $input, $context, $info ) { foreach ( $input as $key => $value ) { switch ( $key ) { case 'clientMutationId': case 'id': case 'orderId': case 'coupons': case 'status': case 'isPaid': break; case 'billing': case 'shipping': $formatted_address = Customer_Mutation::address_input_mapping( $value, $key ); foreach ( $formatted_address as $field => $field_value ) { if ( is_callable( [ $order, "set_{$key}_{$field}" ] ) ) { $order->{"set_{$key}_{$field}"}( $field_value ); } } break; case 'lineItems': case 'shippingLines': case 'feeLines': $item_group_keys = [ 'lineItems' => 'line_item', 'shippingLines' => 'shipping', 'feeLines' => 'fee', ]; $type = $item_group_keys[ $key ]; /** * Action called before an item group is added to an order. * * @param array $value 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", $value, $order, $context, $info ); foreach ( $value as $item_data ) { $item = self::set_item( $item_data, $type, $order, $context, $info ); /** * Action called before an item 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 ); } else { $item->save(); } } /** * Action called after an item group is added to an order. * * @param array $value 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", $value, $order, $context, $info ); break; case 'metaData': if ( is_array( $value ) ) { foreach ( $value as $meta ) { $order->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); } } break; default: $prop = \wc_graphql_camel_case_to_underscore( $key ); if ( is_callable( [ $order, "set_{$prop}" ] ) ) { $order->{"set_{$prop}"}( $value ); } break; }//end switch }//end foreach /** * Action called before changes to order meta are saved. * * @param \WC_Order $order WC_Order instance. * @param array $input Order props array. * @param \WPGraphQL\AppContext $context Request AppContext instance. * @param \GraphQL\Type\Definition\ResolveInfo $info Request ResolveInfo instance. */ do_action( 'graphql_woocommerce_before_order_meta_save', $order, $input, $context, $info ); $order->save_meta_data(); $order->save(); } /** * Applies coupons to WC_Order instance. * * @param \WC_Order $order WC_Order instance. * @param array $coupons Coupon codes to be applied to order. * * @return void */ public static function apply_coupons( $order, $coupons ) { // Remove all coupons first to ensure calculation is correct. foreach ( $order->get_items( 'coupon' ) as $coupon ) { /** * Order item coupon. * * @var \WC_Order_Item_Coupon $coupon */ $order->remove_coupon( $coupon->get_code() ); } foreach ( $coupons as $code ) { $results = $order->apply_coupon( sanitize_text_field( $code ) ); if ( is_wp_error( $results ) ) { do_action( 'graphql_woocommerce_' . $results->get_error_code(), $results, $code, $coupons, $order ); } } $order->save(); } /** * Validates order customer * * @param string $customer_id ID of customer for order. * * @return bool */ public static function validate_customer( $customer_id ) { $id = Utils::get_database_id_from_id( $customer_id ); if ( ! $id ) { return false; } if ( false === get_user_by( 'id', $id ) ) { return false; } // Make sure customer is part of blog. if ( is_multisite() && ! is_user_member_of_blog( $id ) ) { add_user_to_blog( get_current_blog_id(), $id, 'customer' ); } return true; } /** * Purge object when creating. * * @param null|\WC_Order|\WPGraphQL\WooCommerce\Model\Order $order Object data. * @param boolean $force_delete Delete or put in trash. * * @return bool * @throws \GraphQL\Error\UserError Failed to delete order. */ public static function purge( $order, $force_delete = true ) { if ( ! empty( $order ) ) { return $order->delete( $force_delete ); } return false; } }