All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.broadleafcommerce.core.offer.service.processor.ItemOfferProcessorImpl Maven / Gradle / Ivy

There is a newer version: 3.1.15-GA
Show newest version
/*
 * 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.offer.service.processor;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.broadleafcommerce.common.money.Money;
import org.broadleafcommerce.core.offer.domain.Offer;
import org.broadleafcommerce.core.offer.domain.OfferItemCriteria;
import org.broadleafcommerce.core.offer.service.discount.CandidatePromotionItems;
import org.broadleafcommerce.core.offer.service.discount.ItemOfferComparator;
import org.broadleafcommerce.core.offer.service.discount.ItemOfferQtyOneComparator;
import org.broadleafcommerce.core.offer.service.discount.OrderOfferComparator;
import org.broadleafcommerce.core.offer.service.discount.PromotionDiscount;
import org.broadleafcommerce.core.offer.service.discount.domain.PromotableCandidateItemOffer;
import org.broadleafcommerce.core.offer.service.discount.domain.PromotableCandidateOrderOffer;
import org.broadleafcommerce.core.offer.service.discount.domain.PromotableFulfillmentGroup;
import org.broadleafcommerce.core.offer.service.discount.domain.PromotableOrder;
import org.broadleafcommerce.core.offer.service.discount.domain.PromotableOrderItem;
import org.broadleafcommerce.core.offer.service.discount.domain.PromotableOrderItemPriceDetail;
import org.broadleafcommerce.core.offer.service.discount.domain.PromotableOrderItemPriceDetailAdjustment;
import org.broadleafcommerce.core.offer.service.type.OfferType;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;

/**
 * Filter and apply order item offers.
 * 
 * @author jfischer
 *
 */
@Service("blItemOfferProcessor")
public class ItemOfferProcessorImpl extends OrderOfferProcessorImpl implements ItemOfferProcessor {
    
    protected static final Log LOG = LogFactory.getLog(ItemOfferProcessorImpl.class);
    
    /* (non-Javadoc)
     * @see org.broadleafcommerce.core.offer.service.processor.ItemOfferProcessor#filterItemLevelOffer(org.broadleafcommerce.core.order.domain.Order, java.util.List, java.util.List, org.broadleafcommerce.core.offer.domain.Offer)
     */
    @Override
    public void filterItemLevelOffer(PromotableOrder order, List qualifiedItemOffers, Offer offer) {
        boolean isNewFormat = CollectionUtils.isNotEmpty(offer.getQualifyingItemCriteria()) || CollectionUtils.isNotEmpty(offer.getTargetItemCriteria());
        boolean itemLevelQualification = false;
        boolean offerCreated = false;

        for (PromotableOrderItem promotableOrderItem : order.getDiscountableOrderItems()) {
            if(couldOfferApplyToOrder(offer, order, promotableOrderItem)) {
                if (!isNewFormat) {
                    //support legacy offers                   
                    PromotableCandidateItemOffer candidate = createCandidateItemOffer(qualifiedItemOffers, offer, order);
                   
                    if (!candidate.getLegacyCandidateTargets().contains(promotableOrderItem)) {
                        candidate.getLegacyCandidateTargets().add(promotableOrderItem);
                    }
                    offerCreated = true;
                    continue;
                }
                itemLevelQualification = true;
                break;
            }
            for (PromotableFulfillmentGroup fulfillmentGroup : order.getFulfillmentGroups()) {
                if(couldOfferApplyToOrder(offer, order, promotableOrderItem, fulfillmentGroup)) {
                    if (!isNewFormat) {
                        //support legacy offers
                        PromotableCandidateItemOffer candidate = createCandidateItemOffer(qualifiedItemOffers, offer, order);
                        if (!candidate.getLegacyCandidateTargets().contains(promotableOrderItem)) {
                            candidate.getLegacyCandidateTargets().add(promotableOrderItem);
                        }
                        offerCreated = true;
                        continue;
                    }
                    itemLevelQualification = true;
                    break;
                }
            }
        }
        //Item Qualification - new for 1.5!
        if (itemLevelQualification && !offerCreated) {
            CandidatePromotionItems candidates = couldOfferApplyToOrderItems(offer,
                    order.getDiscountableOrderItems(offer.getApplyDiscountToSalePrice()));
            PromotableCandidateItemOffer candidateOffer = null;
            if (candidates.isMatchedQualifier()) {
                //we don't know the final target yet, so put null for the order item for now
                candidateOffer = createCandidateItemOffer(qualifiedItemOffers, offer, order);
                candidateOffer.getCandidateQualifiersMap().putAll(candidates.getCandidateQualifiersMap());
            }
            if (candidates.isMatchedTarget() && candidates.isMatchedQualifier()) {
                if (candidateOffer == null) {
                    //we don't know the final target yet, so put null for the order item for now
                    candidateOffer = createCandidateItemOffer(qualifiedItemOffers, offer, order);
                }

                candidateOffer.getCandidateTargetsMap().putAll(candidates.getCandidateTargetsMap());
            }
        }
    }
    
    /**
     * Create a candidate item offer based on the offer in question and a specific order item
     * 
     * @param qualifiedItemOffers the container list for candidate item offers
     * @param offer the offer in question
     * @return the candidate item offer
     */
    protected PromotableCandidateItemOffer createCandidateItemOffer(List qualifiedItemOffers,
            Offer offer, PromotableOrder promotableOrder) {

        PromotableCandidateItemOffer promotableCandidateItemOffer =
                promotableItemFactory.createPromotableCandidateItemOffer(promotableOrder, offer);
        qualifiedItemOffers.add(promotableCandidateItemOffer);
        
        return promotableCandidateItemOffer;
    }
    
    
    /* (non-Javadoc)
     * @see org.broadleafcommerce.core.offer.service.processor.ItemOfferProcessor#applyAllItemOffers(java.util.List, java.util.List)
     */
    @Override
    public void applyAllItemOffers(List itemOffers, PromotableOrder order) {
        // Iterate through the collection of CandidateItemOffers. Remember that each one is an offer that may apply to a
        // particular OrderItem.  Multiple CandidateItemOffers may contain a reference to the same OrderItem object.
        // The same offer may be applied to different Order Items
        
        for (PromotableCandidateItemOffer itemOffer : itemOffers) {
            if (offerMeetsSubtotalRequirements(order, itemOffer)) {
                applyItemOffer(order, itemOffer);
            }
        }
    }
    
    
    protected boolean offerMeetsSubtotalRequirements(PromotableOrder order, PromotableCandidateItemOffer itemOffer) {
        if (itemOffer.getOffer().getQualifyingItemSubTotal() == null || itemOffer.getOffer().getQualifyingItemSubTotal().lessThanOrEqual(Money.ZERO)) {
            return true;
        }

        //TODO:  Check subtotal requirement before continuing
           
        return false;
    }

    protected boolean isTotalitarianOfferAppliedToAnyItem(PromotableOrder order) {
        List allPriceDetails = order.getAllPromotableOrderItemPriceDetails();       
        for (PromotableOrderItemPriceDetail targetItem : allPriceDetails) {
            if (targetItem.isTotalitarianOfferApplied()) {
                return true;
            }
        }
        return false;
    }
    
    /**
     * Private method used by applyAdjustments to create an OrderItemAdjustment from a CandidateOrderOffer
     * and associates the OrderItemAdjustment to the OrderItem.
     *
     * @param orderOffer a CandidateOrderOffer to apply to an Order
     */
    protected void applyOrderItemAdjustment(PromotableCandidateItemOffer itemOffer, PromotableOrderItemPriceDetail itemPriceDetail) {
        PromotableOrderItemPriceDetailAdjustment promotableOrderItemPriceDetailAdjustment = promotableItemFactory.createPromotableOrderItemPriceDetailAdjustment(itemOffer, itemPriceDetail);        
        itemPriceDetail.addCandidateItemPriceDetailAdjustment(promotableOrderItemPriceDetailAdjustment);
    }
    
    /**
     * The itemOffer has been qualified and prior methods added PromotionDiscount objects onto the ItemPriceDetail.
     * This code will convert the PromotionDiscounts into Adjustments
     * @param order
     * @param itemOffer
     */
    protected void applyAdjustments(PromotableOrder order, PromotableCandidateItemOffer itemOffer) {
        List itemPriceDetails = order.getAllPromotableOrderItemPriceDetails();
        for (PromotableOrderItemPriceDetail itemPriceDetail : itemPriceDetails) {
            for (PromotionDiscount discount : itemPriceDetail.getPromotionDiscounts()) {
                if (discount.getPromotion().equals(itemOffer.getOffer())) {
                    if (itemOffer.getOffer().isTotalitarianOffer() || !itemOffer.getOffer().isCombinableWithOtherOffers()) {
                        // We've decided to apply this adjustment but if it doesn't actually reduce
                        // the value of the item
                        if (adjustmentIsNotGoodEnoughToBeApplied(itemOffer, itemPriceDetail)) {
                            break;
                        }

                    }
                    applyOrderItemAdjustment(itemOffer, itemPriceDetail);
                    break;
                }
            }
        }
    }

    /**
     * Legacy adjustments use the stackable flag instead of item qualifiers and targets
     * @param order
     * @param itemOffer
     */
    protected void applyLegacyAdjustments(PromotableOrder order, PromotableCandidateItemOffer itemOffer) {
        for (PromotableOrderItem item : itemOffer.getLegacyCandidateTargets()) {
            for (PromotableOrderItemPriceDetail itemPriceDetail : item.getPromotableOrderItemPriceDetails()) {
                if (!itemOffer.getOffer().isStackable() || !itemOffer.getOffer().isCombinableWithOtherOffers()) {
                    if (itemPriceDetail.getCandidateItemAdjustments().size() != 0) {
                        continue;
                    }
                } else {
                    if (itemPriceDetail.hasNonCombinableAdjustments()) {
                        continue;
                    }
                }
                applyOrderItemAdjustment(itemOffer, itemPriceDetail);
            }
        }
    }

    /**
     * The adjustment might not be better than the sale price.
     * @param itemOffer
     * @param detail
     * @return
     */
    protected boolean adjustmentIsNotGoodEnoughToBeApplied(PromotableCandidateItemOffer itemOffer,
            PromotableOrderItemPriceDetail detail) {
        if (!itemOffer.getOffer().getApplyDiscountToSalePrice()) {
            Money salePrice = detail.getPromotableOrderItem().getSalePriceBeforeAdjustments();
            Money retailPrice = detail.getPromotableOrderItem().getRetailPriceBeforeAdjustments();
            Money savings = itemOffer.calculateSavingsForOrderItem(detail.getPromotableOrderItem(), 1);
            if (salePrice != null) {
                if (salePrice.lessThan(retailPrice.subtract(savings))) {
                    // Not good enough
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Return false if a totalitarian offer has already been applied and this order already has
     * item adjustments. 
     *      
     * @param order
     * @param itemOffer
     * @return
     */
    protected boolean itemOfferCanBeApplied(PromotableOrder order, PromotableCandidateItemOffer itemOffer) {

        for (PromotableOrderItemPriceDetail detail : order.getAllPromotableOrderItemPriceDetails()) {
            for (PromotableOrderItemPriceDetailAdjustment adjustment : detail.getCandidateItemAdjustments()) {
                if (adjustment.isTotalitarian() || itemOffer.getOffer().isTotalitarianOffer()) {
                    // A totalitarian offer has already been applied or this offer is totalitarian
                    // and another offer was already applied.
                    return false;
                } else if (itemOffer.isLegacyOffer()) {
                    continue;
                } else if (!adjustment.isCombinable() || !itemOffer.getOffer().isCombinableWithOtherOffers()) {
                    // A nonCombinable offer has been applied or this is a non-combinable offer
                    // and adjustments have already been applied.
                    return false;
                }
            }
        }
        return true;
    }
     

    protected void applyItemOffer(PromotableOrder order, PromotableCandidateItemOffer itemOffer) {
        if (itemOfferCanBeApplied(order, itemOffer)) {
            applyItemQualifiersAndTargets(itemOffer, order);
            if (itemOffer.isLegacyOffer()) {
                applyLegacyAdjustments(order, itemOffer);
            } else {
                applyAdjustments(order, itemOffer);
            }
        }
    }

    /**
     * Some promotions can only apply to the retail price.    This method determines whether
     * retailPrice only promotions should be used instead of those that can apply to the sale
     * price as well.
     * 
     * @param order
     * @return
     */
    protected void chooseSaleOrRetailAdjustments(PromotableOrder order) {
        List itemPriceDetails = order.getAllPromotableOrderItemPriceDetails();
        for (PromotableOrderItemPriceDetail itemDetail : itemPriceDetails) {
            itemDetail.chooseSaleOrRetailAdjustments();                
        }
        mergePriceDetails(order);
    }

    /**
     * Checks to see if any priceDetails need to be combined and if so, combines them.
     * 
     * @param order
     * @return
     */
    protected void mergePriceDetails(PromotableOrder order) {
        List items = order.getAllOrderItems();
        for (PromotableOrderItem item : items) {
            item.mergeLikeDetails();
        }
    }

   
    protected void applyItemQualifiersAndTargets(PromotableCandidateItemOffer itemOffer, PromotableOrder order) {
        if (itemOffer.isLegacyOffer()) {
            LOG.warn("The item offer with id " + itemOffer.getOffer().getId() + " is a legacy offer which means that it" +
            		" does not have any item qualifier criteria AND does not have any target item criteria. As a result," +
            		" we are skipping the marking of qualifiers and targets which will cause issues if you are relying on" +
            		" 'maxUsesPerOrder' behavior. To resolve this, qualifier criteria is not required but you must at least" +
            		" create some target item criteria for this offer.");
            return;
        } else {
            markQualifiersAndTargets(order, itemOffer);
            splitDetailsIfNecessary(order.getAllPromotableOrderItemPriceDetails());
        }
    }

    protected List buildPriceDetailListFromOrderItems(List items) {
        List itemPriceDetails = new ArrayList();
        for (PromotableOrderItem item : items) {
            for (PromotableOrderItemPriceDetail detail : item.getPromotableOrderItemPriceDetails()) {
                itemPriceDetails.add(detail);
            }
        }
        return itemPriceDetails;
    }

    /**
     * Loop through ItemCriteria and mark qualifiers required to give the promotion to 1 or more targets.
     * @param itemOffer
     * @param order
     * @return
     */
    protected boolean markQualifiers(PromotableCandidateItemOffer itemOffer, PromotableOrder order) {
        for (OfferItemCriteria itemCriteria : itemOffer.getCandidateQualifiersMap().keySet()) {
            List promotableItems = itemOffer.getCandidateQualifiersMap().get(itemCriteria);

            List priceDetails = buildPriceDetailListFromOrderItems(promotableItems);
            
            Collections.sort(priceDetails, getQualifierItemComparator(itemOffer.getOffer().getApplyDiscountToSalePrice()));

            // Calculate the number of qualifiers needed that will not receive the promotion.  
            // These will be reserved first before the target is assigned.
            int qualifierQtyNeeded = itemCriteria.getQuantity();
            
            for (PromotableOrderItemPriceDetail detail : priceDetails) {
                
                // Mark Qualifiers
                if (qualifierQtyNeeded > 0) {
                    int itemQtyAvailableToBeUsedAsQualifier = detail.getQuantityAvailableToBeUsedAsQualifier(itemOffer);
                    if (itemQtyAvailableToBeUsedAsQualifier > 0) {
                        int qtyToMarkAsQualifier = Math.min(qualifierQtyNeeded, itemQtyAvailableToBeUsedAsQualifier);
                        qualifierQtyNeeded -= qtyToMarkAsQualifier;
                        detail.addPromotionQualifier(itemOffer, itemCriteria, qtyToMarkAsQualifier);
                    }
                }
                
                if (qualifierQtyNeeded == 0) {
                    break;
                }
            }

            if (qualifierQtyNeeded != 0) {
                return false;
            }
        }
        return true;
    }
    
    /**
     * Loop through ItemCriteria and mark targets that can get this promotion to give the promotion to 1 or more targets.
     * @param itemOffer
     * @param order
     * @return
     */
    protected boolean markTargets(PromotableCandidateItemOffer itemOffer, PromotableOrder order) {
        Offer promotion = itemOffer.getOffer();

        if (itemOffer.getCandidateTargetsMap().keySet().isEmpty()) {
            return false;
        }

        for (OfferItemCriteria itemCriteria : itemOffer.getCandidateTargetsMap().keySet()) {
            List promotableItems = itemOffer.getCandidateTargetsMap().get(itemCriteria);

            List priceDetails = buildPriceDetailListFromOrderItems(promotableItems);

            Collections.sort(priceDetails, getTargetItemComparator(itemOffer.getOffer().getApplyDiscountToSalePrice()));

            int targetQtyNeeded = itemCriteria.getQuantity();

            for (PromotableOrderItemPriceDetail detail : priceDetails) {
                int itemQtyAvailableToBeUsedAsTarget = detail.getQuantityAvailableToBeUsedAsTarget(itemOffer);
                if (itemQtyAvailableToBeUsedAsTarget > 0) {
                    if (promotion.isUnlimitedUsePerOrder() || (itemOffer.getUses() < promotion.getMaxUsesPerOrder())) {
                        int qtyToMarkAsTarget = Math.min(targetQtyNeeded, itemQtyAvailableToBeUsedAsTarget);
                        targetQtyNeeded -= qtyToMarkAsTarget;
                        detail.addPromotionDiscount(itemOffer, itemCriteria, qtyToMarkAsTarget);
                    }
                }

                if (targetQtyNeeded == 0) {
                    break;
                }
            }

            if (targetQtyNeeded != 0) {
                return false;
            }
        }

        itemOffer.addUse();
        return true;
    }

    /**
     * Used in {@link #applyItemQualifiersAndTargets(PromotableCandidateItemOffer, PromotableOrder)} allow for customized
     * sorting for which qualifier items should be attempted to be used first for a promotion. Default behavior
     * is to sort descending, so higher-value items are attempted to be qualified first.
     * 
     * @param applyToSalePrice - whether or not the Comparator should use the sale price for comparison
     * @return
     */
    protected Comparator getQualifierItemComparator(final boolean applyToSalePrice) {
        return new Comparator() {
            @Override
            public int compare(PromotableOrderItemPriceDetail o1, PromotableOrderItemPriceDetail o2) {
                Money price = o1.getPromotableOrderItem().getPriceBeforeAdjustments(applyToSalePrice);
                Money price2 = o2.getPromotableOrderItem().getPriceBeforeAdjustments(applyToSalePrice);
                
                // highest amount first
                return price2.compareTo(price);
            }
        };
    }

    /**
     * 

* Used in {@link #applyItemQualifiersAndTargets(PromotableCandidateItemOffer, PromotableOrder)} allow for customized * sorting for which target items the promotion should be attempted to be applied to first. Default behavior is to * sort descending, so higher-value items get the promotion over lesser-valued items. *

*

* Note: By default, both the {@link #getQualifierItemComparator(boolean)} and this target comparator are sorted * in descending order. This means that higher-valued items can be paired with higher-valued items and lower-valued * items can be paired with lower-valued items. This also ensures that you will not have the scenario where 2 * lower-valued items can be used to qualify a higher-valued target. *

* * @param applyToSalePrice - whether or not the Comparator should use the sale price for comparison * @return */ protected Comparator getTargetItemComparator(final boolean applyToSalePrice) { return new Comparator() { @Override public int compare(PromotableOrderItemPriceDetail o1, PromotableOrderItemPriceDetail o2) { Money price = o1.getPromotableOrderItem().getPriceBeforeAdjustments(applyToSalePrice); Money price2 = o2.getPromotableOrderItem().getPriceBeforeAdjustments(applyToSalePrice); // highest amount first return price2.compareTo(price); } }; } @Override public void filterOffers(PromotableOrder order, List filteredOffers, List qualifiedOrderOffers, List qualifiedItemOffers) { // set order subTotal price to total item price without adjustments order.setOrderSubTotalToPriceWithoutAdjustments(); for (Offer offer : filteredOffers) { if(offer.getType().equals(OfferType.ORDER)){ filterOrderLevelOffer(order, qualifiedOrderOffers, offer); } else if(offer.getType().equals(OfferType.ORDER_ITEM)){ filterItemLevelOffer(order, qualifiedItemOffers, offer); } } } /** * This method determines the potential savings for each item offer as if it was the only item offer being applied. * @param itemOffers * @param order */ protected void calculatePotentialSavings(List itemOffers, PromotableOrder order) { if (itemOffers.size() > 1) { for (PromotableCandidateItemOffer itemOffer : itemOffers) { Money potentialSavings = new Money(order.getOrderCurrency()); if (itemOffer.isLegacyOffer()) { for (PromotableOrderItem item : itemOffer.getLegacyCandidateTargets()) { Money savings = itemOffer.calculateSavingsForOrderItem(item, item.getQuantity()); potentialSavings = potentialSavings.add(savings); } } else { markQualifiersAndTargets(order, itemOffer); for (PromotableOrderItemPriceDetail detail : order.getAllPromotableOrderItemPriceDetails()) { PromotableOrderItem item = detail.getPromotableOrderItem(); for (PromotionDiscount discount : detail.getPromotionDiscounts()) { potentialSavings = potentialSavings.add( itemOffer.calculateSavingsForOrderItem(item, discount.getQuantity())); } // Reset state back for next offer detail.getPromotionDiscounts().clear(); detail.getPromotionQualifiers().clear(); } } itemOffer.setPotentialSavings(potentialSavings); if (itemOffer.getUses() == 0) { itemOffer.setPotentialSavingsQtyOne(potentialSavings); } else { itemOffer.setPotentialSavingsQtyOne(potentialSavings.divide(itemOffer.getUses())); } } } } protected void markQualifiersAndTargets(PromotableOrder order, PromotableCandidateItemOffer itemOffer) { boolean matchFound = true; if (itemOffer.isLegacyOffer()) { return; } int count = 1; do { boolean qualifiersFound = markQualifiers(itemOffer, order); boolean targetsFound = markTargets(itemOffer, order); if (qualifiersFound && targetsFound) { finalizeQuantities(order.getAllPromotableOrderItemPriceDetails()); } else { clearAllNonFinalizedQuantities(order.getAllPromotableOrderItemPriceDetails()); matchFound = false; break; } // If we found a match, try again to see if the promotion can be applied again. } while (matchFound); } protected boolean offerListStartsWithNonCombinable(List offerList) { if (offerList.size() > 1) { PromotableCandidateItemOffer offer = offerList.get(0); if (offer.getOffer().isTotalitarianOffer() || !offer.getOffer().isCombinableWithOtherOffers()) { return true; } } return false; } /** * This method could be overridden to potentially run all permutations of offers. * A reasonable alternative is to have a permutation with nonCombinable offers * and another with combinable offers. * * @param offers * @return */ protected List> buildItemOfferPermutations( List offers) { List> listOfOfferLists = new ArrayList>(); // add the default list listOfOfferLists.add(offers); if (offers.size() > 1) { List qtyOneOffers = new ArrayList(offers); Collections.sort(qtyOneOffers, ItemOfferQtyOneComparator.INSTANCE); // We only want to add this additional list when the qty of one list is not identical to the original one for (int i = 0; i < qtyOneOffers.size(); i++) { if (qtyOneOffers.get(i) != offers.get(i)) { listOfOfferLists.add(qtyOneOffers); break; } } } if (offerListStartsWithNonCombinable(offers)) { List listWithoutTotalitarianOrNonCombinables = new ArrayList(offers); Iterator offerIterator = listWithoutTotalitarianOrNonCombinables.iterator(); while (offerIterator.hasNext()) { PromotableCandidateItemOffer offer = offerIterator.next(); if (offer.getOffer().isTotalitarianOffer() || !offer.getOffer().isCombinableWithOtherOffers()) { offerIterator.remove(); } } if (listWithoutTotalitarianOrNonCombinables.size() > 0) { listOfOfferLists.add(listWithoutTotalitarianOrNonCombinables); } } return listOfOfferLists; } protected void determineBestPermutation(List itemOffers, PromotableOrder order) { List> permutations = buildItemOfferPermutations(itemOffers); List bestOfferList = null; Money lowestSubtotal = null; if (permutations.size() > 1) { for (List offerList : permutations) { for (PromotableCandidateItemOffer offer : offerList) { offer.resetUses(); } applyAllItemOffers(offerList, order); chooseSaleOrRetailAdjustments(order); Money testSubtotal = order.calculateSubtotalWithAdjustments(); if (lowestSubtotal == null || testSubtotal.lessThan(lowestSubtotal)) { lowestSubtotal = testSubtotal; bestOfferList = offerList; } // clear price details for (PromotableOrderItem item : order.getDiscountableOrderItems()) { item.resetPriceDetails(); } } } else { bestOfferList = permutations.get(0); } for (PromotableCandidateItemOffer offer : bestOfferList) { offer.resetUses(); } applyAllItemOffers(bestOfferList, order); } @Override @SuppressWarnings("unchecked") public void applyAndCompareOrderAndItemOffers(PromotableOrder order, List qualifiedOrderOffers, List qualifiedItemOffers) { if (!qualifiedItemOffers.isEmpty()) { calculatePotentialSavings(qualifiedItemOffers, order); //after savings have been calculated, uses will have been marked on offers which can effect //the actual application of those offers. Thus the uses for each item offer needs to be reset for (PromotableCandidateItemOffer itemOffer : qualifiedItemOffers) { itemOffer.resetUses(); } // Sort order item offers by priority and potential total discount Collections.sort(qualifiedItemOffers, ItemOfferComparator.INSTANCE); if (qualifiedItemOffers.size() > 1) { determineBestPermutation(qualifiedItemOffers, order); } else { applyAllItemOffers(qualifiedItemOffers, order); } } chooseSaleOrRetailAdjustments(order); order.setOrderSubTotalToPriceWithAdjustments(); if (!qualifiedOrderOffers.isEmpty()) { // Sort order offers by priority and discount Collections.sort(qualifiedOrderOffers, OrderOfferComparator.INSTANCE); //qualifiedOrderOffers = removeTrailingNotCombinableOrderOffers(qualifiedOrderOffers); applyAllOrderOffers(qualifiedOrderOffers, order); } order.setOrderSubTotalToPriceWithAdjustments(); // TODO: only do this if absolutely required. If you find one that no longer qualifies, then // pull it out and reapply. if (!qualifiedOrderOffers.isEmpty() && !qualifiedItemOffers.isEmpty()) { List finalQualifiedOrderOffers = new ArrayList(); order.removeAllCandidateOrderOfferAdjustments(); for (PromotableCandidateOrderOffer candidateOrderOffer : qualifiedOrderOffers) { // recheck the list of order offers and verify if they still apply with the new subtotal /* * Note - there is an edge case possibility where this logic would miss an order promotion * that had a subtotal requirement that was missed because of item deductions, but without * the item deductions, the order promotion would have been included and ended up giving the * customer a better deal than the item deductions. */ if (couldOfferApplyToOrder(candidateOrderOffer.getOffer(), order)) { finalQualifiedOrderOffers.add(candidateOrderOffer); } } // Sort order offers by priority and discount Collections.sort(finalQualifiedOrderOffers, OrderOfferComparator.INSTANCE); if (!finalQualifiedOrderOffers.isEmpty()) { applyAllOrderOffers(finalQualifiedOrderOffers, order); order.setOrderSubTotalToPriceWithAdjustments(); } } } /** * Gets rid of totalitarian and nonCombinable item offers that can't possibly be applied. * @param qualifiedItemOffers */ private void removeTrailingNonCombinableOrTotalitarianOffers(List qualifiedItemOffers) { boolean first = true; Iterator offerIterator = qualifiedItemOffers.iterator(); if (offerIterator.hasNext()) { // ignore the first one. offerIterator.next(); } while (offerIterator.hasNext()) { PromotableCandidateItemOffer itemOffer = offerIterator.next(); if (itemOffer.getOffer().isTotalitarianOffer()) { // Remove Totalitarian offers that aren't the first offer. offerIterator.remove(); } else { if (!itemOffer.isLegacyOffer() && !itemOffer.getOffer().isCombinableWithOtherOffers()) { // Remove nonCombinable offers that aren't the first offer offerIterator.remove(); } } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy