org.broadleafcommerce.core.pricing.service.fulfillment.provider.BandedFulfillmentPricingProvider Maven / Gradle / Ivy
/*
* Copyright 2008-2013 the original author or authors.
*
* 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.
*/
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;
/**
* 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
* If the retail total does not fall within a configured price band, the total cost of fulfillment is zero
*
* Note: For {@link BandedWeightFulfillmentOption}, this assumes that all of your weights have the same units
*
* @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 options = new HashSet();
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 options) throws FulfillmentPriceException {
//Set up the response object
FulfillmentEstimationResponse res = new FulfillmentEstimationResponse();
HashMap shippingPrices = new HashMap();
res.setFulfillmentOptionPrices(shippingPrices);
for (FulfillmentOption option : options) {
if (canCalculateCostForFulfillmentGroup(fulfillmentGroup, option)) {
List 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);
}
}