de.hybris.platform.order.impl.DefaultCalculationService.java Source code

Java tutorial

Introduction

Here is the source code for de.hybris.platform.order.impl.DefaultCalculationService.java

Source

/*
 * [y] hybris Platform
 *
 * Copyright (c) 2000-2013 hybris AG
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of hybris
 * ("Confidential Information"). You shall not disclose such Confidential
 * Information and shall use it only in accordance with the terms of the
 * license agreement you entered into with hybris.
 * 
 *  
 */
package de.hybris.platform.order.impl;

import de.hybris.platform.core.CoreAlgorithms;
import de.hybris.platform.core.model.c2l.CurrencyModel;
import de.hybris.platform.core.model.order.AbstractOrderEntryModel;
import de.hybris.platform.core.model.order.AbstractOrderModel;
import de.hybris.platform.jalo.order.AbstractOrder;
import de.hybris.platform.jalo.order.price.JaloPriceFactoryException;
import de.hybris.platform.order.CalculationService;
import de.hybris.platform.order.exceptions.CalculationException;
import de.hybris.platform.order.strategies.calculation.FindDeliveryCostStrategy;
import de.hybris.platform.order.strategies.calculation.FindDiscountValuesStrategy;
import de.hybris.platform.order.strategies.calculation.FindPaymentCostStrategy;
import de.hybris.platform.order.strategies.calculation.FindPriceStrategy;
import de.hybris.platform.order.strategies.calculation.FindTaxValuesStrategy;
import de.hybris.platform.order.strategies.calculation.OrderRequiresCalculationStrategy;
import de.hybris.platform.servicelayer.exceptions.UnknownIdentifierException;
import de.hybris.platform.servicelayer.i18n.CommonI18NService;
import de.hybris.platform.servicelayer.internal.service.AbstractBusinessService;
import de.hybris.platform.servicelayer.util.ServicesUtil;
import de.hybris.platform.util.Config;
import de.hybris.platform.util.DiscountValue;
import de.hybris.platform.util.PriceValue;
import de.hybris.platform.util.TaxValue;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Required;

/**
 *
 */
public class DefaultCalculationService extends AbstractBusinessService implements CalculationService {

    private static final Logger LOG = Logger.getLogger(DefaultCalculationService.class);
    private List<FindTaxValuesStrategy> findTaxesStrategies;
    private List<FindDiscountValuesStrategy> findDiscountsStrategies;
    private FindPriceStrategy findPriceStrategy;
    private FindDeliveryCostStrategy findDeliveryCostStrategy;
    private FindPaymentCostStrategy findPaymentCostStrategy;
    private OrderRequiresCalculationStrategy orderRequiresCalculationStrategy;

    private CommonI18NService commonI18NService;

    //see PLA-11851
    private boolean taxFreeEntrySupport = false;

    @Override
    public void calculate(final AbstractOrderModel order) throws CalculationException {
        if (orderRequiresCalculationStrategy.requiresCalculation(order)) {
            // -----------------------------
            // first calc all entries
            calculateEntries(order, false);
            // -----------------------------
            // reset own values
            final Map taxValueMap = resetAllValues(order);
            // -----------------------------
            // now calculate all totals
            calculateTotals(order, false, taxValueMap);
            // notify manual discouns - needed?
            //notifyDiscountsAboutCalculation();
        }
    }

    @Override
    public boolean requiresCalculation(final AbstractOrderModel order) {
        ServicesUtil.validateParameterNotNullStandardMessage("order", order);
        return orderRequiresCalculationStrategy.requiresCalculation(order);
    }

    private void setCalculatedStatus(final AbstractOrderModel order) {
        order.setCalculated(Boolean.TRUE);
        getModelService().save(order);
        final List<AbstractOrderEntryModel> entries = order.getEntries();
        if (entries != null) {
            for (final AbstractOrderEntryModel entry : entries) {
                entry.setCalculated(Boolean.TRUE);
            }
            getModelService().saveAll(entries);
        }
    }

    private void setCalculatedStatus(final AbstractOrderEntryModel entry) {
        entry.setCalculated(Boolean.TRUE);
        getModelService().save(entry);
    }

    @Override
    public void calculate(final AbstractOrderModel order, final Date date) throws CalculationException {
        final Date old = order.getDate();
        order.setDate(date);
        try {
            calculate(order);
        } finally {
            order.setDate(old);
            getModelService().save(order);
        }
    }

    @Override
    public void calculateTotals(final AbstractOrderModel order, final boolean recalculate)
            throws CalculationException {
        calculateTotals(order, recalculate, calculateSubtotal(order, recalculate));
    }

    /**
     * calculates all totals. this does not trigger price, tax and discount calculation but takes all currently set
     * price, tax and discount values as base. this method requires the correct subtotal to be set before and the correct
     * tax value map.
     * 
     * @param recalculate
     *           if false calculation is done only if the calculated flag is not set
     * @param taxValueMap
     *           the map { tax value -> Double( sum of all entry totals for this tax ) } obtainable via
     *           {@link #calculateSubtotal(AbstractOrderModel, boolean)}
     * @throws CalculationException
     */
    protected void calculateTotals(final AbstractOrderModel order, final boolean recalculate,
            final Map<TaxValue, Map<Set<TaxValue>, Double>> taxValueMap) throws CalculationException {
        if (recalculate || orderRequiresCalculationStrategy.requiresCalculation(order)) {
            final CurrencyModel curr = order.getCurrency();
            final int digits = curr.getDigits().intValue();
            // subtotal
            final double subtotal = order.getSubtotal().doubleValue();
            // discounts

            final double totalDiscounts = calculateDiscountValues(order, recalculate);
            final double roundedTotalDiscounts = commonI18NService.roundCurrency(totalDiscounts, digits);
            order.setTotalDiscounts(Double.valueOf(roundedTotalDiscounts));
            // set total
            final double total = subtotal + order.getPaymentCost().doubleValue()
                    + order.getDeliveryCost().doubleValue() - roundedTotalDiscounts;
            final double totalRounded = commonI18NService.roundCurrency(total, digits);
            order.setTotalPrice(Double.valueOf(totalRounded));
            // taxes
            final double totalTaxes = calculateTotalTaxValues(//
                    order, recalculate, //
                    digits, //
                    getTaxCorrectionFactor(taxValueMap, subtotal, total, order), //
                    taxValueMap);//
            final double totalRoundedTaxes = commonI18NService.roundCurrency(totalTaxes, digits);
            order.setTotalTax(Double.valueOf(totalRoundedTaxes));
            getModelService().save(order);
            setCalculatedStatus(order);
        }
    }

    private double getTaxCorrectionFactor(final Map<TaxValue, Map<Set<TaxValue>, Double>> taxValueMap,
            final double subtotal, final double total, final AbstractOrderModel order) throws CalculationException {
        // default: adjust taxes relative to total-subtotal ratio
        double factor = total / subtotal;

        if (mustHandleTaxFreeEntries(taxValueMap, subtotal, order)) {
            final double taxFreeSubTotal = getTaxFreeSubTotal(order);

            final double taxedTotal = total - taxFreeSubTotal;
            final double taxedSubTotal = subtotal - taxFreeSubTotal;

            // illegal state: taxed subtotal is <= 0 -> cannot calculate with that
            if (taxedSubTotal <= 0) {
                throw new CalculationException("illegal taxed subtotal " + taxedSubTotal + ", must be > 0");
            }
            // illegal state: taxed total is <= 0 -> no sense in having negative taxes (factor would become negative!)
            if (taxedTotal <= 0) {
                throw new CalculationException("illegal taxed total " + taxedTotal + ", must be > 0");
            }
            factor = taxedTotal / taxedSubTotal;
        }
        return factor;
    }

    //see PLA-11851: we must take special actions in case some entries DO NOT HAVE TAXES on them
    private boolean mustHandleTaxFreeEntries(final Map<TaxValue, Map<Set<TaxValue>, Double>> taxValueMap,
            final double subtotal, final AbstractOrderModel order) {
        return MapUtils.isNotEmpty(taxValueMap) // got taxes at all
                && taxFreeEntrySupport // mode is enabled
                && !isAllEntriesTaxed(taxValueMap, subtotal, order); // check sums whether some entries are contributing to tax map
    }

    /**
     * Calculates the sub total of all order entries with NO tax values.
     */
    private double getTaxFreeSubTotal(final AbstractOrderModel order) {
        double sum = 0;
        for (final AbstractOrderEntryModel e : order.getEntries()) {
            if (CollectionUtils.isEmpty(e.getTaxValues())) {
                sum += e.getTotalPrice().doubleValue();
            }
        }
        return sum;
    }

    private boolean isAllEntriesTaxed(final Map<TaxValue, Map<Set<TaxValue>, Double>> taxValueMap,
            final double subtotal, final AbstractOrderModel order) {
        double sum = 0.0;
        final Set<Set<TaxValue>> consumedTaxGroups = new HashSet<Set<TaxValue>>();
        for (final Map.Entry<TaxValue, Map<Set<TaxValue>, Double>> taxEntry : taxValueMap.entrySet()) {
            for (final Map.Entry<Set<TaxValue>, Double> taxGroupEntry : taxEntry.getValue().entrySet()) {
                if (consumedTaxGroups.add(taxGroupEntry.getKey())) // avoid duplicate occurrences of the same tax group
                {
                    sum += taxGroupEntry.getValue().doubleValue();
                }
            }
        }
        // delta ( 2 digits ) == 10 ^-3 == 0.001
        final double allowedDelta = Math.pow(10, -1 * (order.getCurrency().getDigits().intValue() + 1));
        return Math.abs(subtotal - sum) <= allowedDelta;
    }

    @Override
    public void recalculate(final AbstractOrderModel order) throws CalculationException {
        // -----------------------------
        // first force calc entries
        calculateEntries(order, true);
        // -----------------------------
        // reset own values
        final Map taxValueMap = resetAllValues(order);
        // -----------------------------
        // now recalculate totals
        calculateTotals(order, true, taxValueMap);
        // notify discounts -- needed?
        //         notifyDiscountsAboutCalculation();

    }

    //phase 1 : delegate to Jalo
    @Override
    public void recalculate(final AbstractOrderModel order, final Date date) throws CalculationException {
        final AbstractOrder orderItem = getModelService().getSource(order);
        try {
            orderItem.recalculate(date);
        } catch (final JaloPriceFactoryException e) {
            throw new CalculationException(e);
        }
        refreshOrder(order);
    }

    public void calculateEntries(final AbstractOrderModel order, final boolean forceRecalculate)
            throws CalculationException {
        double subtotal = 0.0;
        for (final AbstractOrderEntryModel e : order.getEntries()) {
            recalculateOrderEntryIfNeeded(e, forceRecalculate);
            subtotal += e.getTotalPrice().doubleValue();
        }
        order.setTotalPrice(Double.valueOf(subtotal));

    }

    @Override
    public void calculateTotals(final AbstractOrderEntryModel entry, final boolean recalculate) {
        if (recalculate || orderRequiresCalculationStrategy.requiresCalculation(entry)) {
            final AbstractOrderModel order = entry.getOrder();
            final CurrencyModel curr = order.getCurrency();
            final int digits = curr.getDigits().intValue();
            final double totalPriceWithoutDiscount = CoreAlgorithms
                    .round(entry.getBasePrice().doubleValue() * entry.getQuantity().longValue(), digits);
            final double quantity = entry.getQuantity().doubleValue();
            /*
             * apply discounts (will be rounded each) convert absolute discount values in case their currency doesn't match
             * the order currency
             */
            //YTODO : use CalculatinService methods to apply discounts
            final List appliedDiscounts = DiscountValue.apply(quantity, totalPriceWithoutDiscount, digits,
                    convertDiscountValues(order, entry.getDiscountValues()), curr.getIsocode());
            entry.setDiscountValues(appliedDiscounts);
            double totalPrice = totalPriceWithoutDiscount;
            for (final Iterator it = appliedDiscounts.iterator(); it.hasNext();) {
                totalPrice -= ((DiscountValue) it.next()).getAppliedValue();
            }
            // set total price
            entry.setTotalPrice(Double.valueOf(totalPrice));
            // apply tax values too
            //YTODO : use CalculatinService methods to apply taxes
            calculateTotalTaxValues(entry);
            getModelService().save(entry);
            setCalculatedStatus(entry);
        }
    }

    protected void calculateTotalTaxValues(final AbstractOrderEntryModel entry) {
        final AbstractOrderModel order = entry.getOrder();
        final double quantity = entry.getQuantity().doubleValue();
        final double totalPrice = entry.getTotalPrice().doubleValue();
        final CurrencyModel curr = order.getCurrency();
        final int digits = curr.getDigits().intValue();

        entry.setTaxValues(TaxValue.apply(quantity, totalPrice, digits, entry.getTaxValues(),
                order.getNet().booleanValue(), curr.getIsocode()));
    }

    private void recalculateOrderEntryIfNeeded(final AbstractOrderEntryModel entry,
            final boolean forceRecalculation) throws CalculationException {
        if (forceRecalculation || orderRequiresCalculationStrategy.requiresCalculation(entry)) {
            resetAllValues(entry);
            calculateTotals(entry, true);
        }
    }

    @Override
    public void recalculate(final AbstractOrderEntryModel entry) throws CalculationException {
        recalculateOrderEntryIfNeeded(entry, false);
    }

    //YTODO - should not be necessary
    private void refreshOrder(final AbstractOrderModel order) {
        getModelService().refresh(order);
        for (final AbstractOrderEntryModel entry : order.getEntries()) {
            getModelService().refresh(entry);
        }
    }

    protected void resetAllValues(final AbstractOrderEntryModel entry) throws CalculationException {
        // taxes
        final Collection<TaxValue> entryTaxes = findTaxValues(entry);
        entry.setTaxValues(entryTaxes);
        final PriceValue pv = findBasePrice(entry);
        final AbstractOrderModel order = entry.getOrder();
        final PriceValue basePrice = convertPriceIfNecessary(pv, order.getNet().booleanValue(), order.getCurrency(),
                entryTaxes);
        entry.setBasePrice(Double.valueOf(basePrice.getValue()));
        final List<DiscountValue> entryDiscounts = findDiscountValues(entry);
        entry.setDiscountValues(entryDiscounts);
    }

    protected Map resetAllValues(final AbstractOrderModel order) throws CalculationException {
        // -----------------------------
        // set subtotal and get tax value map
        final Map<TaxValue, Map<Set<TaxValue>, Double>> taxValueMap = calculateSubtotal(order, false);
        /*
         * filter just relative tax values - payment and delivery prices might require conversion using taxes -> absolute
         * taxes do not apply here TODO: ask someone for absolute taxes and how they apply to delivery cost etc. - this
         * implementation might be wrong now
         */
        final Collection<TaxValue> relativeTaxValues = new LinkedList<TaxValue>();
        for (final Map.Entry<TaxValue, ?> e : taxValueMap.entrySet()) {
            final TaxValue taxValue = e.getKey();
            if (!taxValue.isAbsolute()) {
                relativeTaxValues.add(taxValue);
            }
        }

        //PLA-10914
        final boolean setAdditionalCostsBeforeDiscounts = Config
                .getBoolean("ordercalculation.reset.additionalcosts.before.discounts", true);
        if (setAdditionalCostsBeforeDiscounts) {
            resetAdditionalCosts(order, relativeTaxValues);
        }
        // -----------------------------
        // set discount values ( not applied yet ) - dont needed in model domain (?)
        //removeAllGlobalDiscountValues();
        order.setGlobalDiscountValues(findGlobalDiscounts(order));
        // -----------------------------
        // set delivery costs - convert if net or currency is different

        if (!setAdditionalCostsBeforeDiscounts) {
            resetAdditionalCosts(order, relativeTaxValues);
        }

        return taxValueMap;

    }

    protected void resetAdditionalCosts(final AbstractOrderModel order,
            final Collection<TaxValue> relativeTaxValues) {
        final PriceValue deliCost = findDeliveryCostStrategy.getDeliveryCost(order);
        double deliveryCostValue = 0.0;
        if (deliCost != null) {
            deliveryCostValue = convertPriceIfNecessary(deliCost, order.getNet().booleanValue(),
                    order.getCurrency(), relativeTaxValues).getValue();
        }
        order.setDeliveryCost(Double.valueOf(deliveryCostValue));
        // -----------------------------
        // set payment cost - convert if net or currency is different
        final PriceValue payCost = findPaymentCostStrategy.getPaymentCost(order);
        double paymentCostValue = 0.0;
        if (payCost != null) {
            paymentCostValue = convertPriceIfNecessary(payCost, order.getNet().booleanValue(), order.getCurrency(),
                    relativeTaxValues).getValue();
        }
        order.setPaymentCost(Double.valueOf(paymentCostValue));
    }

    /**
     * converts a PriceValue object into a double matching the target currency and net/gross - state if necessary. this
     * is the case when the given price value has a different net/gross flag or different currency.
     * 
     * @param pv
     *           the base price to convert
     * @param toNet
     *           the target net/gross state
     * @param toCurrency
     *           the target currency
     * @param taxValues
     *           the collection of tax values which apply to this price
     * 
     * @return a new PriceValue containing the converted price or pv in case no conversion was necessary
     */
    //YTODO: refactor to some helper class
    public PriceValue convertPriceIfNecessary(final PriceValue pv, final boolean toNet,
            final CurrencyModel toCurrency, final Collection taxValues) {
        // net - gross - conversion
        double convertedPrice = pv.getValue();
        if (pv.isNet() != toNet) {
            convertedPrice = pv.getOtherPrice(taxValues).getValue();
            convertedPrice = commonI18NService.roundCurrency(convertedPrice, toCurrency.getDigits().intValue());
        }
        // currency conversion
        final String iso = pv.getCurrencyIso();
        if (iso != null && !iso.equals(toCurrency.getIsocode())) {
            try {
                final CurrencyModel basePriceCurrency = commonI18NService.getCurrency(iso);
                convertedPrice = commonI18NService.convertAndRoundCurrency(
                        basePriceCurrency.getConversion().doubleValue(), toCurrency.getConversion().doubleValue(),
                        toCurrency.getDigits().intValue(), convertedPrice);
            } catch (final UnknownIdentifierException e) {
                LOG.warn("Cannot convert from currency '" + iso + "' to currency '" + toCurrency.getIsocode()
                        + "' since '" + iso + "' doesn't exist any more - ignored");
            }
        }
        return new PriceValue(toCurrency.getIsocode(), convertedPrice, toNet);
    }

    protected List convertDiscountValues(final AbstractOrderModel order, final List dvs) {
        if (dvs == null) {
            return null;
        }
        if (dvs.isEmpty()) {
            return dvs;
        }
        //

        final CurrencyModel curr = order.getCurrency();
        final String iso = curr.getIsocode();
        final List tmp = new ArrayList(dvs);
        /*
         * convert absolute discount values to order currency is needed
         */
        final Map<String, CurrencyModel> currencyMap = new HashMap<String, CurrencyModel>(); // just don't search twice for an isocode
        for (int i = 0; i < tmp.size(); i++) {
            final DiscountValue discountValue = (DiscountValue) tmp.get(i);
            if (discountValue.isAbsolute() && !iso.equals(discountValue.getCurrencyIsoCode())) {
                // get currency
                CurrencyModel dCurr = currencyMap.get(discountValue.getCurrencyIsoCode());
                if (dCurr == null) {
                    currencyMap.put(discountValue.getCurrencyIsoCode(),
                            dCurr = commonI18NService.getCurrency(discountValue.getCurrencyIsoCode()));
                }
                // replace old value in temp list
                tmp.set(i,
                        new DiscountValue(discountValue.getCode(),
                                commonI18NService.convertAndRoundCurrency(dCurr.getConversion().doubleValue(),
                                        curr.getConversion().doubleValue(), curr.getDigits().intValue(),
                                        discountValue.getValue()),
                                true, iso));
            }
        }
        return tmp;
    }

    protected Map<TaxValue, Map<Set<TaxValue>, Double>> calculateSubtotal(final AbstractOrderModel order,
            final boolean recalculate) {
        if (recalculate || orderRequiresCalculationStrategy.requiresCalculation(order)) {
            double subtotal = 0.0;
            // entry grouping via map { tax code -> Double }
            final List<AbstractOrderEntryModel> entries = order.getEntries();
            final Map<TaxValue, Map<Set<TaxValue>, Double>> taxValueMap = new LinkedHashMap<TaxValue, Map<Set<TaxValue>, Double>>(
                    entries.size() * 2);

            for (final AbstractOrderEntryModel entry : entries) {
                calculateTotals(entry, recalculate);
                final double entryTotal = entry.getTotalPrice().doubleValue();
                subtotal += entryTotal;
                // use un-applied version of tax values!!!
                final Collection<TaxValue> allTaxValues = entry.getTaxValues();
                final Set<TaxValue> relativeTaxGroupKey = getUnappliedRelativeTaxValues(allTaxValues);
                for (final TaxValue taxValue : allTaxValues) {
                    if (taxValue.isAbsolute()) {
                        addAbsoluteEntryTaxValue(entry.getQuantity().longValue(), taxValue.unapply(), taxValueMap);
                    } else {
                        addRelativeEntryTaxValue(entryTotal, taxValue.unapply(), relativeTaxGroupKey, taxValueMap);
                    }
                }
            }
            // store subtotal
            subtotal = commonI18NService.roundCurrency(subtotal, order.getCurrency().getDigits().intValue());
            order.setSubtotal(Double.valueOf(subtotal));
            return taxValueMap;
        }
        return Collections.EMPTY_MAP;
    }

    /**
     * Returns the total discount for this abstract order.
     * 
     * @param recalculate
     *           <code>true</code> forces a recalculation
     * @return totalDiscounts
     */
    protected double calculateDiscountValues(final AbstractOrderModel order, final boolean recalculate) {
        if (recalculate || orderRequiresCalculationStrategy.requiresCalculation(order)) {
            final List<DiscountValue> discountValues = order.getGlobalDiscountValues();
            if (discountValues != null && !discountValues.isEmpty()) {
                // clean discount value list -- do we still need it?
                //            removeAllGlobalDiscountValues();
                //
                final CurrencyModel curr = order.getCurrency();
                final String iso = curr.getIsocode();

                final int digits = curr.getDigits().intValue();
                final double discountablePrice = order.getSubtotal().doubleValue()
                        + (order.isDiscountsIncludeDeliveryCost() ? order.getDeliveryCost().doubleValue() : 0.0)
                        + (order.isDiscountsIncludePaymentCost() ? order.getPaymentCost().doubleValue() : 0.0);
                /*
                 * apply discounts to this order's total
                 */
                final List appliedDiscounts = DiscountValue.apply(1.0, discountablePrice, digits,
                        convertDiscountValues(order, discountValues), iso);
                // store discount values
                order.setGlobalDiscountValues(appliedDiscounts);
                return DiscountValue.sumAppliedValues(appliedDiscounts);
            }
            return 0.0;
        } else {
            return DiscountValue.sumAppliedValues(order.getGlobalDiscountValues());
        }
    }

    /**
     * @param recalculate
     * @param digits
     * @param taxAdjustmentFactor
     * @param taxValueMap
     * @return total taxes
     */
    protected double calculateTotalTaxValues(final AbstractOrderModel order, final boolean recalculate,
            final int digits, final double taxAdjustmentFactor,
            final Map<TaxValue, Map<Set<TaxValue>, Double>> taxValueMap) {
        if (recalculate || orderRequiresCalculationStrategy.requiresCalculation(order)) {
            final CurrencyModel curr = order.getCurrency();
            final String iso = curr.getIsocode();
            //Do we still need it?
            //removeAllTotalTaxValues();
            final boolean net = order.getNet().booleanValue();
            double totalTaxes = 0.0;
            // compute absolute taxes if necessary
            if (MapUtils.isNotEmpty(taxValueMap)) {
                // adjust total taxes if additional costs or discounts are present
                //   create tax values which contains applied values too
                final Collection orderTaxValues = new ArrayList<TaxValue>(taxValueMap.size());
                for (final Map.Entry<TaxValue, Map<Set<TaxValue>, Double>> taxValueEntry : taxValueMap.entrySet()) {
                    // e.g. VAT_FULL 19%
                    final TaxValue unappliedTaxValue = taxValueEntry.getKey();
                    // e.g. { (VAT_FULL 19%, CUSTOM 2%) -> 10EUR, (VAT_FULL) -> 20EUR } 
                    // or, in case of absolute taxes one single entry
                    // { (ABS_1 4,50EUR) -> 2 (pieces) }
                    final Map<Set<TaxValue>, Double> taxGroups = taxValueEntry.getValue();

                    final TaxValue appliedTaxValue;

                    if (unappliedTaxValue.isAbsolute()) {
                        // absolute tax entries ALWAYS map to a single-entry map -> we'll use a shortcut here:
                        final double quantitySum = taxGroups.entrySet().iterator().next().getValue().doubleValue();
                        appliedTaxValue = calculateAbsoluteTotalTaxValue(curr, iso, digits, net, unappliedTaxValue,
                                quantitySum);
                    } else if (net) {
                        appliedTaxValue = applyNetMixedRate(unappliedTaxValue, taxGroups, digits,
                                taxAdjustmentFactor);
                    } else {
                        appliedTaxValue = applyGrossMixedRate(unappliedTaxValue, taxGroups, digits,
                                taxAdjustmentFactor);
                    }
                    totalTaxes += appliedTaxValue.getAppliedValue();
                    orderTaxValues.add(appliedTaxValue);
                }
                order.setTotalTaxValues(orderTaxValues);
            }
            return totalTaxes;
        } else {
            return order.getTotalTax().doubleValue();
        }
    }

    protected void addRelativeEntryTaxValue(final double entryTotal, final TaxValue taxValue,
            final Set<TaxValue> relativeEntryTaxValues,
            final Map<TaxValue, Map<Set<TaxValue>, Double>> taxValueMap) {
        Double relativeTaxTotalSum = null;
        // A. is tax value already registered ?
        Map<Set<TaxValue>, Double> taxTotalsMap = taxValueMap.get(taxValue);
        if (taxTotalsMap != null) // tax value exists
        {
            // A.1 is set of tax un-applied values already registered by set of all relative tax values ?
            relativeTaxTotalSum = taxTotalsMap.get(relativeEntryTaxValues);
        }
        // B tax value did not exist before
        else {
            taxTotalsMap = new LinkedHashMap<Set<TaxValue>, Double>();
            taxValueMap.put(taxValue, taxTotalsMap);
        }
        taxTotalsMap.put(relativeEntryTaxValues, Double
                .valueOf((relativeTaxTotalSum != null ? relativeTaxTotalSum.doubleValue() : 0d) + entryTotal));

    }

    protected void addAbsoluteEntryTaxValue(final long entryQuantity, final TaxValue taxValue,
            final Map<TaxValue, Map<Set<TaxValue>, Double>> taxValueMap) {
        Map<Set<TaxValue>, Double> taxGroupMap = taxValueMap.get(taxValue);
        Double quantitySum = null;
        final Set<TaxValue> absoluteTaxGroupKey = Collections.singleton(taxValue);
        if (taxGroupMap == null) {
            taxGroupMap = new LinkedHashMap<Set<TaxValue>, Double>(4);
            taxValueMap.put(taxValue, taxGroupMap);
        } else {
            quantitySum = taxGroupMap.get(absoluteTaxGroupKey);
        }
        taxGroupMap.put(absoluteTaxGroupKey,
                Double.valueOf((quantitySum != null ? quantitySum.doubleValue() : 0) + entryQuantity));

    }

    private Set<TaxValue> getUnappliedRelativeTaxValues(final Collection<TaxValue> allTaxValues) {
        if (CollectionUtils.isNotEmpty(allTaxValues)) {
            final Set<TaxValue> ret = new LinkedHashSet<TaxValue>(allTaxValues.size());
            for (final TaxValue appliedTv : allTaxValues) {
                if (!appliedTv.isAbsolute()) {
                    ret.add(appliedTv.unapply());
                }
            }
            return ret;
        } else {
            return Collections.EMPTY_SET;
        }
    }

    protected TaxValue calculateAbsoluteTotalTaxValue(final CurrencyModel curr, final String currencyIso,
            final int digits, final boolean net, TaxValue taxValue, final double cumulatedEntryQuantities) {
        final String taxValueIsoCode = taxValue.getCurrencyIsoCode();
        // convert absolute tax values into order currency if necessary      

        if (taxValueIsoCode != null && !currencyIso.equalsIgnoreCase(taxValueIsoCode)) {
            final CurrencyModel taxCurrency = commonI18NService.getCurrency(taxValueIsoCode);
            final double taxConvertedValue = commonI18NService.convertAndRoundCurrency(
                    taxCurrency.getConversion().doubleValue(), curr.getConversion().doubleValue(), digits,
                    taxValue.getValue());
            taxValue = new TaxValue(taxValue.getCode(), taxConvertedValue, true, 0, currencyIso);
        }
        return taxValue.apply(cumulatedEntryQuantities, 0.0, digits, net, currencyIso);
    }

    private TaxValue applyGrossMixedRate(final TaxValue unappliedTaxValue,
            final Map<Set<TaxValue>, Double> taxGroups, final int digits, final double taxAdjustmentFactor) {
        if (unappliedTaxValue.isAbsolute()) {
            throw new IllegalStateException(
                    "AbstractOrder.applyGrossMixedRate(..) cannot be called for absolute tax value!");
        }
        final double singleTaxRate = unappliedTaxValue.getValue();
        double appliedTaxValueNotRounded = 0.0;
        for (final Map.Entry<Set<TaxValue>, Double> taxGroupEntry : taxGroups.entrySet()) {
            final double groupTaxesRate = TaxValue.sumRelativeTaxValues(taxGroupEntry.getKey());
            final double taxGroupPrice = taxGroupEntry.getValue().doubleValue();

            appliedTaxValueNotRounded += (taxGroupPrice * singleTaxRate) / (100.0 + groupTaxesRate);
        }

        //adjust taxes according to global discounts / costs
        appliedTaxValueNotRounded = appliedTaxValueNotRounded * taxAdjustmentFactor;

        return new TaxValue(//
                unappliedTaxValue.getCode(), //
                unappliedTaxValue.getValue(), //
                false, //
                // always round (even if digits are 0) since relative taxes result in unwanted precision !!! 
                CoreAlgorithms.round(appliedTaxValueNotRounded, Math.max(digits, 0)), //
                null //
        );
    }

    private TaxValue applyNetMixedRate(final TaxValue unappliedTaxValue, final Map<Set<TaxValue>, Double> taxGroups,
            final int digits, final double taxAdjustmentFactor) {
        if (unappliedTaxValue.isAbsolute()) {
            throw new IllegalStateException(
                    "cannot applyGrossMixedRate(..) cannot be called on absolute tax value!");
        }

        // In NET mode we don't care for tax groups since they're only needed to calculated *incldued* taxes!
        // Here we just sum up all group totals...
        double entriesTotalPrice = 0.0;
        for (final Map.Entry<Set<TaxValue>, Double> taxGroupEntry : taxGroups.entrySet()) {
            entriesTotalPrice += taxGroupEntry.getValue().doubleValue();
        }
        // and apply them in one go:
        return unappliedTaxValue.apply(1.0, entriesTotalPrice * taxAdjustmentFactor, digits, true, null);
    }

    protected Collection<TaxValue> findTaxValues(final AbstractOrderEntryModel entry) throws CalculationException {
        if (findTaxesStrategies.isEmpty()) {
            LOG.warn("No strategies for finding tax values could be found!");
            return Collections.<TaxValue>emptyList();
        } else {
            final List<TaxValue> result = new ArrayList<TaxValue>();
            for (final FindTaxValuesStrategy findStrategy : findTaxesStrategies) {
                result.addAll(findStrategy.findTaxValues(entry));
            }
            return result;
        }
    }

    protected List<DiscountValue> findDiscountValues(final AbstractOrderEntryModel entry)
            throws CalculationException {
        if (findDiscountsStrategies.isEmpty()) {
            LOG.warn("No strategies for finding discount values could be found!");
            return Collections.<DiscountValue>emptyList();
        } else {
            final List<DiscountValue> result = new ArrayList<DiscountValue>();
            for (final FindDiscountValuesStrategy findStrategy : findDiscountsStrategies) {
                result.addAll(findStrategy.findDiscountValues(entry));
            }
            return result;
        }
    }

    protected List<DiscountValue> findGlobalDiscounts(final AbstractOrderModel order) throws CalculationException {
        if (findDiscountsStrategies.isEmpty()) {
            LOG.warn("No strategies for finding discount values could be found!");
            return Collections.<DiscountValue>emptyList();
        } else {
            final List<DiscountValue> result = new ArrayList<DiscountValue>();
            for (final FindDiscountValuesStrategy findStrategy : findDiscountsStrategies) {
                result.addAll(findStrategy.findDiscountValues(order));
            }
            return result;
        }
    }

    protected PriceValue findBasePrice(final AbstractOrderEntryModel entry) throws CalculationException {
        return findPriceStrategy.findBasePrice(entry);
    }

    @Required
    public void setCommonI18NService(final CommonI18NService commonI18NService) {
        this.commonI18NService = commonI18NService;
    }

    @Required
    public void setFindTaxesStrategies(final List<FindTaxValuesStrategy> findTaxesStrategies) {
        this.findTaxesStrategies = findTaxesStrategies;
    }

    @Required
    public void setFindDiscountsStrategies(final List<FindDiscountValuesStrategy> findDiscountsStrategies) {
        this.findDiscountsStrategies = findDiscountsStrategies;
    }

    @Required
    public void setFindPriceStrategy(final FindPriceStrategy findPriceStrategy) {
        this.findPriceStrategy = findPriceStrategy;
    }

    @Required
    public void setFindDeliveryCostStrategy(final FindDeliveryCostStrategy findDeliveryCostStrategy) {
        this.findDeliveryCostStrategy = findDeliveryCostStrategy;
    }

    @Required
    public void setFindPaymentCostStrategy(final FindPaymentCostStrategy findPaymentCostStrategy) {
        this.findPaymentCostStrategy = findPaymentCostStrategy;
    }

    @Required
    public void setOrderRequiresCalculationStrategy(
            final OrderRequiresCalculationStrategy orderRequiresCalculationStrategy) {
        this.orderRequiresCalculationStrategy = orderRequiresCalculationStrategy;
    }

    public void setTaxFreeEntrySupport(final boolean taxFreeEntrySupport) {
        this.taxFreeEntrySupport = taxFreeEntrySupport;
    }

    /**
     * @deprecated use {@link #isTaxFreeEntrySupport()}
     */
    @Deprecated
    public boolean getTaxFreeEntrySupport() //NOPMD
    {
        return isTaxFreeEntrySupport();
    }

    public boolean isTaxFreeEntrySupport() {
        return this.taxFreeEntrySupport;
    }

}