com.konakart.bl.modules.ordertotal.productdiscount.ProductDiscount.java Source code

Java tutorial

Introduction

Here is the source code for com.konakart.bl.modules.ordertotal.productdiscount.ProductDiscount.java

Source

//
// (c) 2006 DS Data Systems UK Ltd, All rights reserved.
//
// DS Data Systems and KonaKart and their respective logos, are 
// trademarks of DS Data Systems UK Ltd. All rights reserved.
//
// The information in this document is free software; you can redistribute 
// it and/or modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
// 
// This software is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//

package com.konakart.bl.modules.ordertotal.productdiscount;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.torque.TorqueException;

import com.konakart.app.KKConfiguration;
import com.konakart.app.KKException;
import com.konakart.app.Order;
import com.konakart.app.OrderTotal;
import com.konakart.app.Product;
import com.konakart.app.Promotion;
import com.konakart.app.PromotionResult;
import com.konakart.appif.KKEngIf;
import com.konakart.appif.OrderProductIf;
import com.konakart.bl.modules.BaseModule;
import com.konakart.bl.modules.ordertotal.BaseOrderTotalModule;
import com.konakart.bl.modules.ordertotal.OrderTotalInterface;
import com.konakart.bl.modules.ordertotal.totaldiscount.TotalDiscount;
import com.workingdogs.village.DataSetException;

/**
 * Module that creates an OrderTotal object for applying a percentage discount or an amount discount
 * on a single product normally because of a certain quantity is being purchased. The discount may
 * be applied on prices before or after tax.
 * 
 * The promotion may be activated on a product only if:
 * <ul>
 * <li>The total amount of the order is greater than a minimum amount</li>
 * <li>The total number of a single product ordered is greater than a minimum amount</li>
 * </ul>
 * 
 * There may be multiple valid promotions applicable for an order. If this is the case, the logic
 * applied is the following: All cumulative promotions are summed into one order total object. Then
 * we loop through the order total objects and choose the one that offers the largest discount.
 */
public class ProductDiscount extends BaseOrderTotalModule implements OrderTotalInterface {
    protected static Log log = LogFactory.getLog(ProductDiscount.class);

    // Module name must be the same as the class name although it can be all in lowercase
    private static String code = "ot_product_discount";

    private static String bundleName = BaseModule.basePackage + ".ordertotal.productdiscount.ProductDiscount";

    private static HashMap<Locale, ResourceBundle> resourceBundleMap = new HashMap<Locale, ResourceBundle>();

    private static String mutex = "otProductDiscountMutex";

    /** Hash Map that contains the static data */
    private static Map<String, StaticData> staticDataHM = Collections
            .synchronizedMap(new HashMap<String, StaticData>());

    // Configuration Keys

    private final static String MODULE_ORDER_TOTAL_PRODUCT_DISCOUNT_SORT_ORDER = "MODULE_ORDER_TOTAL_PRODUCT_DISCOUNT_SORT_ORDER";

    private final static String MODULE_ORDER_TOTAL_PRODUCT_DISCOUNT_STATUS = "MODULE_ORDER_TOTAL_PRODUCT_DISCOUNT_STATUS";

    // Message Catalogue Keys
    private final static String MODULE_ORDER_TOTAL_PRODUCT_DISCOUNT_TITLE = "module.order.total.productdiscount.title";

    private final static String MODULE_ORDER_TOTAL_PRODUCT_DISCOUNT_TEXT = "module.order.total.productdiscount.text";

    /**
     * Constructor
     * 
     * @param eng
     * 
     * @throws DataSetException
     * @throws KKException
     * @throws TorqueException
     * 
     */
    public ProductDiscount(KKEngIf eng) throws TorqueException, KKException, DataSetException {
        super.init(eng);

        StaticData sd = staticDataHM.get(getStoreId());

        if (sd == null) {
            synchronized (mutex) {
                sd = staticDataHM.get(getStoreId());
                if (sd == null) {
                    setStaticVariables();
                }
            }
        }
    }

    /**
     * Sets some static variables during setup
     * 
     * @throws KKException
     * 
     */
    public void setStaticVariables() throws KKException {
        KKConfiguration conf;
        StaticData staticData = staticDataHM.get(getStoreId());
        if (staticData == null) {
            staticData = new StaticData();
            staticDataHM.put(getStoreId(), staticData);
        }
        conf = getEng().getConfiguration(MODULE_ORDER_TOTAL_PRODUCT_DISCOUNT_SORT_ORDER);
        if (conf == null) {
            staticData.setSortOrder(0);
        } else {
            staticData.setSortOrder(new Integer(conf.getValue()).intValue());
        }
    }

    /**
     * Returns true or false
     * 
     * @throws KKException
     */
    public boolean isAvailable() throws KKException {
        return isAvailable(getEng(), MODULE_ORDER_TOTAL_PRODUCT_DISCOUNT_STATUS);
    }

    /**
     * Create and return an OrderTotal object for the discount amount. *
     * <p>
     * Custom field usage:
     * <p>
     * <ul>
     * <li>custom1 = Minimum Order Value</li>
     * <li>custom2 = Minimum quantity of a single product</li>
     * <li>custom3 = Discount Applied</li>
     * <li>custom4 = Percentage discount if set to true</li>
     * <li>custom5 = Discount applied to pre-tax value if set to true</li>
     * </ul>
     * If the promotion applies to multiple products, we create an array of order total objects and
     * attach the array to the order total that we return (ot.setOrderTotals(otArray)). The reason
     * for doing this is to get a line item of the order for each discounted product. We still need
     * to populate the order total that we return with the total discount amount because this will
     * be used to compare this promotion with other promotions in order to decide which one to use.
     * 
     * @param order
     * @param dispPriceWithTax
     * @param locale
     * @return Returns an OrderTotal object for this module
     * @throws Exception
     */
    public OrderTotal getOrderTotal(Order order, boolean dispPriceWithTax, Locale locale) throws Exception {
        OrderTotal ot;
        StaticData sd = staticDataHM.get(getStoreId());

        // Get the resource bundle
        ResourceBundle rb = getResourceBundle(mutex, bundleName, resourceBundleMap, locale);
        if (rb == null) {
            throw new KKException("A resource file cannot be found for the country " + locale.getCountry());
        }

        // Get the promotions
        Promotion[] promArray = getPromMgr().getPromotions(code, order);

        // List to contain an order total for each promotion
        List<OrderTotal> orderTotalList = new ArrayList<OrderTotal>();

        if (promArray != null) {

            for (int i = 0; i < promArray.length; i++) {
                Promotion promotion = promArray[i];

                /*
                 * Get the configuration parameters from the promotion
                 */

                // Minimum value for order
                BigDecimal minTotalOrderVal = getCustomBigDecimal(promotion.getCustom1(), 1);

                // Need to order at least this quantity of a single product for promotion to apply
                int minProdQuantity = getCustomInt(promotion.getCustom2(), 2);

                // Actual discount. Could be a percentage or an amount.
                BigDecimal discountApplied = getCustomBigDecimal(promotion.getCustom3(), 3);

                // If set to true it is a percentage. Otherwise it is an amount.
                boolean percentageDiscount = getCustomBoolean(promotion.getCustom4(), 4);

                // If set to true, discount is applied to pre-tax value. Only relevant for
                // percentage discount.
                boolean applyBeforeTax = getCustomBoolean(promotion.getCustom5(), 5);

                // Don't bother going any further if there is no discount
                if (discountApplied == null || discountApplied.equals(new BigDecimal(0))) {
                    continue;
                }

                // Get the order value
                BigDecimal orderValue = null;
                if (applyBeforeTax) {
                    orderValue = order.getSubTotalExTax();
                    log.debug("Order value before tax: " + orderValue);
                } else {
                    orderValue = order.getSubTotalIncTax();
                    log.debug("Order value after tax: " + orderValue);
                }

                // If promotion doesn't cover any of the products in the order then go on to the
                // next promotion
                if (promotion.getApplicableProducts() == null || promotion.getApplicableProducts().length == 0) {
                    continue;
                }

                ot = new OrderTotal();
                ot.setSortOrder(sd.getSortOrder());
                ot.setClassName(code);
                ot.setPromotions(new Promotion[] { promotion });

                // Does promotion only apply to a min order value ?
                if (minTotalOrderVal != null) {
                    if (orderValue.compareTo(minTotalOrderVal) < 0) {
                        // If we haven't reached the minimum amount then continue to the next
                        // promotion
                        continue;
                    }
                }

                // Continue if promotion has no applicable products (should never happen)
                if (promotion.getApplicableProducts() == null) {
                    continue;
                }

                /*
                 * Create a new Order Total module for each discounted product and store in this
                 * list
                 */
                ArrayList<OrderTotal> otList = new ArrayList<OrderTotal>();

                // Loop through promotion products to determine whether to apply a discount
                boolean firstLoop = true;
                for (int j = 0; j < promotion.getApplicableProducts().length; j++) {
                    OrderProductIf op = promotion.getApplicableProducts()[j];
                    if (op != null && op.getQuantity() >= minProdQuantity) {
                        // Get the current total price of the product(s)
                        BigDecimal currentPrice = null;
                        if (applyBeforeTax) {
                            currentPrice = op.getFinalPriceExTax();
                        } else {
                            currentPrice = op.getFinalPriceIncTax();
                        }

                        // Apply the discount
                        BigDecimal discount = null;
                        if (percentageDiscount) {
                            // Apply a percentage discount
                            discount = (currentPrice.multiply(discountApplied)).divide(new BigDecimal(100));
                        } else {
                            // Apply an amount based discount
                            discount = discountApplied.multiply(new BigDecimal(op.getQuantity()));
                        }

                        // Determine whether it is the first discounted product or not
                        String formattedDiscount = getCurrMgr().formatPrice(discount, order.getCurrencyCode());
                        if (firstLoop) {
                            // Set the order total attributes
                            ot.setValue(discount);
                            if (percentageDiscount) {
                                try {
                                    ot.setText(String.format(rb.getString(MODULE_ORDER_TOTAL_PRODUCT_DISCOUNT_TEXT),
                                            "-", formattedDiscount));
                                    ot.setTitle(
                                            String.format(rb.getString(MODULE_ORDER_TOTAL_PRODUCT_DISCOUNT_TITLE),
                                                    "-", discountApplied, "%", op.getName()));
                                } catch (MissingResourceException e) {
                                    ot.setText("-" + formattedDiscount);
                                    // Title looks like "-10% Philips TV"
                                    ot.setTitle("-" + discountApplied + "% " + op.getName());
                                }

                            } else {
                                try {
                                    ot.setText(String.format(rb.getString(MODULE_ORDER_TOTAL_PRODUCT_DISCOUNT_TEXT),
                                            "-", formattedDiscount));
                                    ot.setTitle(
                                            String.format(rb.getString(MODULE_ORDER_TOTAL_PRODUCT_DISCOUNT_TITLE),
                                                    "-", formattedDiscount, "", op.getName()));
                                } catch (MissingResourceException e) {
                                    ot.setText("-" + formattedDiscount);
                                    // Title looks like "-10EUR Philips TV"
                                    ot.setTitle("-" + formattedDiscount + " " + op.getName());
                                }
                            }
                        } else {
                            // Set the order total attributes
                            ot.setValue(ot.getValue().add(discount));
                            ot.setText("-" + getCurrMgr().formatPrice(ot.getValue(), order.getCurrencyCode()));
                            ot.setTitle(ot.getTitle() + "," + op.getName());
                        }
                        firstLoop = false;

                        /*
                         * Create a new Order Total module for each product
                         */
                        OrderTotal singleOt = new OrderTotal();
                        singleOt.setSortOrder(sd.getSortOrder());
                        singleOt.setClassName(code);
                        singleOt.setValue(discount);
                        singleOt.setText("-" + formattedDiscount);
                        if (percentageDiscount) {
                            singleOt.setTitle("-" + discountApplied + "% " + op.getName() + ":");
                        } else {
                            singleOt.setTitle("-" + formattedDiscount + " " + op.getName() + ":");
                        }
                        otList.add(singleOt);
                    }
                }

                /*
                 * If we have more than one discounted product we create an array of order totals
                 * (one for each product) and add the array to the order total to be returned.
                 */
                if (otList.size() > 1) {
                    OrderTotal[] otArray = new OrderTotal[otList.size()];
                    int k = 0;
                    for (Iterator<OrderTotal> iterator = otList.iterator(); iterator.hasNext();) {
                        OrderTotal lot = iterator.next();
                        otArray[k++] = lot;
                    }
                    ot.setOrderTotals(otArray);
                }

                if (ot.getValue() != null) {
                    int scale = new Integer(order.getCurrency().getDecimalPlaces()).intValue();
                    ot.setValue(ot.getValue().setScale(scale, BigDecimal.ROUND_HALF_UP));
                    log.debug("Order total is :" + ot.toString());
                    orderTotalList.add(ot);
                }
            }
        } else {
            // Return null if there are no promotions
            return null;
        }

        // Call a helper method to decide which OrderTotal we should return
        OrderTotal retOT = getDiscountOrderTotalFromList(order, orderTotalList);
        log.debug("Selected order total is: " + retOT);

        return retOT;

    }

    /**
     * Returns an object containing the promotion discount. This method is used to apply the
     * promotion to a single product.
     * 
     * @param product
     * @param promotion
     * @return Returns a PromotionResult object
     * @throws Exception
     */
    public PromotionResult getPromotionResult(Product product, Promotion promotion) throws Exception {

        // Actual discount. Could be a percentage or an amount.
        BigDecimal discountApplied = getCustomBigDecimal(promotion.getCustom3(), 3);

        // Don't bother going any further if there is no discount
        if (discountApplied == null || discountApplied.equals(new BigDecimal(0))) {
            return null;
        }

        // If set to true it is a percentage. Otherwise it is an amount.
        boolean percentageDiscount = getCustomBoolean(promotion.getCustom4(), 4);

        // If set to true, discount is applied to pre-tax value. Only relevant for
        // percentage discount.
        boolean applyBeforeTax = getCustomBoolean(promotion.getCustom5(), 5);

        PromotionResult pd = new PromotionResult();
        pd.setPromotionId(promotion.getId());
        pd.setOrderTotalCode(code);
        pd.setValue(discountApplied);
        pd.setApplyBeforeTax(applyBeforeTax);
        pd.setPercentageDiscount(percentageDiscount);

        return pd;

    }

    public int getSortOrder() {
        StaticData sd;
        try {
            sd = staticDataHM.get(getStoreId());
            return sd.getSortOrder();
        } catch (KKException e) {
            log.error("Can't get the store id", e);
            return 0;
        }
    }

    public String getCode() {
        return code;
    }

    /**
     * Used to store the static data of this module
     */
    protected class StaticData {
        private int sortOrder = -1;

        /**
         * @return the sortOrder
         */
        public int getSortOrder() {
            return sortOrder;
        }

        /**
         * @param sortOrder
         *            the sortOrder to set
         */
        public void setSortOrder(int sortOrder) {
            this.sortOrder = sortOrder;
        }
    }
}