diff --git a/includes/data/mutation/class-cart-mutation.php b/includes/data/mutation/class-cart-mutation.php index c1392af3..4057e8e3 100644 --- a/includes/data/mutation/class-cart-mutation.php +++ b/includes/data/mutation/class-cart-mutation.php @@ -185,14 +185,16 @@ class Cart_Mutation { $the_coupon = new \WC_Coupon( $code ); // Prevent adding coupons by post ID. - if ( strtoupper( $the_coupon->get_code() ) !== strtoupper( $code ) ) { + if ( is_numeric( $code ) && strtoupper( $the_coupon->get_code() ) !== strtoupper( $code ) ) { $reason = __( 'No coupon found with the code provided', 'wp-graphql-woocommerce' ); return false; } // Check it can be used with cart. - if ( ! $the_coupon->is_valid() ) { - $reason = $the_coupon->get_error_message(); + $discounts = new \WC_Discounts( \WC()->cart ); + $valid = $discounts->is_coupon_valid( $the_coupon ); + if ( is_wp_error( $valid ) ) { + $reason = $valid->get_error_message(); return false; } diff --git a/tests/wpunit/CartMutationsTest.php b/tests/wpunit/CartMutationsTest.php index 74ad39a1..6b4ccb65 100644 --- a/tests/wpunit/CartMutationsTest.php +++ b/tests/wpunit/CartMutationsTest.php @@ -1279,4 +1279,182 @@ class CartMutationsTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGraphQL $this->assertQuerySuccessful( $response, $expected ); } + + /** + * Test that fillCart reports coupon errors when coupon minimum spend is not met. + * + * @see https://github.com/wp-graphql/wp-graphql-woocommerce/issues/458 + */ + public function testFillCartReportsCouponMinimumSpendError() { + $product_id = $this->factory->product->createSimple( + [ + 'regular_price' => 10, + 'price' => 10, + ] + ); + + $coupon_id = $this->factory->coupon->create( + [ + 'code' => 'minspend500', + 'discount_type' => 'percent', + 'amount' => 10, + 'minimum_amount' => 500, + ] + ); + + $query = ' + mutation fillCart($input: FillCartInput!) { + fillCart(input: $input) { + added { + key + product { + node { + databaseId + } + } + } + applied { + code + } + cartErrors { + type + reasons + ... on CouponError { + code + reasons + } + } + } + } + '; + + $variables = [ + 'input' => [ + 'items' => [ + [ + 'productId' => $product_id, + 'quantity' => 1, + ], + ], + 'coupons' => [ 'minspend500' ], + ], + ]; + + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedField( 'fillCart.added.0.product.node.databaseId', $product_id ), + ]; + $this->assertQuerySuccessful( $response, $expected ); + + $applied = $this->lodashGet( $response, 'data.fillCart.applied' ); + $this->assertNull( $applied, 'Coupon should not be applied when minimum spend is not met.' ); + + $cart_errors = $this->lodashGet( $response, 'data.fillCart.cartErrors', [] ); + $this->assertNotEmpty( $cart_errors, 'cartErrors should contain the coupon error.' ); + $this->assertEquals( 'INVALID_COUPON', $cart_errors[0]['type'] ); + $this->assertEquals( 'minspend500', $cart_errors[0]['code'] ); + $this->assertNotEmpty( $cart_errors[0]['reasons'], 'Coupon error should have a reason.' ); + } + + /** + * Test that fillCart reports errors for completely invalid coupon codes. + */ + public function testFillCartReportsInvalidCouponCodeError() { + $product_id = $this->factory->product->createSimple( + [ + 'regular_price' => 10, + 'price' => 10, + ] + ); + + $query = ' + mutation fillCart($input: FillCartInput!) { + fillCart(input: $input) { + added { + key + } + applied { + code + } + cartErrors { + type + reasons + ... on CouponError { + code + reasons + } + } + } + } + '; + + $variables = [ + 'input' => [ + 'items' => [ + [ + 'productId' => $product_id, + 'quantity' => 1, + ], + ], + 'coupons' => [ 'nonexistent_coupon' ], + ], + ]; + + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQuerySuccessful( $response, [] ); + + $applied = $this->lodashGet( $response, 'data.fillCart.applied' ); + $this->assertNull( $applied, 'Invalid coupon should not be applied.' ); + + $cart_errors = $this->lodashGet( $response, 'data.fillCart.cartErrors', [] ); + $this->assertNotEmpty( $cart_errors, 'cartErrors should contain the invalid coupon error.' ); + $this->assertEquals( 'INVALID_COUPON', $cart_errors[0]['type'] ); + } + + /** + * Test that applyCoupon returns the same error message regardless of coupon code casing. + * + * @see https://github.com/wp-graphql/wp-graphql-woocommerce/issues/600 + */ + public function testApplyCouponErrorMessageConsistentAcrossCasing() { + $product_id = $this->factory->product->createSimple( + [ + 'regular_price' => 10, + 'price' => 10, + ] + ); + + \WC()->cart->add_to_cart( $product_id ); + + $query = ' + mutation applyCoupon($input: ApplyCouponInput!) { + applyCoupon(input: $input) { + cart { + appliedCoupons { + code + } + } + } + } + '; + + $expected_error = [ + $this->expectedErrorMessage( 'does not exist', self::MESSAGE_CONTAINS ), + ]; + + // Test with lowercase. + $variables = [ 'input' => [ 'code' => 'fakecoupon' ] ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQueryError( $response, $expected_error ); + + // Test with uppercase. + $variables = [ 'input' => [ 'code' => 'FAKECOUPON' ] ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQueryError( $response, $expected_error ); + + // Test with mixed case. + $variables = [ 'input' => [ 'code' => 'FakeCoupon' ] ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQueryError( $response, $expected_error ); + } }