diff --git a/includes/class-core-schema-filters.php b/includes/class-core-schema-filters.php index 33822911..58ba0a39 100644 --- a/includes/class-core-schema-filters.php +++ b/includes/class-core-schema-filters.php @@ -271,12 +271,16 @@ public static function graphql_data_loaders( $loaders, $context ) { $loaders['cart_item'] = &$cart_item_loader; $downloadable_item_loader = new WC_Db_Loader( $context, 'DOWNLOADABLE_ITEM' ); $loaders['downloadable_item'] = &$downloadable_item_loader; + $tax_class_loader = new WC_Db_Loader( $context, 'TAX_CLASS' ); + $loaders['tax_class'] = &$tax_class_loader; $tax_rate_loader = new WC_Db_Loader( $context, 'TAX_RATE' ); $loaders['tax_rate'] = &$tax_rate_loader; $order_item_loader = new WC_Db_Loader( $context, 'ORDER_ITEM' ); $loaders['order_item'] = &$order_item_loader; $shipping_item_loader = new WC_Db_Loader( $context, 'SHIPPING_METHOD' ); $loaders['shipping_method'] = &$shipping_item_loader; + $shipping_zone_loader = new WC_Db_Loader( $context, 'SHIPPING_ZONE' ); + $loaders['shipping_zone'] = &$shipping_zone_loader; return $loaders; } diff --git a/includes/class-type-registry.php b/includes/class-type-registry.php index 723ed8bf..cc012515 100644 --- a/includes/class-type-registry.php +++ b/includes/class-type-registry.php @@ -46,6 +46,8 @@ public function init() { Type\WPEnum\Product_Attribute_Enum::register(); Type\WPEnum\Attribute_Operator_Enum::register(); Type\WPEnum\Currency_Enum::register(); + Type\WPEnum\Shipping_Location_Type_Enum::register(); + Type\WPEnum\WC_Setting_Type_Enum::register(); /** * InputObjects. @@ -67,6 +69,8 @@ public function init() { Type\WPInputObject\Collection_Stats_Where_Args::register(); Type\WPInputObject\Product_Attribute_Filter_Input::register(); Type\WPInputObject\Product_Attribute_Query_Input::register(); + Type\WPInputObject\Shipping_Location_Input::register(); + Type\WPInputObject\WC_Setting_Input::register(); /** * Interfaces. @@ -112,6 +116,10 @@ public function init() { Type\WPObject\Payment_Token_Types::register(); Type\WPObject\Country_State_Type::register(); Type\WPObject\Collection_Stats_Type::register(); + Type\WPObject\Shipping_Zone_Type::register(); + Type\WPObject\Shipping_Location_Type::register(); + Type\WPObject\Tax_Class_Type::register(); + Type\WPObject\WC_Setting_Type::register(); /** * Object fields. @@ -145,6 +153,8 @@ public function init() { Connection\Tax_Rates::register_connections(); Connection\Shipping_Methods::register_connections(); Connection\Payment_Gateways::register_connections(); + Connection\Shipping_Zones::register_connections(); + Connection\Tax_Classes::register_connections(); /** * Mutations. @@ -175,6 +185,19 @@ public function init() { Mutation\Coupon_Delete::register_mutation(); Mutation\Payment_Method_Delete::register_mutation(); Mutation\Payment_Method_Set_Default::register_mutation(); + Mutation\Shipping_Zone_Create::register_mutation(); + Mutation\Shipping_Zone_Delete::register_mutation(); + Mutation\Shipping_Zone_Locations_Clear::register_mutation(); + Mutation\Shipping_Zone_Locations_Update::register_mutation(); + Mutation\Shipping_Zone_Method_Add::register_mutation(); + Mutation\Shipping_Zone_Method_Remove::register_mutation(); + Mutation\Shipping_Zone_Method_Update::register_mutation(); + Mutation\Shipping_Zone_Update::register_mutation(); + Mutation\Tax_Class_Create::register_mutation(); + Mutation\Tax_Class_Delete::register_mutation(); + Mutation\Tax_Rate_Create::register_mutation(); + Mutation\Tax_Rate_Delete::register_mutation(); + Mutation\Tax_Rate_Update::register_mutation(); Mutation\Update_Session::register_mutation(); } } diff --git a/includes/class-wp-graphql-woocommerce.php b/includes/class-wp-graphql-woocommerce.php index d6b18145..1dbadb38 100644 --- a/includes/class-wp-graphql-woocommerce.php +++ b/includes/class-wp-graphql-woocommerce.php @@ -174,6 +174,7 @@ private function includes() { require $include_directory_path . 'model/class-order.php'; require $include_directory_path . 'model/class-order-item.php'; require $include_directory_path . 'model/class-shipping-method.php'; + require $include_directory_path . 'model/class-shipping-zone.php'; require $include_directory_path . 'model/class-tax-rate.php'; // Include data loaders class files. @@ -190,13 +191,15 @@ private function includes() { require $include_directory_path . 'data/connection/class-order-item-connection-resolver.php'; require $include_directory_path . 'data/connection/class-payment-gateway-connection-resolver.php'; require $include_directory_path . 'data/connection/class-product-attribute-connection-resolver.php'; + require $include_directory_path . 'data/connection/class-product-connection-resolver.php'; require $include_directory_path . 'data/connection/class-shipping-method-connection-resolver.php'; + require $include_directory_path . 'data/connection/class-shipping-zone-connection-resolver.php'; + require $include_directory_path . 'data/connection/class-tax-class-connection-resolver.php'; require $include_directory_path . 'data/connection/class-tax-rate-connection-resolver.php'; require $include_directory_path . 'data/connection/class-variation-attribute-connection-resolver.php'; // Include deprecated resolver trait/class files. require $include_directory_path . 'data/connection/class-coupon-connection-resolver.php'; - require $include_directory_path . 'data/connection/class-product-connection-resolver.php'; require $include_directory_path . 'data/connection/class-customer-connection-resolver.php'; // Include mutation processor class files. @@ -205,6 +208,8 @@ private function includes() { require $include_directory_path . 'data/mutation/class-coupon-mutation.php'; require $include_directory_path . 'data/mutation/class-customer-mutation.php'; require $include_directory_path . 'data/mutation/class-order-mutation.php'; + require $include_directory_path . 'data/mutation/class-shipping-mutation.php'; + require $include_directory_path . 'data/mutation/class-settings-mutation.php'; // Include factory class file. require $include_directory_path . 'data/class-factory.php'; @@ -239,6 +244,8 @@ private function includes() { require $include_directory_path . 'type/enum/class-attribute-operator-enum.php'; require $include_directory_path . 'type/enum/class-product-attribute-enum.php'; 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'; // Include interface type class files. require $include_directory_path . 'type/interface/class-attribute.php'; @@ -282,6 +289,10 @@ private function includes() { require $include_directory_path . 'type/object/class-payment-token-types.php'; require $include_directory_path . 'type/object/class-country-state-type.php'; require $include_directory_path . 'type/object/class-collection-stats-type.php'; + require $include_directory_path . 'type/object/class-shipping-location-type.php'; + require $include_directory_path . 'type/object/class-shipping-zone-type.php'; + require $include_directory_path . 'type/object/class-tax-class-type.php'; + require $include_directory_path . 'type/object/class-wc-setting-type.php'; // Include input type class files. require $include_directory_path . 'type/input/class-cart-item-input.php'; @@ -301,6 +312,8 @@ private function includes() { require $include_directory_path . 'type/input/class-collection-stats-where-args.php'; require $include_directory_path . 'type/input/class-product-attribute-filter-input.php'; require $include_directory_path . 'type/input/class-product-attribute-query-input.php'; + require $include_directory_path . 'type/input/class-shipping-location-input.php'; + require $include_directory_path . 'type/input/class-wc-setting-input.php'; // Include mutation type class files. require $include_directory_path . 'mutation/class-cart-add-fee.php'; @@ -329,6 +342,19 @@ private function includes() { require $include_directory_path . 'mutation/class-review-update.php'; require $include_directory_path . 'mutation/class-payment-method-delete.php'; require $include_directory_path . 'mutation/class-payment-method-set-default.php'; + require $include_directory_path . 'mutation/class-shipping-zone-create.php'; + require $include_directory_path . 'mutation/class-shipping-zone-delete.php'; + require $include_directory_path . 'mutation/class-shipping-zone-locations-clear.php'; + require $include_directory_path . 'mutation/class-shipping-zone-locations-update.php'; + require $include_directory_path . 'mutation/class-shipping-zone-method-add.php'; + require $include_directory_path . 'mutation/class-shipping-zone-method-remove.php'; + require $include_directory_path . 'mutation/class-shipping-zone-method-update.php'; + require $include_directory_path . 'mutation/class-shipping-zone-update.php'; + require $include_directory_path . 'mutation/class-tax-class-create.php'; + require $include_directory_path . 'mutation/class-tax-class-delete.php'; + require $include_directory_path . 'mutation/class-tax-rate-create.php'; + require $include_directory_path . 'mutation/class-tax-rate-delete.php'; + require $include_directory_path . 'mutation/class-tax-rate-update.php'; require $include_directory_path . 'mutation/class-update-session.php'; // Include connection class/function files. @@ -342,6 +368,8 @@ private function includes() { require $include_directory_path . 'connection/class-product-attributes.php'; require $include_directory_path . 'connection/class-products.php'; require $include_directory_path . 'connection/class-shipping-methods.php'; + require $include_directory_path . 'connection/class-shipping-zones.php'; + require $include_directory_path . 'connection/class-tax-classes.php'; require $include_directory_path . 'connection/class-tax-rates.php'; require $include_directory_path . 'connection/class-wc-terms.php'; diff --git a/includes/connection/class-shipping-zones.php b/includes/connection/class-shipping-zones.php new file mode 100644 index 00000000..1a39fb9e --- /dev/null +++ b/includes/connection/class-shipping-zones.php @@ -0,0 +1,63 @@ + 'RootQuery', + 'toType' => 'ShippingZone', + 'fromFieldName' => 'shippingZones', + 'connectionArgs' => [], + 'resolve' => static function ( $source, array $args, AppContext $context, ResolveInfo $info ) { + $resolver = new Shipping_Zone_Connection_Resolver( $source, $args, $context, $info ); + + return $resolver->get_connection(); + }, + ], + $args + ); + } + + /** + * Returns array of where args. + * + * @return array + */ + public static function get_connection_args(): array { + return []; + } +} diff --git a/includes/connection/class-tax-classes.php b/includes/connection/class-tax-classes.php new file mode 100644 index 00000000..1e8aff22 --- /dev/null +++ b/includes/connection/class-tax-classes.php @@ -0,0 +1,63 @@ + 'RootQuery', + 'toType' => 'TaxClass', + 'fromFieldName' => 'taxClasses', + 'connectionArgs' => [], + 'resolve' => static function ( $source, array $args, AppContext $context, ResolveInfo $info ) { + $resolver = new Tax_Class_Connection_Resolver( $source, $args, $context, $info ); + + return $resolver->get_connection(); + }, + ], + $args + ); + } + + /** + * Returns array of where args. + * + * @return array + */ + public static function get_connection_args(): array { + return []; + } +} diff --git a/includes/data/connection/class-shipping-method-connection-resolver.php b/includes/data/connection/class-shipping-method-connection-resolver.php index c71779d5..cdd527b3 100644 --- a/includes/data/connection/class-shipping-method-connection-resolver.php +++ b/includes/data/connection/class-shipping-method-connection-resolver.php @@ -11,6 +11,8 @@ namespace WPGraphQL\WooCommerce\Data\Connection; use WPGraphQL\Data\Connection\AbstractConnectionResolver; +use WPGraphQL\WooCommerce\Model\Shipping_Method; +use WPGraphQL\WooCommerce\Model\Shipping_Zone; /** * Class Shipping_Method_Connection_Resolver @@ -31,6 +33,10 @@ public function get_loader_name() { * @return bool */ public function should_execute() { + if ( ! wc_rest_check_manager_permissions( 'shipping_methods', 'read' ) ) { + graphql_debug( __( 'Permission denied.', 'wp-graphql-woocommerce' ) ); + return false; + } return true; } @@ -47,20 +53,22 @@ public function get_query_args() { /** * Executes query * - * @return array|mixed|string[] + * @return int[] */ public function get_query() { - // TODO: Implement get_query() method. - $wc_shipping = \WC_Shipping::instance(); - $methods = $wc_shipping->get_shipping_methods(); + if ( $this->source instanceof Shipping_Zone ) { + $methods = $this->source->methods; + } else { + $wc_shipping = \WC_Shipping::instance(); + $methods = $wc_shipping->get_shipping_methods(); + } + + foreach ( $methods as $method ) { + $this->loader->prime( $method->id, new Shipping_Method( $method ) ); + } // Get shipping method IDs. - $methods = array_map( - static function ( $item ) { - return $item->id; - }, - array_values( $methods ) - ); + $methods = wp_list_pluck( array_values( $methods ), 'id' ); return $methods; } @@ -68,9 +76,9 @@ static function ( $item ) { /** * Return an array of items from the query * - * @return array|mixed + * @return array */ - public function get_ids() { + public function get_ids_from_query() { return ! empty( $this->query ) ? $this->query : []; } diff --git a/includes/data/connection/class-shipping-zone-connection-resolver.php b/includes/data/connection/class-shipping-zone-connection-resolver.php new file mode 100644 index 00000000..32b0a25a --- /dev/null +++ b/includes/data/connection/class-shipping-zone-connection-resolver.php @@ -0,0 +1,107 @@ +get_data() ); + + if ( ! empty( $this->query_args['filters'] ) && is_array( $this->query_args['filters'] ) ) { + foreach ( $this->query_args['filters'] as $filter ) { + $zones = array_filter( $zones, $filter ); + } + } + + return wp_list_pluck( $zones, 'id' ); + } + + /** + * Return an array of items from the query + * + * @return array + */ + public function get_ids_from_query() { + return ! empty( $this->query ) ? $this->query : []; + } + + /** + * Validates offset. + * + * @param mixed $offset Decoded query cursor. + * + * @return bool + */ + public function is_valid_offset( $offset ) { + return is_string( $offset ); + } + + /** + * Validates shipping zone model. + * + * @param array $model Shipping zone model. + * + * @return bool + */ + protected function is_valid_model( $model ) { + return ! empty( $model ) && $model instanceof Shipping_Zone && 0 !== $model->ID; + } +} diff --git a/includes/data/connection/class-tax-class-connection-resolver.php b/includes/data/connection/class-tax-class-connection-resolver.php new file mode 100644 index 00000000..6d0628d5 --- /dev/null +++ b/includes/data/connection/class-tax-class-connection-resolver.php @@ -0,0 +1,113 @@ + 'standard', + 'name' => __( 'Standard rate', 'wp-graphql-woocommerce' ), + ]; + + $classes = \WC_Tax::get_tax_classes(); + + foreach ( $classes as $class ) { + $tax_classes[] = [ + 'slug' => sanitize_title( $class ), + 'name' => $class, + ]; + } + + // Cache cart items for later. + foreach ( $tax_classes as $tax_class ) { + $this->loader->prime( $tax_class['slug'], $tax_class ); + } + + return wp_list_pluck( $tax_classes, 'slug' ); + } + + /** + * Return an array of items from the query + * + * @return array + */ + public function get_ids_from_query() { + return ! empty( $this->query ) ? $this->query : []; + } + + /** + * Validates offset. + * + * @param mixed $offset Decoded query cursor. + * + * @return bool + */ + public function is_valid_offset( $offset ) { + return is_string( $offset ); + } + + /** + * Validates tax class model. + * + * @param array $model Tax class model. + * + * @return bool + */ + protected function is_valid_model( $model ) { + return is_array( $model ) && ! empty( $model['name'] ) && ! empty( $model['slug'] ); + } +} diff --git a/includes/data/connection/class-tax-rate-connection-resolver.php b/includes/data/connection/class-tax-rate-connection-resolver.php index 3bab10c7..9c173c20 100644 --- a/includes/data/connection/class-tax-rate-connection-resolver.php +++ b/includes/data/connection/class-tax-rate-connection-resolver.php @@ -35,6 +35,12 @@ public function get_loader_name() { * @return bool */ public function should_execute() { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + graphql_debug( + __( 'User does not have permission to view tax rates.', 'wp-graphql-woocommerce' ) + ); + return false; + } return true; } diff --git a/includes/data/loader/class-wc-db-loader.php b/includes/data/loader/class-wc-db-loader.php index bcdd55f8..ebb19423 100644 --- a/includes/data/loader/class-wc-db-loader.php +++ b/includes/data/loader/class-wc-db-loader.php @@ -14,6 +14,7 @@ use WPGraphQL\Data\Loader\AbstractDataLoader; use WPGraphQL\WooCommerce\Data\Factory; use WPGraphQL\WooCommerce\Model\Shipping_Method; +use WPGraphQL\WooCommerce\Model\Shipping_Zone; use WPGraphQL\WooCommerce\Model\Tax_Rate; /** @@ -64,6 +65,9 @@ public function loadKeys( array $keys ) { case 'DOWNLOADABLE_ITEM': $loader = [ $this, 'load_downloadable_item_from_id' ]; break; + case 'TAX_CLASS': + $loader = [ $this, 'load_tax_class_from_slug' ]; + break; case 'TAX_RATE': $loader = [ $this, 'load_tax_rate_from_id' ]; break; @@ -73,6 +77,9 @@ public function loadKeys( array $keys ) { case 'SHIPPING_METHOD': $loader = [ $this, 'load_shipping_method_from_id' ]; break; + case 'SHIPPING_ZONE': + $loader = [ $this, 'load_shipping_zone_from_id' ]; + break; default: /** * For adding custom key types to this loader @@ -126,6 +133,25 @@ public function load_downloadable_item_from_id( $id ) { return 0 === $node->get_id() ? $node : null; } + /** + * Returns the tax class connected the provided IDs. + * + * @param int $slug - Tax class slug. + * + * @return array|null + */ + public function load_tax_class_from_slug( $slug ) { + if ( 'standard' === $slug ) { + return [ + 'slug' => 'standard', + 'name' => __( 'Standard rate', 'wp-graphql-woocommerce' ), + ]; + } else { + $tax_class = \WC_Tax::get_tax_class_by( 'slug', $slug ); + return is_array( $tax_class ) && ! empty( $tax_class ) ? $tax_class : null; + } + } + /** * Returns the tax rate connected the provided IDs. * @@ -139,7 +165,7 @@ public function load_tax_rate_from_id( $id ) { /** * Get tax rate from WooCommerce. * - * @var object{ + * @var \stdClass&object{ * tax_rate_id: int, * tax_rate_class: string, * tax_rate_country: string, @@ -151,27 +177,51 @@ public function load_tax_rate_from_id( $id ) { * tax_rate_shipping: bool, * tax_rate_order: int, * tax_rate_city: string, - * tax_rate_postcode: string + * tax_rate_postcode: string, + * tax_rate_postcodes: string, + * tax_rate_cities: string * } $rate */ $rate = \WC_Tax::_get_tax_rate( $id, OBJECT ); if ( ! empty( $rate ) && is_object( $rate ) ) { + $rate->tax_rate_city = ''; + $rate->tax_rate_postcode = ''; + $rate->tax_rate_postcodes = ''; + $rate->tax_rate_cities = ''; + // Get locales from a tax rate. // phpcs:ignore WordPress.DB.DirectDatabaseQuery $locales = $wpdb->get_results( $wpdb->prepare( - "SELECT location_code, location_type + " + SELECT location_code, location_type FROM {$wpdb->prefix}woocommerce_tax_rate_locations - WHERE tax_rate_id = %d", + WHERE tax_rate_id = %d + ", $rate->tax_rate_id ) ); + $cities = []; + $postcodes = []; foreach ( $locales as $locale ) { - if ( empty( $rate->{'tax_rate_' . $locale->location_type} ) ) { - $rate->{'tax_rate_' . $locale->location_type} = []; + if ( 'city' === $locale->location_type ) { + $cities[] = $locale->location_code; + } elseif ( 'postcode' === $locale->location_type ) { + $postcodes[] = $locale->location_code; + } else { + $rate->{'tax_rate_' . $locale->location_type} = $locale->location_code; } - $rate->{'tax_rate_' . $locale->location_type}[] = $locale->location_code; + } + + if ( ! empty( $cities ) ) { + $rate->tax_rate_cities = implode( ';', $cities ); + $rate->tax_rate_city = end( $cities ); + } + + if ( ! empty( $postcodes ) ) { + $rate->tax_rate_postcodes = implode( ';', $postcodes ); + $rate->tax_rate_postcode = end( $postcodes ); } return new Tax_Rate( $rate ); } else { @@ -185,7 +235,7 @@ public function load_tax_rate_from_id( $id ) { * @param int $id - Shipping method ID. * * @return \WPGraphQL\WooCommerce\Model\Shipping_Method - * @access public + * * @throws \GraphQL\Error\UserError Invalid object. */ public function load_shipping_method_from_id( $id ) { @@ -203,6 +253,26 @@ public function load_shipping_method_from_id( $id ) { return new Shipping_Method( $method ); } + /** + * Returns the shipping zone connected the provided IDs. + * + * @param int $id - Shipping zone IDs. + * + * @return \WPGraphQL\WooCommerce\Model\Shipping_Zone|null + */ + public function load_shipping_zone_from_id( $id ) { + /** @var \WC_Shipping_Zone|false $zone */ + $zone = \WC_Shipping_Zones::get_zone( $id ); + + if ( false === $zone ) { + return null; + } + + $zone = new Shipping_Zone( $zone ); + + return $zone; + } + /** * Returns the order item connected the provided IDs. * diff --git a/includes/data/mutation/class-settings-mutation.php b/includes/data/mutation/class-settings-mutation.php new file mode 100644 index 00000000..1de17280 --- /dev/null +++ b/includes/data/mutation/class-settings-mutation.php @@ -0,0 +1,164 @@ + [ + 'src' => true, + 'style' => true, + 'id' => true, + 'class' => true, + ], + ], + wp_kses_allowed_html( 'post' ) + ) + ); + } +} diff --git a/includes/data/mutation/class-shipping-mutation.php b/includes/data/mutation/class-shipping-mutation.php new file mode 100644 index 00000000..56a94346 --- /dev/null +++ b/includes/data/mutation/class-shipping-mutation.php @@ -0,0 +1,110 @@ +> $settings_input Settings input. + * + * @return array + */ + private static function flatten_settings_input( $settings_input ) { + $settings = []; + foreach ( $settings_input as $setting ) { + $settings[ $setting['id'] ] = $setting['value']; + } + return $settings; + } + + /** + * Updates settings on a shipping zone method. + * + * @param int $instance_id Instance ID. + * @param \WC_Shipping_Method $method Shipping method data. + * @param array $settings_input Settings input. + * + * @return \WC_Shipping_Method + */ + public static function set_shipping_zone_method_settings( $instance_id, $method, $settings_input ) { + $settings = self::flatten_settings_input( $settings_input ); + $method->init_instance_settings(); + $instance_settings = $method->instance_settings; + $errors_found = false; + foreach ( $method->get_instance_form_fields() as $key => $field ) { + if ( isset( $settings[ $key ] ) ) { + if ( is_callable( [ Settings_Mutation::class, 'validate_setting_' . $field['type'] . '_field' ] ) ) { + $value = Settings_Mutation::{'validate_setting_' . $field['type'] . '_field'}( $settings[ $key ], $field ); + } else { + $value = Settings_Mutation::validate_setting_text_field( $settings[ $key ], $field ); + } + $instance_settings[ $key ] = $value; + } + } + + update_option( + $method->get_instance_option_key(), + apply_filters( + 'woocommerce_shipping_' . $method->id . '_instance_settings_values', // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound + $instance_settings, + $method + ) + ); + + $method->instance_settings = $instance_settings; + return $method; + } + + /** + * Updates the order of a shipping zone method. + * + * @param int $instance_id Instance ID. + * @param \WC_Shipping_Method $method Shipping method data. + * @param int $order Order. + * + * @return \WC_Shipping_Method + */ + public static function set_shipping_zone_method_order( $instance_id, $method, $order ) { + global $wpdb; + + $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery + "{$wpdb->prefix}woocommerce_shipping_zone_methods", + [ 'method_order' => $order ], + [ 'instance_id' => $instance_id ] + ); + $method->method_order = $order; + + return $method; + } + + /** + * Updates the enabled status of a shipping zone method. + * + * @param int $zone_id Zone ID. + * @param int $instance_id Instance ID. + * @param \WC_Shipping_Method $method Shipping method data. + * @param bool $enabled Enabled status. + * + * @return \WC_Shipping_Method + */ + public static function set_shipping_zone_method_enabled( $zone_id, $instance_id, $method, $enabled ) { + global $wpdb; + + if ( $wpdb->update( "{$wpdb->prefix}woocommerce_shipping_zone_methods", [ 'is_enabled' => $enabled ], [ 'instance_id' => $instance_id ] ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery + do_action( 'woocommerce_shipping_zone_method_status_toggled', $instance_id, $method->id, $zone_id, $enabled ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound + $method->enabled = ( true === $enabled ? 'yes' : 'no' ); + } + + return $method; + } +} diff --git a/includes/model/class-shipping-method.php b/includes/model/class-shipping-method.php index 7015ec2d..4346420f 100644 --- a/includes/model/class-shipping-method.php +++ b/includes/model/class-shipping-method.php @@ -81,4 +81,32 @@ protected function init() { ]; } } + + /** + * Forwards function calls to WC_Data sub-class instance. + * + * @param string $method - function name. + * @param array $args - function call arguments. + * + * @return mixed + * + * @throws \BadMethodCallException Method not found on WC data object. + */ + public function __call( $method, $args ) { + if ( \is_callable( [ $this->data, $method ] ) ) { + return $this->data->$method( ...$args ); + } + + $class = self::class; + throw new \BadMethodCallException( "Call to undefined method {$method} on the {$class}" ); + } + + /** + * Returns the source WC_Data instance + * + * @return \WC_Shipping_Method + */ + public function as_WC_Data() { + return $this->data; + } } diff --git a/includes/model/class-shipping-zone.php b/includes/model/class-shipping-zone.php new file mode 100644 index 00000000..f1268027 --- /dev/null +++ b/includes/model/class-shipping-zone.php @@ -0,0 +1,120 @@ +data = $zone; + $allowed_restricted_fields = [ + 'isRestricted', + 'isPrivate', + 'isPublic', + 'id', + 'databaseId', + ]; + + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound + $restricted_cap = apply_filters( 'shipping_zone_restricted_cap', '' ); + + parent::__construct( $restricted_cap, $allowed_restricted_fields, null ); + } + + /** + * Determines if the order item should be considered private. + * + * @return bool + */ + protected function is_private() { + return false; + } + + /** + * Initializes the Order field resolvers. + */ + protected function init() { + if ( empty( $this->fields ) ) { + $this->fields = [ + 'ID' => function () { + return $this->data->get_id(); + }, + 'id' => function () { + return ! empty( $this->data->get_id() ) ? Relay::toGlobalId( 'shipping_zone', (string) $this->data->get_id() ) : null; + }, + 'databaseId' => function () { + return ! empty( $this->ID ) ? $this->ID : null; + }, + 'name' => function () { + return ! empty( $this->data->get_zone_name() ) ? $this->data->get_zone_name() : null; + }, + 'order' => function () { + return $this->data->get_zone_order(); + }, + 'locations' => function () { + return $this->data->get_zone_locations(); + }, + 'methods' => function () { + return $this->data->get_shipping_methods(); + }, + ]; + } + } + + /** + * Forwards function calls to WC_Data sub-class instance. + * + * @param string $method - function name. + * @param array $args - function call arguments. + * + * @return mixed + * + * @throws \BadMethodCallException Method not found on WC data object. + */ + public function __call( $method, $args ) { + if ( \is_callable( [ $this->data, $method ] ) ) { + return $this->data->$method( ...$args ); + } + + $class = self::class; + throw new \BadMethodCallException( "Call to undefined method {$method} on the {$class}" ); + } + + /** + * Returns the source WC_Data instance + * + * @return \WC_Shipping_Zone + */ + public function as_WC_Data() { + return $this->data; + } +} diff --git a/includes/model/class-tax-rate.php b/includes/model/class-tax-rate.php index 5c3ebbf1..672acc1b 100644 --- a/includes/model/class-tax-rate.php +++ b/includes/model/class-tax-rate.php @@ -28,23 +28,27 @@ * tax_rate_shipping: bool, * tax_rate_order: int, * tax_rate_city: string, - * tax_rate_postcode: string + * tax_rate_postcode: string, + * tax_rate_postcodes: string, + * tax_rate_cities: string * } $data * - * @property int $ID - * @property string $id - * @property string $databaseId - * @property string $country - * @property string $state - * @property string $city - * @property string $postcode - * @property string $rate - * @property string $name - * @property string $priority - * @property bool $compound - * @property bool $shipping - * @property int $order - * @property string $class + * @property int $ID + * @property string $id + * @property string $databaseId + * @property string $country + * @property string $state + * @property string $city + * @property string $postcode + * @property string[] $postcodes + * @property string[] $cities + * @property string $rate + * @property string $name + * @property string $priority + * @property bool $compound + * @property bool $shipping + * @property int $order + * @property string $class * * @package WPGraphQL\WooCommerce\Model */ @@ -52,7 +56,7 @@ class Tax_Rate extends Model { /** * Tax_Rate constructor * - * @param object{ tax_rate_id: int, tax_rate_class: string, tax_rate_country: string, tax_rate_state: string, tax_rate: string, tax_rate_name: string, tax_rate_priority: int, tax_rate_compound: bool, tax_rate_shipping: bool, tax_rate_order: int, tax_rate_city: string, tax_rate_postcode: string } $rate Tax rate object. + * @param object{ tax_rate_id: int, tax_rate_class: string, tax_rate_country: string, tax_rate_state: string, tax_rate: string, tax_rate_name: string, tax_rate_priority: int, tax_rate_compound: bool, tax_rate_shipping: bool, tax_rate_order: int, tax_rate_city: string, tax_rate_postcode: string, tax_rate_postcodes: string, tax_rate_cities: string } $rate Tax rate object. */ public function __construct( $rate ) { $this->data = $rate; @@ -101,10 +105,16 @@ protected function init() { return ! empty( $this->data->tax_rate_state ) ? $this->data->tax_rate_state : null; }, 'city' => function () { - return ! empty( $this->data->tax_rate_city ) ? $this->data->tax_rate_city : [ '*' ]; + return ! empty( $this->data->tax_rate_city ) ? $this->data->tax_rate_city : '*'; }, 'postcode' => function () { - return ! empty( $this->data->tax_rate_postcode ) ? $this->data->tax_rate_postcode : [ '*' ]; + return ! empty( $this->data->tax_rate_postcode ) ? $this->data->tax_rate_postcode : '*'; + }, + 'postcodes' => function () { + return ! empty( $this->data->tax_rate_postcodes ) ? explode( ';', $this->data->tax_rate_postcodes ) : [ '*' ]; + }, + 'cities' => function () { + return ! empty( $this->data->tax_rate_cities ) ? explode( ';', $this->data->tax_rate_cities ) : [ '*' ]; }, 'rate' => function () { return ! empty( $this->data->tax_rate ) ? $this->data->tax_rate : null; @@ -116,10 +126,10 @@ protected function init() { return ! empty( $this->data->tax_rate_priority ) ? $this->data->tax_rate_priority : null; }, 'compound' => function () { - return ! empty( $this->data->tax_rate_compound ) ? $this->data->tax_rate_compound : null; + return isset( $this->data->tax_rate_compound ) ? $this->data->tax_rate_compound : null; }, 'shipping' => function () { - return ! empty( $this->data->tax_rate_shipping ) ? $this->data->tax_rate_shipping : null; + return isset( $this->data->tax_rate_shipping ) ? $this->data->tax_rate_shipping : null; }, 'order' => function () { return ! is_null( $this->data->tax_rate_order ) ? absint( $this->data->tax_rate_order ) : null; diff --git a/includes/mutation/class-shipping-zone-create.php b/includes/mutation/class-shipping-zone-create.php new file mode 100644 index 00000000..6dd1ca1f --- /dev/null +++ b/includes/mutation/class-shipping-zone-create.php @@ -0,0 +1,110 @@ + self::get_input_fields(), + 'outputFields' => self::get_output_fields(), + 'mutateAndGetPayload' => self::mutate_and_get_payload(), + ] + ); + } + + /** + * Defines the mutation input field configuration + * + * @return array + */ + public static function get_input_fields() { + return [ + 'name' => [ + 'type' => [ 'non_null' => 'String' ], + 'description' => __( 'Name of the shipping zone.', 'wp-graphql-woocommerce' ), + ], + 'order' => [ + 'type' => 'Int', + 'description' => __( 'Order of the shipping zone.', 'wp-graphql-woocommerce' ), + ], + ]; + } + + /** + * Defines the mutation output field configuration + * + * @return array + */ + public static function get_output_fields() { + return [ + 'shippingZone' => [ + 'type' => 'ShippingZone', + 'resolve' => static function ( $payload, array $args, AppContext $context ) { + return $context->get_loader( 'shipping_zone' )->load( $payload['zone_id'] ); + }, + ], + ]; + } + + /** + * Defines the mutation data modification closure. + * + * @return callable + */ + public static function mutate_and_get_payload() { + return static function ( $input, AppContext $context, ResolveInfo $info ) { + if ( ! \wc_shipping_enabled() ) { + throw new UserError( __( 'Shipping is disabled.', 'wp-graphql-woocommerce' ), 404 ); + } + + if ( ! \wc_rest_check_manager_permissions( 'settings', 'edit' ) ) { + throw new UserError( __( 'Sorry, you are not allowed to create shipping zones.', 'wp-graphql-woocommerce' ), \rest_authorization_required_code() ); + } + + $zone = new \WC_Shipping_Zone( null ); + $zone->set_zone_name( $input['name'] ); + + if ( ! empty( $input['order'] ) ) { + $zone->set_zone_order( $input['order'] ); + } + + /** + * Filter zone object before saving. + * + * @param \WC_Shipping_Zone $zone The response object. + * @param array $input Request input. + */ + $zone = apply_filters( 'graphql_woocommerce_shipping_zone_create', $zone, $input ); + + $zone_id = $zone->save(); + + if ( 0 === $zone->get_id() ) { + throw new UserError( __( 'Failed to create shipping zone.', 'wp-graphql-woocommerce' ) ); + } + + return [ 'zone_id' => $zone_id ]; + }; + } +} diff --git a/includes/mutation/class-shipping-zone-delete.php b/includes/mutation/class-shipping-zone-delete.php new file mode 100644 index 00000000..e14f2e78 --- /dev/null +++ b/includes/mutation/class-shipping-zone-delete.php @@ -0,0 +1,114 @@ + self::get_input_fields(), + 'outputFields' => self::get_output_fields(), + 'mutateAndGetPayload' => self::mutate_and_get_payload(), + ] + ); + } + + /** + * Defines the mutation input field configuration + * + * @return array + */ + public static function get_input_fields() { + return [ + 'id' => [ + 'type' => [ 'non_null' => 'Int' ], + 'description' => __( 'The ID of the shipping zone to delete.', 'wp-graphql-woocommerce' ), + ], + ]; + } + + /** + * Defines the mutation output field configuration + * + * @return array + */ + public static function get_output_fields() { + return [ + 'shippingZone' => [ + 'type' => 'ShippingZone', + 'resolve' => static function ( $payload ) { + return $payload['shippingZone']; + }, + ], + ]; + } + + /** + * Defines the mutation data modification closure. + * + * @return callable + */ + public static function mutate_and_get_payload() { + return static function ( $input, AppContext $context, ResolveInfo $info ) { + if ( ! \wc_shipping_enabled() ) { + throw new UserError( __( 'Shipping is disabled.', 'wp-graphql-woocommerce' ), 404 ); + } + + if ( ! \wc_rest_check_manager_permissions( 'settings', 'delete' ) ) { + throw new UserError( __( 'Sorry, you are not allowed to delete shipping zones', 'wp-graphql-woocommerce' ), \rest_authorization_required_code() ); + } + + $zone_id = $input['id']; + /** @var \WC_Shipping_Zone|false $zone */ + $zone = \WC_Shipping_Zones::get_zone_by( 'zone_id', $zone_id ); + + if ( false === $zone ) { + throw new UserError( __( 'Invalid shipping zone ID.', 'wp-graphql-woocommerce' ) ); + } + + /** + * Filter zone object returned from the GraphQL API. + * + * @param \WC_Shipping_Zone $zone The response object. + * @param array $input Request input. + */ + $zone = apply_filters( 'graphql_woocommerce_delete_shipping_zone', $zone, $input ); + + $object = $context->get_loader( 'shipping_zone' )->load( $zone_id ); + + /** + * Filter zone model returned from the GraphQL API before deletion. + * + * @param \Shipping_Zone $object The response object. + * @param \WC_Shipping_Zone $zone The zone object. + * @param array $input Request input. + */ + $object = apply_filters( 'graphql_woocommerce_delete_shipping_zone_object', $object, $zone, $input ); + + $zone->delete(); + + return [ 'shippingZone' => $object ]; + }; + } +} diff --git a/includes/mutation/class-shipping-zone-locations-clear.php b/includes/mutation/class-shipping-zone-locations-clear.php new file mode 100644 index 00000000..fd699772 --- /dev/null +++ b/includes/mutation/class-shipping-zone-locations-clear.php @@ -0,0 +1,142 @@ + self::get_input_fields(), + 'outputFields' => self::get_output_fields(), + 'mutateAndGetPayload' => self::mutate_and_get_payload(), + ] + ); + } + + /** + * Defines the mutation input field configuration + * + * @return array + */ + public static function get_input_fields() { + return [ + 'zoneId' => [ + 'type' => [ 'non_null' => 'Int' ], + 'description' => __( 'The ID of the shipping zone to delete.', 'wp-graphql-woocommerce' ), + ], + 'type' => [ + 'type' => 'ShippingLocationTypeEnum', + 'description' => __( 'The type of location to remove.', 'wp-graphql-woocommerce' ), + ], + ]; + } + + /** + * Defines the mutation output field configuration + * + * @return array + */ + public static function get_output_fields() { + return [ + 'shippingZone' => [ + 'type' => 'ShippingZone', + 'resolve' => static function ( $payload, array $args, AppContext $context ) { + return $context->get_loader( 'shipping_zone' )->load( $payload['zone_id'] ); + }, + ], + 'removedLocations' => [ + 'type' => [ 'list_of' => 'ShippingLocation' ], + 'resolve' => static function ( $payload ) { + return ! empty( $payload['removedLocations'] ) + ? array_map( + static function ( $location ) { + return (object) $location; + }, + $payload['removedLocations'], + ) + : []; + }, + ], + ]; + } + + /** + * Defines the mutation data modification closure. + * + * @return callable + */ + public static function mutate_and_get_payload() { + return static function ( $input, AppContext $context, ResolveInfo $info ) { + if ( ! \wc_shipping_enabled() ) { + throw new UserError( __( 'Shipping is disabled.', 'wp-graphql-woocommerce' ), 404 ); + } + + if ( ! \wc_rest_check_manager_permissions( 'settings', 'delete' ) ) { + throw new UserError( __( 'Sorry, you are not allowed to remove shipping locations', 'wp-graphql-woocommerce' ), \rest_authorization_required_code() ); + } + + $zone_id = $input['zoneId']; + /** @var \WC_Shipping_Zone|false $zone */ + $zone = \WC_Shipping_Zones::get_zone_by( 'zone_id', $zone_id ); + + if ( false === $zone ) { + throw new UserError( __( 'Invalid shipping zone ID.', 'wp-graphql-woocommerce' ) ); + } + + if ( 0 === $zone->get_id() ) { + throw new UserError( __( 'Invalid shipping zone ID.', 'wp-graphql-woocommerce' ) ); + } + + $types = [ 'postcode', 'state', 'country', 'continent' ]; + if ( ! empty( $input['type'] ) ) { + $types = [ $input['type'] ]; + } + + $all_locations = $zone->get_zone_locations(); + $locations = array_filter( + $all_locations, + static function ( $location ) use ( $types ) { + return in_array( $location->type, $types, true ); + } + ); + + /** + * Filter zone object before removing the locations. + * + * @param \WC_Shipping_Zone $zone The response object. + * @param array $locations Locations to be removed. + * @param array $input Request input. + */ + $zone = apply_filters( 'graphql_woocommerce_shipping_zone_locations_clear', $zone, $locations, $input ); + + $zone->clear_locations( $types ); + $zone->save(); + + return [ + 'zone_id' => $zone_id, + 'removedLocations' => $locations, + ]; + }; + } +} diff --git a/includes/mutation/class-shipping-zone-locations-update.php b/includes/mutation/class-shipping-zone-locations-update.php new file mode 100644 index 00000000..6326ddf5 --- /dev/null +++ b/includes/mutation/class-shipping-zone-locations-update.php @@ -0,0 +1,140 @@ + self::get_input_fields(), + 'outputFields' => self::get_output_fields(), + 'mutateAndGetPayload' => self::mutate_and_get_payload(), + ] + ); + } + + /** + * Defines the mutation input field configuration + * + * @return array + */ + public static function get_input_fields() { + return [ + 'zoneId' => [ + 'type' => [ 'non_null' => 'Int' ], + 'description' => __( 'The ID of the shipping zone to delete.', 'wp-graphql-woocommerce' ), + ], + 'locations' => [ + 'type' => [ 'list_of' => 'ShippingLocationInput' ], + 'description' => __( 'The locations to add to the shipping zone.', 'wp-graphql-woocommerce' ), + ], + ]; + } + + /** + * Defines the mutation output field configuration + * + * @return array + */ + public static function get_output_fields() { + return [ + 'shippingZone' => [ + 'type' => 'ShippingZone', + 'resolve' => static function ( $payload, array $args, AppContext $context ) { + return $context->get_loader( 'shipping_zone' )->load( $payload['zone_id'] ); + }, + ], + 'locations' => [ + 'type' => [ 'list_of' => 'ShippingLocation' ], + 'resolve' => static function ( $payload ) { + return ! empty( $payload['locations'] ) + ? array_map( + static function ( $location ) { + return (object) $location; + }, + $payload['locations'], + ) + : []; + }, + ], + ]; + } + + /** + * Defines the mutation data modification closure. + * + * @return callable + */ + public static function mutate_and_get_payload() { + return static function ( $input, AppContext $context, ResolveInfo $info ) { + if ( ! \wc_shipping_enabled() ) { + throw new UserError( __( 'Shipping is disabled.', 'wp-graphql-woocommerce' ), 404 ); + } + + if ( ! \wc_rest_check_manager_permissions( 'settings', 'edit' ) ) { + throw new UserError( __( 'Sorry, you are not allowed to update shipping location', 'wp-graphql-woocommerce' ), \rest_authorization_required_code() ); + } + + $zone_id = $input['zoneId']; + /** @var \WC_Shipping_Zone|false $zone */ + $zone = \WC_Shipping_Zones::get_zone_by( 'zone_id', $zone_id ); + + if ( false === $zone ) { + throw new UserError( __( 'Invalid shipping zone ID.', 'wp-graphql-woocommerce' ) ); + } + + if ( 0 === $zone->get_id() ) { + throw new UserError( __( 'Invalid shipping zone ID.', 'wp-graphql-woocommerce' ) ); + } + + $raw_locations = ! empty( $input['locations'] ) ? $input['locations'] : []; + $locations = []; + foreach ( $raw_locations as $location ) { + $type = ! empty( $location['type'] ) ? $location['type'] : 'country'; + + $locations[] = [ + 'type' => $type, + 'code' => $location['code'], + ]; + } + + /** + * Filter zone object before add the locations. + * + * @param \WC_Shipping_Zone $zone The response object. + * @param array $locations Locations to be saved. + * @param array $input Request input. + */ + $zone = apply_filters( 'graphql_woocommerce_shipping_zone_locations_update', $zone, $locations, $input ); + + $zone->set_locations( $locations ); + $zone->save(); + + return [ + 'zone_id' => $zone_id, + 'locations' => $locations, + ]; + }; + } +} diff --git a/includes/mutation/class-shipping-zone-method-add.php b/includes/mutation/class-shipping-zone-method-add.php new file mode 100644 index 00000000..c69266ec --- /dev/null +++ b/includes/mutation/class-shipping-zone-method-add.php @@ -0,0 +1,168 @@ + self::get_input_fields(), + 'outputFields' => self::get_output_fields(), + 'mutateAndGetPayload' => self::mutate_and_get_payload(), + ] + ); + } + + /** + * Defines the mutation input field configuration + * + * @return array + */ + public static function get_input_fields() { + return [ + 'zoneId' => [ + 'type' => [ 'non_null' => 'Int' ], + 'description' => __( 'The ID of the shipping zone to delete.', 'wp-graphql-woocommerce' ), + ], + 'methodId' => [ + 'type' => [ 'non_null' => 'String' ], + 'description' => __( 'The ID of the shipping method to add.', 'wp-graphql-woocommerce' ), + ], + 'enabled' => [ + 'type' => 'Boolean', + 'description' => __( 'Whether the shipping method is enabled or not.', 'wp-graphql-woocommerce' ), + ], + 'order' => [ + 'type' => 'Int', + 'description' => __( 'The order of the shipping method.', 'wp-graphql-woocommerce' ), + ], + 'settings' => [ + 'type' => [ 'list_of' => 'WCSettingInput' ], + 'description' => __( 'The settings for the shipping method.', 'wp-graphql-woocommerce' ), + ], + ]; + } + + /** + * Defines the mutation output field configuration + * + * @return array + */ + public static function get_output_fields() { + return [ + 'shippingZone' => [ + 'type' => 'ShippingZone', + 'resolve' => static function ( $payload, array $args, AppContext $context ) { + return $context->get_loader( 'shipping_zone' )->load( $payload['zone_id'] ); + }, + ], + 'method' => [ + 'type' => 'ShippingZoneToShippingMethodConnectionEdge', + 'resolve' => static function ( $payload, array $args, AppContext $context ) { + return [ + // Call the Shipping_Method constructor directly because "$payload['method']" is a non-scalar value. + 'node' => new Shipping_Method( $payload['method'] ), + 'source' => $context->get_loader( 'shipping_zone' )->load( $payload['zone_id'] ), + ]; + }, + ], + ]; + } + + /** + * Defines the mutation data modification closure. + * + * @return callable + */ + public static function mutate_and_get_payload() { + return static function ( $input, AppContext $context, ResolveInfo $info ) { + if ( ! \wc_shipping_enabled() ) { + throw new UserError( __( 'Shipping is disabled.', 'wp-graphql-woocommerce' ), 404 ); + } + + if ( ! \wc_rest_check_manager_permissions( 'settings', 'edit' ) ) { + throw new UserError( __( 'Sorry, you are not allowed to add shipping methods', 'wp-graphql-woocommerce' ), \rest_authorization_required_code() ); + } + + $method_id = $input['methodId']; + $zone_id = $input['zoneId']; + /** @var \WC_Shipping_Zone|false $zone */ + $zone = \WC_Shipping_Zones::get_zone_by( 'zone_id', $zone_id ); + + if ( false === $zone ) { + throw new UserError( __( 'Invalid shipping zone ID.', 'wp-graphql-woocommerce' ) ); + } + + if ( 0 === $zone->get_id() ) { + throw new UserError( __( 'Invalid shipping zone ID.', 'wp-graphql-woocommerce' ) ); + } + + $instance_id = $zone->add_shipping_method( $method_id ); + $methods = $zone->get_shipping_methods(); + $method = false; + foreach ( $methods as $method_obj ) { + if ( $method_obj->instance_id === $instance_id ) { + $method = $method_obj; + break; + } + } + + if ( false === $method ) { + throw new UserError( __( 'Failed to add shipping method to shipping zone.', 'wp-graphql-woocommerce' ) ); + } + + // Update settings. + if ( ! empty( $input['settings'] ) ) { + $method = Shipping_Mutation::set_shipping_zone_method_settings( $instance_id, $method, $input['settings'] ); + } + + // Update order. + if ( isset( $input['order'] ) ) { + $method = Shipping_Mutation::set_shipping_zone_method_order( $instance_id, $method, $input['order'] ); + } + + // Update if this method is enabled or not. + if ( isset( $input['enabled'] ) ) { + $method = Shipping_Mutation::set_shipping_zone_method_enabled( $zone_id, $instance_id, $method, $input['enabled'] ); + } + + /** + * Filter shipping method object before responding. + * + * @param \WC_Shipping_Method $method The shipping method object. + * @param \WC_Shipping_Zone $zone The response object. + * @param array $input Request input. + */ + $method = apply_filters( 'graphql_woocommerce_shipping_zone_method_add', $method, $zone, $input ); + + return [ + 'zone_id' => $zone_id, + 'zone' => $zone, + 'method' => $method, + ]; + }; + } +} diff --git a/includes/mutation/class-shipping-zone-method-remove.php b/includes/mutation/class-shipping-zone-method-remove.php new file mode 100644 index 00000000..63f23d11 --- /dev/null +++ b/includes/mutation/class-shipping-zone-method-remove.php @@ -0,0 +1,142 @@ + self::get_input_fields(), + 'outputFields' => self::get_output_fields(), + 'mutateAndGetPayload' => self::mutate_and_get_payload(), + ] + ); + } + + /** + * Defines the mutation input field configuration + * + * @return array + */ + public static function get_input_fields() { + return [ + 'zoneId' => [ + 'type' => [ 'non_null' => 'Int' ], + 'description' => __( 'The ID of the shipping zone to delete.', 'wp-graphql-woocommerce' ), + ], + 'instanceId' => [ + 'type' => [ 'non_null' => 'Int' ], + 'description' => __( 'Shipping method instance ID', 'wp-graphql-woocommerce' ), + ], + ]; + } + + /** + * Defines the mutation output field configuration + * + * @return array + */ + public static function get_output_fields() { + return [ + 'shippingZone' => [ + 'type' => 'ShippingZone', + 'resolve' => static function ( $payload, array $args, AppContext $context ) { + return $context->get_loader( 'shipping_zone' )->load( $payload['zone_id'] ); + }, + ], + 'removedMethod' => [ + 'type' => 'ShippingZoneToShippingMethodConnectionEdge', + 'resolve' => static function ( $payload, array $args, AppContext $context ) { + return [ + // Call the Shipping_Method constructor directly because "$payload['method']" is a non-scalar value. + 'node' => new Shipping_Method( $payload['method'] ), + 'source' => $context->get_loader( 'shipping_zone' )->load( $payload['zone_id'] ), + ]; + }, + ], + ]; + } + + /** + * Defines the mutation data modification closure. + * + * @return callable + */ + public static function mutate_and_get_payload() { + return static function ( $input, AppContext $context, ResolveInfo $info ) { + if ( ! \wc_shipping_enabled() ) { + throw new UserError( __( 'Shipping is disabled.', 'wp-graphql-woocommerce' ), 404 ); + } + + if ( ! \wc_rest_check_manager_permissions( 'settings', 'delete' ) ) { + throw new UserError( __( 'Sorry, you are not allowed to remove shipping methods', 'wp-graphql-woocommerce' ), \rest_authorization_required_code() ); + } + + $instance_id = $input['instanceId']; + $zone_id = $input['zoneId']; + /** @var \WC_Shipping_Zone|false $zone */ + $zone = \WC_Shipping_Zones::get_zone_by( 'zone_id', $zone_id ); + + if ( false === $zone ) { + throw new UserError( __( 'Invalid shipping zone ID.', 'wp-graphql-woocommerce' ) ); + } + + if ( 0 === $zone->get_id() ) { + throw new UserError( __( 'Invalid shipping zone ID.', 'wp-graphql-woocommerce' ) ); + } + + $methods = $zone->get_shipping_methods(); + $method = false; + + foreach ( $methods as $shipping_method ) { + if ( $shipping_method->instance_id === $instance_id ) { + $method = $shipping_method; + break; + } + } + + if ( ! $method ) { + throw new UserError( __( 'Invalid shipping method instance ID.', 'wp-graphql-woocommerce' ) ); + } + + /** + * Filter shipping method object before it's removed from the shipping zone. + * + * @param \WC_Shipping_Method $method The shipping method to be deleted. + * @param \WC_Shipping_Zone $zone The shipping zone object. + * @param array $input Request input. + */ + $method = apply_filters( 'graphql_woocommerce_shipping_zone_method_add', $method, $zone, $input ); + + $zone->delete_shipping_method( $instance_id ); + + return [ + 'zone_id' => $zone_id, + 'zone' => $zone, + 'method' => $method, + ]; + }; + } +} diff --git a/includes/mutation/class-shipping-zone-method-update.php b/includes/mutation/class-shipping-zone-method-update.php new file mode 100644 index 00000000..06183c54 --- /dev/null +++ b/includes/mutation/class-shipping-zone-method-update.php @@ -0,0 +1,167 @@ + self::get_input_fields(), + 'outputFields' => self::get_output_fields(), + 'mutateAndGetPayload' => self::mutate_and_get_payload(), + ] + ); + } + + /** + * Defines the mutation input field configuration + * + * @return array + */ + public static function get_input_fields() { + return [ + 'zoneId' => [ + 'type' => [ 'non_null' => 'Int' ], + 'description' => __( 'The ID of the shipping zone to delete.', 'wp-graphql-woocommerce' ), + ], + 'instanceId' => [ + 'type' => [ 'non_null' => 'Int' ], + 'description' => __( 'Shipping method instance ID', 'wp-graphql-woocommerce' ), + ], + 'enabled' => [ + 'type' => 'Boolean', + 'description' => __( 'Whether the shipping method is enabled or not.', 'wp-graphql-woocommerce' ), + ], + 'order' => [ + 'type' => 'Int', + 'description' => __( 'The order of the shipping method.', 'wp-graphql-woocommerce' ), + ], + 'settings' => [ + 'type' => [ 'list_of' => 'WCSettingInput' ], + 'description' => __( 'The settings for the shipping method.', 'wp-graphql-woocommerce' ), + ], + ]; + } + + /** + * Defines the mutation output field configuration + * + * @return array + */ + public static function get_output_fields() { + return [ + 'shippingZone' => [ + 'type' => 'ShippingZone', + 'resolve' => static function ( $payload, array $args, AppContext $context ) { + return $context->get_loader( 'shipping_zone' )->load( $payload['zone_id'] ); + }, + ], + 'method' => [ + 'type' => 'ShippingZoneToShippingMethodConnectionEdge', + 'resolve' => static function ( $payload, array $args, AppContext $context ) { + return [ + // Call the Shipping_Method constructor directly because "$payload['method']" is a non-scalar value. + 'node' => new Shipping_Method( $payload['method'] ), + 'source' => $context->get_loader( 'shipping_zone' )->load( $payload['zone_id'] ), + ]; + }, + ], + ]; + } + + /** + * Defines the mutation data modification closure. + * + * @return callable + */ + public static function mutate_and_get_payload() { + return static function ( $input, AppContext $context, ResolveInfo $info ) { + if ( ! \wc_shipping_enabled() ) { + throw new UserError( __( 'Shipping is disabled.', 'wp-graphql-woocommerce' ), 404 ); + } + + if ( ! \wc_rest_check_manager_permissions( 'settings', 'edit' ) ) { + throw new UserError( __( 'Sorry, you are not allowed to edit shipping methods.', 'wp-graphql-woocommerce' ), \rest_authorization_required_code() ); + } + $instance_id = $input['instanceId']; + $zone_id = $input['zoneId']; + /** @var \WC_Shipping_Zone|false $zone */ + $zone = \WC_Shipping_Zones::get_zone_by( 'zone_id', $zone_id ); + + if ( false === $zone ) { + throw new UserError( __( 'Invalid shipping zone ID.', 'wp-graphql-woocommerce' ) ); + } + + if ( 0 === $zone->get_id() ) { + throw new UserError( __( 'Invalid shipping zone ID.', 'wp-graphql-woocommerce' ) ); + } + + $methods = $zone->get_shipping_methods(); + $method = false; + + foreach ( $methods as $shipping_method ) { + if ( $shipping_method->instance_id === $instance_id ) { + $method = $shipping_method; + break; + } + } + + if ( ! $method ) { + throw new UserError( __( 'Invalid shipping method instance ID.', 'wp-graphql-woocommerce' ) ); + } + + // Update settings. + if ( ! empty( $input['settings'] ) ) { + $method = Shipping_Mutation::set_shipping_zone_method_settings( $instance_id, $method, $input['settings'] ); + } + + // Update order. + if ( isset( $input['order'] ) ) { + $method = Shipping_Mutation::set_shipping_zone_method_order( $instance_id, $method, $input['order'] ); + } + + // Update if this method is enabled or not. + if ( isset( $input['enabled'] ) ) { + $method = Shipping_Mutation::set_shipping_zone_method_enabled( $zone_id, $instance_id, $method, $input['enabled'] ); + } + + /** + * Filter shipping method object before responding. + * + * @param \WC_Shipping_Method $method The shipping method object. + * @param \WC_Shipping_Zone $zone The shipping zone object. + * @param array $input Request input. + */ + $method = apply_filters( 'graphql_woocommerce_shipping_zone_method_update', $method, $zone, $input ); + + return [ + 'zone_id' => $zone_id, + 'zone' => $zone, + 'method' => $method, + ]; + }; + } +} diff --git a/includes/mutation/class-shipping-zone-update.php b/includes/mutation/class-shipping-zone-update.php new file mode 100644 index 00000000..0785bd09 --- /dev/null +++ b/includes/mutation/class-shipping-zone-update.php @@ -0,0 +1,128 @@ + self::get_input_fields(), + 'outputFields' => self::get_output_fields(), + 'mutateAndGetPayload' => self::mutate_and_get_payload(), + ] + ); + } + + /** + * Defines the mutation input field configuration + * + * @return array + */ + public static function get_input_fields() { + return [ + 'id' => [ + 'type' => [ 'non_null' => 'Int' ], + 'description' => __( 'The ID of the shipping zone to update.', 'wp-graphql-woocommerce' ), + ], + 'name' => [ + 'type' => 'String', + 'description' => __( 'Name of the shipping zone.', 'wp-graphql-woocommerce' ), + ], + 'order' => [ + 'type' => 'Int', + 'description' => __( 'Order of the shipping zone.', 'wp-graphql-woocommerce' ), + ], + ]; + } + + /** + * Defines the mutation output field configuration + * + * @return array + */ + public static function get_output_fields() { + return [ + 'shippingZone' => [ + 'type' => 'ShippingZone', + 'resolve' => static function ( $payload, array $args, AppContext $context ) { + return $context->get_loader( 'shipping_zone' )->load( $payload['zone_id'] ); + }, + ], + ]; + } + + /** + * Defines the mutation data modification closure. + * + * @return callable + */ + public static function mutate_and_get_payload() { + return static function ( $input, AppContext $context, ResolveInfo $info ) { + if ( ! \wc_shipping_enabled() ) { + throw new UserError( __( 'Shipping is disabled.', 'wp-graphql-woocommerce' ), 404 ); + } + + if ( ! \wc_rest_check_manager_permissions( 'settings', 'edit' ) ) { + throw new UserError( __( 'Sorry, you are not allowed to edit shipping zones', 'wp-graphql-woocommerce' ), \rest_authorization_required_code() ); + } + + $zone_id = $input['id']; + /** @var \WC_Shipping_Zone|false $zone */ + $zone = \WC_Shipping_Zones::get_zone_by( 'zone_id', $zone_id ); + + if ( false === $zone ) { + throw new UserError( __( 'Invalid shipping zone ID.', 'wp-graphql-woocommerce' ) ); + } + + if ( 0 === $zone->get_id() ) { + throw new UserError( __( 'The "locations not covered by your other zones" zone cannot be updated.', 'wp-graphql-woocommerce' ) ); + } + + $zone_changed = false; + + if ( ! empty( $input['name'] ) ) { + $zone->set_zone_name( $input['name'] ); + $zone_changed = true; + } + + if ( ! empty( $input['order'] ) ) { + $zone->set_zone_order( $input['order'] ); + $zone_changed = true; + } + + if ( $zone_changed ) { + /** + * Filter zone object before saving changes. + * + * @param \WC_Shipping_Zone $zone The response object. + * @param array $input Request input. + */ + $zone = apply_filters( 'graphql_woocommerce_shipping_zone_update', $zone, $input ); + $zone->save(); + } + + return [ 'zone_id' => $zone_id ]; + }; + } +} diff --git a/includes/mutation/class-tax-class-create.php b/includes/mutation/class-tax-class-create.php new file mode 100644 index 00000000..de64cc51 --- /dev/null +++ b/includes/mutation/class-tax-class-create.php @@ -0,0 +1,101 @@ + self::get_input_fields(), + 'outputFields' => self::get_output_fields(), + 'mutateAndGetPayload' => self::mutate_and_get_payload(), + ] + ); + } + + /** + * Defines the mutation input field configuration + * + * @return array + */ + public static function get_input_fields() { + return [ + 'name' => [ + 'type' => [ 'non_null' => 'String' ], + 'description' => __( 'Name of the tax class.', 'wp-graphql-woocommerce' ), + ], + 'slug' => [ + 'type' => 'String', + 'description' => __( 'Slug of the tax class.', 'wp-graphql-woocommerce' ), + ], + ]; + } + + /** + * Defines the mutation output field configuration + * + * @return array + */ + public static function get_output_fields() { + return [ + 'taxClass' => [ + 'type' => 'TaxClass', + 'resolve' => static function ( $payload ) { + return ! empty( $payload['taxClass'] ) ? $payload['taxClass'] : null; + }, + ], + ]; + } + + /** + * Defines the mutation data modification closure. + * + * @return callable + */ + public static function mutate_and_get_payload() { + return static function ( $input, AppContext $context, ResolveInfo $info ) { + if ( ! \wc_rest_check_manager_permissions( 'settings', 'create' ) ) { + throw new UserError( __( 'Sorry, you are not allowed to create tax classes.', 'wp-graphql-woocommerce' ), \rest_authorization_required_code() ); + } + $name = $input['name']; + $slug = ! empty( $input['slug'] ) ? $input['slug'] : ''; + + $tax_class = \WC_Tax::create_tax_class( $name, $slug ); + + if ( is_wp_error( $tax_class ) ) { + throw new UserError( $tax_class->get_error_message() ); + } + + /** + * Filter tax class object before responding. + * + * @param array $tax_class The shipping method object. + * @param array $input Request input. + */ + $tax_class = apply_filters( 'graphql_woocommerce_tax_class_create', $tax_class, $input ); + + return [ 'taxClass' => $tax_class ]; + }; + } +} diff --git a/includes/mutation/class-tax-class-delete.php b/includes/mutation/class-tax-class-delete.php new file mode 100644 index 00000000..0a033b12 --- /dev/null +++ b/includes/mutation/class-tax-class-delete.php @@ -0,0 +1,109 @@ + self::get_input_fields(), + 'outputFields' => self::get_output_fields(), + 'mutateAndGetPayload' => self::mutate_and_get_payload(), + ] + ); + } + + /** + * Defines the mutation input field configuration + * + * @return array + */ + public static function get_input_fields() { + return [ + 'slug' => [ + 'type' => [ 'non_null' => 'String' ], + 'description' => __( 'Slug of the tax class.', 'wp-graphql-woocommerce' ), + ], + ]; + } + + /** + * Defines the mutation output field configuration + * + * @return array + */ + public static function get_output_fields() { + return [ + 'taxClass' => [ + 'type' => 'TaxClass', + 'resolve' => static function ( $payload ) { + return ! empty( $payload['taxClass'] ) ? $payload['taxClass'] : null; + }, + ], + ]; + } + + /** + * Defines the mutation data modification closure. + * + * @return callable + */ + public static function mutate_and_get_payload() { + return static function ( $input, AppContext $context, ResolveInfo $info ) { + if ( ! \wc_rest_check_manager_permissions( 'settings', 'delete' ) ) { + throw new UserError( __( 'Sorry, you are not allowed to delete tax classes.', 'wp-graphql-woocommerce' ), \rest_authorization_required_code() ); + } + $slug = $input['slug']; + + /** @var array|false $tax_class */ + $tax_class = \WC_Tax::get_tax_class_by( 'slug', $slug ); + if ( ! $tax_class ) { + throw new UserError( __( 'Invalid tax class slug.', 'wp-graphql-woocommerce' ) ); + } + + /** + * Action hook before deleting tax class. + * + * @param array $tax_class The tax class object. + * @param array $input Request input. + */ + do_action( 'graphql_woocommerce_before_tax_class_delete', $tax_class, $input ); + + $deleted = \WC_Tax::delete_tax_class_by( 'slug', $slug ); + if ( ! $deleted ) { + throw new UserError( __( 'Failed to delete tax class.', 'wp-graphql-woocommerce' ) ); + } + + /** + * Filter tax class object before responding. + * + * @param array $tax_class The shipping method object. + * @param array $input Request input. + */ + $tax_class = apply_filters( 'graphql_woocommerce_tax_class_delete', $tax_class, $input ); + + return [ 'taxClass' => $tax_class ]; + }; + } +} diff --git a/includes/mutation/class-tax-rate-create.php b/includes/mutation/class-tax-rate-create.php new file mode 100644 index 00000000..b5055377 --- /dev/null +++ b/includes/mutation/class-tax-rate-create.php @@ -0,0 +1,227 @@ + self::get_input_fields(), + 'outputFields' => self::get_output_fields(), + 'mutateAndGetPayload' => [ self::class, 'mutate_and_get_payload' ], + ] + ); + } + + /** + * Defines the mutation input field configuration + * + * @return array + */ + public static function get_input_fields() { + return [ + 'country' => [ + 'type' => 'String', + 'description' => __( 'Country code for the tax rate.', 'wp-graphql-woocommerce' ), + ], + 'state' => [ + 'type' => 'String', + 'description' => __( 'State code for the tax rate.', 'wp-graphql-woocommerce' ), + ], + 'postcodes' => [ + 'type' => [ 'list_of' => 'String' ], + 'description' => __( 'Postcodes for the tax rate.', 'wp-graphql-woocommerce' ), + ], + 'cities' => [ + 'type' => [ 'list_of' => 'String' ], + 'description' => __( 'Cities for the tax rate.', 'wp-graphql-woocommerce' ), + ], + 'rate' => [ + 'type' => 'String', + 'description' => __( 'Tax rate.', 'wp-graphql-woocommerce' ), + ], + 'name' => [ + 'type' => 'String', + 'description' => __( 'Tax rate name.', 'wp-graphql-woocommerce' ), + ], + 'priority' => [ + 'type' => 'Int', + 'description' => __( 'Tax rate priority.', 'wp-graphql-woocommerce' ), + ], + 'compound' => [ + 'type' => 'Boolean', + 'description' => __( 'Whether the tax rate is compound.', 'wp-graphql-woocommerce' ), + ], + 'shipping' => [ + 'type' => 'Boolean', + 'description' => __( 'Whether the tax rate is applied to shipping.', 'wp-graphql-woocommerce' ), + ], + 'order' => [ + 'type' => 'Int', + 'description' => __( 'Tax rate order.', 'wp-graphql-woocommerce' ), + ], + 'class' => [ + 'type' => 'TaxClassEnum', + 'description' => __( 'Tax rate class.', 'wp-graphql-woocommerce' ), + ], + ]; + } + + /** + * Defines the mutation output field configuration + * + * @return array + */ + public static function get_output_fields() { + return [ + 'taxRate' => [ + 'type' => 'TaxRate', + 'resolve' => static function ( array $payload, array $args, AppContext $context ) { + return $context->get_loader( 'tax_rate' )->load( $payload['tax_rate_id'] ); + }, + ], + ]; + } + + /** + * Defines the mutation data modification closure. + * + * @param array $input Mutation input. + * @param \WPGraphQL\AppContext $context AppContext instance. + * @param \GraphQL\Type\Definition\ResolveInfo $info ResolveInfo instance. Can be + * use to get info about the current node in the GraphQL tree. + * + * @throws \GraphQL\Error\UserError Invalid ID provided | Lack of capabilities. + * + * @return array + */ + public static function mutate_and_get_payload( $input, AppContext $context, ResolveInfo $info ) { + $id = ! empty( $input['id'] ) ? $input['id'] : null; + $action = ! $id ? 'create' : 'update'; + $permission = ! $id ? 'create' : 'edit'; + if ( ! \wc_rest_check_manager_permissions( 'settings', $permission ) ) { + throw new UserError( + sprintf( + /* translators: %s: permission */ + __( 'Sorry, you are not allowed to %s tax rates.', 'wp-graphql-woocommerce' ), + $permission + ), + \rest_authorization_required_code() + ); + } + + $current = null; + if ( ! empty( $id ) ) { + /** + * @var object{ + * tax_rate_id: int, + * tax_rate_class: string, + * tax_rate_country: string, + * tax_rate_state: string, + * tax_rate: string, + * tax_rate_name: string, + * tax_rate_priority: int, + * tax_rate_compound: bool, + * tax_rate_shipping: bool, + * tax_rate_order: int, + * tax_rate_city: string, + * tax_rate_postcode: string, + * tax_rate_postcodes: string, + * tax_rate_cities: string + * } $current + */ + $current = \WC_Tax::_get_tax_rate( $id, OBJECT ); + } + + $data = []; + $fields = [ + 'country' => 'tax_rate_country', + 'state' => 'tax_rate_state', + 'rate' => 'tax_rate', + 'name' => 'tax_rate_name', + 'priority' => 'tax_rate_priority', + 'compound' => 'tax_rate_compound', + 'shipping' => 'tax_rate_shipping', + 'order' => 'tax_rate_order', + 'class' => 'tax_rate_class', + ]; + + foreach ( $fields as $key => $field ) { + if ( ! isset( $input[ $key ] ) ) { + continue; + } + + if ( $current && $current->$field === $input[ $key ] ) { + continue; + } + + switch ( $field ) { + case 'tax_rate_priority': + case 'tax_rate_compound': + case 'tax_rate_shipping': + case 'tax_rate_order': + $data[ $field ] = $input[ $key ]; + break; + case 'tax_rate_class': + $data[ $field ] = 'standard' !== $input[ $key ] ? $input[ $key ] : ''; + break; + default: + $data[ $field ] = $input[ $key ]; + break; + } + } + + /** + * Filter tax rate data before creating/updating. + * + * @param array $data The tax rate data. + * @param array $input Request input. + */ + $data = apply_filters( "graphql_woocommerce_before_tax_rate_{$action}_data", $data, $input ); + + if ( ! $id ) { + $id = \WC_Tax::_insert_tax_rate( $data ); + } else { + \WC_Tax::_update_tax_rate( $id, $data ); + } + + if ( isset( $input['cities'] ) ) { + \WC_Tax::_update_tax_rate_cities( $id, join( ';', $input['cities'] ) ); + } + + if ( isset( $input['postcodes'] ) ) { + \WC_Tax::_update_tax_rate_postcodes( $id, join( ';', $input['postcodes'] ) ); + } + + /** + * Filter tax rate object before responding. + * + * @param object $tax_rate_id The shipping method object. + * @param array $input Request input. + */ + do_action( "graphql_woocommerce_tax_rate_{$action}", $id, $input ); + + return [ 'tax_rate_id' => $id ]; + } +} diff --git a/includes/mutation/class-tax-rate-delete.php b/includes/mutation/class-tax-rate-delete.php new file mode 100644 index 00000000..5084494d --- /dev/null +++ b/includes/mutation/class-tax-rate-delete.php @@ -0,0 +1,111 @@ + self::get_input_fields(), + 'outputFields' => self::get_output_fields(), + 'mutateAndGetPayload' => self::mutate_and_get_payload(), + ] + ); + } + + /** + * Defines the mutation input field configuration + * + * @return array + */ + public static function get_input_fields() { + return [ + 'id' => [ + 'type' => [ 'non_null' => 'Int' ], + 'description' => __( 'The ID of the tax rate to update.', 'wp-graphql-woocommerce' ), + ], + ]; + } + + /** + * Defines the mutation output field configuration + * + * @return array + */ + public static function get_output_fields() { + return [ + 'taxRate' => [ + 'type' => 'TaxRate', + 'resolve' => static function ( $payload ) { + return $payload['taxRate']; + }, + ], + ]; + } + + /** + * Defines the mutation data modification closure. + * + * @return callable + */ + public static function mutate_and_get_payload() { + return static function ( $input, AppContext $context, ResolveInfo $info ) { + if ( ! \wc_rest_check_manager_permissions( 'settings', 'delete' ) ) { + throw new UserError( __( 'Sorry, you are not allowed to delete tax rates.', 'wp-graphql-woocommerce' ), \rest_authorization_required_code() ); + } + global $wpdb; + $id = $input['id']; + + $tax = $context->get_loader( 'tax_rate' )->load( $id ); + if ( ! $tax ) { + throw new UserError( __( 'Invalid tax rate ID.', 'wp-graphql-woocommerce' ) ); + } + + /** + * Action before deleting tax rate. + * + * @param object $tax_rate The tax rate object. + * @param array $input Request input. + */ + do_action( 'graphql_woocommerce_before_tax_rate_delete', $tax, $input ); + + \WC_Tax::_delete_tax_rate( $id ); + if ( 0 === $wpdb->rows_affected ) { + throw new UserError( __( 'Failed to delete tax rate.', 'wp-graphql-woocommerce' ) ); + } + + /** + * Filter tax rate object before responding. + * + * @param object $tax_rate The shipping method object. + * @param array $input Request input. + */ + $tax = apply_filters( 'graphql_woocommerce_tax_rate_delete', $tax, $input ); + + return [ + 'taxRate' => $tax, + ]; + }; + } +} diff --git a/includes/mutation/class-tax-rate-update.php b/includes/mutation/class-tax-rate-update.php new file mode 100644 index 00000000..e05eb43a --- /dev/null +++ b/includes/mutation/class-tax-rate-update.php @@ -0,0 +1,67 @@ + self::get_input_fields(), + 'outputFields' => self::get_output_fields(), + 'mutateAndGetPayload' => [ Tax_Rate_Create::class, 'mutate_and_get_payload' ], + ] + ); + } + + /** + * Defines the mutation input field configuration + * + * @return array + */ + public static function get_input_fields() { + return array_merge( + Tax_Rate_Create::get_input_fields(), + [ + 'id' => [ + 'type' => [ 'non_null' => 'Int' ], + 'description' => __( 'The ID of the tax rate to update.', 'wp-graphql-woocommerce' ), + ], + ] + ); + } + + /** + * Defines the mutation output field configuration + * + * @return array + */ + public static function get_output_fields() { + return [ + 'taxRate' => [ + 'type' => 'TaxRate', + 'resolve' => static function ( array $payload, array $args, AppContext $context ) { + return $context->get_loader( 'tax_rate' )->load( $payload['tax_rate_id'] ); + }, + ], + ]; + } +} diff --git a/includes/type/enum/class-id-type-enums.php b/includes/type/enum/class-id-type-enums.php index cd256b02..d1260b05 100644 --- a/includes/type/enum/class-id-type-enums.php +++ b/includes/type/enum/class-id-type-enums.php @@ -101,6 +101,17 @@ public static function register() { ] ); + register_graphql_enum_type( + 'ShippingZoneIdTypeEnum', + [ + 'description' => __( 'The Type of Identifier used to fetch a single Shipping Zone. Default is ID.', 'wp-graphql-woocommerce' ), + 'values' => [ + 'id' => self::get_value( 'id' ), + 'database_id' => self::get_value( 'database_id' ), + ], + ] + ); + register_graphql_enum_type( 'TaxRateIdTypeEnum', [ diff --git a/includes/type/enum/class-shipping-location-type-enum.php b/includes/type/enum/class-shipping-location-type-enum.php new file mode 100644 index 00000000..8e39e574 --- /dev/null +++ b/includes/type/enum/class-shipping-location-type-enum.php @@ -0,0 +1,34 @@ + __( 'A Shipping zone location type.', 'wp-graphql-woocommerce' ), + 'values' => [ + 'COUNTRY' => [ 'value' => 'country' ], + 'CONTINENT' => [ 'value' => 'continent' ], + 'STATE' => [ 'value' => 'state' ], + 'POSTCODE' => [ 'value' => 'postcode' ], + ], + ] + ); + } +} diff --git a/includes/type/enum/class-wc-setting-type-enum.php b/includes/type/enum/class-wc-setting-type-enum.php new file mode 100644 index 00000000..02f5c777 --- /dev/null +++ b/includes/type/enum/class-wc-setting-type-enum.php @@ -0,0 +1,41 @@ + __( 'Type of WC setting.', 'wp-graphql-woocommerce' ), + 'values' => [ + 'TEXT' => [ 'value' => 'text' ], + 'EMAIL' => [ 'value' => 'email' ], + 'NUMBER' => [ 'value' => 'number' ], + 'COLOR' => [ 'value' => 'color' ], + 'PASSWORD' => [ 'value' => 'password' ], + 'TEXTAREA' => [ 'value' => 'textarea' ], + 'SELECT' => [ 'value' => 'select' ], + 'MULTI_SELECT' => [ 'value' => 'multi_select' ], + 'RADIO' => [ 'value' => 'radio' ], + 'IMAGE_WIDTH' => [ 'value' => 'image_width' ], + 'CHECKBOX' => [ 'value' => 'checkbox' ], + ], + ] + ); + } +} diff --git a/includes/type/input/class-shipping-location-input.php b/includes/type/input/class-shipping-location-input.php new file mode 100644 index 00000000..d5d7f828 --- /dev/null +++ b/includes/type/input/class-shipping-location-input.php @@ -0,0 +1,38 @@ + __( 'Shipping lines data.', 'wp-graphql-woocommerce' ), + 'fields' => [ + 'code' => [ + 'type' => 'String', + 'description' => __( 'Shipping location code.', 'wp-graphql-woocommerce' ), + ], + 'type' => [ + 'type' => 'ShippingLocationTypeEnum', + 'description' => __( 'Shipping location type.', 'wp-graphql-woocommerce' ), + ], + ], + ] + ); + } +} diff --git a/includes/type/input/class-wc-setting-input.php b/includes/type/input/class-wc-setting-input.php new file mode 100644 index 00000000..3bf2d566 --- /dev/null +++ b/includes/type/input/class-wc-setting-input.php @@ -0,0 +1,38 @@ + __( 'WooCommerce setting input.', 'wp-graphql-woocommerce' ), + 'fields' => [ + 'id' => [ + 'type' => 'String', + 'description' => __( 'A unique identifier for the setting.', 'wp-graphql-woocommerce' ), + ], + 'value' => [ + 'type' => 'String', + 'description' => __( 'Setting value.', 'wp-graphql-woocommerce' ), + ], + ], + ] + ); + } +} diff --git a/includes/type/object/class-root-query.php b/includes/type/object/class-root-query.php index a8cddaca..bbe9b1d4 100644 --- a/includes/type/object/class-root-query.php +++ b/includes/type/object/class-root-query.php @@ -413,6 +413,10 @@ public static function register_fields() { ], ], 'resolve' => static function ( $source, array $args ) { + if ( ! \wc_rest_check_manager_permissions( 'shipping_methods', 'read' ) ) { + throw new UserError( __( 'Sorry, you cannot view shipping methods.', 'wp-graphql-woocommerce' ), \rest_authorization_required_code() ); + } + $id = isset( $args['id'] ) ? $args['id'] : null; $id_type = isset( $args['idType'] ) ? $args['idType'] : 'global_id'; @@ -434,6 +438,49 @@ public static function register_fields() { return Factory::resolve_shipping_method( $method_id ); }, ], + 'shippingZone' => [ + 'type' => 'ShippingZone', + 'description' => __( 'A shipping zone object', 'wp-graphql-woocommerce' ), + 'args' => [ + 'id' => [ + 'type' => 'ID', + 'description' => __( 'The ID for identifying the shipping zone', 'wp-graphql-woocommerce' ), + ], + 'idType' => [ + 'type' => 'ShippingZoneIdTypeEnum', + 'description' => __( 'Type of ID being used identify shipping zone', 'wp-graphql-woocommerce' ), + ], + ], + 'resolve' => static function ( $source, array $args, AppContext $context ) { + if ( ! \wc_shipping_enabled() ) { + throw new UserError( __( 'Shipping is disabled.', 'wp-graphql-woocommerce' ), 404 ); + } + + if ( ! \wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + throw new UserError( __( 'Permission denied.', 'wp-graphql-woocommerce' ), \rest_authorization_required_code() ); + } + + $id = isset( $args['id'] ) ? $args['id'] : null; + $id_type = isset( $args['idType'] ) ? $args['idType'] : 'global_id'; + + $zone_id = null; + switch ( $id_type ) { + case 'database_id': + $zone_id = $id; + break; + case 'global_id': + default: + $id_components = Relay::fromGlobalId( $id ); + if ( empty( $id_components['id'] ) || empty( $id_components['type'] ) ) { + throw new UserError( __( 'The "id" is invalid', 'wp-graphql-woocommerce' ) ); + } + $zone_id = $id_components['id']; + break; + } + + return $context->get_loader( 'shipping_zone' )->load( $zone_id ); + }, + ], 'taxRate' => [ 'type' => 'TaxRate', 'description' => __( 'A tax rate object', 'wp-graphql-woocommerce' ), @@ -448,6 +495,9 @@ public static function register_fields() { ], ], 'resolve' => static function ( $source, array $args, AppContext $context ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + throw new UserError( __( 'Sorry, you cannot view tax rates.', 'wp-graphql-woocommerce' ), \rest_authorization_required_code() ); + } $id = isset( $args['id'] ) ? $args['id'] : null; $id_type = isset( $args['idType'] ) ? $args['idType'] : 'global_id'; diff --git a/includes/type/object/class-shipping-location-type.php b/includes/type/object/class-shipping-location-type.php new file mode 100644 index 00000000..32d5e871 --- /dev/null +++ b/includes/type/object/class-shipping-location-type.php @@ -0,0 +1,41 @@ + true, + 'description' => __( 'A Shipping zone object', 'wp-graphql-woocommerce' ), + 'fields' => [ + 'code' => [ + 'type' => 'String', + 'description' => __( 'The globally unique identifier for the tax rate.', 'wp-graphql-woocommerce' ), + ], + 'type' => [ + 'type' => 'ShippingLocationTypeEnum', + 'description' => __( 'Shipping zone location name.', 'wp-graphql-woocommerce' ), + ], + ], + ] + ); + } +} diff --git a/includes/type/object/class-shipping-zone-type.php b/includes/type/object/class-shipping-zone-type.php new file mode 100644 index 00000000..1769e0ea --- /dev/null +++ b/includes/type/object/class-shipping-zone-type.php @@ -0,0 +1,140 @@ + __( 'A Shipping zone object', 'wp-graphql-woocommerce' ), + 'interfaces' => [ 'Node' ], + 'fields' => [ + 'id' => [ + 'type' => [ 'non_null' => 'ID' ], + 'description' => __( 'The globally unique identifier for the tax rate.', 'wp-graphql-woocommerce' ), + ], + 'databaseId' => [ + 'type' => 'Int', + 'description' => __( 'The ID of the customer in the database', 'wp-graphql-woocommerce' ), + ], + 'name' => [ + 'type' => 'String', + 'description' => __( 'Shipping zone name.', 'wp-graphql-woocommerce' ), + ], + 'order' => [ + 'type' => 'Int', + 'description' => __( 'Shipping zone order.', 'wp-graphql-woocommerce' ), + ], + 'locations' => [ + 'type' => [ 'list_of' => 'ShippingLocation' ], + 'description' => __( 'Shipping zone locations.', 'wp-graphql-woocommerce' ), + ], + ], + 'connections' => [ + 'methods' => [ + 'toType' => 'ShippingMethod', + 'edgeFields' => [ + 'id' => [ + 'type' => [ 'non_null' => 'ID' ], + 'description' => __( 'The globally unique identifier for the shipping method.', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $edge ) { + if ( isset( $edge['node'] ) ) { + $shipping_method = $edge['node']->as_WC_Data(); + $instance_id = $shipping_method->instance_id; + + return ! empty( $instance_id ) ? \GraphQLRelay\Relay::toGlobalId( 'shipping_zone_method', $instance_id ) : null; + } + return null; + }, + ], + 'instanceId' => [ + 'type' => 'Int', + 'description' => __( 'Shipping method instance ID.', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $edge ) { + if ( isset( $edge['node'] ) ) { + $shipping_method = $edge['node']->as_WC_Data(); + return $shipping_method->instance_id ?? null; + } + return null; + }, + ], + 'order' => [ + 'type' => 'Int', + 'description' => __( 'The order of the shipping method.', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $edge ) { + if ( isset( $edge['node'] ) ) { + $shipping_method = $edge['node']->as_WC_Data(); + return $shipping_method->method_order ?? null; + } + return null; + }, + ], + 'enabled' => [ + 'type' => 'Boolean', + 'description' => __( 'Whether the shipping method is enabled.', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $edge ) { + if ( isset( $edge['node'] ) ) { + /** @var \WC_Shipping_Method $shipping_method */ + $shipping_method = $edge['node']->as_WC_Data(); + return $shipping_method->is_enabled(); + } + return false; + }, + ], + 'settings' => [ + 'type' => [ 'list_of' => 'WCSetting' ], + 'description' => __( 'Shipping method settings.', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $edge ) { + $settings = []; + if ( isset( $edge['node'] ) ) { + $shipping_method = $edge['node']->as_WC_Data(); + $instance_settings = $shipping_method->instance_settings; + $fields = $shipping_method->instance_form_fields; + foreach ( $fields as $key => $field ) { + $default_value = ! empty( $field['default'] ) ? $field['default'] : null; + $value = ! empty( $instance_settings[ $key ] ) ? $instance_settings[ $key ] : $default_value; + $settings[] = array_merge( + $field, + [ + 'id' => $key, + 'value' => $value, + ] + ); + } + } + return $settings; + }, + ], + ], + 'resolve' => static function ( $source, array $args, AppContext $context, ResolveInfo $info ) { + $resolver = new Shipping_Method_Connection_Resolver( $source, $args, $context, $info ); + + return $resolver->get_connection(); + }, + ], + ], + ] + ); + } +} diff --git a/includes/type/object/class-tax-class-type.php b/includes/type/object/class-tax-class-type.php new file mode 100644 index 00000000..dac4f01c --- /dev/null +++ b/includes/type/object/class-tax-class-type.php @@ -0,0 +1,55 @@ + true, + 'description' => __( 'A Tax class object', 'wp-graphql-woocommerce' ), + 'interfaces' => [ 'Node' ], + 'fields' => [ + 'id' => [ + 'type' => [ 'non_null' => 'ID' ], + 'description' => __( 'The globally unique identifier for the tax class.', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source, array $args, $context, $info ) { + return ! empty( $source['slug'] ) ? \GraphQLRelay\Relay::toGlobalId( 'tax_class', $source['slug'] ) : null; + }, + ], + 'slug' => [ + 'type' => 'String', + 'description' => __( 'The globally unique identifier for the tax class.', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source, array $args, $context, $info ) { + return ! empty( $source['slug'] ) ? $source['slug'] : null; + }, + ], + 'name' => [ + 'type' => 'String', + 'description' => __( 'Tax class name.', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source, array $args, $context, $info ) { + return ! empty( $source['name'] ) ? $source['name'] : null; + }, + ], + ], + ] + ); + } +} diff --git a/includes/type/object/class-tax-rate-type.php b/includes/type/object/class-tax-rate-type.php index 7c725074..c4e969a1 100644 --- a/includes/type/object/class-tax-rate-type.php +++ b/includes/type/object/class-tax-rate-type.php @@ -43,12 +43,22 @@ public static function register() { 'description' => __( 'State code.', 'wp-graphql-woocommerce' ), ], 'postcode' => [ - 'type' => [ 'list_of' => 'String' ], - 'description' => __( 'Postcode/ZIP.', 'wp-graphql-woocommerce' ), + 'type' => 'String', + 'description' => __( 'Postcode/ZIP.', 'wp-graphql-woocommerce' ), + 'deprecationReason' => 'Use "postcodes" instead.', ], 'city' => [ + 'type' => 'String', + 'description' => __( 'City name.', 'wp-graphql-woocommerce' ), + 'deprecationReason' => 'Use "cities" instead.', + ], + 'postcodes' => [ + 'type' => [ 'list_of' => 'String' ], + 'description' => __( 'Postcodes/ZIPs.', 'wp-graphql-woocommerce' ), + ], + 'cities' => [ 'type' => [ 'list_of' => 'String' ], - 'description' => __( 'City name.', 'wp-graphql-woocommerce' ), + 'description' => __( 'City names.', 'wp-graphql-woocommerce' ), ], 'rate' => [ 'type' => 'String', diff --git a/includes/type/object/class-wc-setting-type.php b/includes/type/object/class-wc-setting-type.php new file mode 100644 index 00000000..bd634eb8 --- /dev/null +++ b/includes/type/object/class-wc-setting-type.php @@ -0,0 +1,89 @@ + true, + 'description' => __( 'A WC setting object', 'wp-graphql-woocommerce' ), + 'fields' => [ + 'id' => [ + 'type' => [ 'non_null' => 'ID' ], + 'description' => __( 'The globally unique identifier for the WC setting.', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source, array $args, $context, $info ) { + return ! empty( $source['id'] ) ? $source['id'] : null; + }, + ], + 'label' => [ + 'type' => 'String', + 'description' => __( 'A human readable label for the setting used in user interfaces.', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source, array $args, $context, $info ) { + return ! empty( $source['title'] ) ? $source['title'] : null; + }, + ], + 'description' => [ + 'type' => 'String', + 'description' => __( 'A human readable description for the setting used in user interfaces.', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source, array $args, $context, $info ) { + return ! empty( $source['description'] ) ? $source['description'] : null; + }, + ], + 'type' => [ + 'type' => 'WCSettingTypeEnum', + 'description' => __( 'Type of setting.', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source, array $args, $context, $info ) { + return ! empty( $source['type'] ) ? $source['type'] : null; + }, + ], + 'value' => [ + 'type' => 'String', + 'description' => __( 'Setting value.', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source, array $args, $context, $info ) { + return ! empty( $source['value'] ) ? $source['value'] : null; + }, + ], + 'default' => [ + 'type' => 'String', + 'description' => __( 'Default value for the setting.', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source, array $args, $context, $info ) { + return ! empty( $source['default'] ) ? $source['default'] : null; + }, + ], + 'tip' => [ + 'type' => 'String', + 'description' => __( 'Additional help text shown to the user about the setting', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source, array $args, $context, $info ) { + return ! empty( $source['desc_tip'] ) ? $source['desc_tip'] : null; + }, + ], + 'placeholder' => [ + 'type' => 'String', + 'description' => __( 'Placeholder text to be displayed in text inputs.', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source, array $args, $context, $info ) { + return ! empty( $source['placeholder'] ) ? $source['placeholder'] : null; + }, + ], + ], + ] + ); + } +} diff --git a/tests/_support/Factory/ShippingZoneFactory.php b/tests/_support/Factory/ShippingZoneFactory.php index 0d38c02f..32a0ea8c 100644 --- a/tests/_support/Factory/ShippingZoneFactory.php +++ b/tests/_support/Factory/ShippingZoneFactory.php @@ -17,10 +17,12 @@ class ShippingZoneFactory extends \WP_UnitTest_Factory_For_Thing { public function __construct( $factory = null ) { parent::__construct( $factory ); + $this->dummy = Dummy::instance(); $this->default_generation_definitions = [ - 'zone_name' => '', + 'zone_name' => 'Test Shipping Zone ' . $this->dummy->number(), + 'zone_order' => 0, ]; - $this->dummy = Dummy::instance(); + } public function create_object( $args ) { @@ -60,9 +62,37 @@ public function create_object( $args ) { } public function update_object( $object, $fields ) { - if ( ! $object instanceof \WC_Customer && 0 !== absint( $object ) ) { + if ( ! $object instanceof \WC_Shipping_Zone && 0 !== absint( $object ) ) { $object = $this->get_object_by_id( $object ); } + + if ( ! empty( $fields['countries'] ) ) { + foreach ( $fields['countries'] as $country ) { + $object->add_location( $country, 'country' ); + } + } + if ( ! empty( $fields['states'] ) ) { + foreach ( $fields['states'] as $state ) { + $object->add_location( $state, 'state' ); + } + } + if ( ! empty( $fields['postcode'] ) ) { + foreach ( $fields['postcode'] as $postcode ) { + $object->add_location( $postcode, 'postcode' ); + } + } + + if ( ! empty( $fields['shipping_method'] ) ) { + $object->add_shipping_method( $fields['shipping_method'] ); + } + + foreach ( $fields as $key => $value ) { + if ( is_callable( [ $object, "set_{$key}" ] ) ) { + $object->{"set_{$key}"}( $value ); + } + } + + return $object->save(); } public function get_object_by_id( $id ) { @@ -81,6 +111,11 @@ public function getAllZones() { return false; } + public function reloadShippingMethods() { + \WC_Cache_Helper::get_transient_version( 'shipping', true ); + WC()->shipping()->load_shipping_methods(); + } + public function createLegacyFlatRate( $args = [] ) { $flat_rate_settings = array_merge( [ @@ -95,8 +130,7 @@ public function createLegacyFlatRate( $args = [] ) { ); update_option( 'woocommerce_flat_rate_settings', $flat_rate_settings ); update_option( 'woocommerce_flat_rate', [] ); - \WC_Cache_Helper::get_transient_version( 'shipping', true ); - \WC()->shipping()->load_shipping_methods(); + $this->reloadShippingMethods(); return 'legacy_flat_rate'; } @@ -113,8 +147,7 @@ public function createLegacyFreeShipping( $args = [] ) { ); update_option( 'woocommerce_free_shipping_settings', $free_shipping_settings ); update_option( 'woocommerce_free_shipping', [] ); - \WC_Cache_Helper::get_transient_version( 'shipping', true ); - WC()->shipping()->load_shipping_methods(); + $this->reloadShippingMethods(); return 'legacy_free_shipping'; } diff --git a/tests/_support/Factory/TaxClassFactory.php b/tests/_support/Factory/TaxClassFactory.php new file mode 100644 index 00000000..8ea201dc --- /dev/null +++ b/tests/_support/Factory/TaxClassFactory.php @@ -0,0 +1,58 @@ +default_generation_definitions = [ + 'name' => '', + 'slug' => '', + ]; + $this->dummy = Dummy::instance(); + } + + public function create_object( $args = [] ) { + $name = ! empty( $args['name'] ) ? $args['name'] : 'TaxClassNo' . Dummy::instance()->number(); + $slug = ! empty( $args['slug'] ) ? $args['slug'] : ''; + + $tax_class = \WC_Tax::create_tax_class( $name, $slug ); + + if ( is_wp_error( $tax_class ) ) { + \codecept_debug( $tax_class->get_error_message() ); + throw new \Exception( $tax_class->get_error_message() ); + } + + return $tax_class; + } + + public function update_object( $object, $fields ) { + throw new \Exception( 'You doing it wrong. You can only create or delete tax classes.' ); + } + + public function get_object_by_id( $slug ) { + $tax_class = \WC_Tax::get_tax_class_by( 'slug', $slug ); + if ( ! $tax_class ) { + return null; + } + + if ( is_wp_error( $tax_class ) ) { + \codecept_debug( $tax_class->get_error_message() ); + return null; + } + + return $tax_class; + } +} diff --git a/tests/_support/TestCase/WooGraphQLTestCase.php b/tests/_support/TestCase/WooGraphQLTestCase.php index 541dcf0c..15420eb3 100644 --- a/tests/_support/TestCase/WooGraphQLTestCase.php +++ b/tests/_support/TestCase/WooGraphQLTestCase.php @@ -42,6 +42,7 @@ public function setUp(): void { 'Coupon', 'Customer', 'ShippingZone', + 'TaxClass', 'TaxRate', 'Order', 'Refund', diff --git a/tests/wpunit/ShippingMethodQueriesTest.php b/tests/wpunit/ShippingMethodQueriesTest.php index 4ed26698..786fc76d 100644 --- a/tests/wpunit/ShippingMethodQueriesTest.php +++ b/tests/wpunit/ShippingMethodQueriesTest.php @@ -2,24 +2,10 @@ use GraphQLRelay\Relay; -class ShippingMethodQueriesTest extends \Codeception\TestCase\WPTestCase { - private $shop_manager; - private $customer; - private $method; - private $helper; - - public function setUp(): void { - parent::setUp(); - - $this->shop_manager = $this->factory->user->create( [ 'role' => 'shop_manager' ] ); - $this->customer = $this->factory->user->create( [ 'role' => 'customer' ] ); - $this->helper = $this->getModule( '\Helper\Wpunit' )->shipping_method(); - $this->method = 'flat_rate'; - } +class ShippingMethodQueriesTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGraphQLTestCase { - // tests public function testShippingMethodQueryAndArgs() { - $id = Relay::toGlobalId( 'shipping_method', $this->method ); + $id = Relay::toGlobalId( 'shipping_method', 'flat_rate' ); $query = ' query( $id: ID!, $idType: ShippingMethodIdTypeEnum ) { @@ -35,61 +21,46 @@ public function testShippingMethodQueryAndArgs() { /** * Assertion One * - * Test "ID" ID type. + * Confirm permission check is working */ - $variables = [ - 'id' => $id, - 'idType' => 'ID', - ]; - $actual = graphql( - [ - 'query' => $query, - 'variables' => $variables, - ] - ); - $expected = [ 'data' => [ 'shippingMethod' => $this->helper->print_query( $this->method ) ] ]; + $variables = [ 'id' => $id ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); - // use --debug flag to view. - codecept_debug( $actual ); + $this->assertQueryError( $response ); - $this->assertEquals( $expected, $actual ); + // Login as shop manager. + $this->loginAsShopManager(); /** * Assertion Two * - * Test "DATABASE_ID" ID type. + * Test "ID" ID type. */ - $variables = [ - 'id' => $this->method, - 'idType' => 'DATABASE_ID', + $variables = [ 'id' => $id ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedField( 'shippingMethod.id', $id ), + $this->expectedField( 'shippingMethod.databaseId', 'flat_rate' ), + $this->expectedField( 'shippingMethod.title', self::NOT_NULL ), + $this->expectedField( 'shippingMethod.description', self::NOT_NULL ), ]; - $actual = graphql( - [ - 'query' => $query, - 'variables' => $variables, - ] - ); - $expected = [ 'data' => [ 'shippingMethod' => $this->helper->print_query( $this->method ) ] ]; - // use --debug flag to view. - codecept_debug( $actual ); + $this->assertQuerySuccessful( $response, $expected ); + + /** + * Assertion Three + * + * Test "DATABASE_ID" ID type. + */ + $variables = [ 'id' => 'flat_rate', 'idType' => 'DATABASE_ID' ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); - $this->assertEquals( $expected, $actual ); + $this->assertQuerySuccessful( $response, $expected ); } public function testShippingMethodsQuery() { - $wc_shipping = WC_Shipping::instance(); - $methods = array_values( - array_map( - static function ( $method ) { - return [ 'id' => Relay::toGlobalId( 'shipping_method', $method->id ) ]; - }, - $wc_shipping->get_shipping_methods() - ) - ); - $query = ' - query shippingMethodsQuery { + query { shippingMethods { nodes { id @@ -101,14 +72,28 @@ static function ( $method ) { /** * Assertion One * - * Tests query + * Confirm permission check is working */ - $actual = do_graphql_request( $query, 'shippingMethodQuery' ); - $expected = [ 'data' => [ 'shippingMethods' => [ 'nodes' => $methods ] ] ]; + $response = $this->graphql( compact( 'query' ) ); + $this->assertQuerySuccessful( $response, [ $this->expectedField( 'shippingMethods.nodes', self::IS_FALSY ) ] ); + + // Login as shop manager. + $this->loginAsShopManager(); - // use --debug flag to view. - codecept_debug( $actual ); + /** + * Assertion One + * + * Tests query + */ + $response = $this->graphql( compact( 'query' ) ); + $expected = array_map( + function( $method ) { + return $this->expectedField( 'shippingMethods.nodes.#.id', $this->toRelayId( 'shipping_method', $method->id ) ); + }, + array_values( WC_Shipping::instance()->get_shipping_methods() ) + ); + - $this->assertEquals( $expected, $actual ); + $this->assertQuerySuccessful( $response, $expected ); } } diff --git a/tests/wpunit/ShippingZoneMutationsTest.php b/tests/wpunit/ShippingZoneMutationsTest.php new file mode 100644 index 00000000..6ff065da --- /dev/null +++ b/tests/wpunit/ShippingZoneMutationsTest.php @@ -0,0 +1,676 @@ + [ + 'name' => 'Test Shipping Zone', + 'order' => 0 + ] + ]; + + // Execute the request. + $response = $this->graphql( compact( 'query', 'variables' ) ); + + // Confirm permissions error. + $this->assertQueryError( $response ); + + // Login as admin and re-execute expecting success. + $this->loginAsShopManager(); + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'createShippingZone.shippingZone', + [ + $this->expectedField( 'id', self::NOT_NULL ), + $this->expectedField( 'name', 'Test Shipping Zone' ), + $this->expectedField( 'order', 0 ), + ] + ), + ]; + + // Validate the response. + $this->assertQuerySuccessful( $response, $expected ); + } + + public function testUpdateShippingZoneMutation() { + // Create a shipping zone. + $shipping_zone_id = $this->factory->shipping_zone->create( + [ + 'zone_name' => 'Test Shipping Zone', + 'zone_order' => 0, + ] + ); + + // Prepare the request. + $query = 'mutation ($input: UpdateShippingZoneInput!) { + updateShippingZone(input: $input) { + shippingZone { + id + databaseId + name + order + } + } + }'; + + // Prepare the variables. + $variables = [ + 'input' => [ + 'id' => $shipping_zone_id, + 'name' => 'Updated Shipping Zone', + 'order' => 1 + ] + ]; + + // Execute the request. + $response = $this->graphql( compact( 'query', 'variables' ) ); + + // Confirm permissions error. + $this->assertQueryError( $response ); + + // Login as admin and re-execute expecting success. + $this->loginAsShopManager(); + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'updateShippingZone.shippingZone', + [ + $this->expectedField( 'id', $this->toRelayId( 'shipping_zone', $shipping_zone_id ) ), + $this->expectedField( 'databaseId', $shipping_zone_id ), + $this->expectedField( 'name', 'Updated Shipping Zone' ), + $this->expectedField( 'order', 1 ), + ] + ), + ]; + + // Validate the response. + $this->assertQuerySuccessful( $response, $expected ); + } + + public function testDeleteShippingZoneMutation() { + // Create a shipping zone. + $shipping_zone = $this->factory->shipping_zone->create_and_get(); + + // Prepare the request. + $query = 'mutation ($input: DeleteShippingZoneInput!) { + deleteShippingZone(input: $input) { + shippingZone { + id + databaseId + name + order + } + } + }'; + + // Prepare the variables. + $variables = [ + 'input' => [ + 'id' => $shipping_zone->get_id() + ] + ]; + + // Execute the request. + $response = $this->graphql( compact( 'query', 'variables' ) ); + + // Confirm permissions error. + $this->assertQueryError( $response ); + + // Login as admin and re-execute expecting success. + $this->loginAsShopManager(); + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'deleteShippingZone.shippingZone', + [ + $this->expectedField( 'id', $this->toRelayId( 'shipping_zone', $shipping_zone->get_id() ) ), + $this->expectedField( 'databaseId', $shipping_zone->get_id() ), + $this->expectedField( 'name', $shipping_zone->get_zone_name() ), + $this->expectedField( 'order', $shipping_zone->get_zone_order() ), + ] + ), + ]; + + // Validate the response. + $this->assertQuerySuccessful( $response, $expected ); + } + + public function testUpdateShippingZoneLocationsMutation() { + // Create a shipping zone. + $shipping_zone_id = $this->factory->shipping_zone->create(); + + // Prepare the request. + $query = 'mutation ($input: UpdateShippingZoneLocationsInput!) { + updateShippingZoneLocations(input: $input) { + shippingZone { + id + locations { + code + type + } + } + locations { + code + type + } + } + }'; + + // Prepare the variables. + $variables = [ + 'input' => [ + 'zoneId' => $shipping_zone_id, + 'locations' => [ + [ + 'code' => 'US', + ], + [ + 'code' => 'CALIFORNIA', + 'type' => 'STATE', + ], + [ + 'code' => '12345', + 'type' => 'POSTCODE', + ], + [ + 'code' => 'NA', + 'type' => 'CONTINENT', + ], + ] + ] + ]; + + // Execute the request. + $response = $this->graphql( compact( 'query', 'variables' ) ); + + // Confirm permissions error. + $this->assertQueryError( $response ); + + // Login as admin and re-execute expecting success. + $this->loginAsShopManager(); + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'updateShippingZoneLocations.shippingZone', + [ + $this->expectedField( 'id', $this->toRelayId( 'shipping_zone', $shipping_zone_id ) ), + $this->expectedObject( + 'locations.0', + [ + $this->expectedField( 'code', 'US' ), + $this->expectedField( 'type', 'COUNTRY' ) + ] + ), + $this->expectedObject( + 'locations.1', + [ + $this->expectedField( 'code', 'CALIFORNIA' ), + $this->expectedField( 'type', 'STATE' ) + ] + ), + $this->expectedObject( + 'locations.2', + [ + $this->expectedField( 'code', '12345' ), + $this->expectedField( 'type', 'POSTCODE' ) + ] + ), + $this->expectedObject( + 'locations.3', + [ + $this->expectedField( 'code', 'NA' ), + $this->expectedField( 'type', 'CONTINENT' ) + ] + ) + ] + ), + $this->expectedObject( + 'updateShippingZoneLocations.locations.0', + [ + $this->expectedField( 'code', 'US' ), + $this->expectedField( 'type', 'COUNTRY' ) + ] + ), + $this->expectedObject( + 'updateShippingZoneLocations.locations.1', + [ + $this->expectedField( 'code', 'CALIFORNIA' ), + $this->expectedField( 'type', 'STATE' ) + ] + ), + $this->expectedObject( + 'updateShippingZoneLocations.locations.2', + [ + $this->expectedField( 'code', '12345' ), + $this->expectedField( 'type', 'POSTCODE' ) + ] + ), + $this->expectedObject( + 'updateShippingZoneLocations.locations.3', + [ + $this->expectedField( 'code', 'NA' ), + $this->expectedField( 'type', 'CONTINENT' ) + ] + ) + ]; + + // Validate the response. + $this->assertQuerySuccessful( $response, $expected ); + } + + public function testClearShippingZoneLocationsMutation() { + // Create a shipping zone. + $shipping_zone = $this->factory->shipping_zone->create_and_get(); + + // Add a location to the shipping zone. + $shipping_zone->add_location( 'US', 'country' ); + $shipping_zone->save(); + + // Prepare the request. + $query = 'mutation ($input: ClearShippingZoneLocationsInput!) { + clearShippingZoneLocations(input: $input) { + shippingZone { + id + locations { + code + } + } + removedLocations { + code + type + } + } + }'; + + // Prepare the variables. + $variables = [ + 'input' => [ 'zoneId' => $shipping_zone->get_id() ] + ]; + + // Execute the request. + $response = $this->graphql( compact( 'query', 'variables' ) ); + + // Confirm permissions error. + $this->assertQueryError( $response ); + + // Login as admin and re-execute expecting success. + $this->loginAsShopManager(); + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'clearShippingZoneLocations.shippingZone', + [ + $this->expectedField( 'id', $this->toRelayId( 'shipping_zone', $shipping_zone->get_id() ) ), + $this->expectedField( 'locations', self::IS_FALSY ) + ] + ), + $this->expectedObject( + 'clearShippingZoneLocations.removedLocations.0', + [ + $this->expectedField( 'code', 'US' ), + $this->expectedField( 'type', 'COUNTRY' ) + ] + ) + ]; + + // Validate the response. + $this->assertQuerySuccessful( $response, $expected ); + } + + public function testAddMethodToShippingZoneMutation() { + // Create a shipping zone. + $shipping_zone_id = $this->factory->shipping_zone->create(); + + // Prepare the request. + $query = 'mutation ($input: AddMethodToShippingZoneInput!) { + addMethodToShippingZone(input: $input) { + shippingZone { + id + methods { + edges { + id + instanceId + order + enabled + settings { + id + label + description + type + value + default + tip + placeholder + } + node { + id + databaseId + title + description + } + } + } + } + method { + id + instanceId + order + enabled + settings { + id + label + description + type + value + default + tip + placeholder + } + node { + id + databaseId + title + description + } + } + } + }'; + + // Prepare the variables. + $variables = [ + 'input' => [ + 'zoneId' => $shipping_zone_id, + 'methodId' => 'flat_rate', + 'order' => 0, + 'enabled' => true, + 'settings' => [ + [ + 'id' => 'cost', + 'value' => '10' + ] + ] + ] + ]; + + // Execute the request. + $response = $this->graphql( compact( 'query', 'variables' ) ); + + // Confirm permissions error. + $this->assertQueryError( $response ); + + // Login as admin and re-execute expecting success. + $this->loginAsShopManager(); + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'addMethodToShippingZone.shippingZone', + [ + $this->expectedField( 'id', $this->toRelayId( 'shipping_zone', $shipping_zone_id ) ), + $this->expectedNode( + 'methods.edges', + [ + $this->expectedField( 'id', self::NOT_NULL ), + $this->expectedField( 'instanceId', self::NOT_NULL ), + $this->expectedField( 'order', 0 ), + $this->expectedField( 'enabled', true ), + $this->expectedNode( + 'settings', + [ + $this->expectedField( 'id', 'cost' ), + $this->expectedField( 'value', '10' ) + ] + ), + $this->expectedObject( + 'node', + [ + $this->expectedField( 'id', $this->toRelayId( 'shipping_method', 'flat_rate' ) ), + $this->expectedField( 'databaseId', self::NOT_NULL ), + $this->expectedField( 'title', self::NOT_NULL ), + ] + ) + ], + 0 + ), + ] + ), + $this->expectedObject( + 'addMethodToShippingZone.method', + [ + $this->expectedField( 'id', self::NOT_NULL ), + $this->expectedField( 'instanceId', self::NOT_NULL ), + $this->expectedField( 'order', 0 ), + $this->expectedField( 'enabled', true ), + $this->expectedNode( + 'settings', + [ + $this->expectedField( 'id', 'cost' ), + $this->expectedField( 'value', '10' ) + ] + ), + $this->expectedObject( + 'node', + [ + $this->expectedField( 'id', $this->toRelayId( 'shipping_method', 'flat_rate' ) ), + $this->expectedField( 'databaseId', self::NOT_NULL ), + $this->expectedField( 'title', self::NOT_NULL ), + ] + ) + ] + ) + ]; + + // Validate the response. + $this->assertQuerySuccessful( $response, $expected ); + } + + public function testUpdateMethodOnShippingZoneMutation() { + // Create a shipping zone. + $shipping_zone = $this->factory->shipping_zone->create_and_get(); + $instance_id = $shipping_zone->add_shipping_method( 'flat_rate' ); + + // Prepare the request. + $query = 'mutation ($input: UpdateMethodOnShippingZoneInput!) { + updateMethodOnShippingZone(input: $input) { + shippingZone { + id + methods { + edges { + id + instanceId + order + enabled + settings { + id + label + description + type + value + default + tip + placeholder + } + node { + id + title + description + } + } + } + } + method { + id + instanceId + order + enabled + settings { + id + label + description + type + value + default + tip + placeholder + } + node { + id + title + description + } + } + } + }'; + + // Prepare the variables. + $variables = [ + 'input' => [ + 'zoneId' => $shipping_zone->get_id(), + 'instanceId' => $instance_id, + 'settings' => [ + [ + 'id' => 'cost', + 'value' => '10' + ] + ], + ] + ]; + + // Execute the request. + $response = $this->graphql( compact( 'query', 'variables' ) ); + + // Confirm permissions error. + $this->assertQueryError( $response ); + + // Login as admin and re-execute expecting success. + $this->loginAsShopManager(); + $response = $this->graphql( compact( 'query', 'variables' ) ); + + $expected = [ + $this->expectedObject( + 'updateMethodOnShippingZone.shippingZone', + [ + $this->expectedField( 'id', $this->toRelayId( 'shipping_zone', $shipping_zone->get_id() ) ), + $this->expectedNode( + 'methods.edges', + [ + $this->expectedField( 'id', $this->toRelayId( 'shipping_zone_method', $instance_id ) ), + $this->expectedField( 'instanceId', $instance_id ), + $this->expectedField( 'order', self::NOT_NULL ), + $this->expectedField( 'enabled', true ), + $this->expectedNode( + 'settings', + [ + $this->expectedField( 'id', 'cost' ), + $this->expectedField( 'value', '10' ) + ] + ), + $this->expectedField( 'node.id', $this->toRelayId( 'shipping_method', 'flat_rate' ) ), + ], + 0 + ), + ] + ), + $this->expectedObject( + 'updateMethodOnShippingZone.method', + [ + $this->expectedField( 'id', $this->toRelayId( 'shipping_zone_method', $instance_id ) ), + $this->expectedField( 'instanceId', $instance_id ), + $this->expectedField( 'order', self::NOT_NULL ), + $this->expectedField( 'enabled', true ), + $this->expectedNode( + 'settings', + [ + $this->expectedField( 'id', 'cost' ), + $this->expectedField( 'value', '10' ) + ] + ), + $this->expectedField( 'node.id', $this->toRelayId( 'shipping_method', 'flat_rate' ) ), + ] + ) + ]; + + // Validate the response. + $this->assertQuerySuccessful( $response, $expected ); + } + + public function testRemoveMethodFromShippingZoneMutation() { + // Create a shipping zone and add the shipping method. + $shipping_zone = $this->factory->shipping_zone->create_and_get(); + $instance_id = $shipping_zone->add_shipping_method( 'flat_rate' ); + $shipping_method = new \WC_Shipping_Flat_Rate( $instance_id ); + + // Prepare the request. + $query = 'mutation ($input: RemoveMethodFromShippingZoneInput!) { + removeMethodFromShippingZone(input: $input) { + shippingZone { + id + methods { + edges { + order + enabled + node { id } + } + } + } + removedMethod { + order + enabled + node { id } + } + } + }'; + + // Prepare the variables. + $variables = [ + 'input' => [ + 'zoneId' => $shipping_zone->get_id(), + 'instanceId' => $instance_id + ] + ]; + + // Execute the request. + $response = $this->graphql( compact( 'query', 'variables' ) ); + + // Confirm permissions error. + $this->assertQueryError( $response ); + + // Login as admin and re-execute expecting success. + $this->loginAsShopManager(); + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'removeMethodFromShippingZone.shippingZone', + [ + $this->expectedField( 'id', $this->toRelayId( 'shipping_zone', $shipping_zone->get_id() ) ), + $this->expectedField( 'methods.edges', self::IS_FALSY ), + ] + ), + $this->expectedObject( + 'removeMethodFromShippingZone.removedMethod', + [ + $this->expectedField( 'enabled', $shipping_method->is_enabled() ), + $this->expectedNode( + 'node', + [ $this->expectedField( 'id', $this->toRelayId( 'shipping_method', $shipping_method->id ) ) ] + ) + ] + ), + ]; + + // Validate the response. + $this->assertQuerySuccessful( $response, $expected ); + } +} diff --git a/tests/wpunit/ShippingZoneQueriesTest.php b/tests/wpunit/ShippingZoneQueriesTest.php new file mode 100644 index 00000000..34bc934e --- /dev/null +++ b/tests/wpunit/ShippingZoneQueriesTest.php @@ -0,0 +1,168 @@ +factory->shipping_zone->create_and_get(); + + // Add location. + $shipping_zone->add_location( 'US', 'country' ); + $shipping_zone->save(); + + + // Add shipping method. + $instance_id = $shipping_zone->add_shipping_method( 'flat_rate' ); + $shipping_method = null; + foreach ( $shipping_zone->get_shipping_methods() as $method ) { + if ( $method->instance_id === $instance_id ) { + $shipping_method = $method; + break; + } + } + $instance_settings = $shipping_method->instance_settings; + $instance_settings['cost'] = 10.00; + update_option( $shipping_method->get_instance_option_key(), $instance_settings ); + + // Prepare the request. + $query = 'query ($id: ID!) { + shippingZone(id: $id) { + id + name + order + locations { + code + type + } + methods { + edges { + id + instanceId + order + enabled + settings { + id + label + description + type + value + default + tip + placeholder + } + node { + id + title + description + } + } + } + } + }'; + + // Prepare the variables. + $variables = [ 'id' => $this->toRelayId( 'shipping_zone', $shipping_zone->get_id() ) ]; + + // Execute the request expecting failure. + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQueryError( $response ); + + // Login as shop manager. + $this->loginAsShopManager(); + + // Execute the request expecting success. + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'shippingZone', + [ + $this->expectedField( 'id', $this->toRelayId( 'shipping_zone', $shipping_zone->get_id() ) ), + $this->expectedField( 'name', $shipping_zone->get_zone_name() ), + $this->expectedField( 'order', $shipping_zone->get_zone_order() ), + $this->expectedNode( + 'locations', + [ + $this->expectedField( 'code', 'US' ), + $this->expectedField( 'type', 'COUNTRY' ) + ] + ), + $this->expectedNode( + 'methods.edges', + [ + $this->expectedField( 'id', $this->toRelayId( 'shipping_zone_method', $instance_id ) ), + $this->expectedField( 'instanceId', $instance_id ), + $this->expectedField( 'order', $shipping_method->method_order ), + $this->expectedField( 'enabled', $shipping_method->is_enabled() ), + $this->expectedNode( + 'settings', + [ + $this->expectedField( 'id', 'cost' ), + $this->expectedField( 'label', 'Cost' ), + $this->expectedField( 'description', self::NOT_FALSY ), + $this->expectedField( 'type', 'TEXT' ), + $this->expectedField( 'value', '10' ), + $this->expectedField( 'default', self::IS_NULL ), + $this->expectedField( 'placeholder', self::IS_NULL ), + ], + ) + ], + 0 + ) + ] + ) + ]; + + // Validate the response. + $this->assertQuerySuccessful( $response, $expected ); + } + + public function testShippingZonesQuery() { + // Create a shipping zones. + $shipping_zones = $this->factory->shipping_zone->create_many( 3 ); + + // Prepare the request. + $query = 'query { + shippingZones { + nodes { + id + } + } + }'; + + /** + * Assertion One + * + * Confirm permission check is working + */ + $response = $this->graphql( compact( 'query' ) ); + $this->assertQuerySuccessful( $response, [ $this->expectedField( 'shippingZones.nodes', self::IS_FALSY ) ] ); + + // Login as shop manager. + $this->loginAsShopManager(); + + // Execute the request. + $response = $this->graphql( compact( 'query' ) ); + $expected = [ + $this->expectedNode( + 'shippingZones.nodes', + [ + $this->expectedField( 'id', $this->toRelayId( 'shipping_zone', $shipping_zones[0] ) ) + ] + ), + $this->expectedNode( + 'shippingZones.nodes', + [ + $this->expectedField( 'id', $this->toRelayId( 'shipping_zone', $shipping_zones[1] ) ) + ] + ), + $this->expectedNode( + 'shippingZones.nodes', + [ + $this->expectedField( 'id', $this->toRelayId( 'shipping_zone', $shipping_zones[2] ) ) + ] + ) + ]; + + // Validate the response. + $this->assertQuerySuccessful( $response, $expected ); + } +} diff --git a/tests/wpunit/TaxClassMutationsTest.php b/tests/wpunit/TaxClassMutationsTest.php new file mode 100644 index 00000000..a272902e --- /dev/null +++ b/tests/wpunit/TaxClassMutationsTest.php @@ -0,0 +1,93 @@ + [ + 'name' => 'Test Tax Class', + 'slug' => 'test-tax-class' + ] + ]; + + // Execute the request expecting failure due to missing permissions. + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQueryError( $response ); + + // Login as shop manager. + $this->loginAsShopManager(); + + // Execute the request. + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'createTaxClass.taxClass', + [ + $this->expectedField( 'name', 'Test Tax Class' ), + $this->expectedField( 'slug', 'test-tax-class' ) + ] + ), + ]; + + // Validate the response. + $this->assertQuerySuccessful( $response, $expected ); + } + + public function testDeleteTaxClassMutation() { + // Create a tax class. + $tax_class = $this->factory->tax_class->create(); + + // Prepare the request. + $query = 'mutation ($input: DeleteTaxClassInput!) { + deleteTaxClass(input: $input) { + taxClass { + name + slug + } + } + }'; + + // Prepare the variables. + $variables = [ + 'input' => [ + 'slug' => $tax_class['slug'] + ] + ]; + + // Execute the request expecting failure due to missing permissions. + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQueryError( $response ); + + // Login as shop manager. + $this->loginAsShopManager(); + + // Execute the request. + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'deleteTaxClass.taxClass', + [ + $this->expectedField( 'name', $tax_class['name'] ), + $this->expectedField( 'slug', $tax_class['slug'] ) + ] + ), + ]; + + // Validate the response. + $this->assertQuerySuccessful( $response, $expected ); + + // Ensure the tax class was deleted. + $tax_class = $this->factory->tax_class->get_object_by_id( $tax_class['slug'] ); + $this->assertNull( $tax_class ); + } +} diff --git a/tests/wpunit/TaxClassQueriesTest.php b/tests/wpunit/TaxClassQueriesTest.php new file mode 100644 index 00000000..cdc3052a --- /dev/null +++ b/tests/wpunit/TaxClassQueriesTest.php @@ -0,0 +1,47 @@ +factory->tax_class->create_many( 2 ); + + // Prepare the request. + $query = '{ + taxClasses { + nodes { + name + slug + } + } + }'; + + // Execute the request expecting failure due to missing permissions. + $response = $this->graphql( compact( 'query' ) ); + $this->assertQuerySuccessful( $response, [ $this->expectedField( 'taxClasses.nodes', self::IS_FALSY ) ] ); + + // Login as shop manager. + $this->loginAsShopManager(); + + // Execute the request. + $response = $this->graphql( compact( 'query' ) ); + $expected = [ + $this->expectedNode( + 'taxClasses.nodes', + [ + $this->expectedField( 'name', $tax_classes[0]['name'] ), + $this->expectedField( 'slug', $tax_classes[0]['slug'] ), + ] + ), + $this->expectedNode( + 'taxClasses.nodes', + [ + $this->expectedField( 'name', $tax_classes[1]['name'] ), + $this->expectedField( 'slug', $tax_classes[1]['slug'] ), + ] + ), + ]; + + // Validate the response. + $this->assertQuerySuccessful( $response, $expected ); + } +} diff --git a/tests/wpunit/TaxRateMutationsTest.php b/tests/wpunit/TaxRateMutationsTest.php new file mode 100644 index 00000000..c91338ac --- /dev/null +++ b/tests/wpunit/TaxRateMutationsTest.php @@ -0,0 +1,221 @@ +factory->tax_class->create(); + + // Prepare the request. + $query = 'mutation ($input: CreateTaxRateInput!) { + createTaxRate(input: $input) { + taxRate { + id + rate + country + state + postcode + city + postcodes + cities + priority + compound + shipping + order + class + } + } + }'; + + // Prepare the variables. + $variables = [ + 'input' => [ + 'rate' => '10', + 'country' => 'US', + 'state' => 'CA', + 'postcodes' => [ '12345', '67890' ], + 'cities' => [ 'Los Angeles', 'San Francisco' ], + 'priority' => 1, + 'compound' => false, + 'shipping' => false, + 'order' => 0, + 'class' => WPEnumType::get_safe_name( $tax_class['slug'] ) + ] + ]; + + // Execute the request expecting failure due to missing permissions. + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQueryError( $response ); + + // Login as shop manager. + $this->loginAsShopManager(); + + // Execute the request. + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'createTaxRate.taxRate', + [ + $this->expectedField( 'id', self::NOT_NULL ), + $this->expectedField( 'rate', self::NOT_FALSY ), + $this->expectedField( 'country', 'US' ), + $this->expectedField( 'state', 'CA' ), + $this->expectedField( 'postcode', '67890' ), + $this->expectedField( 'postcodes.0', '12345' ), + $this->expectedField( 'postcodes.1', '67890' ), + $this->expectedField( 'city', 'SAN FRANCISCO' ), + $this->expectedField( 'cities.0', 'LOS ANGELES' ), + $this->expectedField( 'cities.1', 'SAN FRANCISCO' ), + $this->expectedField( 'priority', 1 ), + $this->expectedField( 'compound', false ), + $this->expectedField( 'shipping', false ), + $this->expectedField( 'order', 0 ), + $this->expectedField( 'class', WPEnumType::get_safe_name( $tax_class['slug'] ) ) + ] + ) + ]; + + // Validate the response. + $this->assertQuerySuccessful( $response, $expected ); + } + + public function testUpdateTaxRateMutation() { + // Create a tax class. + $old_tax_class = $this->factory->tax_class->create(); + $new_tax_class = $this->factory->tax_class->create(); + + // Create a tax rate. + $tax_rate_id = $this->factory->tax_rate->create( [ 'class' => $old_tax_class['slug'] ] ); + + // Prepare the request. + $query = 'mutation ($input: UpdateTaxRateInput!) { + updateTaxRate(input: $input) { + taxRate { + id + rate + country + state + postcodes + cities + priority + compound + shipping + order + class + } + } + }'; + + // Prepare the variables. + $variables = [ + 'input' => [ + 'id' => $tax_rate_id, + 'rate' => '20', + 'country' => 'US', + 'state' => 'NY', + 'postcodes' => [ '54321', '09876' ], + 'cities' => [ 'New York', 'Buffalo' ], + 'priority' => 2, + 'compound' => true, + 'shipping' => true, + 'order' => 1, + 'class' => WPEnumType::get_safe_name( $new_tax_class['slug'] ) + ] + ]; + + // Execute the request expecting failure due to missing permissions. + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQueryError( $response ); + + // Login as shop manager. + $this->loginAsShopManager(); + + // Execute the request. + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'updateTaxRate.taxRate', + [ + $this->expectedField( 'id', $this->toRelayId( 'tax_rate', $tax_rate_id ) ), + $this->expectedField( 'rate', self::NOT_FALSY ), + $this->expectedField( 'country', 'US' ), + $this->expectedField( 'state', 'NY' ), + $this->expectedField( 'postcodes.0', '54321' ), + $this->expectedField( 'postcodes.1', '09876' ), + $this->expectedField( 'cities.0', 'NEW YORK' ), + $this->expectedField( 'cities.1', 'BUFFALO' ), + $this->expectedField( 'priority', 2 ), + $this->expectedField( 'compound', true ), + $this->expectedField( 'shipping', true ), + $this->expectedField( 'order', 1 ), + $this->expectedField( 'class', WPEnumType::get_safe_name( $new_tax_class['slug'] ) ), + ] + ) + ]; + + // Validate the response. + $this->assertQuerySuccessful( $response, $expected ); + } + + public function testDeleteTaxRateMutation() { + // Create a tax rate. + $tax_rate = $this->factory->tax_rate->create_and_get( + [ + 'rate' => '30', + 'country' => 'US', + 'state' => 'TX', + 'class' => 'zero-rate', + ] + ); + + // Prepare the request. + $query = 'mutation ($input: DeleteTaxRateInput!) { + deleteTaxRate(input: $input) { + taxRate { + id + rate + country + state + class + } + } + }'; + + // Prepare the variables. + $variables = [ + 'input' => [ + 'id' => absint( $tax_rate->tax_rate_id ), + ] + ]; + + // Execute the request expecting failure due to missing permissions. + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQueryError( $response ); + + // Login as shop manager. + $this->loginAsShopManager(); + + // Execute the request. + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'deleteTaxRate.taxRate', + [ + $this->expectedField( 'id', $this->toRelayId( 'tax_rate', $tax_rate->tax_rate_id ) ), + $this->expectedField( 'rate', self::NOT_FALSY ), + $this->expectedField( 'country', 'US' ), + $this->expectedField( 'state', 'TX' ), + $this->expectedField( 'class', 'ZERO_RATE' ), + ] + ) + ]; + + // Validate the response. + $this->assertQuerySuccessful( $response, $expected ); + + // Ensure the tax rate has been deleted. + $tax_rate = $this->factory->tax_rate->get_object_by_id( $tax_rate->tax_rate_id ); + $this->assertNull( $tax_rate ); + } +} diff --git a/tests/wpunit/TaxRateQueriesTest.php b/tests/wpunit/TaxRateQueriesTest.php index 00774d95..c0552798 100644 --- a/tests/wpunit/TaxRateQueriesTest.php +++ b/tests/wpunit/TaxRateQueriesTest.php @@ -9,8 +9,8 @@ public function expectedTaxRateData( $rate_id ) { $this->expectedField( 'taxRate.databaseId', absint( $rate->tax_rate_id ) ), $this->expectedField( 'taxRate.country', ! empty( $rate->tax_rate_country ) ? $rate->tax_rate_country : self::IS_NULL ), $this->expectedField( 'taxRate.state', ! empty( $rate->tax_rate_state ) ? $rate->tax_rate_state : self::IS_NULL ), - $this->expectedField( 'taxRate.postcode', ! empty( $rate->tax_rate_postcode ) ? $rate->tax_rate_postcode : [ '*' ] ), - $this->expectedField( 'taxRate.city', ! empty( $rate->tax_rate_city ) ? $rate->tax_rate_city : [ '*' ] ), + $this->expectedField( 'taxRate.postcode', ! empty( $rate->tax_rate_postcode ) ? $rate->tax_rate_postcode : '*' ), + $this->expectedField( 'taxRate.city', ! empty( $rate->tax_rate_city ) ? $rate->tax_rate_city : '*' ), $this->expectedField( 'taxRate.rate', ! empty( $rate->tax_rate ) ? $rate->tax_rate : self::IS_NULL ), $this->expectedField( 'taxRate.name', ! empty( $rate->tax_rate_name ) ? $rate->tax_rate_name : self::IS_NULL ), $this->expectedField( 'taxRate.priority', absint( $rate->tax_rate_priority ) ), @@ -50,6 +50,14 @@ class } '; + // Execute the request expecting failure due to missing permissions. + $variables = [ 'id' => $this->toRelayId( 'tax_rate', $rate ) ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQueryError( $response ); + + // Login as shop manager. + $this->loginAsShopManager(); + /** * Assertion One * @@ -119,6 +127,13 @@ class } '; + // Execute the request expecting failure due to missing permissions. + $response = $this->graphql( compact( 'query' ) ); + $this->assertQuerySuccessful( $response, [ $this->expectedField( 'taxRates.nodes', self::IS_FALSY ) ] ); + + // Login as shop manager. + $this->loginAsShopManager(); + /** * Assertion One *