Adds support for Cart tax_lines (#940)

* Adds support for tax_lines

* devops: cleanup cart tax lines code style and add unit tests

* feat: add tax-aware cost, subtotal, and taxTotal fields to ShippingRate (#802)

* feat: add format arg to ShippingRate cost/subtotal/taxTotal fields, update test for RAW format

* devops: CartQueriesTest updated

---------

Co-authored-by: Geoff Taylor <geoff@axistaylor.com>
This commit is contained in:
MoscoviumAlchemist
2026-03-26 20:09:53 -04:00
committed by GitHub
co-authored by Geoff Taylor
parent f1299846c7
commit ef26bb2479
4 changed files with 345 additions and 9 deletions
+61
View File
@@ -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.
*
@@ -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 );
},
],
],
+4 -4
View File
@@ -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
}
}
+216 -1
View File
@@ -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 );
}
}