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..d3432ed278 --- /dev/null +++ b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/dto/AddToCartInput.java @@ -0,0 +1,61 @@ +/*- + * #%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 java.util.List; + +public class AddToCartInput { + + private String productId; + private String skuId; + private int quantity; + private List itemAttributes; + + 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; + } + + 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/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..0d4c69b404 --- /dev/null +++ b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/CartMutationResolver.java @@ -0,0 +1,177 @@ +/*- + * #%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.ItemAttributeInput; +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.HashMap; +import java.util.List; +import java.util.Map; + +@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()); + 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); + + 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 = requireActiveCart(); + 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 = 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(); + 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 = 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); + 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..834a9fd884 --- /dev/null +++ b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/resolvers/CartQueryResolver.java @@ -0,0 +1,93 @@ +/*- + * #%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) { + Order order = orderService.findOrderById(id); + return filterOrderForCurrentCustomer(order); + } + + @QueryMapping + public Order orderByNumber(@Argument String 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 + 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..e3c22a8154 --- /dev/null +++ b/core/broadleaf-framework-web/src/main/resources/graphql/schema.graphqls @@ -0,0 +1,204 @@ +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_OWNED + ARCHIVED +} + +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