From 8ed857a6865710882428a02f8145a1b0d2bec0aa Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Wed, 22 Apr 2026 22:25:07 +0000
Subject: [PATCH 1/5] =?UTF-8?q?GraphQL=20Foundation=20=E2=80=94=20dependen?=
=?UTF-8?q?cies,=20schema,=20and=20configuration?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Add spring-boot-starter-graphql and graphql-java-extended-scalars to root pom
dependencyManagement, wired into broadleaf-framework-web
- Exclude transitive graphql-java from extended-scalars to align with the
graphql-java version managed by spring-boot-starter-graphql
- Pin reactive-streams to 1.0.4 to satisfy dependency convergence
- Add GraphQL schema under core/broadleaf-framework-web/src/main/resources/graphql
covering catalog, cart, checkout, and account types, queries and mutations
- Add GraphQLContextInterceptor that populates CustomerState and CartState
- Add GraphQLExceptionResolver mapping Broadleaf domain exceptions to
structured GraphQL errors with stable code extensions
- Add GraphQLConfig registering BigDecimal scalar aliased to GraphQLFloat
- Add graphql-config.properties enabling GraphiQL and the /graphql endpoint
---
core/broadleaf-framework-web/pom.xml | 8 +
.../core/web/graphql/GraphQLConfig.java | 41 +++
.../graphql/GraphQLContextInterceptor.java | 173 ++++++++++++
.../web/graphql/GraphQLExceptionResolver.java | 91 ++++++
.../main/resources/graphql-config.properties | 20 ++
.../main/resources/graphql/schema.graphqls | 258 ++++++++++++++++++
pom.xml | 24 ++
7 files changed, 615 insertions(+)
create mode 100644 core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLConfig.java
create mode 100644 core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLContextInterceptor.java
create mode 100644 core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLExceptionResolver.java
create mode 100644 core/broadleaf-framework-web/src/main/resources/graphql-config.properties
create mode 100644 core/broadleaf-framework-web/src/main/resources/graphql/schema.graphqls
diff --git a/core/broadleaf-framework-web/pom.xml b/core/broadleaf-framework-web/pom.xml
index c732740dce..0c7dcfa040 100644
--- a/core/broadleaf-framework-web/pom.xml
+++ b/core/broadleaf-framework-web/pom.xml
@@ -94,5 +94,13 @@
org.springframework.security
spring-security-oauth2-client
+
+ org.springframework.boot
+ spring-boot-starter-graphql
+
+
+ com.graphql-java
+ graphql-java-extended-scalars
+
diff --git a/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLConfig.java b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLConfig.java
new file mode 100644
index 0000000000..1983079159
--- /dev/null
+++ b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLConfig.java
@@ -0,0 +1,41 @@
+/*-
+ * #%L
+ * BroadleafCommerce Framework Web
+ * %%
+ * Copyright (C) 2009 - 2026 Broadleaf Commerce
+ * %%
+ * Licensed under the Broadleaf Fair Use License Agreement, Version 1.0
+ * (the "Fair Use License" located at http://license.broadleafcommerce.org/fair_use_license-1.0.txt)
+ * unless the restrictions on use therein are violated and require payment to Broadleaf in which case
+ * the Broadleaf End User License Agreement (EULA), Version 1.1
+ * (the "Commercial License" located at http://license.broadleafcommerce.org/commercial_license-1.1.txt)
+ * shall apply.
+ *
+ * Alternatively, the Commercial License may be replaced with a mutually agreed upon license (the "Custom License")
+ * between you and Broadleaf Commerce. You may not use this file except in compliance with the applicable license.
+ * #L%
+ */
+package org.broadleafcommerce.core.web.graphql;
+
+import graphql.Scalars;
+import graphql.scalars.ExtendedScalars;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.graphql.execution.RuntimeWiringConfigurer;
+
+/**
+ * Registers Broadleaf-specific GraphQL runtime wiring, including the custom
+ * {@code BigDecimal} scalar aliased to the standard GraphQL {@code Float} scalar.
+ */
+@Configuration
+public class GraphQLConfig {
+
+ @Bean
+ public RuntimeWiringConfigurer runtimeWiringConfigurer() {
+ return wiringBuilder -> wiringBuilder.scalar(
+ ExtendedScalars.newAliasedScalar("BigDecimal")
+ .aliasedScalar(Scalars.GraphQLFloat)
+ .build()
+ );
+ }
+}
diff --git a/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLContextInterceptor.java b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLContextInterceptor.java
new file mode 100644
index 0000000000..14ccad20ba
--- /dev/null
+++ b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLContextInterceptor.java
@@ -0,0 +1,173 @@
+/*-
+ * #%L
+ * BroadleafCommerce Framework Web
+ * %%
+ * Copyright (C) 2009 - 2026 Broadleaf Commerce
+ * %%
+ * Licensed under the Broadleaf Fair Use License Agreement, Version 1.0
+ * (the "Fair Use License" located at http://license.broadleafcommerce.org/fair_use_license-1.0.txt)
+ * unless the restrictions on use therein are violated and require payment to Broadleaf in which case
+ * the Broadleaf End User License Agreement (EULA), Version 1.1
+ * (the "Commercial License" located at http://license.broadleafcommerce.org/commercial_license-1.1.txt)
+ * shall apply.
+ *
+ * Alternatively, the Commercial License may be replaced with a mutually agreed upon license (the "Custom License")
+ * between you and Broadleaf Commerce. You may not use this file except in compliance with the applicable license.
+ * #L%
+ */
+package org.broadleafcommerce.core.web.graphql;
+
+import org.apache.commons.lang3.math.NumberUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.broadleafcommerce.common.util.StringUtil;
+import org.broadleafcommerce.common.web.BroadleafRequestContext;
+import org.broadleafcommerce.core.order.domain.Order;
+import org.broadleafcommerce.core.order.service.OrderService;
+import org.broadleafcommerce.core.web.order.CartState;
+import org.broadleafcommerce.profile.core.domain.Customer;
+import org.broadleafcommerce.profile.core.service.CustomerService;
+import org.broadleafcommerce.profile.web.core.CustomerState;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.graphql.server.WebGraphQlInterceptor;
+import org.springframework.graphql.server.WebGraphQlRequest;
+import org.springframework.graphql.server.WebGraphQlResponse;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+import org.springframework.web.context.request.ServletWebRequest;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import jakarta.servlet.http.HttpServletRequest;
+import reactor.core.publisher.Mono;
+
+/**
+ * A {@link WebGraphQlInterceptor} that establishes the Broadleaf customer and cart
+ * context for GraphQL requests. The customer identity is resolved following the same
+ * precedence used by {@code RestApiCustomerStateFilter}: request attribute first, then
+ * request parameter, then request header. If no customer can be resolved, an anonymous
+ * customer is created. Once the customer is established, the active cart is populated
+ * into {@link CartState} for downstream resolvers.
+ */
+@Component
+public class GraphQLContextInterceptor implements WebGraphQlInterceptor {
+
+ public static final String CUSTOMER_ID_ATTRIBUTE = "customerId";
+ public static final String BLC_RULE_MAP_PARAM = "blRuleMap";
+
+ protected static final Log LOG = LogFactory.getLog(GraphQLContextInterceptor.class);
+
+ protected final CustomerService customerService;
+ protected final OrderService orderService;
+
+ @Autowired
+ public GraphQLContextInterceptor(
+ @Qualifier("blCustomerService") CustomerService customerService,
+ @Qualifier("blOrderService") OrderService orderService
+ ) {
+ this.customerService = customerService;
+ this.orderService = orderService;
+ }
+
+ @Override
+ public Mono intercept(WebGraphQlRequest request, Chain chain) {
+ HttpServletRequest httpRequest = extractHttpRequest(request);
+ if (httpRequest != null) {
+ populateCustomerAndCart(httpRequest);
+ }
+ return chain.next(request);
+ }
+
+ protected HttpServletRequest extractHttpRequest(WebGraphQlRequest request) {
+ RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+ if (requestAttributes instanceof ServletRequestAttributes servletRequestAttributes) {
+ return servletRequestAttributes.getRequest();
+ }
+ return null;
+ }
+
+ protected void populateCustomerAndCart(HttpServletRequest request) {
+ Customer customer = resolveCustomer(request);
+ if (customer == null) {
+ customer = customerService.createCustomer();
+ CustomerState.setCustomer(customer);
+ setupCustomerForRuleProcessing(customer, request);
+ }
+ populateCart(customer);
+ }
+
+ protected Customer resolveCustomer(HttpServletRequest request) {
+ String customerId = readCustomerId(request);
+
+ if (customerId != null && customerId.trim().length() > 0) {
+ if (NumberUtils.isCreatable(customerId)) {
+ Customer customer = customerService.readCustomerById(Long.valueOf(customerId));
+ if (customer != null) {
+ ensureWebRequest(request);
+ CustomerState.setCustomer(customer);
+ setupCustomerForRuleProcessing(customer, request);
+ return customer;
+ }
+ } else {
+ LOG.warn(String.format("The customer id passed in '%s' was not a number", StringUtil.sanitize(customerId)));
+ }
+ } else if (LOG.isDebugEnabled()) {
+ LOG.debug("No customer ID was found for the GraphQL request. In order to look up a customer for the request"
+ + " send a request parameter or request header for the '" + CUSTOMER_ID_ATTRIBUTE + "' attribute");
+ }
+
+ Customer existingCustomer = CustomerState.getCustomer();
+ if (existingCustomer != null) {
+ return existingCustomer;
+ }
+
+ ensureWebRequest(request);
+ return null;
+ }
+
+ protected String readCustomerId(HttpServletRequest request) {
+ String customerId = null;
+ if (request.getAttribute(CUSTOMER_ID_ATTRIBUTE) != null) {
+ customerId = String.valueOf(request.getAttribute(CUSTOMER_ID_ATTRIBUTE));
+ }
+ if (customerId == null) {
+ customerId = request.getParameter(CUSTOMER_ID_ATTRIBUTE);
+ }
+ if (customerId == null) {
+ customerId = request.getHeader(CUSTOMER_ID_ATTRIBUTE);
+ }
+ return customerId;
+ }
+
+ protected void ensureWebRequest(HttpServletRequest request) {
+ BroadleafRequestContext ctx = BroadleafRequestContext.getBroadleafRequestContext();
+ if (ctx != null && ctx.getWebRequest() == null) {
+ ctx.setWebRequest(new ServletWebRequest(request));
+ }
+ }
+
+ protected void populateCart(Customer customer) {
+ if (customer == null) {
+ return;
+ }
+ Order cart = orderService.findCartForCustomer(customer);
+ if (cart != null) {
+ CartState.setCart(cart);
+ }
+ }
+
+ protected void setupCustomerForRuleProcessing(Customer customer, HttpServletRequest request) {
+ @SuppressWarnings("unchecked")
+ Map ruleMap = (Map) request.getAttribute(BLC_RULE_MAP_PARAM);
+ if (ruleMap == null) {
+ ruleMap = new HashMap<>();
+ }
+ ruleMap.put("customer", customer);
+ request.setAttribute(BLC_RULE_MAP_PARAM, ruleMap);
+ }
+
+}
diff --git a/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLExceptionResolver.java b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLExceptionResolver.java
new file mode 100644
index 0000000000..1fc2c8e1f2
--- /dev/null
+++ b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLExceptionResolver.java
@@ -0,0 +1,91 @@
+/*-
+ * #%L
+ * BroadleafCommerce Framework Web
+ * %%
+ * Copyright (C) 2009 - 2026 Broadleaf Commerce
+ * %%
+ * Licensed under the Broadleaf Fair Use License Agreement, Version 1.0
+ * (the "Fair Use License" located at http://license.broadleafcommerce.org/fair_use_license-1.0.txt)
+ * unless the restrictions on use therein are violated and require payment to Broadleaf in which case
+ * the Broadleaf End User License Agreement (EULA), Version 1.1
+ * (the "Commercial License" located at http://license.broadleafcommerce.org/commercial_license-1.1.txt)
+ * shall apply.
+ *
+ * Alternatively, the Commercial License may be replaced with a mutually agreed upon license (the "Custom License")
+ * between you and Broadleaf Commerce. You may not use this file except in compliance with the applicable license.
+ * #L%
+ */
+package org.broadleafcommerce.core.web.graphql;
+
+import graphql.ErrorClassification;
+import graphql.ErrorType;
+import graphql.GraphQLError;
+import graphql.GraphqlErrorBuilder;
+import graphql.schema.DataFetchingEnvironment;
+import org.broadleafcommerce.core.checkout.service.exception.CheckoutException;
+import org.broadleafcommerce.core.offer.service.exception.OfferMaxUseExceededException;
+import org.broadleafcommerce.core.order.service.exception.AddToCartException;
+import org.broadleafcommerce.core.order.service.exception.IllegalCartOperationException;
+import org.broadleafcommerce.core.order.service.exception.RemoveFromCartException;
+import org.broadleafcommerce.core.order.service.exception.UpdateCartException;
+import org.broadleafcommerce.core.pricing.service.exception.PricingException;
+import org.springframework.graphql.execution.DataFetcherExceptionResolverAdapter;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * Maps Broadleaf domain exceptions to structured GraphQL errors with stable
+ * {@code code} extensions so clients can branch on a known error taxonomy.
+ */
+@Component
+public class GraphQLExceptionResolver extends DataFetcherExceptionResolverAdapter {
+
+ public static final String CODE_ADD_TO_CART = "ADD_TO_CART_ERROR";
+ public static final String CODE_PRICING = "PRICING_ERROR";
+ public static final String CODE_OFFER_MAX_USE_EXCEEDED = "OFFER_MAX_USE_EXCEEDED";
+ public static final String CODE_ILLEGAL_CART_OPERATION = "ILLEGAL_CART_OPERATION";
+ public static final String CODE_CHECKOUT = "CHECKOUT_ERROR";
+ public static final String CODE_REMOVE_FROM_CART = "REMOVE_FROM_CART_ERROR";
+ public static final String CODE_UPDATE_CART = "UPDATE_CART_ERROR";
+
+ @Override
+ protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
+ if (ex instanceof AddToCartException) {
+ return buildError(ex, env, ErrorType.ValidationError, CODE_ADD_TO_CART);
+ }
+ if (ex instanceof RemoveFromCartException) {
+ return buildError(ex, env, ErrorType.ValidationError, CODE_REMOVE_FROM_CART);
+ }
+ if (ex instanceof UpdateCartException) {
+ return buildError(ex, env, ErrorType.ValidationError, CODE_UPDATE_CART);
+ }
+ if (ex instanceof IllegalCartOperationException) {
+ return buildError(ex, env, ErrorType.ValidationError, CODE_ILLEGAL_CART_OPERATION);
+ }
+ if (ex instanceof OfferMaxUseExceededException) {
+ return buildError(ex, env, ErrorType.ValidationError, CODE_OFFER_MAX_USE_EXCEEDED);
+ }
+ if (ex instanceof CheckoutException) {
+ return buildError(ex, env, ErrorType.ValidationError, CODE_CHECKOUT);
+ }
+ if (ex instanceof PricingException) {
+ return buildError(ex, env, ErrorType.DataFetchingException, CODE_PRICING);
+ }
+ return null;
+ }
+
+ protected GraphQLError buildError(
+ Throwable ex,
+ DataFetchingEnvironment env,
+ ErrorClassification classification,
+ String code
+ ) {
+ return GraphqlErrorBuilder.newError(env)
+ .message(ex.getMessage())
+ .errorType(classification)
+ .extensions(Map.of("code", code))
+ .build();
+ }
+
+}
diff --git a/core/broadleaf-framework-web/src/main/resources/graphql-config.properties b/core/broadleaf-framework-web/src/main/resources/graphql-config.properties
new file mode 100644
index 0000000000..bdbfa221b0
--- /dev/null
+++ b/core/broadleaf-framework-web/src/main/resources/graphql-config.properties
@@ -0,0 +1,20 @@
+###
+# #%L
+# BroadleafCommerce Framework Web
+# %%
+# Copyright (C) 2009 - 2026 Broadleaf Commerce
+# %%
+# Licensed under the Broadleaf Fair Use License Agreement, Version 1.0
+# (the "Fair Use License" located at http://license.broadleafcommerce.org/fair_use_license-1.0.txt)
+# unless the restrictions on use therein are violated and require payment to Broadleaf in which case
+# the Broadleaf End User License Agreement (EULA), Version 1.1
+# (the "Commercial License" located at http://license.broadleafcommerce.org/commercial_license-1.1.txt)
+# shall apply.
+#
+# Alternatively, the Commercial License may be replaced with a mutually agreed upon license (the "Custom License")
+# between you and Broadleaf Commerce. You may not use this file except in compliance with the applicable license.
+# #L%
+###
+spring.graphql.graphiql.enabled=true
+spring.graphql.path=/graphql
+spring.graphql.schema.locations=classpath:graphql/
diff --git a/core/broadleaf-framework-web/src/main/resources/graphql/schema.graphqls b/core/broadleaf-framework-web/src/main/resources/graphql/schema.graphqls
new file mode 100644
index 0000000000..a7b0e6b4e1
--- /dev/null
+++ b/core/broadleaf-framework-web/src/main/resources/graphql/schema.graphqls
@@ -0,0 +1,258 @@
+type Query {
+ # Catalog
+ product(id: ID!): Product
+ productByExternalId(externalId: String!): Product
+ productByUri(uri: String!): Product
+ products(name: String, limit: Int = 25, offset: Int = 0): [Product!]!
+ category(id: ID!): Category
+ categoryByUri(uri: String!): Category
+ categories(name: String, limit: Int = 25, offset: Int = 0): [Category!]!
+ sku(id: ID!): Sku
+ skuByUpc(upc: String!): Sku
+ search(query: String!, limit: Int = 25, offset: Int = 0): [Product!]!
+
+ # Cart & Orders
+ cart: Order
+ order(id: ID!): Order
+ orderByNumber(orderNumber: String!): Order
+ orderHistory(status: OrderStatus): [Order!]!
+
+ # Account
+ customer: Customer
+ customerAddresses: [CustomerAddress!]!
+}
+
+type Mutation {
+ # Cart
+ addToCart(input: AddToCartInput!): Order!
+ updateCartItemQuantity(orderItemId: ID!, quantity: Int!): Order!
+ removeFromCart(orderItemId: ID!): Order!
+ applyPromoCode(code: String!): PromoCodeResult!
+ removePromoCode(offerCodeId: ID!): Order!
+
+ # Checkout
+ saveShippingInfo(input: ShippingInfoInput!): Order!
+ saveBillingInfo(input: BillingInfoInput!): Order!
+ saveOrderEmail(email: String!): Order!
+ processCheckout(orderId: ID!): CheckoutResult!
+
+ # Account
+ updateAccount(input: UpdateAccountInput!): Customer!
+ changePassword(input: ChangePasswordInput!): Boolean!
+ addCustomerAddress(input: CustomerAddressInput!): CustomerAddress!
+ updateCustomerAddress(input: CustomerAddressInput!): CustomerAddress!
+ removeCustomerAddress(id: ID!): Boolean!
+}
+
+type Product {
+ id: ID!
+ name: String
+ description: String
+ longDescription: String
+ url: String
+ manufacturer: String
+ defaultCategory: Category
+ defaultSku: Sku
+ additionalSkus: [Sku!]!
+ productOptions: [ProductOption!]!
+}
+
+type Category {
+ id: ID!
+ name: String
+ description: String
+ longDescription: String
+ url: String
+ activeSubCategories(limit: Int = 25, offset: Int = 0): [Category!]!
+ products(limit: Int = 25, offset: Int = 0): [Product!]!
+}
+
+type Sku {
+ id: ID!
+ name: String
+ description: String
+ salePrice: Money
+ retailPrice: Money
+ upc: String
+ quantityAvailable: Int
+ available: Boolean
+}
+
+type Money {
+ amount: BigDecimal
+ currency: String
+}
+
+type Order {
+ id: ID!
+ orderNumber: String
+ status: OrderStatus
+ subTotal: Money
+ total: Money
+ totalTax: Money
+ totalShipping: Money
+ orderItems: [OrderItem!]!
+ fulfillmentGroups: [FulfillmentGroup!]!
+ payments: [OrderPayment!]!
+ emailAddress: String
+ customer: Customer
+ addedOfferCodes: [OfferCode!]!
+ itemCount: Int
+}
+
+type OrderItem {
+ id: ID!
+ name: String
+ quantity: Int
+ price: Money
+ salePrice: Money
+ retailPrice: Money
+ totalPrice: Money
+ product: Product
+ sku: Sku
+}
+
+type FulfillmentGroup {
+ id: ID!
+ address: Address
+ fulfillmentOption: FulfillmentOption
+ fulfillmentPrice: Money
+ items: [FulfillmentGroupItem!]!
+}
+
+type FulfillmentGroupItem {
+ id: ID!
+ orderItem: OrderItem
+ quantity: Int
+}
+
+type FulfillmentOption {
+ id: ID!
+ name: String
+ description: String
+ price: Money
+}
+
+type OrderPayment {
+ id: ID!
+ type: String
+ amount: Money
+ billingAddress: Address
+}
+
+type OfferCode {
+ id: ID!
+ offerCode: String
+}
+
+type Customer {
+ id: ID!
+ firstName: String
+ lastName: String
+ emailAddress: String
+ username: String
+}
+
+type CustomerAddress {
+ id: ID!
+ addressName: String
+ address: Address
+}
+
+type Address {
+ id: ID!
+ addressLine1: String
+ addressLine2: String
+ city: String
+ stateProvinceRegion: String
+ postalCode: String
+ country: String
+ phonePrimary: String
+}
+
+type ProductOption {
+ id: ID!
+ label: String
+ type: String
+ required: Boolean
+ values: [ProductOptionValue!]!
+}
+
+type ProductOptionValue {
+ id: ID!
+ attributeValue: String
+ displayOrder: Int
+ priceAdjustment: Money
+}
+
+type PromoCodeResult {
+ order: Order!
+ promoAdded: Boolean!
+ exception: String
+}
+
+type CheckoutResult {
+ order: Order!
+ success: Boolean!
+ errorMessage: String
+}
+
+enum OrderStatus {
+ IN_PROCESS
+ SUBMITTED
+ CANCELLED
+}
+
+scalar BigDecimal
+
+input AddToCartInput {
+ productId: ID!
+ skuId: ID
+ quantity: Int!
+ itemAttributes: [ItemAttributeInput!]
+}
+
+input ItemAttributeInput {
+ name: String!
+ value: String!
+}
+
+input ShippingInfoInput {
+ address: AddressInput!
+ fulfillmentOptionId: ID!
+ personalMessage: String
+ deliveryMessage: String
+}
+
+input BillingInfoInput {
+ address: AddressInput!
+ paymentType: String
+}
+
+input AddressInput {
+ addressLine1: String!
+ addressLine2: String
+ city: String!
+ stateProvinceRegion: String!
+ postalCode: String!
+ country: String!
+ phonePrimary: String
+}
+
+input UpdateAccountInput {
+ firstName: String
+ lastName: String
+ emailAddress: String
+}
+
+input ChangePasswordInput {
+ currentPassword: String!
+ newPassword: String!
+ newPasswordConfirm: String!
+}
+
+input CustomerAddressInput {
+ id: ID
+ addressName: String!
+ address: AddressInput!
+}
diff --git a/pom.xml b/pom.xml
index 6836d3b269..dd28511854 100644
--- a/pom.xml
+++ b/pom.xml
@@ -47,6 +47,8 @@
6.5.9
3.3.6
3.5.13
+ 1.3.4
+ 22.0
2.21.2
1.18.44
1.5.32
@@ -1202,6 +1204,28 @@
spring-boot-autoconfigure
${spring.boot.version}
+
+ org.springframework.boot
+ spring-boot-starter-graphql
+ ${spring.boot.version}
+
+
+ com.graphql-java
+ graphql-java-extended-scalars
+ ${graphql-java-extended-scalars.version}
+
+
+ com.graphql-java
+ graphql-java
+
+
+
+
+
+ org.reactivestreams
+ reactive-streams
+ 1.0.4
+
org.springframework.security
spring-security-oauth2-client
From fa74884d50828a5545fea5a7f98efec450802d44 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Wed, 22 Apr 2026 22:32:29 +0000
Subject: [PATCH 2/5] Address review: fix customer id parsing and relocate
GraphQL properties
- Use NumberUtils.isDigits instead of isCreatable to avoid NumberFormatException
on inputs like hex, decimal, scientific, or type-qualified strings that
isCreatable accepts but Long.valueOf cannot parse.
- Move GraphQL endpoint defaults into the module's existing
config/bc/web/common.properties so they are loaded by Broadleaf's
FrameworkCommonClasspathPropertySource. Remove the classpath-root
graphql-config.properties, which was never picked up by Spring Boot or
Broadleaf's environment configurer.
---
.../graphql/GraphQLContextInterceptor.java | 2 +-
.../resources/config/bc/web/common.properties | 7 +++++++
.../main/resources/graphql-config.properties | 20 -------------------
3 files changed, 8 insertions(+), 21 deletions(-)
delete mode 100644 core/broadleaf-framework-web/src/main/resources/graphql-config.properties
diff --git a/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLContextInterceptor.java b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLContextInterceptor.java
index 14ccad20ba..194b6cae1a 100644
--- a/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLContextInterceptor.java
+++ b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLContextInterceptor.java
@@ -104,7 +104,7 @@ protected Customer resolveCustomer(HttpServletRequest request) {
String customerId = readCustomerId(request);
if (customerId != null && customerId.trim().length() > 0) {
- if (NumberUtils.isCreatable(customerId)) {
+ if (NumberUtils.isDigits(customerId)) {
Customer customer = customerService.readCustomerById(Long.valueOf(customerId));
if (customer != null) {
ensureWebRequest(request);
diff --git a/core/broadleaf-framework-web/src/main/resources/config/bc/web/common.properties b/core/broadleaf-framework-web/src/main/resources/config/bc/web/common.properties
index f98baafc07..4770c1d49c 100644
--- a/core/broadleaf-framework-web/src/main/resources/config/bc/web/common.properties
+++ b/core/broadleaf-framework-web/src/main/resources/config/bc/web/common.properties
@@ -40,3 +40,10 @@ googleAnalytics4.tagIdForProperty=
# This approach can be more efficient for large catalogs and more easily support dynamic URL building
allowProductResolutionUsingIdParam=false
allowCategoryResolutionUsingIdParam=false
+
+# GraphQL endpoint configuration. Schema files are loaded from classpath:graphql/
+# and GraphiQL is enabled by default; override in a consuming application's
+# common.properties to disable for production environments.
+spring.graphql.graphiql.enabled=true
+spring.graphql.path=/graphql
+spring.graphql.schema.locations=classpath:graphql/
diff --git a/core/broadleaf-framework-web/src/main/resources/graphql-config.properties b/core/broadleaf-framework-web/src/main/resources/graphql-config.properties
deleted file mode 100644
index bdbfa221b0..0000000000
--- a/core/broadleaf-framework-web/src/main/resources/graphql-config.properties
+++ /dev/null
@@ -1,20 +0,0 @@
-###
-# #%L
-# BroadleafCommerce Framework Web
-# %%
-# Copyright (C) 2009 - 2026 Broadleaf Commerce
-# %%
-# Licensed under the Broadleaf Fair Use License Agreement, Version 1.0
-# (the "Fair Use License" located at http://license.broadleafcommerce.org/fair_use_license-1.0.txt)
-# unless the restrictions on use therein are violated and require payment to Broadleaf in which case
-# the Broadleaf End User License Agreement (EULA), Version 1.1
-# (the "Commercial License" located at http://license.broadleafcommerce.org/commercial_license-1.1.txt)
-# shall apply.
-#
-# Alternatively, the Commercial License may be replaced with a mutually agreed upon license (the "Custom License")
-# between you and Broadleaf Commerce. You may not use this file except in compliance with the applicable license.
-# #L%
-###
-spring.graphql.graphiql.enabled=true
-spring.graphql.path=/graphql
-spring.graphql.schema.locations=classpath:graphql/
From 646d89a38e172cb5a50d3fe389b1ad43adf10c96 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Wed, 22 Apr 2026 22:40:54 +0000
Subject: [PATCH 3/5] Address review: null-safe exception message in
GraphQLExceptionResolver
Some Broadleaf exceptions (e.g. PricingException, AddToCartException) have
no-arg constructors that leave the message null. GraphqlErrorBuilder.build()
asserts message is non-null and would otherwise throw, preventing the
structured error from being returned. Fall back to the exception's simple
class name when no message is present.
---
.../core/web/graphql/GraphQLExceptionResolver.java | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLExceptionResolver.java b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLExceptionResolver.java
index 1fc2c8e1f2..d313216f55 100644
--- a/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLExceptionResolver.java
+++ b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLExceptionResolver.java
@@ -81,8 +81,9 @@ protected GraphQLError buildError(
ErrorClassification classification,
String code
) {
+ String message = ex.getMessage() != null ? ex.getMessage() : ex.getClass().getSimpleName();
return GraphqlErrorBuilder.newError(env)
- .message(ex.getMessage())
+ .message(message)
.errorType(classification)
.extensions(Map.of("code", code))
.build();
From fae686bb0d32f489e40bddc6b3adb37ec6545cb6 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Wed, 22 Apr 2026 22:49:44 +0000
Subject: [PATCH 4/5] Address review: use ExtendedScalars.GraphQLBigDecimal for
monetary precision
Aliasing BigDecimal to GraphQLFloat silently degrades values to IEEE 754
doubles, which can corrupt large monetary amounts that Broadleaf's Money
type stores as java.math.BigDecimal specifically to preserve exact decimal
precision. Register the schema's BigDecimal scalar using
ExtendedScalars.GraphQLBigDecimal, which serializes with full precision.
---
.../core/web/graphql/GraphQLConfig.java | 13 +++++--------
1 file changed, 5 insertions(+), 8 deletions(-)
diff --git a/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLConfig.java b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLConfig.java
index 1983079159..7069b09085 100644
--- a/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLConfig.java
+++ b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLConfig.java
@@ -17,25 +17,22 @@
*/
package org.broadleafcommerce.core.web.graphql;
-import graphql.Scalars;
import graphql.scalars.ExtendedScalars;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
/**
- * Registers Broadleaf-specific GraphQL runtime wiring, including the custom
- * {@code BigDecimal} scalar aliased to the standard GraphQL {@code Float} scalar.
+ * Registers Broadleaf-specific GraphQL runtime wiring, including the
+ * {@code BigDecimal} scalar used for monetary amounts. The scalar preserves
+ * the full precision of {@link java.math.BigDecimal}; serializing via
+ * {@code GraphQLFloat} would silently degrade values to IEEE 754 doubles.
*/
@Configuration
public class GraphQLConfig {
@Bean
public RuntimeWiringConfigurer runtimeWiringConfigurer() {
- return wiringBuilder -> wiringBuilder.scalar(
- ExtendedScalars.newAliasedScalar("BigDecimal")
- .aliasedScalar(Scalars.GraphQLFloat)
- .build()
- );
+ return wiringBuilder -> wiringBuilder.scalar(ExtendedScalars.GraphQLBigDecimal);
}
}
From efb3400dc8776864da1fe49d391aef96b32f95b5 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Wed, 22 Apr 2026 22:59:34 +0000
Subject: [PATCH 5/5] Address review: guard against NumberFormatException on
oversized customer ids
isDigits returns true for all-digit strings that overflow Long.MAX_VALUE
(e.g. a very long numeric header supplied by a malicious client), which
Long.valueOf then rejects with NumberFormatException, failing the entire
GraphQL request. Wrap the parse in a helper that returns null on overflow
and falls through to the anonymous-customer path.
---
.../web/graphql/GraphQLContextInterceptor.java | 18 +++++++++++++++---
1 file changed, 15 insertions(+), 3 deletions(-)
diff --git a/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLContextInterceptor.java b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLContextInterceptor.java
index 194b6cae1a..dc97093ed5 100644
--- a/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLContextInterceptor.java
+++ b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLContextInterceptor.java
@@ -104,8 +104,9 @@ protected Customer resolveCustomer(HttpServletRequest request) {
String customerId = readCustomerId(request);
if (customerId != null && customerId.trim().length() > 0) {
- if (NumberUtils.isDigits(customerId)) {
- Customer customer = customerService.readCustomerById(Long.valueOf(customerId));
+ Long parsedId = parseCustomerId(customerId);
+ if (parsedId != null) {
+ Customer customer = customerService.readCustomerById(parsedId);
if (customer != null) {
ensureWebRequest(request);
CustomerState.setCustomer(customer);
@@ -113,7 +114,7 @@ protected Customer resolveCustomer(HttpServletRequest request) {
return customer;
}
} else {
- LOG.warn(String.format("The customer id passed in '%s' was not a number", StringUtil.sanitize(customerId)));
+ LOG.warn(String.format("The customer id passed in '%s' was not a valid long", StringUtil.sanitize(customerId)));
}
} else if (LOG.isDebugEnabled()) {
LOG.debug("No customer ID was found for the GraphQL request. In order to look up a customer for the request"
@@ -129,6 +130,17 @@ protected Customer resolveCustomer(HttpServletRequest request) {
return null;
}
+ protected Long parseCustomerId(String customerId) {
+ if (!NumberUtils.isDigits(customerId)) {
+ return null;
+ }
+ try {
+ return Long.valueOf(customerId);
+ } catch (NumberFormatException ex) {
+ return null;
+ }
+ }
+
protected String readCustomerId(HttpServletRequest request) {
String customerId = null;
if (request.getAttribute(CUSTOMER_ID_ATTRIBUTE) != null) {