Java tutorial
/* * #%L * BroadleafCommerce Framework * %% * Copyright (C) 2009 - 2013 Broadleaf Commerce * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ package org.broadleafcommerce.core.order.strategy; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.broadleafcommerce.core.catalog.domain.Sku; import org.broadleafcommerce.core.order.dao.FulfillmentGroupItemDao; import org.broadleafcommerce.core.order.domain.BundleOrderItem; import org.broadleafcommerce.core.order.domain.DiscreteOrderItem; import org.broadleafcommerce.core.order.domain.FulfillmentGroup; import org.broadleafcommerce.core.order.domain.FulfillmentGroupItem; import org.broadleafcommerce.core.order.domain.Order; import org.broadleafcommerce.core.order.domain.OrderItem; import org.broadleafcommerce.core.order.service.FulfillmentGroupService; import org.broadleafcommerce.core.order.service.OrderItemService; import org.broadleafcommerce.core.order.service.OrderService; import org.broadleafcommerce.core.order.service.call.FulfillmentGroupItemRequest; import org.broadleafcommerce.core.order.service.type.FulfillmentType; import org.broadleafcommerce.core.order.service.workflow.CartOperationRequest; import org.broadleafcommerce.core.pricing.service.exception.PricingException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Map.Entry; import javax.annotation.Resource; /** * @author Andre Azzolini (apazzolini) */ @Service("blFulfillmentGroupItemStrategy") public class FulfillmentGroupItemStrategyImpl implements FulfillmentGroupItemStrategy { private static final Log LOG = LogFactory.getLog(FulfillmentGroupItemStrategyImpl.class); @Resource(name = "blFulfillmentGroupService") protected FulfillmentGroupService fulfillmentGroupService; @Resource(name = "blOrderItemService") protected OrderItemService orderItemService; @Resource(name = "blOrderService") protected OrderService orderService; @Resource(name = "blFulfillmentGroupItemDao") protected FulfillmentGroupItemDao fgItemDao; protected boolean removeEmptyFulfillmentGroups = true; @Value("${singleFulfillmentGroup.fgItem.sync.qty:false}") private boolean singleFulfillmentGroupSyncFGItemQty; @Override public CartOperationRequest onItemAdded(CartOperationRequest request) throws PricingException { Order order = request.getOrder(); OrderItem orderItem = request.getOrderItem(); Map<FulfillmentType, FulfillmentGroup> fulfillmentGroups = new HashMap<FulfillmentType, FulfillmentGroup>(); FulfillmentGroup nullFulfillmentTypeGroup = null; //First, let's organize fulfillment groups according to their fulfillment type //We'll use the first of each type that we find. Implementors can choose to move groups / items around later. if (order.getFulfillmentGroups() != null) { for (FulfillmentGroup group : order.getFulfillmentGroups()) { if (group.getType() == null) { if (nullFulfillmentTypeGroup == null) { nullFulfillmentTypeGroup = group; } } else { if (fulfillmentGroups.get(group.getType()) == null) { fulfillmentGroups.put(group.getType(), group); } } } } if (orderItem instanceof BundleOrderItem) { //We only care about the discrete order items List<DiscreteOrderItem> itemsToAdd = new ArrayList<DiscreteOrderItem>( ((BundleOrderItem) orderItem).getDiscreteOrderItems()); for (DiscreteOrderItem doi : itemsToAdd) { FulfillmentGroup fulfillmentGroup = null; FulfillmentType type = resolveFulfillmentType(doi); if (type == null) { //Use the fulfillment group with a null type fulfillmentGroup = nullFulfillmentTypeGroup; } else { if (FulfillmentType.PHYSICAL_PICKUP_OR_SHIP.equals(type)) { //This is really a special case. "PICKUP_OR_SHIP" is convenient to allow a sku to be picked up or shipped. //However, it is ambiguous when actually trying to create a fulfillment group. So we default to "PHYSICAL_SHIP". type = FulfillmentType.PHYSICAL_SHIP; } //Use the fulfillment group with the specified type fulfillmentGroup = fulfillmentGroups.get(type); } //If the null type or specified type, above were null, then we need to create a new fulfillment group boolean createdFulfillmentGroup = false; if (fulfillmentGroup == null) { fulfillmentGroup = fulfillmentGroupService.createEmptyFulfillmentGroup(); //Set the type fulfillmentGroup.setType(type); fulfillmentGroup.setOrder(order); order.getFulfillmentGroups().add(fulfillmentGroup); createdFulfillmentGroup = true; } fulfillmentGroup = addItemToFulfillmentGroup(order, doi, doi.getQuantity() * orderItem.getQuantity(), fulfillmentGroup); order = fulfillmentGroup.getOrder(); // If we had to create a new fulfillment group, then ensure that this will operate correctly for the next set // of fulfillment groups if (createdFulfillmentGroup) { if (type == null) { nullFulfillmentTypeGroup = fulfillmentGroup; } else { fulfillmentGroups.put(type, fulfillmentGroup); } } } } else if (orderItem instanceof DiscreteOrderItem) { DiscreteOrderItem doi = (DiscreteOrderItem) orderItem; FulfillmentGroup fulfillmentGroup = null; FulfillmentType type = resolveFulfillmentType(doi); if (type == null) { //Use the fulfillment group with a null type fulfillmentGroup = nullFulfillmentTypeGroup; } else { if (FulfillmentType.PHYSICAL_PICKUP_OR_SHIP.equals(type)) { //This is really a special case. "PICKUP_OR_SHIP" is convenient to allow a sku to be picked up or shipped. //However, it is ambiguous when actually trying to create a fulfillment group. So we default to "PHYSICAL_SHIP". type = FulfillmentType.PHYSICAL_SHIP; } //Use the fulfillment group with the specified type fulfillmentGroup = fulfillmentGroups.get(type); } //If the null type or specified type, above were null, then we need to create a new fulfillment group if (fulfillmentGroup == null) { fulfillmentGroup = fulfillmentGroupService.createEmptyFulfillmentGroup(); //Set the type fulfillmentGroup.setType(type); fulfillmentGroup.setOrder(order); order.getFulfillmentGroups().add(fulfillmentGroup); } fulfillmentGroup = addItemToFulfillmentGroup(order, orderItem, fulfillmentGroup); order = fulfillmentGroup.getOrder(); } else { FulfillmentGroup fulfillmentGroup = nullFulfillmentTypeGroup; if (fulfillmentGroup == null) { fulfillmentGroup = fulfillmentGroupService.createEmptyFulfillmentGroup(); fulfillmentGroup.setOrder(order); order.getFulfillmentGroups().add(fulfillmentGroup); } fulfillmentGroup = addItemToFulfillmentGroup(order, orderItem, fulfillmentGroup); } return request; } /** * Resolves the fulfillment type based on the order item. The OOB implementation uses the {@link DiscreteOrderItem#getSku()} * to then invoke {@link #resolveFulfillmentType(Sku)}. * * @param discreteOrderItem * @return */ protected FulfillmentType resolveFulfillmentType(DiscreteOrderItem discreteOrderItem) { return resolveFulfillmentType(discreteOrderItem.getSku()); } protected FulfillmentType resolveFulfillmentType(Sku sku) { if (sku.getFulfillmentType() != null) { return sku.getFulfillmentType(); } if (sku.getDefaultProduct() != null && sku.getDefaultProduct().getDefaultCategory() != null) { return sku.getDefaultProduct().getDefaultCategory().getFulfillmentType(); } return null; } protected FulfillmentGroup addItemToFulfillmentGroup(Order order, OrderItem orderItem, FulfillmentGroup fulfillmentGroup) throws PricingException { return this.addItemToFulfillmentGroup(order, orderItem, orderItem.getQuantity(), fulfillmentGroup); } protected FulfillmentGroup addItemToFulfillmentGroup(Order order, OrderItem orderItem, int quantity, FulfillmentGroup fulfillmentGroup) throws PricingException { FulfillmentGroupItemRequest fulfillmentGroupItemRequest = new FulfillmentGroupItemRequest(); fulfillmentGroupItemRequest.setOrder(order); fulfillmentGroupItemRequest.setOrderItem(orderItem); fulfillmentGroupItemRequest.setQuantity(quantity); fulfillmentGroupItemRequest.setFulfillmentGroup(fulfillmentGroup); return fulfillmentGroupService.addItemToFulfillmentGroup(fulfillmentGroupItemRequest, false, false); } @Override public CartOperationRequest onItemUpdated(CartOperationRequest request) throws PricingException { Order order = request.getOrder(); OrderItem orderItem = request.getOrderItem(); Integer orderItemQuantityDelta = request.getOrderItemQuantityDelta(); if (orderItemQuantityDelta == 0) { // If the quantity didn't change, nothing needs to happen return request; } else { List<FulfillmentGroupItem> fgisToDelete = new ArrayList<FulfillmentGroupItem>(); if (orderItem instanceof BundleOrderItem) { List<OrderItem> itemsToUpdate = new ArrayList<OrderItem>( ((BundleOrderItem) orderItem).getDiscreteOrderItems()); for (OrderItem oi : itemsToUpdate) { int quantityPer = oi.getQuantity(); fgisToDelete.addAll(updateItemQuantity(order, oi, (quantityPer * orderItemQuantityDelta))); } } else { fgisToDelete.addAll(updateItemQuantity(order, orderItem, orderItemQuantityDelta)); } request.setFgisToDelete(fgisToDelete); } return request; } protected List<FulfillmentGroupItem> updateItemQuantity(Order order, OrderItem orderItem, Integer orderItemQuantityDelta) throws PricingException { List<FulfillmentGroupItem> fgisToDelete = new ArrayList<FulfillmentGroupItem>(); boolean done = false; if (orderItemQuantityDelta > 0) { // If the quantity is now greater, we can simply add quantity to the first // fulfillment group we find that has that order item in it. for (FulfillmentGroup fg : order.getFulfillmentGroups()) { for (FulfillmentGroupItem fgItem : fg.getFulfillmentGroupItems()) { if (!done && fgItem.getOrderItem().equals(orderItem)) { fgItem.setQuantity(fgItem.getQuantity() + orderItemQuantityDelta); done = true; } } } } else { // The quantity has been decremented. We must ensure that the appropriate number // of fulfillment group items are decremented as well. int remainingToDecrement = -1 * orderItemQuantityDelta; for (FulfillmentGroup fg : order.getFulfillmentGroups()) { for (FulfillmentGroupItem fgItem : fg.getFulfillmentGroupItems()) { if (fgItem.getOrderItem().equals(orderItem)) { if (!done && fgItem.getQuantity() == remainingToDecrement) { // Quantity matches exactly. Simply remove the item. fgisToDelete.add(fgItem); done = true; } else if (!done && fgItem.getQuantity() > remainingToDecrement) { // We have enough quantity in this fg item to facilitate the entire requsted update fgItem.setQuantity(fgItem.getQuantity() - remainingToDecrement); done = true; } else if (!done) { // We do not have enough quantity. We'll remove this item and continue searching // for the remainder. remainingToDecrement = remainingToDecrement - fgItem.getQuantity(); fgisToDelete.add(fgItem); } } } } } if (!done) { throw new IllegalStateException( "Could not find matching fulfillment group item for the given order item"); } return fgisToDelete; } @Override public CartOperationRequest onItemRemoved(CartOperationRequest request) { Order order = request.getOrder(); OrderItem orderItem = request.getOrderItem(); if (orderItem instanceof BundleOrderItem) { List<OrderItem> itemsToRemove = new ArrayList<OrderItem>( ((BundleOrderItem) orderItem).getDiscreteOrderItems()); for (OrderItem oi : itemsToRemove) { request.getFgisToDelete() .addAll(fulfillmentGroupService.getFulfillmentGroupItemsForOrderItem(order, oi)); } } else { request.getFgisToDelete() .addAll(fulfillmentGroupService.getFulfillmentGroupItemsForOrderItem(order, orderItem)); } return request; } @Override public CartOperationRequest verify(CartOperationRequest request) throws PricingException { Order order = request.getOrder(); if (isRemoveEmptyFulfillmentGroups() && order.getFulfillmentGroups() != null) { ListIterator<FulfillmentGroup> fgIter = order.getFulfillmentGroups().listIterator(); while (fgIter.hasNext()) { FulfillmentGroup fg = fgIter.next(); if (fg.getFulfillmentGroupItems() == null || fg.getFulfillmentGroupItems().size() == 0) { fgIter.remove(); fulfillmentGroupService.delete(fg); } } } Map<Long, Integer> oiQuantityMap = new HashMap<Long, Integer>(); List<OrderItem> expandedOrderItems = new ArrayList<OrderItem>(); Map<Long, FulfillmentGroupItem> fgItemMap = new HashMap<Long, FulfillmentGroupItem>(); for (OrderItem oi : order.getOrderItems()) { if (oi instanceof BundleOrderItem) { for (DiscreteOrderItem doi : ((BundleOrderItem) oi).getDiscreteOrderItems()) { expandedOrderItems.add(doi); } } else if (oi instanceof DiscreteOrderItem) { expandedOrderItems.add(oi); } else { expandedOrderItems.add(oi); } } for (OrderItem oi : expandedOrderItems) { Integer oiQuantity = oiQuantityMap.get(oi.getId()); if (oiQuantity == null) { oiQuantity = 0; } if (oi instanceof DiscreteOrderItem && ((DiscreteOrderItem) oi).getBundleOrderItem() != null) { oiQuantity += ((DiscreteOrderItem) oi).getBundleOrderItem().getQuantity() * oi.getQuantity(); } else { oiQuantity += oi.getQuantity(); } oiQuantityMap.put(oi.getId(), oiQuantity); } for (FulfillmentGroup fg : order.getFulfillmentGroups()) { for (FulfillmentGroupItem fgi : fg.getFulfillmentGroupItems()) { Long oiId = fgi.getOrderItem().getId(); Integer oiQuantity = oiQuantityMap.get(oiId); if (oiQuantity == null) { throw new IllegalStateException( "Fulfillment group items and discrete order items are not in sync. DiscreteOrderItem id: " + oiId); } oiQuantity -= fgi.getQuantity(); oiQuantityMap.put(oiId, oiQuantity); fgItemMap.put(fgi.getId(), fgi); } } for (Entry<Long, Integer> entry : oiQuantityMap.entrySet()) { if (!entry.getValue().equals(0)) { if (useSingleFulfillmentGroupQtySync(order.getFulfillmentGroups())) { LOG.warn("Not enough fulfillment group items found for DiscreteOrderItem id:" + entry.getKey()); // There are edge cases where the OrderItem and FulfillmentGroupItem quantities can fall out of sync. If this happens // we set the FGItem to the correct quantity from the OrderItem and save/reprice the order to synchronize them. FulfillmentGroupItem fgItem = fgItemMap.get(entry.getKey()); for (OrderItem oi : expandedOrderItems) { if (oi.getId().equals(fgItem.getOrderItem().getId())) { LOG.warn("Synchronizing FulfillmentGroupItem to match OrderItem [" + entry.getKey() + "] quantity of : " + oi.getQuantity()); fgItem.setQuantity(oi.getQuantity()); } } // We price the order in order to get the right amount after the qty change order = orderService.save(order, true); request.setOrder(order); } else { throw new IllegalStateException( "Not enough fulfillment group items found for DiscreteOrderItem id: " + entry.getKey()); } } } return request; } private boolean useSingleFulfillmentGroupQtySync(List<FulfillmentGroup> fulfillmentGroups) { return singleFulfillmentGroupSyncFGItemQty && CollectionUtils.isNotEmpty(fulfillmentGroups) && fulfillmentGroups.size() == 1; } @Override public boolean isRemoveEmptyFulfillmentGroups() { return removeEmptyFulfillmentGroups; } @Override public void setRemoveEmptyFulfillmentGroups(boolean removeEmptyFulfillmentGroups) { this.removeEmptyFulfillmentGroups = removeEmptyFulfillmentGroups; } }