001/*
002 * Copyright 2014 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.springframework.batch.sample.domain.order.internal.validator;
017
018import java.math.BigDecimal;
019import java.util.ArrayList;
020import java.util.Date;
021import java.util.List;
022
023import org.springframework.batch.sample.domain.order.Address;
024import org.springframework.batch.sample.domain.order.BillingInfo;
025import org.springframework.batch.sample.domain.order.Customer;
026import org.springframework.batch.sample.domain.order.LineItem;
027import org.springframework.batch.sample.domain.order.Order;
028import org.springframework.batch.sample.domain.order.ShippingInfo;
029import org.springframework.util.StringUtils;
030import org.springframework.validation.Errors;
031import org.springframework.validation.Validator;
032
033public class OrderValidator implements Validator {
034
035        private static final List<String> CARD_TYPES = new ArrayList<String>();
036        private static final List<String> SHIPPER_IDS = new ArrayList<String>();
037        private static final List<String> SHIPPER_TYPES = new ArrayList<String>();
038        private static final long MAX_ID = 9999999999L;
039        private static final BigDecimal BD_MIN = new BigDecimal("0.0");
040        private static final BigDecimal BD_MAX = new BigDecimal("99999999.99");
041        private static final BigDecimal BD_PERC_MAX = new BigDecimal("100.0");
042        private static final int MAX_QUANTITY = 9999;
043        private static final BigDecimal BD_100 = new BigDecimal("100.00");
044
045        static {
046                CARD_TYPES.add("VISA");
047                CARD_TYPES.add("AMEX");
048                CARD_TYPES.add("ECMC");
049                CARD_TYPES.add("DCIN");
050                CARD_TYPES.add("PAYP");
051
052                SHIPPER_IDS.add("FEDX");
053                SHIPPER_IDS.add("UPS");
054                SHIPPER_IDS.add("DHL");
055                SHIPPER_IDS.add("DPD");
056
057                SHIPPER_TYPES.add("STD");
058                SHIPPER_TYPES.add("EXP");
059                SHIPPER_TYPES.add("AMS");
060                SHIPPER_TYPES.add("AME");
061        }
062
063        @Override
064        public boolean supports(Class<?> arg0) {
065                return arg0.isAssignableFrom(Order.class);
066        }
067
068        @Override
069        public void validate(Object arg0, Errors errors) {
070                Order item = null;
071                try {
072                        item = (Order) arg0;
073                } catch (ClassCastException cce) {
074                        errors.reject("Incorrect type");
075                }
076
077                if(item != null) {
078                        validateOrder(item, errors);
079                        validateCustomer(item.getCustomer(), errors);
080                        validateAddress(item.getBillingAddress(), errors, "billingAddress");
081                        validateAddress(item.getShippingAddress(), errors, "shippingAddress");
082                        validatePayment(item.getBilling(), errors);
083                        validateShipping(item.getShipping(), errors);
084                        validateLineItems(item.getLineItems(), errors);
085                }
086        }
087
088        protected void validateLineItems(List<LineItem> lineItems, Errors errors) {
089                boolean ids = true;
090                boolean prices = true;
091                boolean discounts = true;
092                boolean shippingPrices = true;
093                boolean handlingPrices = true;
094                boolean quantities = true;
095                boolean totalPrices = true;
096
097                for (LineItem lineItem : lineItems) {
098                        if(lineItem.getItemId() <= 0 || lineItem.getItemId() > MAX_ID) {
099                                ids = false;
100                        }
101
102                        if((BD_MIN.compareTo(lineItem.getPrice()) > 0) || (BD_MAX.compareTo(lineItem.getPrice()) < 0)) {
103                                prices = false;
104                        }
105
106                        if (BD_MIN.compareTo(lineItem.getDiscountPerc()) != 0) {
107                                //DiscountPerc must be between 0.0 and 100.0
108                                if ((BD_MIN.compareTo(lineItem.getDiscountPerc()) > 0)
109                                                || (BD_PERC_MAX.compareTo(lineItem.getDiscountPerc()) < 0)
110                                                || (BD_MIN.compareTo(lineItem.getDiscountAmount()) != 0)) { //only one of DiscountAmount and DiscountPerc should be non-zero
111                                        discounts = false;
112                                }
113                        } else {
114                                //DiscountAmount must be between 0.0 and item.price
115                                if ((BD_MIN.compareTo(lineItem.getDiscountAmount()) > 0)
116                                                || (lineItem.getPrice().compareTo(lineItem.getDiscountAmount()) < 0)) {
117                                        discounts = false;
118                                }
119                        }
120
121                        if ((BD_MIN.compareTo(lineItem.getShippingPrice()) > 0) || (BD_MAX.compareTo(lineItem.getShippingPrice()) < 0)) {
122                                shippingPrices = false;
123                        }
124
125                        if ((BD_MIN.compareTo(lineItem.getHandlingPrice()) > 0) || (BD_MAX.compareTo(lineItem.getHandlingPrice()) < 0)) {
126                                handlingPrices = false;
127                        }
128
129                        if ((lineItem.getQuantity() <= 0) || (lineItem.getQuantity() > MAX_QUANTITY)) {
130                                quantities = false;
131                        }
132
133
134                        if ((BD_MIN.compareTo(lineItem.getTotalPrice()) > 0)
135                                        || (BD_MAX.compareTo(lineItem.getTotalPrice()) < 0)) {
136                                totalPrices = false;
137                        }
138
139                        //calculate total price
140
141                        //discount coefficient = (100.00 - discountPerc) / 100.00
142                        BigDecimal coef = BD_100.subtract(lineItem.getDiscountPerc())
143                                        .divide(BD_100, 4, BigDecimal.ROUND_HALF_UP);
144
145                        //discountedPrice = (price * coefficient) - discountAmount
146                        //at least one of discountPerc and discountAmount is 0 - this is validated by ValidateDiscountsFunction
147                        BigDecimal discountedPrice = lineItem.getPrice().multiply(coef)
148                                        .subtract(lineItem.getDiscountAmount());
149
150                        //price for single item = discountedPrice + shipping + handling
151                        BigDecimal singleItemPrice = discountedPrice.add(lineItem.getShippingPrice())
152                                        .add(lineItem.getHandlingPrice());
153
154                        //total price = singleItemPrice * quantity
155                        BigDecimal quantity = new BigDecimal(lineItem.getQuantity());
156                        BigDecimal totalPrice = singleItemPrice.multiply(quantity)
157                                        .setScale(2, BigDecimal.ROUND_HALF_UP);
158
159                        //calculatedPrice should equal to item.totalPrice
160                        if (totalPrice.compareTo(lineItem.getTotalPrice()) != 0) {
161                                totalPrices = false;
162                        }
163                }
164
165                String lineItemsFieldName = "lineItems";
166
167                if(!ids) {
168                        errors.rejectValue(lineItemsFieldName, "error.lineitems.id");
169                }
170
171                if(!prices) {
172                        errors.rejectValue(lineItemsFieldName, "error.lineitems.price");
173                }
174
175                if(!discounts) {
176                        errors.rejectValue(lineItemsFieldName, "error.lineitems.discount");
177                }
178
179                if(!shippingPrices) {
180                        errors.rejectValue(lineItemsFieldName, "error.lineitems.shipping");
181                }
182
183                if(!handlingPrices) {
184                        errors.rejectValue(lineItemsFieldName, "error.lineitems.handling");
185                }
186
187                if(!quantities) {
188                        errors.rejectValue(lineItemsFieldName, "error.lineitems.quantity");
189                }
190
191                if(!totalPrices) {
192                        errors.rejectValue(lineItemsFieldName, "error.lineitems.totalprice");
193                }
194        }
195
196        protected void validateShipping(ShippingInfo shipping, Errors errors) {
197                if(!SHIPPER_IDS.contains(shipping.getShipperId())) {
198                        errors.rejectValue("shipping.shipperId", "error.shipping.shipper");
199                }
200
201                if(!SHIPPER_TYPES.contains(shipping.getShippingTypeId())) {
202                        errors.rejectValue("shipping.shippingTypeId", "error.shipping.type");
203                }
204
205                if(StringUtils.hasText(shipping.getShippingInfo())) {
206                        validateStringLength(shipping.getShippingInfo(), errors, "shipping.shippingInfo", "error.shipping.shippinginfo.length", 100);
207                }
208        }
209
210        protected void validatePayment(BillingInfo billing, Errors errors) {
211                if(!CARD_TYPES.contains(billing.getPaymentId())) {
212                        errors.rejectValue("billing.paymentId", "error.billing.type");
213                }
214
215                if(!billing.getPaymentDesc().matches("[A-Z]{4}-[0-9]{10,11}")) {
216                        errors.rejectValue("billing.paymentDesc", "error.billing.desc");
217                }
218        }
219
220        protected void validateAddress(Address address, Errors errors,
221                        String prefix) {
222                if(address != null) {
223                        if(StringUtils.hasText(address.getAddressee())) {
224                                validateStringLength(address.getAddressee(), errors, prefix + ".addressee", "error.baddress.addresse.length", 60);
225                        }
226
227                        validateStringLength(address.getAddrLine1(), errors, prefix + ".addrLine1", "error.baddress.addrline1.length", 50);
228
229                        if(StringUtils.hasText(address.getAddrLine2())) {
230                                validateStringLength(address.getAddrLine2(), errors, prefix + ".addrLine2", "error.baddress.addrline2.length", 50);
231                        }
232                        validateStringLength(address.getCity(), errors, prefix + ".city", "error.baddress.city.length", 30);
233                        validateStringLength(address.getZipCode(), errors, prefix + ".zipCode", "error.baddress.zipcode.length", 5);
234
235                        if(StringUtils.hasText(address.getZipCode()) && !address.getZipCode().matches("[0-9]{5}")) {
236                                errors.rejectValue(prefix + ".zipCode", "error.baddress.zipcode.format");
237                        }
238
239                        if((!StringUtils.hasText(address.getState()) && ("United States".equals(address.getCountry())) || StringUtils.hasText(address.getState()) && address.getState().length() != 2)) {
240                                errors.rejectValue(prefix + ".state", "error.baddress.state.length");
241                        }
242
243                        validateStringLength(address.getCountry(), errors, prefix + ".country", "error.baddress.country.length", 50);
244                }
245        }
246
247        protected void validateStringLength(String string, Errors errors,
248                        String field, String message, int length) {
249                if(!StringUtils.hasText(string) || string.length() > length) {
250                        errors.rejectValue(field, message);
251                }
252        }
253
254        protected void validateCustomer(Customer customer, Errors errors) {
255                if(!customer.isRegistered() && customer.isBusinessCustomer()) {
256                        errors.rejectValue("customer.registered", "error.customer.registration");
257                }
258
259                if(!StringUtils.hasText(customer.getCompanyName()) && customer.isBusinessCustomer()) {
260                        errors.rejectValue("customer.companyName", "error.customer.companyname");
261                }
262
263                if(!StringUtils.hasText(customer.getFirstName()) && !customer.isBusinessCustomer()) {
264                        errors.rejectValue("customer.firstName", "error.customer.firstname");
265                }
266
267                if(!StringUtils.hasText(customer.getLastName()) && !customer.isBusinessCustomer()) {
268                        errors.rejectValue("customer.lastName", "error.customer.lastname");
269                }
270
271                if(customer.isRegistered() && (customer.getRegistrationId() < 0 || customer.getRegistrationId() >= 99999999L)) {
272                        errors.rejectValue("customer.registrationId", "error.customer.registrationid");
273                }
274        }
275
276        protected void validateOrder(Order item, Errors errors) {
277                if(item.getOrderId() < 0 || item.getOrderId() > 9999999999L) {
278                        errors.rejectValue("orderId", "error.order.id");
279                }
280
281                if(new Date().compareTo(item.getOrderDate()) < 0) {
282                        errors.rejectValue("orderDate", "error.order.date.future");
283                }
284
285                if(item.getLineItems() != null && item.getTotalLines() != item.getLineItems().size()) {
286                        errors.rejectValue("totalLines", "error.order.lines.badcount");
287                }
288        }
289}