diff --git a/includes/type/object/class-cart-type.php b/includes/type/object/class-cart-type.php index d3091afe..1e12b773 100644 --- a/includes/type/object/class-cart-type.php +++ b/includes/type/object/class-cart-type.php @@ -28,6 +28,7 @@ class Cart_Type { self::register_cart_fee(); self::register_cart_tax(); self::register_applied_coupon(); + self::register_cart_tax_line(); self::register_cart(); } @@ -372,11 +373,71 @@ class Cart_Type { return ! empty( $applied_coupons ) ? $applied_coupons : null; }, ], + 'taxLines' => [ + 'type' => [ 'list_of' => 'CartTaxLine' ], + 'description' => __( 'Cart tax lines itemized', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $cart ) { + if ( 'itemized' !== get_option( 'woocommerce_tax_total_display' ) ) { + return []; + } + + $cart_tax_totals = $cart->get_tax_totals(); + $decimals = wc_get_price_decimals(); + $tax_lines = []; + + foreach ( $cart_tax_totals as $cart_tax_total ) { + $tax_lines[] = [ + 'name' => $cart_tax_total->label, + 'price' => number_format( $cart_tax_total->amount, $decimals ), + 'rate' => \WC_Tax::get_rate_percent( $cart_tax_total->tax_rate_id ), + ]; + } + + return $tax_lines; + }, + ], ], $other_fields ); } + /** + * Registers the "CartTaxLine" type. + * + * @return void + */ + public static function register_cart_tax_line() { + register_graphql_object_type( + 'CartTaxLine', + [ + 'description' => __( 'The cart tax line object', 'wp-graphql-woocommerce' ), + 'fields' => [ + 'name' => [ + 'type' => 'String', + 'description' => __( 'Tax line name', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source ) { + return ! empty( $source['name'] ) ? $source['name'] : null; + }, + ], + 'price' => [ + 'type' => 'String', + 'description' => __( 'Tax line price', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source ) { + return ! empty( $source['price'] ) ? wc_graphql_price( $source['price'] ) : null; + }, + ], + 'rate' => [ + 'type' => 'String', + 'description' => __( 'Tax line rate', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source ) { + return ! empty( $source['rate'] ) ? $source['rate'] : null; + }, + ], + ], + ] + ); + } + /** * Returns the "Cart" type connections. * diff --git a/includes/type/object/class-shipping-rate-type.php b/includes/type/object/class-shipping-rate-type.php index 6afc7ab1..a1ac7a1a 100644 --- a/includes/type/object/class-shipping-rate-type.php +++ b/includes/type/object/class-shipping-rate-type.php @@ -54,10 +54,70 @@ class Shipping_Rate_Type { }, ], 'cost' => [ - 'type' => 'Float', - 'description' => __( 'Shipping rate cost', 'wp-graphql-woocommerce' ), - 'resolve' => static function ( $source ) { - return ! is_null( $source->get_cost() ) ? $source->get_cost() : null; + 'type' => 'String', + 'description' => __( 'Shipping rate cost. Includes tax when woocommerce_tax_display_cart is set to incl.', 'wp-graphql-woocommerce' ), + 'args' => [ + 'format' => [ + 'type' => 'PricingFieldFormatEnum', + 'description' => __( 'Format of the price', 'wp-graphql-woocommerce' ), + ], + ], + 'resolve' => static function ( $source, array $args ) { + $cost = $source->get_cost(); + if ( is_null( $cost ) ) { + return null; + } + + if ( 'incl' === get_option( 'woocommerce_tax_display_cart' ) ) { + $cost = floatval( $cost ) + floatval( $source->get_shipping_tax() ); + } + + if ( isset( $args['format'] ) && 'raw' === $args['format'] ) { + return $cost; + } + + return \wc_graphql_price( strval( $cost ) ); + }, + ], + 'subtotal' => [ + 'type' => 'String', + 'description' => __( 'Shipping rate cost before tax.', 'wp-graphql-woocommerce' ), + 'args' => [ + 'format' => [ + 'type' => 'PricingFieldFormatEnum', + 'description' => __( 'Format of the price', 'wp-graphql-woocommerce' ), + ], + ], + 'resolve' => static function ( $source, array $args ) { + $cost = $source->get_cost(); + if ( is_null( $cost ) ) { + return null; + } + + if ( isset( $args['format'] ) && 'raw' === $args['format'] ) { + return $cost; + } + + return \wc_graphql_price( $cost ); + }, + ], + 'taxTotal' => [ + 'type' => 'String', + 'description' => __( 'Shipping rate tax total.', 'wp-graphql-woocommerce' ), + 'args' => [ + 'format' => [ + 'type' => 'PricingFieldFormatEnum', + 'description' => __( 'Format of the price', 'wp-graphql-woocommerce' ), + ], + ], + 'resolve' => static function ( $source, array $args ) { + $tax = $source->get_shipping_tax(); + + if ( isset( $args['format'] ) && 'raw' === $args['format'] ) { + return $tax; + } + + return \wc_graphql_price( $tax ); }, ], ], diff --git a/tests/functional/QLSessionHandlerCest.php b/tests/functional/QLSessionHandlerCest.php index 4bf11072..d6e72125 100644 --- a/tests/functional/QLSessionHandlerCest.php +++ b/tests/functional/QLSessionHandlerCest.php @@ -475,7 +475,7 @@ class QLSessionHandlerCest { supportsShippingCalculator rates { id - cost + cost(format: RAW) label } } @@ -497,14 +497,14 @@ class QLSessionHandlerCest { $I->expectNode( 'rates', [ - $I->expectField( 'cost', 0 ), + $I->expectField( 'cost', '0' ), $I->expectField( 'label', 'Flat rate' ), ] ), $I->expectNode( 'rates', [ - $I->expectField( 'cost', 0 ), + $I->expectField( 'cost', '0' ), $I->expectField( 'label', 'Free shipping' ), ] ), @@ -528,7 +528,7 @@ class QLSessionHandlerCest { supportsShippingCalculator rates { id - cost + cost(format: RAW) label } } diff --git a/tests/wpunit/CartQueriesTest.php b/tests/wpunit/CartQueriesTest.php index 4ea2f4ff..619347b2 100644 --- a/tests/wpunit/CartQueriesTest.php +++ b/tests/wpunit/CartQueriesTest.php @@ -546,6 +546,221 @@ class CartQueriesTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGraphQLTe ] ); - + } + + /** + * Test cart taxLines field returns itemized tax data. + */ + public function testCartTaxLines() { + // Enable taxes and set itemized display. + update_option( 'woocommerce_calc_taxes', 'yes' ); + update_option( 'woocommerce_tax_total_display', 'itemized' ); + + // Create a tax rate. + $tax_rate_id = $this->factory->tax_rate->create( + [ + 'country' => '', + 'rate' => '10.0000', + 'name' => 'Test Tax', + 'priority' => 1, + 'compound' => 0, + 'shipping' => 1, + 'class' => '', + ] + ); + + // Create a product and add to cart. + $product_id = $this->factory->product->createSimple( [ 'regular_price' => '100', 'virtual' => true ] ); + WC()->cart->add_to_cart( $product_id ); + WC()->cart->calculate_totals(); + + $query = ' + query { + cart { + taxLines { + name + price + rate + } + } + } + '; + + $response = $this->graphql( compact( 'query' ) ); + + $expected = [ + $this->expectedNode( + 'cart.taxLines', + [ + $this->expectedField( 'name', 'Test Tax' ), + $this->expectedField( 'price', '$10.00' ), + $this->expectedField( 'rate', '10%' ), + ] + ), + ]; + + $this->assertQuerySuccessful( $response, $expected ); + } + + /** + * Test cart taxLines returns empty when display is not itemized. + */ + public function testCartTaxLinesEmptyWhenNotItemized() { + update_option( 'woocommerce_calc_taxes', 'yes' ); + update_option( 'woocommerce_tax_total_display', 'single' ); + + $tax_rate_id = $this->factory->tax_rate->create( + [ + 'country' => '', + 'rate' => '10.0000', + 'name' => 'Test Tax', + 'priority' => 1, + 'compound' => 0, + 'shipping' => 1, + 'class' => '', + ] + ); + + $product_id = $this->factory->product->createSimple( [ 'regular_price' => '100', 'virtual' => true ] ); + WC()->cart->add_to_cart( $product_id ); + WC()->cart->calculate_totals(); + + $query = ' + query { + cart { + taxLines { + name + price + rate + } + } + } + '; + + $response = $this->graphql( compact( 'query' ) ); + + $expected = [ + $this->expectedField( 'cart.taxLines', static::IS_FALSY ), + ]; + + $this->assertQuerySuccessful( $response, $expected ); + } + + /** + * Test shipping rate cost includes tax when woocommerce_tax_display_cart is incl. + */ + public function testShippingRateCostIncludesTax() { + // Enable taxes and set display to include tax. + update_option( 'woocommerce_calc_taxes', 'yes' ); + update_option( 'woocommerce_tax_display_cart', 'incl' ); + + // Create a tax rate that applies to shipping. + $this->factory->tax_rate->create( + [ + 'country' => '', + 'rate' => '10.0000', + 'name' => 'Shipping Tax', + 'priority' => 1, + 'compound' => 0, + 'shipping' => 1, + 'class' => '', + ] + ); + + // Create a physical product and add to cart. + $product_id = $this->factory->product->createSimple( [ 'regular_price' => '50' ] ); + WC()->cart->add_to_cart( $product_id ); + + // Set shipping address so rates are calculated. + WC()->customer->set_shipping_country( 'GB' ); + WC()->customer->set_shipping_postcode( 'CB23 1AB' ); + WC()->cart->calculate_totals(); + + $query = ' + query { + cart { + availableShippingMethods { + rates { + id + label + cost + subtotal + taxTotal + } + } + } + } + '; + + $response = $this->graphql( compact( 'query' ) ); + + // Flat rate is $10, tax is 10% = $1. Cost should be $11 (incl tax), subtotal $10. + $expected = [ + $this->expectedField( 'cart.availableShippingMethods.0.rates.0.cost', '$11.00' ), + $this->expectedField( 'cart.availableShippingMethods.0.rates.0.subtotal', '$10.00' ), + $this->expectedField( 'cart.availableShippingMethods.0.rates.0.taxTotal', '$1.00' ), + ]; + + $this->assertQuerySuccessful( $response, $expected ); + } + + /** + * Test shipping rate cost excludes tax when woocommerce_tax_display_cart is excl. + */ + public function testShippingRateCostExcludesTax() { + // Enable taxes and set display to exclude tax. + update_option( 'woocommerce_calc_taxes', 'yes' ); + update_option( 'woocommerce_tax_display_cart', 'excl' ); + + $this->factory->tax_rate->create( + [ + 'country' => '', + 'rate' => '10.0000', + 'name' => 'Shipping Tax', + 'priority' => 1, + 'compound' => 0, + 'shipping' => 1, + 'class' => '', + ] + ); + + $product_id = $this->factory->product->createSimple( [ 'regular_price' => '50' ] ); + WC()->cart->add_to_cart( $product_id ); + + WC()->customer->set_shipping_country( 'GB' ); + WC()->customer->set_shipping_postcode( 'CB23 1AB' ); + WC()->cart->calculate_totals(); + + $query = ' + query { + cart { + availableShippingMethods { + rates { + id + label + cost + subtotal + taxTotal + } + } + } + } + '; + + $response = $this->graphql( compact( 'query' ) ); + + // Cost and subtotal should both be $10 (excl tax), taxTotal still $1. + $expected = [ + $this->expectedObject( + 'cart.availableShippingMethods.#', + [ + $this->expectedField( 'rates.0.cost', '$10.00' ), + $this->expectedField( 'rates.0.subtotal', '$10.00' ), + $this->expectedField( 'rates.0.taxTotal', '$1.00' ), + ] + ), + ]; + + $this->assertQuerySuccessful( $response, $expected ); } }