package com.salesmanager.core.business.modules.integration.shipping.impl;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.inject.Inject;
import org.apache.commons.digester.Digester;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.salesmanager.core.business.constants.Constants;
import com.salesmanager.core.business.services.reference.country.CountryService;
import com.salesmanager.core.business.utils.DataUtils;
import com.salesmanager.core.business.utils.ProductPriceUtils;
import com.salesmanager.core.constants.MeasureUnit;
import com.salesmanager.core.model.common.Delivery;
import com.salesmanager.core.model.merchant.MerchantStore;
import com.salesmanager.core.model.reference.country.Country;
import com.salesmanager.core.model.reference.language.Language;
import com.salesmanager.core.model.shipping.PackageDetails;
import com.salesmanager.core.model.shipping.ShippingConfiguration;
import com.salesmanager.core.model.shipping.ShippingOption;
import com.salesmanager.core.model.shipping.ShippingOrigin;
import com.salesmanager.core.model.shipping.ShippingQuote;
import com.salesmanager.core.model.system.CustomIntegrationConfiguration;
import com.salesmanager.core.model.system.IntegrationConfiguration;
import com.salesmanager.core.model.system.IntegrationModule;
import com.salesmanager.core.model.system.ModuleConfig;
import com.salesmanager.core.modules.integration.IntegrationException;
import com.salesmanager.core.modules.integration.shipping.model.ShippingQuoteModule;
/**
* Integrates with USPS online API
* @author casams1
*
*/
public class USPSShippingQuote implements ShippingQuoteModule {
private static final Logger LOGGER = LoggerFactory.getLogger(USPSShippingQuote.class);
@Inject
private ProductPriceUtils productPriceUtils;
@Inject
private CountryService countryService;
@Override
public void validateModuleConfiguration(
IntegrationConfiguration integrationConfiguration,
MerchantStore store) throws IntegrationException {
List<String> errorFields = null;
//validate integrationKeys['account']
Map<String,String> keys = integrationConfiguration.getIntegrationKeys();
if(keys==null || StringUtils.isBlank(keys.get("account"))) {
errorFields = new ArrayList<String>();
errorFields.add("identifier");
}
//validate at least one integrationOptions['packages']
Map<String,List<String>> options = integrationConfiguration.getIntegrationOptions();
if(options==null) {
errorFields = new ArrayList<String>();
errorFields.add("identifier");
}
List<String> packages = options.get("packages");
if(packages==null || packages.size()==0) {
if(errorFields==null) {
errorFields = new ArrayList<String>();
}
errorFields.add("packages");
}
/* List<String> services = options.get("services");
if(services==null || services.size()==0) {
if(errorFields==null) {
errorFields = new ArrayList<String>();
}
errorFields.add("services");
}
if(services!=null && services.size()>3) {
if(errorFields==null) {
errorFields = new ArrayList<String>();
}
errorFields.add("services");
}*/
if(errorFields!=null) {
IntegrationException ex = new IntegrationException(IntegrationException.ERROR_VALIDATION_SAVE);
ex.setErrorFields(errorFields);
throw ex;
}
}
@Override
public List<ShippingOption> getShippingQuotes(
ShippingQuote shippingQuote,
List<PackageDetails> packages, BigDecimal orderTotal,
Delivery delivery, ShippingOrigin origin, MerchantStore store,
IntegrationConfiguration configuration, IntegrationModule module,
ShippingConfiguration shippingConfiguration, Locale locale)
throws IntegrationException {
if (packages == null) {
return null;
}
if(StringUtils.isBlank(delivery.getPostalCode())) {
return null;
}
// only applies to Canada and US
/* Country country = delivery.getCountry();
if(!country.getIsoCode().equals("US") || !country.getIsoCode().equals("US")){
throw new IntegrationException("USPS Not configured for shipping in country " + country.getIsoCode());
}*/
// supports en and fr
String language = locale.getLanguage();
if (!language.equals(Locale.FRENCH.getLanguage())
&& !language.equals(Locale.ENGLISH.getLanguage())) {
language = Locale.ENGLISH.getLanguage();
}
// if store is not CAD /** maintained in the currency **/
/* if (!store.getCurrency().equals(Constants.CURRENCY_CODE_CAD)) {
total = CurrencyUtil.convertToCurrency(total, store.getCurrency(),
Constants.CURRENCY_CODE_CAD);
}*/
Language lang = store.getDefaultLanguage();
HttpGet httpget = null;
Reader xmlreader = null;
String pack = configuration.getIntegrationOptions().get("packages").get(0);
try {
Map<String,Country> countries = countryService.getCountriesMap(lang);
Country destination = countries.get(delivery.getCountry().getIsoCode());
Map<String,String> keys = configuration.getIntegrationKeys();
if(keys==null || StringUtils.isBlank(keys.get("account"))) {
return null;//TODO can we return null
}
String host = null;
String protocol = null;
String port = null;
String url = null;
//against which environment are we using the service
String env = configuration.getEnvironment();
//must be US
if(!store.getCountry().getIsoCode().equals("US")) {
throw new IntegrationException("Can't use the service for store country code ");
}
Map<String, ModuleConfig> moduleConfigsMap = module.getModuleConfigs();
for(String key : moduleConfigsMap.keySet()) {
ModuleConfig moduleConfig = (ModuleConfig)moduleConfigsMap.get(key);
if(moduleConfig.getEnv().equals(env)) {
host = moduleConfig.getHost();
protocol = moduleConfig.getScheme();
port = moduleConfig.getPort();
url = moduleConfig.getUri();
}
}
StringBuilder xmlheader = new StringBuilder();
if(store.getCountry().getIsoCode().equals(delivery.getCountry().getIsoCode())) {
xmlheader.append("<RateV3Request USERID=\"").append(keys.get("account")).append("\">");
} else {
xmlheader.append("<IntlRateRequest USERID=\"").append(keys.get("account")).append("\">");
}
StringBuilder xmldatabuffer = new StringBuilder();
double totalW = 0;
double totalH = 0;
double totalL = 0;
double totalG = 0;
double totalP = 0;
for (PackageDetails detail : packages) {
// need size in inch
double w = DataUtils.getMeasure(detail.getShippingWidth(),
store, MeasureUnit.IN.name());
double h = DataUtils.getMeasure(detail.getShippingHeight(),
store, MeasureUnit.IN.name());
double l = DataUtils.getMeasure(detail.getShippingLength(),
store, MeasureUnit.IN.name());
totalW = totalW + w;
totalH = totalH + h;
totalL = totalL + l;
// Girth = Length + (Width x 2) + (Height x 2)
double girth = l + (w * 2) + (h * 2);
totalG = totalG + girth;
// need weight in pounds
double p = DataUtils.getWeight(detail.getShippingWeight(), store, MeasureUnit.LB.name());
totalP = totalP + p;
}
/* BigDecimal convertedOrderTotal = CurrencyUtil.convertToCurrency(
orderTotal, store.getCurrency(),
Constants.CURRENCY_CODE_USD);*/
// calculate total shipping volume
// ship date is 3 days from here
Calendar c = Calendar.getInstance();
c.setTime(new Date());
c.add(Calendar.DATE, 3);
Date newDate = c.getTime();
SimpleDateFormat format = new SimpleDateFormat(Constants.DEFAULT_DATE_FORMAT);
String shipDate = format.format(newDate);
int i = 1;
// need pounds and ounces
int pounds = (int) totalP;
String ouncesString = String.valueOf(totalP - pounds);
int ouncesIndex = ouncesString.indexOf(".");
String ounces = "00";
if (ouncesIndex > -1) {
ounces = ouncesString.substring(ouncesIndex + 1);
}
String size = "REGULAR";
if (totalL + totalG <= 64) {
size = "REGULAR";
} else if (totalL + totalG <= 108) {
size = "LARGE";
} else {
size = "OVERSIZE";
}
/**
* Domestic <Package ID="1ST"> <Service>ALL</Service>
* <ZipOrigination>90210</ZipOrigination>
* <ZipDestination>96698</ZipDestination> <Pounds>8</Pounds>
* <Ounces>32</Ounces> <Container/> <Size>REGULAR</Size>
* <Machinable>true</Machinable> </Package>
*
* //MAXWEIGHT=70 lbs
*
*
* //domestic container default=VARIABLE whiteSpace=collapse
* enumeration=VARIABLE enumeration=FLAT RATE BOX enumeration=FLAT
* RATE ENVELOPE enumeration=LG FLAT RATE BOX
* enumeration=RECTANGULAR enumeration=NONRECTANGULAR
*
* //INTL enumeration=Package enumeration=Postcards or aerogrammes
* enumeration=Matter for the blind enumeration=Envelope
*
* Size May be left blank in situations that do not Size. Defined as
* follows: REGULAR: package plus girth is 84 inches or less; LARGE:
* package length plus girth measure more than 84 inches not more
* than 108 inches; OVERSIZE: package length plus girth is more than
* 108 but not 130 inches. For example: <Size>REGULAR</Size>
*
* International <Package ID="1ST"> <Machinable>true</Machinable>
* <MailType>Envelope</MailType> <Country>Canada</Country>
* <Length>0</Length> <Width>0</Width> <Height>0</Height>
* <ValueOfContents>250</ValueOfContents> </Package>
*
* <Package ID="2ND"> <Pounds>4</Pounds> <Ounces>3</Ounces>
* <MailType>Package</MailType> <GXG> <Length>46</Length>
* <Width>14</Width> <Height>15</Height> <POBoxFlag>N</POBoxFlag>
* <GiftFlag>N</GiftFlag> </GXG>
* <ValueOfContents>250</ValueOfContents> <Country>Japan</Country>
* </Package>
*/
xmldatabuffer.append("<Package ID=\"").append(i).append("\">");
if(store.getCountry().getIsoCode().equals(delivery.getCountry().getIsoCode())) {
xmldatabuffer.append("<Service>");
xmldatabuffer.append("ALL");
xmldatabuffer.append("</Service>");
xmldatabuffer.append("<ZipOrigination>");
xmldatabuffer.append(DataUtils
.trimPostalCode(store.getStorepostalcode()));
xmldatabuffer.append("</ZipOrigination>");
xmldatabuffer.append("<ZipDestination>");
xmldatabuffer.append(DataUtils
.trimPostalCode(delivery.getPostalCode()));
xmldatabuffer.append("</ZipDestination>");
xmldatabuffer.append("<Pounds>");
xmldatabuffer.append(pounds);
xmldatabuffer.append("</Pounds>");
xmldatabuffer.append("<Ounces>");
xmldatabuffer.append(ounces);
xmldatabuffer.append("</Ounces>");
xmldatabuffer.append("<Container>");
xmldatabuffer.append(pack);
xmldatabuffer.append("</Container>");
xmldatabuffer.append("<Size>");
xmldatabuffer.append(size);
xmldatabuffer.append("</Size>");
xmldatabuffer.append("<Machinable>true</Machinable>");//TODO must be changed if not machinable
xmldatabuffer.append("<ShipDate>");
xmldatabuffer.append(shipDate);
xmldatabuffer.append("</ShipDate>");
} else {
// if international
xmldatabuffer.append("<Pounds>");
xmldatabuffer.append(pounds);
xmldatabuffer.append("</Pounds>");
xmldatabuffer.append("<Ounces>");
xmldatabuffer.append(ounces);
xmldatabuffer.append("</Ounces>");
xmldatabuffer.append("<MailType>");
xmldatabuffer.append(pack);
xmldatabuffer.append("</MailType>");
xmldatabuffer.append("<ValueOfContents>");
xmldatabuffer.append(productPriceUtils.getAdminFormatedAmount(store, orderTotal));
xmldatabuffer.append("</ValueOfContents>");
xmldatabuffer.append("<Country>");
xmldatabuffer.append(destination.getName());
xmldatabuffer.append("</Country>");
}
// if international & CXG
/*
* xmldatabuffer.append("<CXG>"); xmldatabuffer.append("<Length>");
* xmldatabuffer.append(""); xmldatabuffer.append("</Length>");
* xmldatabuffer.append("<Width>"); xmldatabuffer.append("");
* xmldatabuffer.append("</Width>");
* xmldatabuffer.append("<Height>"); xmldatabuffer.append("");
* xmldatabuffer.append("</Height>");
* xmldatabuffer.append("<POBoxFlag>"); xmldatabuffer.append("");
* xmldatabuffer.append("</POBoxFlag>");
* xmldatabuffer.append("<GiftFlag>"); xmldatabuffer.append("");
* xmldatabuffer.append("</GiftFlag>");
* xmldatabuffer.append("</CXG>");
*/
/*
* xmldatabuffer.append("<Width>"); xmldatabuffer.append(totalW);
* xmldatabuffer.append("</Width>");
* xmldatabuffer.append("<Length>"); xmldatabuffer.append(totalL);
* xmldatabuffer.append("</Length>");
* xmldatabuffer.append("<Height>"); xmldatabuffer.append(totalH);
* xmldatabuffer.append("</Height>");
* xmldatabuffer.append("<Girth>"); xmldatabuffer.append(totalG);
* xmldatabuffer.append("</Girth>");
*/
xmldatabuffer.append("</Package>");
String xmlfooter = "</RateV3Request>";
if(!store.getCountry().getIsoCode().equals(delivery.getCountry().getIsoCode())) {
xmlfooter = "</IntlRateRequest>";
}
StringBuilder xmlbuffer = new StringBuilder().append(xmlheader.toString()).append(
xmldatabuffer.toString()).append(xmlfooter.toString());
LOGGER.debug("USPS QUOTE REQUEST " + xmlbuffer.toString());
//HttpClient client = new HttpClient();
CloseableHttpClient httpclient = HttpClients.createDefault();
@SuppressWarnings("deprecation")
String encoded = java.net.URLEncoder.encode(xmlbuffer.toString());
String completeUri = url + "?API=RateV3&XML=" + encoded;
if(!store.getCountry().getIsoCode().equals(delivery.getCountry().getIsoCode())) {
completeUri = url + "?API=IntlRate&XML=" + encoded;
}
// ?API=RateV3
httpget = new HttpGet(protocol + "://" + host + ":" + port
+ completeUri);
// RequestEntity entity = new
// StringRequestEntity(xmlbuffer.toString(),"text/plain","UTF-8");
// httpget.setRequestEntity(entity);
ResponseHandler<String> responseHandler = new ResponseHandler<String>() {
@Override
public String handleResponse(
final HttpResponse response) throws ClientProtocolException, IOException {
int status = response.getStatusLine().getStatusCode();
if (status >= 200 && status < 300) {
HttpEntity entity = response.getEntity();
return entity != null ? EntityUtils.toString(entity) : null;
} else {
LOGGER.error("Communication Error with ups quote " + status);
throw new ClientProtocolException("UPS quote communication error " + status);
}
}
};
String data = httpclient.execute(httpget, responseHandler);
/* int result = client.executeMethod(httpget);
if (result != 200) {
LOGGER.error("Communication Error with usps quote " + result + " "
+ protocol + "://" + host + ":" + port + url);
throw new Exception("USPS quote communication error " + result);
}*/
//data = httpget.getResponseBodyAsString();
LOGGER.debug("usps quote response " + data);
USPSParsedElements parsed = new USPSParsedElements();
/**
* <RateV3Response> <Package ID="1ST">
* <ZipOrigination>44106</ZipOrigination>
* <ZipDestination>20770</ZipDestination>
*/
Digester digester = new Digester();
digester.push(parsed);
if(store.getCountry().getIsoCode().equals(delivery.getCountry().getIsoCode())) {
digester.addCallMethod("Error/Description",
"setError", 0);
digester.addCallMethod("RateV3Response/Package/Error/Description",
"setError", 0);
digester
.addObjectCreate(
"RateV3Response/Package/Postage",
ShippingOption.class);
digester.addSetProperties("RateV3Response/Package/Postage",
"CLASSID", "optionId");
digester.addCallMethod(
"RateV3Response/Package/Postage/MailService",
"setOptionName", 0);
digester.addCallMethod(
"RateV3Response/Package/Postage/MailService",
"setOptionCode", 0);
digester.addCallMethod("RateV3Response/Package/Postage/Rate",
"setOptionPriceText", 0);
//digester
// .addCallMethod(
// "RateV3Response/Package/Postage/Commitment/CommitmentDate",
// "estimatedNumberOfDays", 0);
digester.addSetNext("RateV3Response/Package/Postage",
"addOption");
} else {
digester.addCallMethod("Error/Description",
"setError", 0);
digester.addCallMethod("IntlRateResponse/Package/Error/Description",
"setError", 0);
digester
.addObjectCreate(
"IntlRateResponse/Package/Service",
ShippingOption.class);
digester.addSetProperties("IntlRateResponse/Package/Service",
"ID", "optionId");
digester.addCallMethod(
"IntlRateResponse/Package/Service/SvcDescription",
"setOptionName", 0);
digester.addCallMethod(
"IntlRateResponse/Package/Service/SvcDescription",
"setOptionCode", 0);
digester.addCallMethod(
"IntlRateResponse/Package/Service/Postage",
"setOptionPriceText", 0);
//digester.addCallMethod(
// "IntlRateResponse/Package/Service/SvcCommitments",
// "setEstimatedNumberOfDays", 0);
digester.addSetNext("IntlRateResponse/Package/Service",
"addOption");
}
// <?xml
// version="1.0"?><AddressValidationResponse><Response><TransactionReference><CustomerContext>SalesManager
// Data</CustomerContext><XpciVersion>1.0</XpciVersion></TransactionReference><ResponseStatusCode>0</ResponseStatusCode><ResponseStatusDescription>Failure</ResponseStatusDescription><Error><ErrorSeverity>Hard</ErrorSeverity><ErrorCode>10002</ErrorCode><ErrorDescription>The
// XML document is well formed but the document is not
// valid</ErrorDescription><ErrorLocation><ErrorLocationElementName>AddressValidationRequest</ErrorLocationElementName></ErrorLocation></Error></Response></AddressValidationResponse>
//<?xml version="1.0"?>
//<IntlRateResponse><Package ID="1"><Error><Number>-2147218046</Number>
//<Source>IntlPostage;clsIntlPostage.GetCountryAndRestirctedServiceId;clsIntlPostage.CalcAllPostageDimensionsXML;IntlRate.ProcessRequest</Source>
//<Description>Invalid Country Name</Description><HelpFile></HelpFile><HelpContext>1000440</HelpContext></Error></Package></IntlRateResponse>
xmlreader = new StringReader(data);
digester.parse(xmlreader);
if (!StringUtils.isBlank(parsed.getError())) {
LOGGER.error("Can't process USPS message= "
+ parsed.getError());
throw new IntegrationException(parsed.getError());
}
if (!StringUtils.isBlank(parsed.getStatusCode())
&& !parsed.getStatusCode().equals("1")) {
LOGGER.error("Can't process USPS statusCode="
+ parsed.getStatusCode() + " message= "
+ parsed.getError());
throw new IntegrationException(parsed.getError());
}
if (parsed.getOptions() == null || parsed.getOptions().size() == 0) {
LOGGER.warn("No options returned from USPS");
throw new IntegrationException(parsed.getError());
}
/* String carrier = getShippingMethodDescription(locale);
// cost is in USD, need to do conversion
MerchantConfiguration rtdetails = config
.getMerchantConfiguration(ShippingConstants.MODULE_SHIPPING_DISPLAY_REALTIME_QUOTES);
int displayQuoteDeliveryTime = ShippingConstants.NO_DISPLAY_RT_QUOTE_TIME;
if (rtdetails != null) {
if (!StringUtils.isBlank(rtdetails.getConfigurationValue1())) {// display
// or
// not
// quotes
try {
displayQuoteDeliveryTime = Integer.parseInt(rtdetails
.getConfigurationValue1());
} catch (Exception e) {
log.error("Display quote is not an integer value ["
+ rtdetails.getConfigurationValue1() + "]");
}
}
}
LabelUtil labelUtil = LabelUtil.getInstance();*/
// Map serviceMap =
// com.salesmanager.core.util.ShippingUtil.buildServiceMap("usps",locale);
@SuppressWarnings("unchecked")
List<ShippingOption> shippingOptions = parsed.getOptions();
/* List<ShippingOption> returnOptions = null;
if (shippingOptions != null && shippingOptions.size() > 0) {
returnOptions = new ArrayList<ShippingOption>();
// Map selectedintlservices =
// (Map)config.getConfiguration("service-global-usps");
// need to create a Map of LABEL - LABLEL
// Iterator servicesIterator =
// selectedintlservices.keySet().iterator();
// Map services = new HashMap();
// ResourceBundle bundle = ResourceBundle.getBundle("usps",
// locale);
// while(servicesIterator.hasNext()) {
// String key = (String)servicesIterator.next();
// String value =
// bundle.getString("shipping.quote.services.label." + key);
// services.put(value, key);
// }
for(ShippingOption option : shippingOptions) {
StringBuilder description = new StringBuilder();
description.append(option.getOptionName());
//if (displayQuoteDeliveryTime == ShippingConstants.DISPLAY_RT_QUOTE_TIME) {
if (shippingConfiguration.getShippingDescription()==ShippingDescription.LONG_DESCRIPTION) {
if (option.getEstimatedNumberOfDays()>0) {
description.append(" (").append(
option.getEstimatedNumberOfDays()).append(
" ").append(
" d")
.append(")");
}
}
option.setDescription(description.toString());
// get currency
if (!option.getCurrency().equals(store.getCurrency())) {
option.setOptionPrice(CurrencyUtil.convertToCurrency(
option.getOptionPrice(), option.getCurrency(),
store.getCurrency()));
}
// if(!services.containsKey(option.getOptionCode())) {
// if(returnColl==null) {
// returnColl = new ArrayList();
// }
// returnColl.add(option);
// }
returnOptions.add(option);
}
// if(options.size()==0) {
// CommonService.logServiceMessage(store.getMerchantId(),
// " none of the service code returned by UPS [" +
// selectedintlservices.keySet().toArray(new
// String[selectedintlservices.size()]) +
// "] for this shipping is in your selection list");
// }
}*/
return shippingOptions;
} catch (Exception e1) {
LOGGER.error("Error in USPS shipping quote ",e1);
throw new IntegrationException(e1);
} finally {
if (xmlreader != null) {
try {
xmlreader.close();
} catch (Exception ignore) {
}
}
if (httpget != null) {
httpget.releaseConnection();
}
}
}
@Override
public CustomIntegrationConfiguration getCustomModuleConfiguration(
MerchantStore store) throws IntegrationException {
//nothing to do
return null;
}
}
class USPSParsedElements {
private String statusCode;
private String statusMessage;
private String error = "";
private String errorCode = "";
private List<ShippingOption> options = new ArrayList<ShippingOption>();
public void addOption(ShippingOption option) {
options.add(option);
}
public List getOptions() {
return options;
}
public String getStatusCode() {
return statusCode;
}
public void setStatusCode(String statusCode) {
this.statusCode = statusCode;
}
public String getStatusMessage() {
return statusMessage;
}
public void setStatusMessage(String statusMessage) {
this.statusMessage = statusMessage;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
public String getErrorCode() {
return errorCode;
}
public void setErrorCode(String errorCode) {
this.errorCode = errorCode;
}
}