From 1a098b2ba733c9f8122e87692af1fbc38d3dfa70 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:21:38 +0000 Subject: [PATCH 1/4] Add GraphQL foundation and cart/order resolvers Co-Authored-By: Arjun Mishra --- core/broadleaf-framework-web/pom.xml | 4 + .../graphql/GraphQLContextInterceptor.java | 89 ++++++++ .../web/graphql/GraphQLExceptionResolver.java | 73 +++++++ .../core/web/graphql/dto/AddToCartInput.java | 50 +++++ .../core/web/graphql/dto/PromoCodeResult.java | 46 ++++ .../resolvers/CartMutationResolver.java | 156 ++++++++++++++ .../graphql/resolvers/CartQueryResolver.java | 78 +++++++ .../graphql/resolvers/OrderFieldResolver.java | 72 +++++++ .../resolvers/OrderItemFieldResolver.java | 46 ++++ .../resources/graphql-application.properties | 20 ++ .../main/resources/graphql/schema.graphqls | 203 ++++++++++++++++++ pom.xml | 12 ++ 12 files changed, 849 insertions(+) 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/java/org/broadleafcommerce/core/web/graphql/dto/AddToCartInput.java create mode 100644 core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/dto/PromoCodeResult.java create mode 100644 core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/CartMutationResolver.java create mode 100644 core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/CartQueryResolver.java create mode 100644 core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/OrderFieldResolver.java create mode 100644 core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/OrderItemFieldResolver.java create mode 100644 core/broadleaf-framework-web/src/main/resources/graphql-application.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..4a8752142e 100644 --- a/core/broadleaf-framework-web/pom.xml +++ b/core/broadleaf-framework-web/pom.xml @@ -94,5 +94,9 @@ org.springframework.security spring-security-oauth2-client + + org.springframework.boot + spring-boot-starter-graphql + 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..4a781bc8c4 --- /dev/null +++ b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLContextInterceptor.java @@ -0,0 +1,89 @@ +/*- + * #%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.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 jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import reactor.core.publisher.Mono; + +/** + * Populates the Broadleaf customer and cart context for GraphQL requests. This mirrors the identity + * resolution pattern used by {@code RestApiCustomerStateFilter} so that query/mutation resolvers + * can rely on {@link CustomerState} and {@link CartState}. + */ +@Component +public class GraphQLContextInterceptor implements WebGraphQlInterceptor { + + @Autowired + @Qualifier("blCustomerService") + protected CustomerService customerService; + + @Autowired + @Qualifier("blOrderService") + protected OrderService orderService; + + @Override + public Mono intercept(WebGraphQlRequest request, Chain chain) { + RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); + if (attributes instanceof ServletRequestAttributes servletAttributes) { + HttpServletRequest httpRequest = servletAttributes.getRequest(); + HttpServletResponse httpResponse = servletAttributes.getResponse(); + populateContext(httpRequest, httpResponse); + } + return chain.next(request); + } + + protected void populateContext(HttpServletRequest request, HttpServletResponse response) { + BroadleafRequestContext brc = BroadleafRequestContext.getBroadleafRequestContext(); + if (brc != null && brc.getWebRequest() == null) { + brc.setWebRequest(new ServletWebRequest(request, response)); + } + + Customer customer = CustomerState.getCustomer(); + if (customer == null) { + customer = customerService.createCustomer(); + CustomerState.setCustomer(customer); + } + + if (CartState.getCart() == null && customer != null && customer.getId() != null) { + Order cart = orderService.findCartForCustomer(customer); + if (cart != null) { + CartState.setCart(cart); + } + } + } + +} 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..b43cf918b2 --- /dev/null +++ b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLExceptionResolver.java @@ -0,0 +1,73 @@ +/*- + * #%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.GraphQLError; +import graphql.GraphqlErrorBuilder; +import graphql.schema.DataFetchingEnvironment; +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.graphql.execution.ErrorType; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +@Component +public class GraphQLExceptionResolver extends DataFetcherExceptionResolverAdapter { + + @Override + protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) { + String code = resolveCode(ex); + Map extensions = new HashMap<>(); + extensions.put("code", code); + return GraphqlErrorBuilder.newError(env) + .errorType(ErrorType.INTERNAL_ERROR) + .message(ex.getMessage() != null ? ex.getMessage() : code) + .extensions(extensions) + .build(); + } + + protected String resolveCode(Throwable ex) { + if (ex instanceof OfferMaxUseExceededException) { + return "OFFER_MAX_USE_EXCEEDED"; + } + if (ex instanceof AddToCartException) { + return "ADD_TO_CART_ERROR"; + } + if (ex instanceof RemoveFromCartException) { + return "REMOVE_FROM_CART_ERROR"; + } + if (ex instanceof UpdateCartException) { + return "UPDATE_CART_ERROR"; + } + if (ex instanceof IllegalCartOperationException) { + return "ILLEGAL_CART_OPERATION"; + } + if (ex instanceof PricingException) { + return "PRICING_ERROR"; + } + return "INTERNAL_ERROR"; + } + +} diff --git a/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/dto/AddToCartInput.java b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/dto/AddToCartInput.java new file mode 100644 index 0000000000..cf8d68136a --- /dev/null +++ b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/dto/AddToCartInput.java @@ -0,0 +1,50 @@ +/*- + * #%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.dto; + +public class AddToCartInput { + + private String productId; + private String skuId; + private int quantity; + + public String getProductId() { + return productId; + } + + public void setProductId(String productId) { + this.productId = productId; + } + + public String getSkuId() { + return skuId; + } + + public void setSkuId(String skuId) { + this.skuId = skuId; + } + + public int getQuantity() { + return quantity; + } + + public void setQuantity(int quantity) { + this.quantity = quantity; + } + +} diff --git a/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/dto/PromoCodeResult.java b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/dto/PromoCodeResult.java new file mode 100644 index 0000000000..50aa6582d1 --- /dev/null +++ b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/dto/PromoCodeResult.java @@ -0,0 +1,46 @@ +/*- + * #%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.dto; + +import org.broadleafcommerce.core.order.domain.Order; + +public class PromoCodeResult { + + private final Order order; + private final boolean promoAdded; + private final String errorMessage; + + public PromoCodeResult(Order order, boolean promoAdded, String errorMessage) { + this.order = order; + this.promoAdded = promoAdded; + this.errorMessage = errorMessage; + } + + public Order getOrder() { + return order; + } + + public boolean isPromoAdded() { + return promoAdded; + } + + public String getErrorMessage() { + return errorMessage; + } + +} diff --git a/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/CartMutationResolver.java b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/CartMutationResolver.java new file mode 100644 index 0000000000..5793d73976 --- /dev/null +++ b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/CartMutationResolver.java @@ -0,0 +1,156 @@ +/*- + * #%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.resolvers; + +import org.apache.commons.collections4.CollectionUtils; +import org.broadleafcommerce.core.offer.domain.OfferCode; +import org.broadleafcommerce.core.offer.service.OfferService; +import org.broadleafcommerce.core.offer.service.exception.OfferAlreadyAddedException; +import org.broadleafcommerce.core.offer.service.exception.OfferException; +import org.broadleafcommerce.core.offer.service.exception.OfferExpiredException; +import org.broadleafcommerce.core.offer.service.exception.OfferMaxUseExceededException; +import org.broadleafcommerce.core.order.domain.NullOrderImpl; +import org.broadleafcommerce.core.order.domain.Order; +import org.broadleafcommerce.core.order.service.OrderService; +import org.broadleafcommerce.core.order.service.call.OrderItemRequestDTO; +import org.broadleafcommerce.core.order.service.exception.AddToCartException; +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.broadleafcommerce.core.web.graphql.dto.AddToCartInput; +import org.broadleafcommerce.core.web.graphql.dto.PromoCodeResult; +import org.broadleafcommerce.core.web.order.CartState; +import org.broadleafcommerce.core.web.service.UpdateCartService; +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.data.method.annotation.Argument; +import org.springframework.graphql.data.method.annotation.MutationMapping; +import org.springframework.stereotype.Controller; + +import java.util.List; + +@Controller +public class CartMutationResolver { + + @Autowired + @Qualifier("blOrderService") + protected OrderService orderService; + + @Autowired + @Qualifier("blOfferService") + protected OfferService offerService; + + @Autowired + @Qualifier("blUpdateCartService") + protected UpdateCartService updateCartService; + + @MutationMapping + public Order addToCart(@Argument AddToCartInput input) throws AddToCartException, PricingException { + Order cart = CartState.getCart(); + if (cart == null || cart instanceof NullOrderImpl) { + cart = orderService.createNewCartForCustomer(CustomerState.getCustomer()); + CartState.setCart(cart); + } + + OrderItemRequestDTO itemRequest = new OrderItemRequestDTO(); + itemRequest.setProductId(Long.parseLong(input.getProductId())); + if (input.getSkuId() != null) { + itemRequest.setSkuId(Long.parseLong(input.getSkuId())); + } + itemRequest.setQuantity(input.getQuantity()); + + updateCartService.validateAddToCartRequest(itemRequest, cart); + + cart = orderService.addItem(cart.getId(), itemRequest, false); + cart = orderService.save(cart, true); + CartState.setCart(cart); + return cart; + } + + @MutationMapping + public Order updateCartItemQuantity(@Argument Long orderItemId, @Argument int quantity) + throws UpdateCartException, PricingException, RemoveFromCartException { + Order cart = CartState.getCart(); + OrderItemRequestDTO itemRequest = new OrderItemRequestDTO(); + itemRequest.setOrderItemId(orderItemId); + itemRequest.setQuantity(quantity); + cart = orderService.updateItemQuantity(cart.getId(), itemRequest, true); + cart = orderService.save(cart, false); + CartState.setCart(cart); + return cart; + } + + @MutationMapping + public Order removeFromCart(@Argument Long orderItemId) + throws PricingException, RemoveFromCartException { + Order cart = CartState.getCart(); + cart = orderService.removeItem(cart.getId(), orderItemId, false); + cart = orderService.save(cart, true); + CartState.setCart(cart); + return cart; + } + + @MutationMapping + public PromoCodeResult applyPromoCode(@Argument String code) throws PricingException { + Order cart = CartState.getCart(); + boolean promoAdded = false; + String errorMessage = null; + + if (cart != null && !(cart instanceof NullOrderImpl)) { + List offerCodes = offerService.lookupAllOfferCodesByCode(code); + if (CollectionUtils.isNotEmpty(offerCodes)) { + for (OfferCode offerCode : offerCodes) { + try { + cart = orderService.addOfferCode(cart, offerCode, false); + promoAdded = true; + } catch (OfferMaxUseExceededException e) { + errorMessage = "Use Limit Exceeded"; + } catch (OfferExpiredException e) { + errorMessage = "Offer Has Expired"; + } catch (OfferAlreadyAddedException e) { + errorMessage = "Offer Has Already Been Added"; + } catch (OfferException e) { + errorMessage = "An Unknown Offer Error Has Occurred"; + } + } + if (errorMessage == null) { + cart = orderService.save(cart, true); + CartState.setCart(cart); + } + } else { + errorMessage = "Unknown Code"; + } + } else { + errorMessage = "Invalid Cart"; + } + + return new PromoCodeResult(cart, promoAdded, errorMessage); + } + + @MutationMapping + public Order removePromoCode(@Argument Long offerCodeId) throws PricingException { + Order cart = CartState.getCart(); + OfferCode offerCode = offerService.findOfferCodeById(offerCodeId); + cart = orderService.removeOfferCode(cart, offerCode, false); + cart = orderService.save(cart, true); + CartState.setCart(cart); + return cart; + } + +} diff --git a/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/CartQueryResolver.java b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/CartQueryResolver.java new file mode 100644 index 0000000000..d4d33d4198 --- /dev/null +++ b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/CartQueryResolver.java @@ -0,0 +1,78 @@ +/*- + * #%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.resolvers; + +import org.broadleafcommerce.core.order.domain.Order; +import org.broadleafcommerce.core.order.service.OrderService; +import org.broadleafcommerce.core.order.service.type.OrderStatus; +import org.broadleafcommerce.profile.core.domain.Customer; +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.data.method.annotation.Argument; +import org.springframework.graphql.data.method.annotation.QueryMapping; +import org.springframework.stereotype.Controller; + +import java.util.Collections; +import java.util.List; + +@Controller +public class CartQueryResolver { + + @Autowired + @Qualifier("blOrderService") + protected OrderService orderService; + + @QueryMapping + public Order cart() { + Customer customer = CustomerState.getCustomer(); + if (customer == null) { + return null; + } + return orderService.findCartForCustomer(customer); + } + + @QueryMapping + public Order order(@Argument Long id) { + return orderService.findOrderById(id); + } + + @QueryMapping + public Order orderByNumber(@Argument String orderNumber) { + return orderService.findOrderByOrderNumber(orderNumber); + } + + @QueryMapping + public List orderHistory(@Argument String status) { + Customer customer = CustomerState.getCustomer(); + if (customer == null) { + return Collections.emptyList(); + } + if (status != null) { + OrderStatus orderStatus = OrderStatus.getInstance(status); + return orderService.findOrdersForCustomer(customer, orderStatus); + } + return orderService.findOrdersForCustomer(customer); + } + + @QueryMapping + public Customer customer() { + return CustomerState.getCustomer(); + } + +} diff --git a/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/OrderFieldResolver.java b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/OrderFieldResolver.java new file mode 100644 index 0000000000..ebd331c8f5 --- /dev/null +++ b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/OrderFieldResolver.java @@ -0,0 +1,72 @@ +/*- + * #%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.resolvers; + +import org.broadleafcommerce.common.money.Money; +import org.broadleafcommerce.core.order.domain.FulfillmentGroup; +import org.broadleafcommerce.core.order.domain.Order; +import org.broadleafcommerce.core.order.domain.OrderItem; +import org.broadleafcommerce.core.order.service.OrderService; +import org.broadleafcommerce.core.order.service.type.OrderStatus; +import org.broadleafcommerce.core.payment.domain.OrderPayment; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.graphql.data.method.annotation.SchemaMapping; +import org.springframework.stereotype.Controller; + +import java.util.List; + +@Controller +public class OrderFieldResolver { + + @Autowired + @Qualifier("blOrderService") + protected OrderService orderService; + + @SchemaMapping(typeName = "Order", field = "orderItems") + public List orderItems(Order order) { + return order.getOrderItems(); + } + + @SchemaMapping(typeName = "Order", field = "fulfillmentGroups") + public List fulfillmentGroups(Order order) { + return order.getFulfillmentGroups(); + } + + @SchemaMapping(typeName = "Order", field = "payments") + public List payments(Order order) { + return orderService.findPaymentsForOrder(order); + } + + @SchemaMapping(typeName = "Order", field = "subTotal") + public Money subTotal(Order order) { + return order.getSubTotal(); + } + + @SchemaMapping(typeName = "Order", field = "total") + public Money total(Order order) { + return order.getTotal(); + } + + @SchemaMapping(typeName = "Order", field = "status") + public String status(Order order) { + OrderStatus status = order.getStatus(); + return status != null ? status.getType() : null; + } + +} diff --git a/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/OrderItemFieldResolver.java b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/OrderItemFieldResolver.java new file mode 100644 index 0000000000..67ed4ca05c --- /dev/null +++ b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/OrderItemFieldResolver.java @@ -0,0 +1,46 @@ +/*- + * #%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.resolvers; + +import org.broadleafcommerce.core.catalog.domain.Product; +import org.broadleafcommerce.core.catalog.domain.Sku; +import org.broadleafcommerce.core.order.domain.DiscreteOrderItem; +import org.broadleafcommerce.core.order.domain.OrderItem; +import org.springframework.graphql.data.method.annotation.SchemaMapping; +import org.springframework.stereotype.Controller; + +@Controller +public class OrderItemFieldResolver { + + @SchemaMapping(typeName = "OrderItem", field = "product") + public Product product(OrderItem orderItem) { + if (orderItem instanceof DiscreteOrderItem discreteOrderItem) { + return discreteOrderItem.getProduct(); + } + return null; + } + + @SchemaMapping(typeName = "OrderItem", field = "sku") + public Sku sku(OrderItem orderItem) { + if (orderItem instanceof DiscreteOrderItem discreteOrderItem) { + return discreteOrderItem.getSku(); + } + return null; + } + +} diff --git a/core/broadleaf-framework-web/src/main/resources/graphql-application.properties b/core/broadleaf-framework-web/src/main/resources/graphql-application.properties new file mode 100644 index 0000000000..bdbfa221b0 --- /dev/null +++ b/core/broadleaf-framework-web/src/main/resources/graphql-application.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..6c85b96e0f --- /dev/null +++ b/core/broadleaf-framework-web/src/main/resources/graphql/schema.graphqls @@ -0,0 +1,203 @@ +type Query { + cart: Order + order(id: ID!): Order + orderByNumber(orderNumber: String!): Order + orderHistory(status: OrderStatus): [Order!]! + customer: Customer +} + +type Mutation { + addToCart(input: AddToCartInput!): Order! + updateCartItemQuantity(orderItemId: ID!, quantity: Int!): Order! + removeFromCart(orderItemId: ID!): Order! + applyPromoCode(code: String!): PromoCodeResult! + removePromoCode(offerCodeId: ID!): Order! +} + +type Money { + amount: BigDecimal! + currency: String +} + +scalar BigDecimal + +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 + url: String + description: String + longDescription: String + activeStartDate: String + activeEndDate: String +} + +type Sku { + id: ID! + name: String + description: String + longDescription: String + salePrice: Money + retailPrice: Money + cost: Money + upc: String + quantityAvailable: Int + available: Boolean + activeStartDate: String + activeEndDate: String +} + +type ProductOption { + id: ID! + label: String + type: String + required: Boolean + allowedValues: [ProductOptionValue!] +} + +type ProductOptionValue { + id: ID! + attributeValue: String + displayOrder: Int + priceAdjustment: Money +} + +type Order { + id: ID! + orderNumber: String + status: OrderStatus + subTotal: Money + total: Money + totalTax: Money + totalShipping: Money + emailAddress: String + orderItems: [OrderItem!] + fulfillmentGroups: [FulfillmentGroup!] + payments: [OrderPayment!] +} + +enum OrderStatus { + IN_PROCESS + NAMED + SUBMITTED + CANCELLED + QUOTE + CSR +} + +type OrderItem { + id: ID! + name: String + quantity: Int + price: Money + salePrice: Money + retailPrice: Money + totalPrice: Money + product: Product + sku: Sku + orderItemAttributes: [OrderItemAttribute!] +} + +type OrderItemAttribute { + id: ID! + name: String + value: String +} + +type FulfillmentGroup { + id: ID! + address: Address + shippingPrice: Money + merchandiseTotal: Money + total: Money + type: String + fulfillmentGroupItems: [FulfillmentGroupItem!] +} + +type FulfillmentGroupItem { + id: ID! + quantity: Int + totalItemAmount: Money + orderItem: OrderItem +} + +type Address { + id: ID! + firstName: String + lastName: String + addressLine1: String + addressLine2: String + city: String + stateProvinceRegion: String + postalCode: String + country: String + phonePrimary: String +} + +type OrderPayment { + id: ID! + amount: Money + type: String + gatewayType: String + referenceNumber: String +} + +type Customer { + id: ID! + firstName: String + lastName: String + emailAddress: String + username: String +} + +type PromoCodeResult { + order: Order! + promoAdded: Boolean! + errorMessage: String +} + +input AddToCartInput { + productId: ID! + skuId: ID + quantity: Int! + itemAttributes: [ItemAttributeInput!] +} + +input ItemAttributeInput { + name: String! + value: String! +} + +input ShippingInfoInput { + address: AddressInput! + fulfillmentOption: String +} + +input AddressInput { + firstName: String + lastName: String + addressLine1: String! + addressLine2: String + city: String! + stateProvinceRegion: String! + postalCode: String! + country: String! + phonePrimary: String +} + +input BillingInfoInput { + address: AddressInput! + paymentType: String! +} diff --git a/pom.xml b/pom.xml index 6836d3b269..69bfa2a1a2 100644 --- a/pom.xml +++ b/pom.xml @@ -581,6 +581,18 @@ test + + + org.springframework.boot + spring-boot-starter-graphql + ${spring.boot.version} + + + org.reactivestreams + reactive-streams + 1.0.4 + + org.hibernate From 8dddcdbfb88dee8629013d5ce6db7489b94113d1 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:27:37 +0000 Subject: [PATCH 2/4] Address review: schema enum, ownership check, active cart guard Co-Authored-By: Arjun Mishra --- .../resolvers/CartMutationResolver.java | 12 ++++++++++-- .../graphql/resolvers/CartQueryResolver.java | 19 +++++++++++++++++-- .../main/resources/graphql/schema.graphqls | 3 ++- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/CartMutationResolver.java b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/CartMutationResolver.java index 5793d73976..20d2440edd 100644 --- a/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/CartMutationResolver.java +++ b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/CartMutationResolver.java @@ -86,7 +86,7 @@ public Order addToCart(@Argument AddToCartInput input) throws AddToCartException @MutationMapping public Order updateCartItemQuantity(@Argument Long orderItemId, @Argument int quantity) throws UpdateCartException, PricingException, RemoveFromCartException { - Order cart = CartState.getCart(); + Order cart = requireActiveCart(); OrderItemRequestDTO itemRequest = new OrderItemRequestDTO(); itemRequest.setOrderItemId(orderItemId); itemRequest.setQuantity(quantity); @@ -99,13 +99,21 @@ public Order updateCartItemQuantity(@Argument Long orderItemId, @Argument int qu @MutationMapping public Order removeFromCart(@Argument Long orderItemId) throws PricingException, RemoveFromCartException { - Order cart = CartState.getCart(); + Order cart = requireActiveCart(); cart = orderService.removeItem(cart.getId(), orderItemId, false); cart = orderService.save(cart, true); CartState.setCart(cart); return cart; } + protected Order requireActiveCart() { + Order cart = CartState.getCart(); + if (cart == null || cart instanceof NullOrderImpl) { + throw new IllegalStateException("No active cart for the current customer"); + } + return cart; + } + @MutationMapping public PromoCodeResult applyPromoCode(@Argument String code) throws PricingException { Order cart = CartState.getCart(); diff --git a/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/CartQueryResolver.java b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/CartQueryResolver.java index d4d33d4198..834a9fd884 100644 --- a/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/CartQueryResolver.java +++ b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/CartQueryResolver.java @@ -49,12 +49,27 @@ public Order cart() { @QueryMapping public Order order(@Argument Long id) { - return orderService.findOrderById(id); + Order order = orderService.findOrderById(id); + return filterOrderForCurrentCustomer(order); } @QueryMapping public Order orderByNumber(@Argument String orderNumber) { - return orderService.findOrderByOrderNumber(orderNumber); + Order order = orderService.findOrderByOrderNumber(orderNumber); + return filterOrderForCurrentCustomer(order); + } + + protected Order filterOrderForCurrentCustomer(Order order) { + if (order == null) { + return null; + } + Customer currentCustomer = CustomerState.getCustomer(); + Customer orderCustomer = order.getCustomer(); + if (currentCustomer == null || orderCustomer == null + || !currentCustomer.equals(orderCustomer)) { + return null; + } + return order; } @QueryMapping diff --git a/core/broadleaf-framework-web/src/main/resources/graphql/schema.graphqls b/core/broadleaf-framework-web/src/main/resources/graphql/schema.graphqls index 6c85b96e0f..6c87152fac 100644 --- a/core/broadleaf-framework-web/src/main/resources/graphql/schema.graphqls +++ b/core/broadleaf-framework-web/src/main/resources/graphql/schema.graphqls @@ -94,7 +94,8 @@ enum OrderStatus { SUBMITTED CANCELLED QUOTE - CSR + CSR_OWNED + ARCHIVED } type OrderItem { From fb99d837ae8dfa3f2445d2d15351de39034d0c04 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:34 +0000 Subject: [PATCH 3/4] Address review: nullable PromoCodeResult.order, guard removePromoCode Co-Authored-By: Arjun Mishra --- .../core/web/graphql/resolvers/CartMutationResolver.java | 2 +- .../src/main/resources/graphql/schema.graphqls | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/CartMutationResolver.java b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/CartMutationResolver.java index 20d2440edd..c3a959b2eb 100644 --- a/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/CartMutationResolver.java +++ b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/CartMutationResolver.java @@ -153,7 +153,7 @@ public PromoCodeResult applyPromoCode(@Argument String code) throws PricingExcep @MutationMapping public Order removePromoCode(@Argument Long offerCodeId) throws PricingException { - Order cart = CartState.getCart(); + Order cart = requireActiveCart(); OfferCode offerCode = offerService.findOfferCodeById(offerCodeId); cart = orderService.removeOfferCode(cart, offerCode, false); cart = orderService.save(cart, true); diff --git a/core/broadleaf-framework-web/src/main/resources/graphql/schema.graphqls b/core/broadleaf-framework-web/src/main/resources/graphql/schema.graphqls index 6c87152fac..e3c22a8154 100644 --- a/core/broadleaf-framework-web/src/main/resources/graphql/schema.graphqls +++ b/core/broadleaf-framework-web/src/main/resources/graphql/schema.graphqls @@ -164,7 +164,7 @@ type Customer { } type PromoCodeResult { - order: Order! + order: Order promoAdded: Boolean! errorMessage: String } From 354255b94f695bbd9770f4137a5cec260372ae18 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:38:19 +0000 Subject: [PATCH 4/4] Address review: itemAttributes in AddToCartInput, null-check OfferCode in removePromoCode Co-Authored-By: Arjun Mishra --- .../core/web/graphql/dto/AddToCartInput.java | 11 +++++ .../web/graphql/dto/ItemAttributeInput.java | 41 +++++++++++++++++++ .../resolvers/CartMutationResolver.java | 13 ++++++ 3 files changed, 65 insertions(+) create mode 100644 core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/dto/ItemAttributeInput.java diff --git a/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/dto/AddToCartInput.java b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/dto/AddToCartInput.java index cf8d68136a..d3432ed278 100644 --- a/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/dto/AddToCartInput.java +++ b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/dto/AddToCartInput.java @@ -17,11 +17,14 @@ */ package org.broadleafcommerce.core.web.graphql.dto; +import java.util.List; + public class AddToCartInput { private String productId; private String skuId; private int quantity; + private List itemAttributes; public String getProductId() { return productId; @@ -47,4 +50,12 @@ public void setQuantity(int quantity) { this.quantity = quantity; } + public List getItemAttributes() { + return itemAttributes; + } + + public void setItemAttributes(List itemAttributes) { + this.itemAttributes = itemAttributes; + } + } diff --git a/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/dto/ItemAttributeInput.java b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/dto/ItemAttributeInput.java new file mode 100644 index 0000000000..fc5a86745d --- /dev/null +++ b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/dto/ItemAttributeInput.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.dto; + +public class ItemAttributeInput { + + private String name; + private String value; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + +} diff --git a/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/CartMutationResolver.java b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/CartMutationResolver.java index c3a959b2eb..0d4c69b404 100644 --- a/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/CartMutationResolver.java +++ b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/CartMutationResolver.java @@ -33,6 +33,7 @@ import org.broadleafcommerce.core.order.service.exception.UpdateCartException; import org.broadleafcommerce.core.pricing.service.exception.PricingException; import org.broadleafcommerce.core.web.graphql.dto.AddToCartInput; +import org.broadleafcommerce.core.web.graphql.dto.ItemAttributeInput; import org.broadleafcommerce.core.web.graphql.dto.PromoCodeResult; import org.broadleafcommerce.core.web.order.CartState; import org.broadleafcommerce.core.web.service.UpdateCartService; @@ -43,7 +44,9 @@ import org.springframework.graphql.data.method.annotation.MutationMapping; import org.springframework.stereotype.Controller; +import java.util.HashMap; import java.util.List; +import java.util.Map; @Controller public class CartMutationResolver { @@ -74,6 +77,13 @@ public Order addToCart(@Argument AddToCartInput input) throws AddToCartException itemRequest.setSkuId(Long.parseLong(input.getSkuId())); } itemRequest.setQuantity(input.getQuantity()); + if (CollectionUtils.isNotEmpty(input.getItemAttributes())) { + Map attributes = new HashMap<>(); + for (ItemAttributeInput attr : input.getItemAttributes()) { + attributes.put(attr.getName(), attr.getValue()); + } + itemRequest.setItemAttributes(attributes); + } updateCartService.validateAddToCartRequest(itemRequest, cart); @@ -155,6 +165,9 @@ public PromoCodeResult applyPromoCode(@Argument String code) throws PricingExcep public Order removePromoCode(@Argument Long offerCodeId) throws PricingException { Order cart = requireActiveCart(); OfferCode offerCode = offerService.findOfferCodeById(offerCodeId); + if (offerCode == null) { + throw new IllegalArgumentException("Offer code not found for id: " + offerCodeId); + } cart = orderService.removeOfferCode(cart, offerCode, false); cart = orderService.save(cart, true); CartState.setCart(cart);