/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.ofbiz.shipment.thirdparty.fedex; import java.io.StringWriter; import java.math.BigDecimal; import java.sql.Timestamp; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import org.apache.ofbiz.base.util.Base64; import org.apache.ofbiz.base.util.Debug; import org.apache.ofbiz.base.util.GeneralException; import org.apache.ofbiz.base.util.HttpClient; import org.apache.ofbiz.base.util.HttpClientException; import org.apache.ofbiz.base.util.UtilDateTime; import org.apache.ofbiz.base.util.UtilMisc; import org.apache.ofbiz.base.util.UtilProperties; import org.apache.ofbiz.base.util.UtilValidate; import org.apache.ofbiz.base.util.UtilXml; import org.apache.ofbiz.base.util.template.FreeMarkerWorker; import org.apache.ofbiz.entity.Delegator; import org.apache.ofbiz.entity.GenericEntityException; import org.apache.ofbiz.entity.GenericValue; import org.apache.ofbiz.entity.condition.EntityCondition; import org.apache.ofbiz.entity.condition.EntityOperator; import org.apache.ofbiz.entity.util.EntityQuery; import org.apache.ofbiz.entity.util.EntityUtil; import org.apache.ofbiz.entity.util.EntityUtilProperties; import org.apache.ofbiz.party.party.PartyHelper; import org.apache.ofbiz.service.DispatchContext; import org.apache.ofbiz.service.GenericServiceException; import org.apache.ofbiz.service.LocalDispatcher; import org.apache.ofbiz.service.ServiceUtil; import org.apache.ofbiz.shipment.shipment.ShipmentServices; import org.w3c.dom.Document; import org.w3c.dom.Element; /** * Fedex Shipment Services * * Implementation of Fedex shipment interface using Ship Manager Direct API * * TODO: FDXShipDeleteRequest/Reply (on error and via service call) * TODO: FDXCloseRequest/Reply * TODO: FDXRateRequest/Reply * TODO: FDXTrackRequest/Reply * TODO: International shipments * TODO: Multi-piece shipments * TODO: Freight shipments */ public class FedexServices { public final static String module = FedexServices.class.getName(); public final static String shipmentPropertiesFile = "shipment.properties"; public static final String resourceError = "ProductUiLabels"; /** * Opens a URL to Fedex and makes a request. * * @param xmlString XML message to send * @param delegator the delegator * @param shipmentGatewayConfigId the shipmentGatewayConfigId * @param resource resource file name * @return XML string response from FedEx * @throws FedexConnectException */ public static String sendFedexRequest(String xmlString, Delegator delegator, String shipmentGatewayConfigId, String resource, Locale locale) throws FedexConnectException { String url = getShipmentGatewayConfigValue(delegator, shipmentGatewayConfigId, "connectUrl", resource, "shipment.fedex.connect.url"); if (UtilValidate.isEmpty(url)) { throw new FedexConnectException(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexConnectUrlIncomplete", locale)); } // xmlString should contain the auth document at the beginning // all documents require an <?xml version="1.0" encoding="UTF-8" ?> header if (! xmlString.matches("^(?s)<\\?xml\\s+version=\"1\\.0\"\\s+encoding=\"UTF-8\"\\s*\\?>.*")) { throw new FedexConnectException(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexXmlHeaderMalformed", locale)); } // prepare the connect string url = url.trim(); String timeOutStr = getShipmentGatewayConfigValue(delegator, shipmentGatewayConfigId, "connectTimeout", resource, "shipment.fedex.connect.timeout", "60"); int timeout = 60; try { timeout = Integer.parseInt(timeOutStr); } catch (NumberFormatException e) { Debug.logError(e, "Unable to set timeout to " + timeOutStr + " using default " + timeout); } if (Debug.verboseOn()) { Debug.logVerbose("Fedex Connect URL : " + url, module); Debug.logVerbose("Fedex XML String : " + xmlString, module); } HttpClient http = new HttpClient(url); http.setTimeout(timeout * 1000); String response = null; try { response = http.post(xmlString); } catch (HttpClientException e) { Debug.logError(e, "Problem connecting to Fedex server", module); throw new FedexConnectException(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexConnectUrlProblem", UtilMisc.toMap("errorString", e.toString()), locale)); } if (response == null) { throw new FedexConnectException(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexReceivedNullResponse", locale)); } if (Debug.verboseOn()) { Debug.logVerbose("Fedex Response : " + response, module); } return response; } /* * Register a Fedex account for shipping by obtaining the meter number */ public static Map<String, Object> fedexSubscriptionRequest(DispatchContext dctx, Map<String, ? extends Object> context) { Delegator delegator = dctx.getDelegator(); String shipmentGatewayConfigId = (String) context.get("shipmentGatewayConfigId"); String resource = (String) context.get("configProps"); Locale locale = (Locale) context.get("locale"); List<Object> errorList = new LinkedList<Object> (); Boolean replaceMeterNumber = (Boolean) context.get("replaceMeterNumber"); if (! replaceMeterNumber.booleanValue()) { String meterNumber = getShipmentGatewayConfigValue(delegator, shipmentGatewayConfigId, "accessMeterNumber", resource, "shipment.fedex.access.meterNumber"); if (UtilValidate.isNotEmpty(meterNumber)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexMeterNumberAlreadyExists", UtilMisc.toMap("meterNumber", meterNumber), locale)); } } String companyPartyId = (String) context.get("companyPartyId"); String contactPartyName = (String) context.get("contactPartyName"); Map<String, Object> result = new HashMap<String, Object>(); String accountNumber = getShipmentGatewayConfigValue(delegator, shipmentGatewayConfigId, "accessAccountNbr", resource, "shipment.fedex.access.accountNbr"); if (UtilValidate.isEmpty(accountNumber)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexAccountNumberNotFound", locale)); } if (UtilValidate.isEmpty(contactPartyName)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexContactNameCannotBeEmpty", locale)); } String companyName = null; GenericValue postalAddress = null; String phoneNumber = null; String faxNumber = null; String emailAddress = null; try { // Make sure the company exists GenericValue companyParty = EntityQuery.use(delegator).from("Party").where("partyId", companyPartyId).cache().queryOne(); if (companyParty == null) { String errorMessage = "Party with partyId " + companyPartyId + " does not exist"; Debug.logError(errorMessage, module); return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexCompanyPartyDoesNotExists", UtilMisc.toMap("companyPartyId", companyPartyId), locale)); } // Get the company name (required by Fedex) companyName = PartyHelper.getPartyName(companyParty); if (UtilValidate.isEmpty(companyName)) { String errorMessage = "Party with partyId " + companyPartyId + " has no name"; Debug.logError(errorMessage, module); return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexCompanyPartyHasNoName", UtilMisc.toMap("companyPartyId", companyPartyId), locale)); } // Get the contact information for the company List<GenericValue> partyContactDetails = EntityQuery.use(delegator).from("PartyContactDetailByPurpose") .where("partyId", companyPartyId) .filterByDate(UtilDateTime.nowTimestamp(), "fromDate", "thruDate", "purposeFromDate", "purposeThruDate") .queryList(); // Get the first valid postal address (address1, city, postalCode and countryGeoId are required by Fedex) List<EntityCondition> postalAddressConditions = new LinkedList<EntityCondition>(); postalAddressConditions.add(EntityCondition.makeCondition("contactMechTypeId", EntityOperator.EQUALS, "POSTAL_ADDRESS")); postalAddressConditions.add(EntityCondition.makeCondition("address1", EntityOperator.NOT_EQUAL, null)); postalAddressConditions.add(EntityCondition.makeCondition("address1", EntityOperator.NOT_EQUAL, "")); postalAddressConditions.add(EntityCondition.makeCondition("city", EntityOperator.NOT_EQUAL, null)); postalAddressConditions.add(EntityCondition.makeCondition("city", EntityOperator.NOT_EQUAL, "")); postalAddressConditions.add(EntityCondition.makeCondition("postalCode", EntityOperator.NOT_EQUAL, null)); postalAddressConditions.add(EntityCondition.makeCondition("postalCode", EntityOperator.NOT_EQUAL, "")); postalAddressConditions.add(EntityCondition.makeCondition("countryGeoId", EntityOperator.NOT_EQUAL, null)); postalAddressConditions.add(EntityCondition.makeCondition("countryGeoId", EntityOperator.NOT_EQUAL, "")); List<GenericValue> postalAddresses = EntityUtil.filterByCondition(partyContactDetails, EntityCondition.makeCondition(postalAddressConditions, EntityOperator.AND)); // Fedex requires USA or Canada addresses to have a state/province ID, so filter out the ones without postalAddressConditions.clear(); postalAddressConditions.add(EntityCondition.makeCondition("countryGeoId", EntityOperator.IN, UtilMisc.toList("CAN", "USA"))); postalAddressConditions.add(EntityCondition.makeCondition("stateProvinceGeoId", EntityOperator.EQUALS, null)); postalAddresses = EntityUtil.filterOutByCondition(postalAddresses, EntityCondition.makeCondition(postalAddressConditions, EntityOperator.AND)); postalAddressConditions.clear(); postalAddressConditions.add(EntityCondition.makeCondition("countryGeoId", EntityOperator.IN, UtilMisc.toList("CAN", "USA"))); postalAddressConditions.add(EntityCondition.makeCondition("stateProvinceGeoId", EntityOperator.EQUALS, "")); postalAddresses = EntityUtil.filterOutByCondition(postalAddresses, EntityCondition.makeCondition(postalAddressConditions, EntityOperator.AND)); postalAddress = EntityUtil.getFirst(postalAddresses); if (UtilValidate.isEmpty(postalAddress)) { String errorMessage = "Party with partyId " + companyPartyId + " does not have a current, fully populated postal address"; Debug.logError(errorMessage, module); return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexCompanyPartyHasNotPostalAddress", UtilMisc.toMap("companyPartyId", companyPartyId), locale)); } GenericValue countryGeo = EntityQuery.use(delegator).from("Geo").where("geoId", postalAddress.getString("countryGeoId")).cache().queryOne(); String countryCode = countryGeo.getString("geoCode"); String stateOrProvinceCode = null; // Only add the StateOrProvinceCode element if the address is in USA or Canada if (countryCode.equals("CA") || countryCode.equals("US")) { GenericValue stateProvinceGeo = EntityQuery.use(delegator).from("Geo").where("geoId", postalAddress.getString("stateProvinceGeoId")).cache().queryOne(); stateOrProvinceCode = stateProvinceGeo.getString("geoCode"); } // Get the first valid primary phone number (required by Fedex) List<EntityCondition> phoneNumberConditions = new LinkedList<EntityCondition>(); phoneNumberConditions.add(EntityCondition.makeCondition("contactMechTypeId", EntityOperator.EQUALS, "TELECOM_NUMBER")); phoneNumberConditions.add(EntityCondition.makeCondition("contactMechPurposeTypeId", EntityOperator.EQUALS, "PRIMARY_PHONE")); phoneNumberConditions.add(EntityCondition.makeCondition("areaCode", EntityOperator.NOT_EQUAL, null)); phoneNumberConditions.add(EntityCondition.makeCondition("areaCode", EntityOperator.NOT_EQUAL, "")); phoneNumberConditions.add(EntityCondition.makeCondition("contactNumber", EntityOperator.NOT_EQUAL, null)); phoneNumberConditions.add(EntityCondition.makeCondition("contactNumber", EntityOperator.NOT_EQUAL, "")); List<GenericValue> phoneNumbers = EntityUtil.filterByCondition(partyContactDetails, EntityCondition.makeCondition(phoneNumberConditions, EntityOperator.AND)); GenericValue phoneNumberValue = EntityUtil.getFirst(phoneNumbers); if (UtilValidate.isEmpty(phoneNumberValue)) { String errorMessage = "Party with partyId " + companyPartyId + " does not have a current, fully populated primary phone number"; Debug.logError(errorMessage, module); return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexCompanyPartyHasNotPrimaryPhoneNumber", UtilMisc.toMap("companyPartyId", companyPartyId), locale)); } phoneNumber = phoneNumberValue.getString("areaCode") + phoneNumberValue.getString("contactNumber"); // Fedex doesn't want the North American country code if (UtilValidate.isNotEmpty(phoneNumberValue.getString("countryCode")) && !(countryCode.equals("CA") || countryCode.equals("US"))) { phoneNumber = phoneNumberValue.getString("countryCode") + phoneNumber; } phoneNumber = phoneNumber.replaceAll("[^+\\d]", ""); // Get the first valid fax number List<EntityCondition> faxNumberConditions = new LinkedList<EntityCondition>(); faxNumberConditions.add(EntityCondition.makeCondition("contactMechTypeId", EntityOperator.EQUALS, "TELECOM_NUMBER")); faxNumberConditions.add(EntityCondition.makeCondition("contactMechPurposeTypeId", EntityOperator.EQUALS, "FAX_NUMBER")); faxNumberConditions.add(EntityCondition.makeCondition("areaCode", EntityOperator.NOT_EQUAL, null)); faxNumberConditions.add(EntityCondition.makeCondition("areaCode", EntityOperator.NOT_EQUAL, "")); faxNumberConditions.add(EntityCondition.makeCondition("contactNumber", EntityOperator.NOT_EQUAL, null)); faxNumberConditions.add(EntityCondition.makeCondition("contactNumber", EntityOperator.NOT_EQUAL, "")); List<GenericValue> faxNumbers = EntityUtil.filterByCondition(partyContactDetails, EntityCondition.makeCondition(faxNumberConditions, EntityOperator.AND)); GenericValue faxNumberValue = EntityUtil.getFirst(faxNumbers); if (! UtilValidate.isEmpty(faxNumberValue)) { faxNumber = faxNumberValue.getString("areaCode") + faxNumberValue.getString("contactNumber"); // Fedex doesn't want the North American country code if (UtilValidate.isNotEmpty(faxNumberValue.getString("countryCode")) && !(countryCode.equals("CA") || countryCode.equals("US"))) { faxNumber = faxNumberValue.getString("countryCode") + faxNumber; } faxNumber = faxNumber.replaceAll("[^+\\d]", ""); } // Get the first valid email address List<EntityCondition> emailConditions = new LinkedList<EntityCondition>(); emailConditions.add(EntityCondition.makeCondition("contactMechTypeId", EntityOperator.EQUALS, "EMAIL_ADDRESS")); emailConditions.add(EntityCondition.makeCondition("infoString", EntityOperator.NOT_EQUAL, null)); emailConditions.add(EntityCondition.makeCondition("infoString", EntityOperator.NOT_EQUAL, "")); List<GenericValue> emailAddresses = EntityUtil.filterByCondition(partyContactDetails, EntityCondition.makeCondition(emailConditions, EntityOperator.AND)); GenericValue emailAddressValue = EntityUtil.getFirst(emailAddresses); if (! UtilValidate.isEmpty(emailAddressValue)) { emailAddress = emailAddressValue.getString("infoString"); } // Get the location of the Freemarker (XML) template for the FDXSubscriptionRequest String templateLocation = getShipmentGatewayConfigValue(delegator, shipmentGatewayConfigId, "templateSubscription", resource, "shipment.fedex.template.subscription.location"); if (UtilValidate.isEmpty(templateLocation)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexSubscriptionTemplateLocationNotFound", UtilMisc.toMap("templateLocation", templateLocation), locale)); } // Populate the Freemarker context Map<String, Object> subscriptionRequestContext = new HashMap<String, Object>(); subscriptionRequestContext.put("AccountNumber", accountNumber); subscriptionRequestContext.put("PersonName", contactPartyName); subscriptionRequestContext.put("CompanyName", companyName); subscriptionRequestContext.put("PhoneNumber", phoneNumber); if (UtilValidate.isNotEmpty(faxNumber)) { subscriptionRequestContext.put("FaxNumber", faxNumber); } if (UtilValidate.isNotEmpty(emailAddress)) { subscriptionRequestContext.put("EMailAddress", emailAddress); } subscriptionRequestContext.put("Line1", postalAddress.getString("address1")); if (UtilValidate.isNotEmpty(postalAddress.getString("address2"))) { subscriptionRequestContext.put("Line2", postalAddress.getString("address2")); } subscriptionRequestContext.put("City", postalAddress.getString("city")); if (UtilValidate.isNotEmpty(stateOrProvinceCode)) { subscriptionRequestContext.put("StateOrProvinceCode", stateOrProvinceCode); } subscriptionRequestContext.put("PostalCode", postalAddress.getString("postalCode")); subscriptionRequestContext.put("CountryCode", countryCode); StringWriter outWriter = new StringWriter(); try { FreeMarkerWorker.renderTemplate(templateLocation, subscriptionRequestContext, outWriter); } catch (Exception e) { String errorMessage = "Cannot send Fedex subscription request: Failed to render Fedex XML Subscription Request Template [" + templateLocation + "]."; Debug.logError(e, errorMessage, module); return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexSubscriptionTemplateError", UtilMisc.toMap("templateLocation", templateLocation, "errorString", e.getMessage()), locale)); } String fDXSubscriptionRequestString = outWriter.toString(); // Send the request String fDXSubscriptionReplyString = null; try { fDXSubscriptionReplyString = sendFedexRequest(fDXSubscriptionRequestString, delegator, shipmentGatewayConfigId, resource, locale); Debug.logInfo("Fedex response for FDXSubscriptionRequest:" + fDXSubscriptionReplyString, module); } catch (FedexConnectException e) { String errorMessage = "Error sending Fedex request for FDXSubscriptionRequest: " + e.toString(); Debug.logError(e, errorMessage, module); return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexSubscriptionTemplateSendingError", UtilMisc.toMap("errorString", e.toString()), locale)); } Document fDXSubscriptionReplyDocument = null; try { fDXSubscriptionReplyDocument = UtilXml.readXmlDocument(fDXSubscriptionReplyString, false); Debug.logInfo("Fedex response for FDXSubscriptionRequest:" + fDXSubscriptionReplyString, module); } catch (Exception e) { String errorMessage = "Error parsing the FDXSubscriptionRequest response: " + e.toString(); Debug.logError(e, errorMessage, module); return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexSubscriptionTemplateParsingError", UtilMisc.toMap("errorString", e.toString()), locale)); } Element fedexSubscriptionReplyElement = fDXSubscriptionReplyDocument.getDocumentElement(); handleErrors(fedexSubscriptionReplyElement, errorList, locale); if (UtilValidate.isNotEmpty(errorList)) { return ServiceUtil.returnError(errorList); } String meterNumber = UtilXml.childElementValue(fedexSubscriptionReplyElement, "MeterNumber"); result.put("meterNumber", meterNumber); } catch (GenericEntityException e) { Debug.logError(e, module); return ServiceUtil.returnError(e.getMessage()); } return result; } /** * * Send a FDXShipRequest via the Ship Manager Direct API */ public static Map<String, Object> fedexShipRequest(DispatchContext dctx, Map<String, ? extends Object> context) { Delegator delegator = dctx.getDelegator(); LocalDispatcher dispatcher = dctx.getDispatcher(); Locale locale = (Locale) context.get("locale"); String shipmentId = (String) context.get("shipmentId"); String shipmentRouteSegmentId = (String) context.get("shipmentRouteSegmentId"); Map<String, Object> shipmentGatewayConfig = ShipmentServices.getShipmentGatewayConfigFromShipment(delegator, shipmentId, locale); String shipmentGatewayConfigId = (String) shipmentGatewayConfig.get("shipmentGatewayConfigId"); String resource = (String) shipmentGatewayConfig.get("configProps"); if (UtilValidate.isEmpty(shipmentGatewayConfigId) && UtilValidate.isEmpty(resource)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexGatewayNotAvailable", locale)); } // Get the location of the Freemarker (XML) template for the FDXShipRequest String templateLocation = getShipmentGatewayConfigValue(delegator, shipmentGatewayConfigId, "templateShipment", resource, "shipment.fedex.template.ship.location"); if (UtilValidate.isEmpty(templateLocation)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexShipmentTemplateLocationNotFound", UtilMisc.toMap("templateLocation", templateLocation), locale)); } // Get the Fedex account number String accountNumber = getShipmentGatewayConfigValue(delegator, shipmentGatewayConfigId, "accessAccountNbr", resource, "shipment.fedex.access.accountNbr"); if (UtilValidate.isEmpty(accountNumber)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexAccountNumberNotFound", locale)); } // Get the Fedex meter number String meterNumber = getShipmentGatewayConfigValue(delegator, shipmentGatewayConfigId, "accessMeterNumber", resource, "shipment.fedex.access.meterNumber"); if (UtilValidate.isEmpty(meterNumber)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexMeterNumberNotFound", UtilMisc.toMap("meterNumber", meterNumber), locale)); } // Get the weight units to be used in the request String weightUomId = EntityUtilProperties.getPropertyValue(shipmentPropertiesFile, "shipment.default.weight.uom", delegator); if (UtilValidate.isEmpty(weightUomId)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentDefaultWeightUomIdNotFound", locale)); } else if (! ("WT_lb".equals(weightUomId) || "WT_kg".equals(weightUomId))) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentDefaultWeightUomIdNotValid", locale)); } // Get the dimension units to be used in the request String dimensionsUomId = EntityUtilProperties.getPropertyValue(shipmentPropertiesFile, "shipment.default.dimension.uom", delegator); if (UtilValidate.isEmpty(dimensionsUomId)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentDefaultDimensionUomIdNotFound", locale)); } else if (! ("LEN_in".equals(dimensionsUomId) || "LEN_cm".equals(dimensionsUomId))) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentDefaultDimensionUomIdNotValid", locale)); } // Get the label image type to be returned String labelImageType = getShipmentGatewayConfigValue(delegator, shipmentGatewayConfigId, "labelImageType", resource, "shipment.fedex.labelImageType"); if (UtilValidate.isEmpty(labelImageType)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexLabelImageTypeNotFound", locale)); } else if (! ("PDF".equals(labelImageType) || "PNG".equals(labelImageType))) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexLabelImageTypeNotValid", locale)); } // Get the default dropoff type String dropoffType = getShipmentGatewayConfigValue(delegator, shipmentGatewayConfigId, "defaultDropoffType", resource, "shipment.fedex.default.dropoffType"); if (UtilValidate.isEmpty(dropoffType)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexDropoffTypeNotFound", locale)); } try { Map<String, Object> shipRequestContext = new HashMap<String, Object>(); // Get the shipment and the shipmentRouteSegment GenericValue shipment = EntityQuery.use(delegator).from("Shipment").where("shipmentId", shipmentId).queryOne(); if (UtilValidate.isEmpty(shipment)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "ProductShipmentNotFoundId", locale) + shipmentId); } GenericValue shipmentRouteSegment = EntityQuery.use(delegator).from("ShipmentRouteSegment").where("shipmentId", shipmentId, "shipmentRouteSegmentId", shipmentRouteSegmentId).queryOne(); if (UtilValidate.isEmpty(shipmentRouteSegment)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "ProductShipmentRouteSegmentNotFound", UtilMisc.toMap("shipmentId", shipmentId, "shipmentRouteSegmentId", shipmentRouteSegmentId), locale)); } // Determine the Fedex carrier String carrierPartyId = shipmentRouteSegment.getString("carrierPartyId"); if (! "FEDEX".equals(carrierPartyId)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexNotRouteSegmentCarrier", UtilMisc.toMap("shipmentRouteSegmentId", shipmentRouteSegmentId, "shipmentId", shipmentId), locale)); } // Check the shipmentRouteSegment's carrier status if (UtilValidate.isNotEmpty(shipmentRouteSegment.getString("carrierServiceStatusId")) && !"SHRSCS_NOT_STARTED".equals(shipmentRouteSegment.getString("carrierServiceStatusId"))) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexRouteSegmentStatusNotStarted", UtilMisc.toMap("shipmentRouteSegmentId", shipmentRouteSegmentId, "shipmentId", shipmentId, "shipmentRouteSegmentStatus", shipmentRouteSegment.getString("carrierServiceStatusId")), locale)); } // Translate shipmentMethodTypeId to Fedex service code and carrier code String shipmentMethodTypeId = shipmentRouteSegment.getString("shipmentMethodTypeId"); GenericValue carrierShipmentMethod = EntityQuery.use(delegator).from("CarrierShipmentMethod").where("shipmentMethodTypeId", shipmentMethodTypeId, "partyId", "FEDEX", "roleTypeId", "CARRIER").queryOne(); if (UtilValidate.isEmpty(carrierShipmentMethod)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexRouteSegmentCarrierShipmentMethodNotFound", UtilMisc.toMap("shipmentId", shipmentId, "shipmentRouteSegmentId", shipmentRouteSegmentId, "carrierPartyId", carrierPartyId, "shipmentMethodTypeId", shipmentMethodTypeId), locale)); } if (UtilValidate.isEmpty(carrierShipmentMethod.getString("carrierServiceCode"))) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexNoCarrieServiceCode", UtilMisc.toMap("shipmentMethodTypeId", shipmentMethodTypeId), locale)); } String service = carrierShipmentMethod.getString("carrierServiceCode"); // CarrierCode is FDXG only for FEDEXGROUND and GROUNDHOMEDELIVERY services. boolean isGroundService = service.equals("FEDEXGROUND") || service.equals("GROUNDHOMEDELIVERY"); String carrierCode = isGroundService ? "FDXG" : "FDXE"; // Determine the currency by trying the shipmentRouteSegment, then the Shipment, then the framework's default currency, and finally default to USD String currencyCode = null; if (UtilValidate.isNotEmpty(shipmentRouteSegment.getString("currencyUomId"))) { currencyCode = shipmentRouteSegment.getString("currencyUomId"); } else if (UtilValidate.isNotEmpty(shipmentRouteSegment.getString("currencyUomId"))) { currencyCode = shipment.getString("currencyUomId"); } else { currencyCode = EntityUtilProperties.getPropertyValue("general", "currency.uom.id.default", "USD", delegator); } // Get and validate origin postal address GenericValue originPostalAddress = shipmentRouteSegment.getRelatedOne("OriginPostalAddress", false); if (UtilValidate.isEmpty(originPostalAddress)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentRouteSegmentOriginPostalAddressNotFound", UtilMisc.toMap("shipmentId", shipmentId, "shipmentRouteSegmentId", shipmentRouteSegmentId), locale)); } else if (UtilValidate.isEmpty(originPostalAddress.getString("address1")) || UtilValidate.isEmpty(originPostalAddress.getString("city")) || UtilValidate.isEmpty(originPostalAddress.getString("postalCode")) || UtilValidate.isEmpty(originPostalAddress.getString("countryGeoId"))) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentRouteSegmentOriginPostalAddressNotComplete", UtilMisc.toMap("shipmentId", shipmentId, "shipmentRouteSegmentId", shipmentRouteSegmentId), locale)); } GenericValue originCountryGeo = originPostalAddress.getRelatedOne("CountryGeo", false); if (UtilValidate.isEmpty(originCountryGeo)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentRouteSegmentOriginCountryGeoNotFound", UtilMisc.toMap("shipmentId", shipmentId, "shipmentRouteSegmentId", shipmentRouteSegmentId), locale)); } String originAddressCountryCode = originCountryGeo.getString("geoCode"); String originAddressStateOrProvinceCode = null; // Only add the StateOrProvinceCode element if the address is in USA or Canada if (originAddressCountryCode.equals("CA") || originAddressCountryCode.equals("US")) { if (UtilValidate.isEmpty(originPostalAddress.getString("stateProvinceGeoId"))) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentRouteSegmentOriginStateProvinceGeoIdRequired", UtilMisc.toMap("contactMechId", originPostalAddress.getString("contactMechId"), "shipmentId", shipmentId, "shipmentRouteSegmentId", shipmentRouteSegmentId), locale)); } GenericValue stateProvinceGeo = EntityQuery.use(delegator).from("Geo").where("geoId", originPostalAddress.getString("stateProvinceGeoId")).cache().queryOne(); originAddressStateOrProvinceCode = stateProvinceGeo.getString("geoCode"); } // Get and validate origin telecom number GenericValue originTelecomNumber = shipmentRouteSegment.getRelatedOne("OriginTelecomNumber", false); if (UtilValidate.isEmpty(originTelecomNumber)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentRouteSegmentOriginTelecomNumberNotFound", UtilMisc.toMap("shipmentId", shipmentId, "shipmentRouteSegmentId", shipmentRouteSegmentId), locale)); } String originContactPhoneNumber = originTelecomNumber.getString("areaCode") + originTelecomNumber.getString("contactNumber"); // Fedex doesn't want the North American country code if (UtilValidate.isNotEmpty(originTelecomNumber.getString("countryCode")) && !(originAddressCountryCode.equals("CA") || originAddressCountryCode.equals("US"))) { originContactPhoneNumber = originTelecomNumber.getString("countryCode") + originContactPhoneNumber; } originContactPhoneNumber = originContactPhoneNumber.replaceAll("[^+\\d]", ""); // Get the origin contact name from the owner of the origin facility GenericValue partyFrom = null; GenericValue originFacility = shipment.getRelatedOne("OriginFacility", false); if (UtilValidate.isEmpty(originFacility)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexOriginFacilityRequired", UtilMisc.toMap("shipmentId", shipmentId, "shipmentRouteSegmentId", shipmentRouteSegmentId), locale)); } else { partyFrom = originFacility.getRelatedOne("OwnerParty", false); if (UtilValidate.isEmpty(partyFrom)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexOwnerPartyRequired", UtilMisc.toMap("shipmentId", shipmentId, "shipmentRouteSegmentId", shipmentRouteSegmentId, "facilityId", originFacility.getString("facilityId")), locale)); } } String originContactKey = "PERSON".equals(partyFrom.getString("partyTypeId")) ? "OriginContactPersonName" : "OriginContactCompanyName"; String originContactName = PartyHelper.getPartyName(partyFrom, false); if (UtilValidate.isEmpty(originContactName)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexPartyFromHasNoName", UtilMisc.toMap("shipmentId", shipmentId, "shipmentRouteSegmentId", shipmentRouteSegmentId), locale)); } // Get and validate destination postal address GenericValue destinationPostalAddress = shipmentRouteSegment.getRelatedOne("DestPostalAddress", false); if (UtilValidate.isEmpty(destinationPostalAddress)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentRouteSegmentDestPostalAddressNotFound", UtilMisc.toMap("shipmentId", shipmentId, "shipmentRouteSegmentId", shipmentRouteSegmentId), locale)); } else if (UtilValidate.isEmpty(destinationPostalAddress.getString("address1")) || UtilValidate.isEmpty(destinationPostalAddress.getString("city")) || UtilValidate.isEmpty(destinationPostalAddress.getString("postalCode")) || UtilValidate.isEmpty(destinationPostalAddress.getString("countryGeoId"))) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentRouteSegmentDestPostalAddressIncomplete", UtilMisc.toMap("shipmentId", shipmentId, "shipmentRouteSegmentId", shipmentRouteSegmentId), locale)); } GenericValue destinationCountryGeo = destinationPostalAddress.getRelatedOne("CountryGeo", false); if (UtilValidate.isEmpty(destinationCountryGeo)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentRouteSegmentDestCountryGeoNotFound", UtilMisc.toMap("shipmentId", shipmentId, "shipmentRouteSegmentId", shipmentRouteSegmentId), locale)); } String destinationAddressCountryCode = destinationCountryGeo.getString("geoCode"); String destinationAddressStateOrProvinceCode = null; // Only add the StateOrProvinceCode element if the address is in USA or Canada if (destinationAddressCountryCode.equals("CA") || destinationAddressCountryCode.equals("US")) { if (UtilValidate.isEmpty(destinationPostalAddress.getString("stateProvinceGeoId"))) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentRouteSegmentDestStateProvinceGeoIdNotFound", UtilMisc.toMap("contactMechId", destinationPostalAddress.getString("contactMechId"), "shipmentId", shipmentId, "shipmentRouteSegmentId", shipmentRouteSegmentId), locale)); } GenericValue stateProvinceGeo = EntityQuery.use(delegator).from("Geo").where("geoId", destinationPostalAddress.getString("stateProvinceGeoId")).cache().queryOne(); destinationAddressStateOrProvinceCode = stateProvinceGeo.getString("geoCode"); } // Get and validate destination telecom number GenericValue destinationTelecomNumber = shipmentRouteSegment.getRelatedOne("DestTelecomNumber", false); if (UtilValidate.isEmpty(destinationTelecomNumber)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentRouteSegmentDestTelecomNumberNotFound", UtilMisc.toMap("shipmentId", shipmentId, "shipmentRouteSegmentId", shipmentRouteSegmentId), locale)); } String destinationContactPhoneNumber = destinationTelecomNumber.getString("areaCode") + destinationTelecomNumber.getString("contactNumber"); // Fedex doesn't want the North American country code if (UtilValidate.isNotEmpty(destinationTelecomNumber.getString("countryCode")) && !(destinationAddressCountryCode.equals("CA") || destinationAddressCountryCode.equals("US"))) { destinationContactPhoneNumber = destinationTelecomNumber.getString("countryCode") + destinationContactPhoneNumber; } destinationContactPhoneNumber = destinationContactPhoneNumber.replaceAll("[^+\\d]", ""); // Get the destination contact name String destinationPartyId = shipment.getString("partyIdTo"); if (UtilValidate.isEmpty(destinationPartyId)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexPartyToRequired", UtilMisc.toMap("shipmentId", shipmentId, "shipmentRouteSegmentId", shipmentRouteSegmentId), locale)); } GenericValue partyTo = EntityQuery.use(delegator).from("Party").where("partyId", destinationPartyId).queryOne(); String destinationContactKey = "PERSON".equals(partyTo.getString("partyTypeId")) ? "DestinationContactPersonName" : "DestinationContactCompanyName"; String destinationContactName = PartyHelper.getPartyName(partyTo, false); if (UtilValidate.isEmpty(destinationContactName)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexPartyToHasNoName", UtilMisc.toMap("shipmentId", shipmentId, "shipmentRouteSegmentId", shipmentRouteSegmentId), locale)); } String homeDeliveryType = null; Timestamp homeDeliveryDate = null; if ("GROUNDHOMEDELIVERY".equals(service)) { // Determine the home-delivery instructions homeDeliveryType = shipmentRouteSegment.getString("homeDeliveryType"); if (UtilValidate.isNotEmpty(homeDeliveryType)) { if (! (homeDeliveryType.equals("DATECERTAIN") || homeDeliveryType.equals("EVENING") || homeDeliveryType.equals("APPOINTMENT"))) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexHomeDeliveryTypeInvalid", UtilMisc.toMap("shipmentId", shipmentId, "shipmentRouteSegmentId", shipmentRouteSegmentId), locale)); } } homeDeliveryDate = shipmentRouteSegment.getTimestamp("homeDeliveryDate"); if (UtilValidate.isEmpty(homeDeliveryDate)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexHomeDeliveryDateRequired", UtilMisc.toMap("shipmentId", shipmentId, "shipmentRouteSegmentId", shipmentRouteSegmentId), locale)); } else if (homeDeliveryDate.before(UtilDateTime.nowTimestamp())) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexHomeDeliveryDateBeforeCurrentDate", UtilMisc.toMap("shipmentId", shipmentId, "shipmentRouteSegmentId", shipmentRouteSegmentId), locale)); } } List<GenericValue> shipmentPackageRouteSegs = shipmentRouteSegment.getRelated("ShipmentPackageRouteSeg", null, UtilMisc.toList("+shipmentPackageSeqId"), false); if (UtilValidate.isEmpty(shipmentPackageRouteSegs)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentPackageRouteSegsNotFound", UtilMisc.toMap("shipmentId", shipmentId, "shipmentRouteSegmentId", shipmentRouteSegmentId), locale)); } if (shipmentPackageRouteSegs.size() != 1) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexMultiplePackagesNotSupported", locale)); } // TODO: Multi-piece shipments, including logic to cancel packages 1-n if FDXShipRequest n+1 fails // Populate the Freemarker context with the non-package-related information shipRequestContext.put("AccountNumber", accountNumber); shipRequestContext.put("MeterNumber", meterNumber); shipRequestContext.put("CarrierCode", carrierCode); shipRequestContext.put("ShipDate", UtilDateTime.nowTimestamp()); shipRequestContext.put("ShipTime", UtilDateTime.nowTimestamp()); shipRequestContext.put("DropoffType", dropoffType); shipRequestContext.put("Service", service); shipRequestContext.put("WeightUnits", weightUomId.equals("WT_kg") ? "KGS" : "LBS"); shipRequestContext.put("CurrencyCode", currencyCode); shipRequestContext.put("PayorType", "SENDER"); shipRequestContext.put(originContactKey, originContactName); shipRequestContext.put("OriginContactPhoneNumber", originContactPhoneNumber); shipRequestContext.put("OriginAddressLine1", originPostalAddress.getString("address1")); if (UtilValidate.isNotEmpty(originPostalAddress.getString("address2"))) { shipRequestContext.put("OriginAddressLine2", originPostalAddress.getString("address2")); } shipRequestContext.put("OriginAddressCity", originPostalAddress.getString("city")); if (UtilValidate.isNotEmpty(originAddressStateOrProvinceCode)) { shipRequestContext.put("OriginAddressStateOrProvinceCode", originAddressStateOrProvinceCode); } shipRequestContext.put("OriginAddressPostalCode", originPostalAddress.getString("postalCode")); shipRequestContext.put("OriginAddressCountryCode", originAddressCountryCode); shipRequestContext.put(destinationContactKey, destinationContactName); shipRequestContext.put("DestinationContactPhoneNumber", destinationContactPhoneNumber); shipRequestContext.put("DestinationAddressLine1", destinationPostalAddress.getString("address1")); if (UtilValidate.isNotEmpty(destinationPostalAddress.getString("address2"))) { shipRequestContext.put("DestinationAddressLine2", destinationPostalAddress.getString("address2")); } shipRequestContext.put("DestinationAddressCity", destinationPostalAddress.getString("city")); if (UtilValidate.isNotEmpty(destinationAddressStateOrProvinceCode)) { shipRequestContext.put("DestinationAddressStateOrProvinceCode", destinationAddressStateOrProvinceCode); } shipRequestContext.put("DestinationAddressPostalCode", destinationPostalAddress.getString("postalCode")); shipRequestContext.put("DestinationAddressCountryCode", destinationAddressCountryCode); shipRequestContext.put("LabelType", "2DCOMMON"); // Required type for FDXShipRequest. Not directly in the FTL because it shouldn't be changed. shipRequestContext.put("LabelImageType", labelImageType); if (UtilValidate.isNotEmpty(homeDeliveryType)) { shipRequestContext.put("HomeDeliveryType", homeDeliveryType); } if (homeDeliveryDate != null) { shipRequestContext.put("HomeDeliveryDate", homeDeliveryDate); } // Get the weight from the ShipmentRouteSegment first, which overrides all later weight computations boolean hasBillingWeight = false; BigDecimal billingWeight = shipmentRouteSegment.getBigDecimal("billingWeight"); String billingWeightUomId = shipmentRouteSegment.getString("billingWeightUomId"); if ((billingWeight != null) && (billingWeight.compareTo(BigDecimal.ZERO) > 0)) { hasBillingWeight = true; if (billingWeightUomId == null) { Debug.logWarning("Shipment Route Segment missing billingWeightUomId in shipmentId " + shipmentId + ", assuming default shipment.fedex.weightUomId of " + weightUomId + " from " + shipmentPropertiesFile, module); billingWeightUomId = weightUomId; } // Convert the weight if necessary if (! billingWeightUomId.equals(weightUomId)) { Map<String, Object> results = dispatcher.runSync("convertUom", UtilMisc.<String, Object>toMap("uomId", billingWeightUomId, "uomIdTo", weightUomId, "originalValue", billingWeight)); if (ServiceUtil.isError(results) || (results.get("convertedValue") == null)) { Debug.logWarning("Unable to convert billing weights for shipmentId " + shipmentId , module); // Try getting the weight from package instead hasBillingWeight = false; } else { billingWeight = (BigDecimal) results.get("convertedValue"); } } } // Loop through Shipment segments (NOTE: only one supported, loop is here for future refactoring reference) for (GenericValue shipmentPackageRouteSeg: shipmentPackageRouteSegs) { GenericValue shipmentPackage = shipmentPackageRouteSeg.getRelatedOne("ShipmentPackage", false); GenericValue shipmentBoxType = shipmentPackage.getRelatedOne("ShipmentBoxType", false); // FedEx requires the packaging type String packaging = null; if (UtilValidate.isEmpty(shipmentBoxType)) { packaging = getShipmentGatewayConfigValue(delegator, shipmentGatewayConfigId, "defaultPackagingType", resource, "shipment.fedex.default.packagingType"); if (UtilValidate.isEmpty(packaging)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexPackingTypeNotConfigured", UtilMisc.toMap("shipmentPackageSeqId", shipmentPackage.getString("shipmentPackageSeqId"), "shipmentId", shipmentId), locale)); } Debug.logWarning("Package " + shipmentPackage.getString("shipmentPackageSeqId") + " of shipment " + shipmentId + " has no packaging type set - defaulting to " + packaging, module); } else { packaging = shipmentBoxType.getString("shipmentBoxTypeId"); } // Make sure that the packaging type is valid for FedEx GenericValue carrierShipmentBoxType = EntityQuery.use(delegator).from("CarrierShipmentBoxType").where("partyId", "FEDEX", "shipmentBoxTypeId", packaging).queryOne(); if (UtilValidate.isEmpty(carrierShipmentBoxType)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexPackingTypeInvalid", UtilMisc.toMap("shipmentPackageSeqId", shipmentPackage.getString("shipmentPackageSeqId"), "shipmentId", shipmentId), locale)); } else if (UtilValidate.isEmpty(carrierShipmentBoxType.getString("packagingTypeCode"))) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexPackingTypeMissing", UtilMisc.toMap("shipmentPackageSeqId", shipmentPackage.getString("shipmentPackageSeqId"), "shipmentId", shipmentId), locale)); } packaging = carrierShipmentBoxType.getString("packagingTypeCode"); // Determine the dimensions of the package BigDecimal dimensionsLength = null; BigDecimal dimensionsWidth = null; BigDecimal dimensionsHeight = null; if (shipmentBoxType != null) { dimensionsLength = shipmentBoxType.getBigDecimal("boxLength"); dimensionsWidth = shipmentBoxType.getBigDecimal("boxWidth"); dimensionsHeight = shipmentBoxType.getBigDecimal("boxHeight"); String boxDimensionsUomId = null; GenericValue boxDimensionsUom = shipmentBoxType.getRelatedOne("DimensionUom", false); if (! UtilValidate.isEmpty(boxDimensionsUom)) { boxDimensionsUomId = boxDimensionsUom.getString("uomId"); } else { Debug.logWarning("Packaging type for package " + shipmentPackage.getString("shipmentPackageSeqId") + " of shipmentRouteSegment " + shipmentRouteSegmentId + " of shipment " + shipmentId + " is missing dimensionUomId, assuming default shipment.default.dimension.uom of " + dimensionsUomId + " from " + shipmentPropertiesFile, module); boxDimensionsUomId = dimensionsUomId; } if (dimensionsLength != null && dimensionsLength.compareTo(BigDecimal.ZERO) > 0) { if (! boxDimensionsUomId.equals(dimensionsUomId)) { Map<String, Object> results = dispatcher.runSync("convertUom", UtilMisc.<String, Object>toMap("uomId", boxDimensionsUomId, "uomIdTo", dimensionsUomId, "originalValue", dimensionsLength)); if (ServiceUtil.isError(results) || (results.get("convertedValue") == null)) { Debug.logWarning("Unable to convert length for package " + shipmentPackage.getString("shipmentPackageSeqId") + " of shipmentRouteSegment " + shipmentRouteSegmentId + " of shipment " + shipmentId , module); dimensionsLength = null; } else { dimensionsLength = (BigDecimal) results.get("convertedValue"); } } } if (dimensionsWidth != null && dimensionsWidth.compareTo(BigDecimal.ZERO) > 0) { if (! boxDimensionsUomId.equals(dimensionsUomId)) { Map<String, Object> results = dispatcher.runSync("convertUom", UtilMisc.<String, Object>toMap("uomId", boxDimensionsUomId, "uomIdTo", dimensionsUomId, "originalValue", dimensionsWidth)); if (ServiceUtil.isError(results) || (results.get("convertedValue") == null)) { Debug.logWarning("Unable to convert width for package " + shipmentPackage.getString("shipmentPackageSeqId") + " of shipmentRouteSegment " + shipmentRouteSegmentId + " of shipment " + shipmentId , module); dimensionsWidth = null; } else { dimensionsWidth = (BigDecimal) results.get("convertedValue"); } } } if (dimensionsHeight != null && dimensionsHeight.compareTo(BigDecimal.ZERO) > 0) { if (! boxDimensionsUomId.equals(dimensionsUomId)) { Map<String, Object> results = dispatcher.runSync("convertUom", UtilMisc.<String, Object>toMap("uomId", boxDimensionsUomId, "uomIdTo", dimensionsUomId, "originalValue", dimensionsHeight)); if (ServiceUtil.isError(results) || (results.get("convertedValue") == null)) { Debug.logWarning("Unable to convert height for package " + shipmentPackage.getString("shipmentPackageSeqId") + " of shipmentRouteSegment " + shipmentRouteSegmentId + " of shipment " + shipmentId , module); dimensionsHeight = null; } else { dimensionsHeight = (BigDecimal) results.get("convertedValue"); } } } } // Determine the package weight (possibly overriden by route segment billing weight) BigDecimal packageWeight = null; if (! hasBillingWeight) { if (UtilValidate.isNotEmpty(shipmentPackage.getString("weight"))) { packageWeight = shipmentPackage.getBigDecimal("weight"); } else { // Use default weight if available try { packageWeight = EntityUtilProperties.getPropertyAsBigDecimal(shipmentPropertiesFile, "shipment.default.weight.value", BigDecimal.ZERO); } catch (NumberFormatException ne) { Debug.logWarning("Default shippable weight not configured (shipment.default.weight.value), assuming 1.0" + weightUomId , module); packageWeight = BigDecimal.ONE; } } // Convert weight if necessary String packageWeightUomId = shipmentPackage.getString("weightUomId"); if (UtilValidate.isEmpty(packageWeightUomId)) { Debug.logWarning("Shipment Route Segment missing weightUomId in shipmentId " + shipmentId + ", assuming shipment.default.weight.uom of " + weightUomId + " from " + shipmentPropertiesFile, module); packageWeightUomId = weightUomId; } if (! packageWeightUomId.equals(weightUomId)) { Map<String, Object> results = dispatcher.runSync("convertUom", UtilMisc.<String, Object>toMap("uomId", packageWeightUomId, "uomIdTo", weightUomId, "originalValue", packageWeight)); if (ServiceUtil.isError(results) || (results.get("convertedValue") == null)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexWeightOfPackageCannotBeConverted", UtilMisc.toMap("shipmentPackageSeqId", shipmentPackage.getString("shipmentPackageSeqId"), "shipmentRouteSegmentId", shipmentRouteSegmentId, "shipmentId", shipmentId), locale)); } else { packageWeight = (BigDecimal) results.get("convertedValue"); } } } BigDecimal weight = hasBillingWeight ? billingWeight : packageWeight; if (weight == null || weight.compareTo(BigDecimal.ZERO) < 0) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexWeightOfPackageNotAvailable", UtilMisc.toMap("shipmentPackageSeqId", shipmentPackage.getString("shipmentPackageSeqId"), "shipmentRouteSegmentId", shipmentRouteSegmentId, "shipmentId", shipmentId), locale)); } // Populate the Freemarker context with package-related information shipRequestContext.put("CustomerReference", shipmentId + ":" + shipmentRouteSegmentId + ":" + shipmentPackage.getString("shipmentPackageSeqId")); shipRequestContext.put("DropoffType", dropoffType); shipRequestContext.put("Packaging", packaging); if (UtilValidate.isNotEmpty(dimensionsUomId) && dimensionsLength != null && dimensionsLength.setScale(0, BigDecimal.ROUND_HALF_UP).compareTo(BigDecimal.ZERO) > 0 && dimensionsWidth != null && dimensionsWidth.setScale(0, BigDecimal.ROUND_HALF_UP).compareTo(BigDecimal.ZERO) > 0 && dimensionsHeight != null && dimensionsHeight.setScale(0, BigDecimal.ROUND_HALF_UP).compareTo(BigDecimal.ZERO) > 0) { shipRequestContext.put("DimensionsUnits", dimensionsUomId.equals("LEN_in") ? "IN" : "CM"); shipRequestContext.put("DimensionsLength", dimensionsLength.setScale(0, BigDecimal.ROUND_HALF_UP).toString()); shipRequestContext.put("DimensionsWidth", dimensionsWidth.setScale(0, BigDecimal.ROUND_HALF_UP).toString()); shipRequestContext.put("DimensionsHeight", dimensionsHeight.setScale(0, BigDecimal.ROUND_HALF_UP).toString()); } shipRequestContext.put("Weight", weight.setScale(1, BigDecimal.ROUND_UP).toString()); } StringWriter outWriter = new StringWriter(); try { FreeMarkerWorker.renderTemplate(templateLocation, shipRequestContext, outWriter); } catch (Exception e) { String errorMessage = "Cannot confirm Fedex shipment: Failed to render Fedex XML Ship Request Template [" + templateLocation + "]."; Debug.logError(e, errorMessage, module); return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexShipmentTemplateError", UtilMisc.toMap("templateLocation", templateLocation, "errorString", e.getMessage()), locale)); } // Pass the request string to the sending method String fDXShipRequestString = outWriter.toString(); String fDXShipReplyString = null; try { fDXShipReplyString = sendFedexRequest(fDXShipRequestString, delegator, shipmentGatewayConfigId, resource, locale); if (Debug.verboseOn()) { Debug.logVerbose(fDXShipReplyString, module); } } catch (FedexConnectException e) { String errorMessage = "Error sending Fedex request for FDXShipRequest: "; Debug.logError(e, errorMessage, module); return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexShipmentTemplateSendingError", UtilMisc.toMap("errorString", e.toString()), locale)); } // Pass the reply to the handler method return handleFedexShipReply(fDXShipReplyString, shipmentRouteSegment, shipmentPackageRouteSegs, locale); } catch (GenericEntityException e) { Debug.logError(e, module); return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexShipmentTemplateServiceError", UtilMisc.toMap("errorString", e.toString()), locale)); } catch (GenericServiceException se) { Debug.logError(se, module); return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexShipmentTemplateServiceError", UtilMisc.toMap("errorString", se.toString()), locale)); } } /** * Extract the tracking number and shipping label from the FDXShipReply XML string * @param fDXShipReplyString * @param shipmentRouteSegment * @param shipmentPackageRouteSegs * @throws GenericEntityException */ public static Map<String, Object> handleFedexShipReply(String fDXShipReplyString, GenericValue shipmentRouteSegment, List<GenericValue> shipmentPackageRouteSegs, Locale locale) throws GenericEntityException { List<Object> errorList = new LinkedList<Object>(); GenericValue shipmentPackageRouteSeg = shipmentPackageRouteSegs.get(0); Document fdxShipReplyDocument = null; try { fdxShipReplyDocument = UtilXml.readXmlDocument(fDXShipReplyString, false); } catch (Exception e) { String errorMessage = "Error parsing the FDXShipReply: " + e.toString(); Debug.logError(e, errorMessage, module); // TODO Cancel the package } if (UtilValidate.isEmpty(fdxShipReplyDocument)) { return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexShipmentTemplateParsingError", locale)); } // Tracking number: Tracking/TrackingNumber Element rootElement = fdxShipReplyDocument.getDocumentElement(); handleErrors(rootElement, errorList, locale); if (UtilValidate.isNotEmpty(errorList)) { return ServiceUtil.returnError(errorList); } Element trackingElement = UtilXml.firstChildElement(rootElement, "Tracking"); String trackingNumber = UtilXml.childElementValue(trackingElement, "TrackingNumber"); // Label: Labels/OutboundLabel Element labelElement = UtilXml.firstChildElement(rootElement, "Labels"); String encodedImageString = UtilXml.childElementValue(labelElement, "OutboundLabel"); if (UtilValidate.isEmpty(encodedImageString)) { Debug.logError("Cannot find FDXShipReply label. FDXShipReply document is: " + fDXShipReplyString, module); return ServiceUtil.returnError(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexShipmentTemplateLabelNotFound", UtilMisc.toMap("shipmentPackageRouteSeg", shipmentPackageRouteSeg, "fDXShipReplyString", fDXShipReplyString), locale)); } byte[] labelBytes = Base64.base64Decode(encodedImageString.getBytes()); if (labelBytes != null) { // Store in db blob shipmentPackageRouteSeg.setBytes("labelImage", labelBytes); } else { Debug.logInfo("Failed to either decode returned FedEx label or no data found in Labels/OutboundLabel.", module); // TODO: Cancel the package } shipmentPackageRouteSeg.set("trackingCode", trackingNumber); shipmentPackageRouteSeg.set("labelHtml", encodedImageString); shipmentPackageRouteSeg.store(); shipmentRouteSegment.set("trackingIdNumber", trackingNumber); shipmentRouteSegment.put("carrierServiceStatusId", "SHRSCS_CONFIRMED"); shipmentRouteSegment.store(); return ServiceUtil.returnSuccess(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexShipmentConfirmed", locale)); } public static void handleErrors(Element rootElement, List<Object> errorList, Locale locale) { Element errorElement = null; if ("Error".equalsIgnoreCase(rootElement.getNodeName())) { errorElement = rootElement; } else { errorElement = UtilXml.firstChildElement(rootElement, "Error"); } if (UtilValidate.isNotEmpty(errorElement)) { Element errorCodeElement = UtilXml.firstChildElement(errorElement, "Code"); Element errorMessageElement = UtilXml.firstChildElement(errorElement, "Message"); if (errorCodeElement != null || errorMessageElement != null) { String errorCode = UtilXml.childElementValue(errorElement, "Code"); String errorMessage = UtilXml.childElementValue(errorElement, "Message"); if (UtilValidate.isNotEmpty(errorCode) || UtilValidate.isNotEmpty(errorMessage)) { errorList.add(UtilProperties.getMessage(resourceError, "FacilityShipmentFedexErrorMessage", UtilMisc.toMap("errorCode", errorCode, "errorMessage", errorMessage), locale)); } } } } private static String getShipmentGatewayConfigValue(Delegator delegator, String shipmentGatewayConfigId, String shipmentGatewayConfigParameterName, String resource, String parameterName) { String returnValue = ""; if (UtilValidate.isNotEmpty(shipmentGatewayConfigId)) { try { GenericValue fedex = EntityQuery.use(delegator).from("ShipmentGatewayFedex").where("shipmentGatewayConfigId", shipmentGatewayConfigId).queryOne(); if (fedex != null) { Object fedexField = fedex.get(shipmentGatewayConfigParameterName); if (fedexField != null) { returnValue = fedexField.toString().trim(); } } } catch (GenericEntityException e) { Debug.logError(e, module); } } else { String value = EntityUtilProperties.getPropertyValue(resource, parameterName, delegator); if (value != null) { returnValue = value.trim(); } } return returnValue; } private static String getShipmentGatewayConfigValue(Delegator delegator, String shipmentGatewayConfigId, String shipmentGatewayConfigParameterName, String resource, String parameterName, String defaultValue) { String returnValue = getShipmentGatewayConfigValue(delegator, shipmentGatewayConfigId, shipmentGatewayConfigParameterName, resource, parameterName); if (UtilValidate.isEmpty(returnValue)) { returnValue = defaultValue; } return returnValue; } } @SuppressWarnings("serial") class FedexConnectException extends GeneralException { FedexConnectException() { super(); } FedexConnectException(String msg) { super(msg); } FedexConnectException(Throwable t) { super(t); } FedexConnectException(String msg, Throwable t) { super(msg, t); } }