org.broadleafcommerce.core.pricing.service.fulfillment.provider.BandedFulfillmentPricingProvider.java Source code

Java tutorial

Introduction

Here is the source code for org.broadleafcommerce.core.pricing.service.fulfillment.provider.BandedFulfillmentPricingProvider.java

Source

/*
 * #%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.pricing.service.fulfillment.provider;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.broadleafcommerce.common.currency.util.BroadleafCurrencyUtils;
import org.broadleafcommerce.common.money.Money;
import org.broadleafcommerce.common.util.UnitOfMeasureUtil;
import org.broadleafcommerce.common.util.WeightUnitOfMeasureType;
import org.broadleafcommerce.common.vendor.service.exception.FulfillmentPriceException;
import org.broadleafcommerce.core.catalog.domain.Sku;
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.FulfillmentOption;
import org.broadleafcommerce.core.order.fulfillment.domain.BandedPriceFulfillmentOption;
import org.broadleafcommerce.core.order.fulfillment.domain.BandedWeightFulfillmentOption;
import org.broadleafcommerce.core.order.fulfillment.domain.FulfillmentBand;
import org.broadleafcommerce.core.order.fulfillment.domain.FulfillmentPriceBand;
import org.broadleafcommerce.core.order.fulfillment.domain.FulfillmentWeightBand;
import org.broadleafcommerce.core.order.service.type.FulfillmentBandResultAmountType;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * <p>Used in conjunction with {@link BandedPriceFulfillmentOption} and {@link BandedWeightFulfillmentOption}. 
 *  If 2 bands are configured equal to each other (meaning, there are 2 {@link FulfillmentPriceBand}s that have the 
 *  same retail price minimum or 2 {@link FulfillmentWeightBand}s that have the same minimum weight), 
 *  this will choose the cheaper rate of the 2</p>
 * <p>If the retail total does not fall within a configured price band, the total cost of fulfillment is zero</p>
 * <p>
 * Note: For {@link BandedWeightFulfillmentOption}, this assumes that all of your weights have the same units
 * </p>
 * @author Phillip Verheyden
 * @see {@link BandedPriceFulfillmentOption}, {@link FulfillmentPriceBand}
 */
public class BandedFulfillmentPricingProvider implements FulfillmentPricingProvider {

    protected static final Log LOG = LogFactory.getLog(BandedFulfillmentPricingProvider.class);

    @Override
    public boolean canCalculateCostForFulfillmentGroup(FulfillmentGroup fulfillmentGroup,
            FulfillmentOption option) {
        return (option instanceof BandedPriceFulfillmentOption)
                || (option instanceof BandedWeightFulfillmentOption);
    }

    @Override
    public FulfillmentGroup calculateCostForFulfillmentGroup(FulfillmentGroup fulfillmentGroup)
            throws FulfillmentPriceException {
        if (fulfillmentGroup.getFulfillmentGroupItems().size() == 0) {
            LOG.warn("fulfillment group (" + fulfillmentGroup.getId()
                    + ") does not contain any fulfillment group items. Unable to price banded shipping");
            fulfillmentGroup.setShippingPrice(Money.ZERO);
            fulfillmentGroup.setSaleShippingPrice(Money.ZERO);
            fulfillmentGroup.setRetailShippingPrice(Money.ZERO);
            return fulfillmentGroup;
        }

        if (canCalculateCostForFulfillmentGroup(fulfillmentGroup, fulfillmentGroup.getFulfillmentOption())) {
            //In this case, the estimation logic is the same as calculation logic. Call the estimation service to get the prices.
            HashSet<FulfillmentOption> options = new HashSet<FulfillmentOption>();
            options.add(fulfillmentGroup.getFulfillmentOption());
            FulfillmentEstimationResponse response = estimateCostForFulfillmentGroup(fulfillmentGroup, options);
            fulfillmentGroup.setSaleShippingPrice(
                    response.getFulfillmentOptionPrices().get(fulfillmentGroup.getFulfillmentOption()));
            fulfillmentGroup.setRetailShippingPrice(
                    response.getFulfillmentOptionPrices().get(fulfillmentGroup.getFulfillmentOption()));
            fulfillmentGroup.setShippingPrice(
                    response.getFulfillmentOptionPrices().get(fulfillmentGroup.getFulfillmentOption()));

            return fulfillmentGroup;
        }

        throw new FulfillmentPriceException(
                "An unsupported FulfillmentOption was passed to the calculateCostForFulfillmentGroup method");
    }

    @Override
    public FulfillmentEstimationResponse estimateCostForFulfillmentGroup(FulfillmentGroup fulfillmentGroup,
            Set<FulfillmentOption> options) throws FulfillmentPriceException {

        //Set up the response object
        FulfillmentEstimationResponse res = new FulfillmentEstimationResponse();
        HashMap<FulfillmentOption, Money> shippingPrices = new HashMap<FulfillmentOption, Money>();
        res.setFulfillmentOptionPrices(shippingPrices);

        for (FulfillmentOption option : options) {
            if (canCalculateCostForFulfillmentGroup(fulfillmentGroup, option)) {

                List<? extends FulfillmentBand> bands = null;
                if (option instanceof BandedPriceFulfillmentOption) {
                    bands = ((BandedPriceFulfillmentOption) option).getBands();
                } else if (option instanceof BandedWeightFulfillmentOption) {
                    bands = ((BandedWeightFulfillmentOption) option).getBands();
                }

                if (bands == null || bands.isEmpty()) {
                    //Something is misconfigured. There are no bands associated with this fulfillment option
                    throw new IllegalStateException(
                            "There were no Fulfillment Price Bands configured for a BandedPriceFulfillmentOption with ID: "
                                    + option.getId());
                }

                //Calculate the amount that the band will be applied to
                BigDecimal retailTotal = BigDecimal.ZERO;
                BigDecimal flatTotal = BigDecimal.ZERO;

                BigDecimal weightTotal = BigDecimal.ZERO;
                boolean foundCandidateForBand = false;
                for (FulfillmentGroupItem fulfillmentGroupItem : fulfillmentGroup.getFulfillmentGroupItems()) {

                    //If this item has a Sku associated with it which also has a flat rate for this fulfillment option, don't add it to the price
                    //or weight total but instead tack it onto the final rate
                    boolean addToTotal = true;
                    Sku sku = null;
                    if (fulfillmentGroupItem.getOrderItem() instanceof DiscreteOrderItem) {
                        sku = ((DiscreteOrderItem) fulfillmentGroupItem.getOrderItem()).getSku();
                    } else if (fulfillmentGroupItem.getOrderItem() instanceof BundleOrderItem) {
                        sku = ((BundleOrderItem) fulfillmentGroupItem.getOrderItem()).getSku();
                    }

                    if (sku != null && option.getUseFlatRates()) {
                        BigDecimal rate = sku.getFulfillmentFlatRates().get(option);
                        if (rate != null) {
                            addToTotal = false;
                            flatTotal = flatTotal.add(rate);
                        }
                    }

                    if (addToTotal) {
                        foundCandidateForBand = true;
                        BigDecimal price = (fulfillmentGroupItem.getTotalItemAmount() != null)
                                ? fulfillmentGroupItem.getTotalItemAmount().getAmount()
                                : null;
                        if (price == null) {
                            price = fulfillmentGroupItem.getOrderItem().getAveragePrice().getAmount()
                                    .multiply(BigDecimal.valueOf(fulfillmentGroupItem.getQuantity()));
                        }
                        retailTotal = retailTotal.add(price);

                        if (sku != null && sku.getWeight() != null && sku.getWeight().getWeight() != null) {
                            BigDecimal convertedWeight = convertWeight(sku.getWeight().getWeight(),
                                    sku.getWeight().getWeightUnitOfMeasure())
                                            .multiply(BigDecimal.valueOf(fulfillmentGroupItem.getQuantity()));
                            weightTotal = weightTotal.add(convertedWeight);
                        }
                    }
                }

                //Used to keep track of the lowest price when there is are bands that have the same
                //minimum amount
                BigDecimal lowestBandFulfillmentPrice = null;
                //Used to keep track of the amount for the lowest band fulfillment price. Used to compare
                //if 2 bands are configured for the same minimum amount
                BigDecimal lowestBandFulfillmentPriceMinimumAmount = BigDecimal.ZERO;

                if (foundCandidateForBand) {
                    for (FulfillmentBand band : bands) {

                        BigDecimal bandMinimumAmount = BigDecimal.ZERO;
                        boolean foundMatch = false;
                        if (band instanceof FulfillmentPriceBand) {
                            bandMinimumAmount = ((FulfillmentPriceBand) band).getRetailPriceMinimumAmount();
                            foundMatch = retailTotal.compareTo(bandMinimumAmount) >= 0;
                        } else if (band instanceof FulfillmentWeightBand) {
                            bandMinimumAmount = ((FulfillmentWeightBand) band).getMinimumWeight();
                            foundMatch = weightTotal.compareTo(bandMinimumAmount) >= 0;
                        }

                        if (foundMatch) {
                            //So far, we've found a potential match
                            //Now, determine if this is a percentage or actual amount
                            FulfillmentBandResultAmountType resultAmountType = band.getResultAmountType();
                            BigDecimal bandFulfillmentPrice = null;
                            if (FulfillmentBandResultAmountType.RATE.equals(resultAmountType)) {
                                bandFulfillmentPrice = band.getResultAmount();
                            } else if (FulfillmentBandResultAmountType.PERCENTAGE.equals(resultAmountType)) {
                                //Since this is a percentage, we calculate the result amount based on retailTotal and the band percentage
                                bandFulfillmentPrice = retailTotal.multiply(band.getResultAmount());
                            } else {
                                LOG.warn("Unknown FulfillmentBandResultAmountType: " + resultAmountType.getType()
                                        + " Should be RATE or PERCENTAGE. Ignoring.");
                            }

                            if (bandFulfillmentPrice != null) {

                                //haven't initialized the lowest price yet so just take on this one
                                if (lowestBandFulfillmentPrice == null) {
                                    lowestBandFulfillmentPrice = bandFulfillmentPrice;
                                    lowestBandFulfillmentPriceMinimumAmount = bandMinimumAmount;
                                }

                                //If there is a duplicate price band (meaning, 2 price bands are configured with the same miniumum retail price)
                                //then the lowest fulfillment amount should only be updated if the result of the current band being looked at
                                //is cheaper
                                if (lowestBandFulfillmentPriceMinimumAmount.compareTo(bandMinimumAmount) == 0) {
                                    if (bandFulfillmentPrice.compareTo(lowestBandFulfillmentPrice) <= 0) {
                                        lowestBandFulfillmentPrice = bandFulfillmentPrice;
                                        lowestBandFulfillmentPriceMinimumAmount = bandMinimumAmount;
                                    }
                                } else if (bandMinimumAmount
                                        .compareTo(lowestBandFulfillmentPriceMinimumAmount) > 0) {
                                    lowestBandFulfillmentPrice = bandFulfillmentPrice;
                                    lowestBandFulfillmentPriceMinimumAmount = bandMinimumAmount;
                                }

                            } else {
                                throw new IllegalStateException("Bands must have a non-null fulfillment price");
                            }
                        }
                    }
                }

                //If I didn't find a valid band, initialize the fulfillment price to zero
                if (lowestBandFulfillmentPrice == null) {
                    lowestBandFulfillmentPrice = BigDecimal.ZERO;
                }
                //add the flat rate amount calculated on the Sku
                lowestBandFulfillmentPrice = lowestBandFulfillmentPrice.add(flatTotal);

                shippingPrices.put(option, BroadleafCurrencyUtils.getMoney(lowestBandFulfillmentPrice,
                        fulfillmentGroup.getOrder().getCurrency()));
            }
        }

        return res;
    }

    /**
     * Default implementation is to convert everything to pounds for consistent weight types
     * 
     * @param weight
     * @param type
     * @return
     */
    protected BigDecimal convertWeight(BigDecimal weight, WeightUnitOfMeasureType type) {
        return UnitOfMeasureUtil.findPounds(weight, type);
    }

}