<file_sep>package com.thoughtworks.interview.utils;

import java.math.BigDecimal;

import com.thoughtworks.interview.model.Discount;
import com.thoughtworks.interview.model.Item;
import com.thoughtworks.interview.model.Membership;

/**
 * Calculate item price
 * 
 * @author WeiWei
 * 
 */
public class DiscountCalculator {

	public static double getSubTotal(Item item, int qty) {
		return getSubTotal(item, qty, null);
	}


	public static double getSavings(Item item, int qty) {
		return getSavings(item, qty, null);
	}

	public static double getSubTotal(Item item, int qty, Membership member) {
		double sub_total = 0;
		switch (item.getDiscount()){
			case ALLSALE:
				if (qty > 2) {
					sub_total = chargeBuyTwoGetOneFree(item, qty, member);
				} else {
					sub_total = chargeFivePercentOff(item, qty, member);
				}
				break;
			case BUYTWOGETONEFREE:
				sub_total = chargeBuyTwoGetOneFree(item, qty, member);
				break;
			case FIVEPERCENTOFF:
				sub_total = chargeFivePercentOff(item, qty, member);
				break;
			default:
				sub_total = item.getPrice() * qty;
				break;
		}
		return sub_total;
	}

	public static double getSavings(Item item, int qty, Membership member) {
		double savings;
		/**
		 * If item is available for both two sale, when item match buy two get
		 * another free condition, use this rule then use 5% off rule.
		 */
		switch (item.getDiscount()) {
			case BUYTWOGETONEFREE:
				savings = saveBuyTwoGetOneFree(item, qty, member);
				break;
			case FIVEPERCENTOFF:
				savings = saveFivePercentOff(item, qty, member);
				break;
			case ALLSALE:
				if (qty > 2) {
					savings = saveBuyTwoGetOneFree(item, qty, member);
				} else {
					savings = saveFivePercentOff(item, qty, member);
				}
				break;
			default:
				savings = 0;
				break;
		}
		return savings;
	}
	
	private static double chargeFivePercentOff(Item item, int qty,Membership member) {
		double sub_total;
		if(null == member){
			member = Membership.NOMEMBER;
		}
		if(member.toString() == "NORMALMEMBER"){
			sub_total = item.getPrice() * qty * Math.min(Discount.FIVEPERCENTOFF.getRate(), member.getRate());
		}else{
			sub_total = item.getPrice() * qty * Discount.FIVEPERCENTOFF.getRate() * member.getRate();
		}
		return sub_total;
	}
	
	private static double chargeBuyTwoGetOneFree(Item item, int qty,Membership member) {
		double sub_total =item.getPrice() * (qty - Math.ceil(qty / 3));
		if(null != member && member.toString() == "GOLDMEMBER"){
			sub_total = sub_total * member.getRate();
		}
		return sub_total;
	}
	
	public static double saveFivePercentOff(Item item, int qty, Membership member) {
		if(null == member){
			member = Membership.NOMEMBER;
		}
		double savings = item.getPrice() * qty * new BigDecimal(1-Discount.FIVEPERCENTOFF.getRate()).setScale(2,BigDecimal.ROUND_HALF_UP).doubleValue();
		switch (member) {
		case GOLDMEMBER:
			savings += (item.getPrice() * qty - savings) * new BigDecimal(1-member.getRate()).setScale(2,BigDecimal.ROUND_HALF_UP).doubleValue();
			break;
		default:
			savings = item.getPrice() * qty * new BigDecimal(1-Math.min(Discount.FIVEPERCENTOFF.getRate(), member.getRate())).setScale(2,BigDecimal.ROUND_HALF_UP).doubleValue();
			break;
		}
		return savings;
	}
	
	private static double saveBuyTwoGetOneFree(Item item, int qty, Membership member) {
		double savings = item.getPrice() * Math.ceil(qty / 3);
		if(null != member && member.toString() == "GOLDMEMBER"){
			savings += (item.getPrice() * qty - savings) * new BigDecimal(1-member.getRate()).setScale(2,BigDecimal.ROUND_HALF_UP).doubleValue();
		}
		return savings;
	}
}
<file_sep>package com.thoughtworks.interview.model;

public enum Discount {
	NODISCOUNT(1),FIVEPERCENTOFF(0.95),BUYTWOGETONEFREE(0.3),ALLSALE(0);
	
	private double rate;
	
	private Discount(double rate){
		this.setRate(rate);
	}
	
	public static Discount nameOf(String name){
		if(name.toLowerCase().equals("fivepercentoff")){
			return FIVEPERCENTOFF;
		}
		if(name.toLowerCase().equals("buytwogetonefree")){
			return BUYTWOGETONEFREE;
		}
		if(name.toLowerCase().equals("allsale")){
			return ALLSALE;
		}
		return NODISCOUNT;
	}

	public double getRate() {
		return rate;
	}

	public void setRate(double rate) {
		this.rate = rate;
	}
}
<file_sep>package com.thoughtworks.interview.service;

import java.util.List;

import com.thoughtworks.interview.exception.ItemNotExsitException;
import com.thoughtworks.interview.model.Discount;
import com.thoughtworks.interview.model.Item;

/**
 * Basic item operation services
 * @author WeiWei
 *
 */
public interface ItemService {
	/**
	 * Set discount type
	 * @param itemSerial
	 * @param discount
	 * @return
	 */
	boolean setDiscount(String itemSerial,Discount discount) throws ItemNotExsitException;
	/**
	 * Get all items
	 * @return
	 */
	List<Item> getItems();
	/**
	 * Get an item by item serial
	 * @param itemSerial
	 * @return
	 * @throws ItemNotExsitException 
	 */
	Item getItem(String itemSerial) throws ItemNotExsitException;
}
<file_sep>package com.thoughtworks.interview.utils;

import java.lang.reflect.Member;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import com.thoughtworks.interview.model.Discount;
import com.thoughtworks.interview.model.Item;
import com.thoughtworks.interview.model.Membership;

public class DiscountCalculatorTest {

	private static Item item;
	
	@Before
	public void init(){
		item = new Item("足球", 99.00, "个","运动用品", "ITEM0000001");
	}
	
	@After
	public void end(){
		item = null;
	}
	
	@Test
	public void testTwoDiscount() {
		int qty = 3;
		double sub_total;
		double savings;
		item.setDiscount(Discount.ALLSALE);
		/**
		 * If quantity equals or is greater than three, item matches buy two get another one free rule. 
		 */
		sub_total = DiscountCalculator.getSubTotal(item,qty);
		savings = DiscountCalculator.getSavings(item, qty);
		Assert.assertNotEquals(sub_total, item.getPrice() * qty);
		Assert.assertEquals(item.getPrice()*qty, sub_total + savings,0);
		/**
		 * If quantity is less than three, item matches 5% off rule. 
		 */
		qty = 2;
		sub_total = DiscountCalculator.getSubTotal(item,qty);
		savings = DiscountCalculator.getSavings(item, qty);
		Assert.assertEquals(sub_total, item.getPrice()*qty*0.95,0);
	}

	/**
	 * 5% Off
	 */
	@Test
	public void testFivePercentOff() {
		double sub_total;
		double savings;
		int qty = 3;
		item.setDiscount(Discount.FIVEPERCENTOFF);
		sub_total = DiscountCalculator.getSubTotal(item,qty);
		savings = DiscountCalculator.getSavings(item, qty);
		Assert.assertEquals(sub_total, item.getPrice()*qty*0.95,0.001);
		Assert.assertEquals(item.getPrice()*qty*0.05, savings,0.001);
	}
	
	/**
	 * Buy two get another same one free
	 */
	@Test
	public void testBuyTwoGetOneFree() {
		int qty = 3;
		double sub_total;
		double savings;
		item.setDiscount(Discount.BUYTWOGETONEFREE);
		sub_total = DiscountCalculator.getSubTotal(item,qty);
		savings = DiscountCalculator.getSavings(item, qty);
		Assert.assertNotEquals(sub_total, item.getPrice() * qty);
		Assert.assertEquals(item.getPrice()*qty, sub_total + savings,0);
		qty = 5;
		sub_total = DiscountCalculator.getSubTotal(item,qty);
		savings = DiscountCalculator.getSavings(item, qty);
		Assert.assertEquals(sub_total,item.getPrice() * (qty - Math.ceil(qty/3)),0);
		Assert.assertEquals(savings,item.getPrice() * Math.ceil(qty/3),0);
	}

	/**
	 * No sale
	 */
	@Test
	public void testNoDiscount() {
		int qty = 3;
		double sub_total = DiscountCalculator.getSubTotal(item, qty);
		double savings = DiscountCalculator.getSavings(item, qty);
		Assert.assertTrue(sub_total == item.getPrice() * qty);
		Assert.assertEquals(Double.parseDouble("0.0"), savings,0);
	}
	
	@Test
	public void testFivePercentOffWithMembership(){
		//normal membership
		item.setDiscount(Discount.FIVEPERCENTOFF);
		int qty = 3;
		double sub_total = DiscountCalculator.getSubTotal(item, qty, Membership.NORMALMEMBER);
		double savings = DiscountCalculator.getSavings(item, qty, Membership.NORMALMEMBER);
		Assert.assertEquals(261.36, sub_total,1e-6);
		Assert.assertEquals(35.64, savings,1e-6);
		//gold membership
		sub_total = DiscountCalculator.getSubTotal(item, qty, Membership.GOLDMEMBER);
		savings = DiscountCalculator.getSavings(item, qty, Membership.GOLDMEMBER);
		Assert.assertEquals(211.6125, sub_total,1e-6);
		Assert.assertEquals(85.3875, savings,1e-6);
	}
	
	@Test
	public void testBuyTwoGetOneFreeWithMembership(){
		//normal membership
		item.setDiscount(Discount.BUYTWOGETONEFREE);
		int qty = 3;
		double sub_total = DiscountCalculator.getSubTotal(item, qty, Membership.NORMALMEMBER);
		double savings = DiscountCalculator.getSavings(item, qty, Membership.NORMALMEMBER);
		Assert.assertEquals(198, sub_total,1e-6);
		Assert.assertEquals(99, savings,1e-6);
		//gold membership
		sub_total = DiscountCalculator.getSubTotal(item, qty, Membership.GOLDMEMBER);
		savings = DiscountCalculator.getSavings(item, qty, Membership.GOLDMEMBER);
		Assert.assertEquals(148.5, sub_total,1e-6);
		Assert.assertEquals(148.5, savings,1e-6);
	}
	
	
}
<file_sep>package com.thoughtworks.interview.model;

public enum Membership {
	
	NOMEMBER(1),NORMALMEMBER(0.88),GOLDMEMBER(0.75);

	private double rate;

	private Membership(double rate) {
		this.setRate(rate);
	}

	public double getRate() {
		return rate;
	}

	public void setRate(double rate) {
		this.rate = rate;
	}
	
	
}
<file_sep>package com.thoughtworks.interview.utils;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.math.NumberUtils;

import com.alibaba.fastjson.JSONObject;

public class CommonTools {
	
	/**
	 * Convert inputed string to Json object
	 * @param str ['itemName','itemName',...]
	 * @return {"itemName":amount,...}
	 */
	public static JSONObject StringtoJson(String str){
		String regex = "'[^']+'";
		Matcher m = Pattern.compile(regex).matcher(str);
		ArrayList<String> al = new ArrayList<String>();
		while(m.find()){
			al.add(m.group(0).replace("'", ""));
		}
		/*
		 * Filter same element
		 */
		HashSet<String> hs=new HashSet<String>(al);  
        Map<String, Object> map = new HashMap<String, Object>();
        for(Iterator<String> it=hs.iterator();it.hasNext();){
        	int count = 0;
        	String key = it.next().toString().replace("'", "");
        	for(int i =0;i<al.size();i++){
        		if(key.equals(al.get(i))){
        			count++;
        		}
        	}
        	if(key.contains("-")){
        		count = NumberUtils.toInt(key.split("-")[1]);
        		key = key.split("-")[0];
        	}
        	map.put(key, count);
        }
        return new JSONObject(map);
	}
	
	/**
	 * To identify the result of division
	 * @param d1 result of division
	 * @return
	 */
	public static boolean isExactDivision(double d1){
	    return d1 == Math.floor(d1) && d1 > 0.00;
	}

}
<file_sep>package com.thoughtworks.interview.interceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

/**
 * Character encoding interceptor
 * @author WeiWei
 *
 */
public class CharacterEncodingInterceptor implements HandlerInterceptor{
	
	private String characterEncoding = "utf-8";

	public String getCharacterEncoding() {
		return characterEncoding;
	}

	public void setCharacterEncoding(String characterEncoding) {
		this.characterEncoding = characterEncoding;
	}

	@Override
	public boolean preHandle(HttpServletRequest request,
			HttpServletResponse response, Object object)
			throws Exception {
		request.setCharacterEncoding(characterEncoding);
		return true;
	}

	@Override
	public void postHandle(HttpServletRequest paramHttpServletRequest,
			HttpServletResponse paramHttpServletResponse, Object paramObject,
			ModelAndView paramModelAndView) throws Exception {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void afterCompletion(HttpServletRequest paramHttpServletRequest,
			HttpServletResponse paramHttpServletResponse, Object paramObject,
			Exception paramException) throws Exception {
		// TODO Auto-generated method stub
		
	}
	

}
<file_sep>package com.thoughtworks.interview.exception;

public class ItemNotExsitException extends Exception {

	/**
	 * 
	 */
	private static final long serialVersionUID = -6908756668840994884L;
	
	public ItemNotExsitException(){
		super();
	}
	
	public ItemNotExsitException(String msg){
		super(msg);
	}

}
<file_sep>package com.thoughtworks.interview.utils;

import org.junit.Assert;
import org.junit.Test;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

public class CommonToolsTest {

	@Test
	public void testStringToJson(){
		String str = "['ITEM000001','ITEM000001','ITEM000001', 'ITEM000001', 'ITEM000001', 'ITEM000003-2', 'ITEM000005'," +
				" 'ITEM000005','ITEM000005']";
		String expectStr = "{\"ITEM000005\":3,\"ITEM000003\":2,\"ITEM000001\":5}";
		JSONObject json = JSON.parseObject(expectStr);
		Assert.assertEquals(json, CommonTools.StringtoJson(str));
	}
	
	@Test
	public void testIsExactDivision(){
		double d1 = 2.0;
		double d2 = 2.5;
		Assert.assertTrue(CommonTools.isExactDivision(d1));
		Assert.assertTrue(!CommonTools.isExactDivision(d2));
	}
	
}
<file_sep>package com.thoughtworks.interview.service.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.thoughtworks.interview.exception.ItemNotExsitException;
import com.thoughtworks.interview.model.Discount;
import com.thoughtworks.interview.model.Item;
import com.thoughtworks.interview.service.ItemService;

public class ItemServiceImpl implements ItemService {
	
	/**
	 * Default item list
	 */
	private static Map<String,Item> DEFAULT_ITEM_MAP = new HashMap<String, Item>();
	
	static{
		DEFAULT_ITEM_MAP.put("ITEM0000001",new Item("足球", 99.00, "个","运动用品", "ITEM0000001"));
		DEFAULT_ITEM_MAP.put("ITEM0000002",new Item("篮球", 99.00, "个", "运动用品", "ITEM0000002"));
		DEFAULT_ITEM_MAP.put("ITEM0000003",new Item("羽毛球", 5.00, "个", "运动用品", "ITEM0000003"));
		DEFAULT_ITEM_MAP.put("ITEM0000004",new Item("乒乓球", 4.00, "个", "运动用品", "ITEM0000004"));
		DEFAULT_ITEM_MAP.put("ITEM0000005",new Item("足球鞋", 300.00, "双", "运动鞋类", "ITEM0000005"));
		DEFAULT_ITEM_MAP.put("ITEM0000006",new Item("篮球鞋", 400.00, "双", "运动鞋类", "ITEM0000006"));
		DEFAULT_ITEM_MAP.put("ITEM0000007",new Item("羽毛球鞋", 500.00, "双", "运动鞋类", "ITEM0000007"));
		DEFAULT_ITEM_MAP.put("ITEM0000008",new Item("乒乓球鞋", 300.00, "双", "运动鞋类", "ITEM0000008"));
		DEFAULT_ITEM_MAP.put("ITEM0000009",new Item("羽毛球球拍", 650.00, "副", "运动用品", "ITEM0000009"));
		DEFAULT_ITEM_MAP.put("ITEM0000010",new Item("乒乓球球拍", 99.00, "副", "运动用品", "ITEM0000010"));
		DEFAULT_ITEM_MAP.put("ITEM0000011",new Item("苹果", 3.00, "斤", "水果", "ITEM0000011"));
		DEFAULT_ITEM_MAP.put("ITEM0000012",new Item("香蕉", 4.00, "斤", "水果", "ITEM0000012"));
	}

	@Override
	public boolean setDiscount(String itemSerial, Discount discount) throws ItemNotExsitException {
		if(!DEFAULT_ITEM_MAP.containsKey(itemSerial)){
			throw new ItemNotExsitException();
		}
		Item item = DEFAULT_ITEM_MAP.get(itemSerial);
		item.setDiscount(discount);
		DEFAULT_ITEM_MAP.put(itemSerial, item);
		return true;
	}

	@Override
	public List<Item> getItems() {
		List<Item> items = new ArrayList<Item>();
		for(String key:DEFAULT_ITEM_MAP.keySet()){
			items.add(DEFAULT_ITEM_MAP.get(key));
		}
		return items;
	}

	@Override
	public Item getItem(String itemSerial) throws ItemNotExsitException {
		if(!DEFAULT_ITEM_MAP.containsKey(itemSerial)){
			throw new ItemNotExsitException();
		}
		return DEFAULT_ITEM_MAP.get(itemSerial);
	}

}
