0001: /*******************************************************************************
0002: * Licensed to the Apache Software Foundation (ASF) under one
0003: * or more contributor license agreements. See the NOTICE file
0004: * distributed with this work for additional information
0005: * regarding copyright ownership. The ASF licenses this file
0006: * to you under the Apache License, Version 2.0 (the
0007: * "License"); you may not use this file except in compliance
0008: * with the License. You may obtain a copy of the License at
0009: *
0010: * http://www.apache.org/licenses/LICENSE-2.0
0011: *
0012: * Unless required by applicable law or agreed to in writing,
0013: * software distributed under the License is distributed on an
0014: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
0015: * KIND, either express or implied. See the License for the
0016: * specific language governing permissions and limitations
0017: * under the License.
0018: *******************************************************************************/package org.ofbiz.shipment.thirdparty.fedex;
0019:
0020: import org.ofbiz.base.util.*;
0021: import org.ofbiz.base.util.template.FreeMarkerWorker;
0022: import org.ofbiz.service.DispatchContext;
0023: import org.ofbiz.service.ServiceUtil;
0024: import org.ofbiz.service.LocalDispatcher;
0025: import org.ofbiz.service.GenericServiceException;
0026: import org.ofbiz.entity.GenericValue;
0027: import org.ofbiz.entity.GenericDelegator;
0028: import org.ofbiz.entity.GenericEntityException;
0029: import org.ofbiz.entity.condition.EntityExpr;
0030: import org.ofbiz.entity.condition.EntityOperator;
0031: import org.ofbiz.entity.condition.EntityConditionList;
0032: import org.ofbiz.entity.util.EntityUtil;
0033: import org.ofbiz.party.party.PartyHelper;
0034: import org.w3c.dom.Document;
0035: import org.w3c.dom.Element;
0036: import org.xml.sax.SAXException;
0037:
0038: import javax.xml.parsers.ParserConfigurationException;
0039: import java.util.*;
0040: import java.io.IOException;
0041: import java.io.StringWriter;
0042: import java.math.BigDecimal;
0043: import java.sql.Timestamp;
0044:
0045: /**
0046: * Fedex Shipment Services
0047: *
0048: * Implementation of Fedex shipment interface using Ship Manager Direct API
0049: *
0050: * TODO: FDXShipDeleteRequest/Reply (on error and via service call)
0051: * TODO: FDXCloseRequest/Reply
0052: * TODO: FDXRateRequest/Reply
0053: * TODO: FDXTrackRequest/Reply
0054: * TODO: International shipments
0055: * TODO: Multi-piece shipments
0056: * TODO: Freight shipments
0057: */
0058: public class FedexServices {
0059:
0060: public final static String module = FedexServices.class.getName();
0061: public final static String shipmentPropertiesFile = "shipment.properties";
0062:
0063: /**
0064: * Opens a URL to Fedex and makes a request.
0065: *
0066: * @param xmlString XML message to send
0067: * @return XML string response from FedEx
0068: * @throws FedexConnectException
0069: */
0070: public static String sendFedexRequest(String xmlString)
0071: throws FedexConnectException {
0072: String url = UtilProperties.getPropertyValue(
0073: shipmentPropertiesFile, "shipment.fedex.connect.url");
0074: if (url == null) {
0075: throw new FedexConnectException(
0076: "Incomplete connection URL; check your Fedex configuration");
0077: }
0078:
0079: // xmlString should contain the auth document at the beginning
0080: // all documents require an <?xml version="1.0" encoding="UTF-8" ?> header
0081: if (!xmlString
0082: .matches("^(?s)<\\?xml\\s+version=\"1\\.0\"\\s+encoding=\"UTF-8\"\\s*\\?>.*")) {
0083: throw new FedexConnectException("XML header is malformed");
0084: }
0085:
0086: // prepare the connect string
0087: url = url.trim();
0088:
0089: String timeOutStr = UtilProperties.getPropertyValue(
0090: shipmentPropertiesFile,
0091: "shipment.fedex.connect.timeout", "60");
0092: int timeout = 60;
0093: try {
0094: timeout = Integer.parseInt(timeOutStr);
0095: } catch (NumberFormatException e) {
0096: Debug.logError(e, "Unable to set timeout to " + timeOutStr
0097: + " using default " + timeout);
0098: }
0099:
0100: if (Debug.verboseOn()) {
0101: Debug.logVerbose("Fedex Connect URL : " + url, module);
0102: Debug.logVerbose("Fedex XML String : " + xmlString, module);
0103: }
0104:
0105: HttpClient http = new HttpClient(url);
0106: http.setTimeout(timeout * 1000);
0107: String response = null;
0108: try {
0109: response = http.post(xmlString);
0110: } catch (HttpClientException e) {
0111: Debug.logError(e, "Problem connecting to Fedex server",
0112: module);
0113: throw new FedexConnectException("URL Connection problem", e);
0114: }
0115:
0116: if (response == null) {
0117: throw new FedexConnectException("Received a null response");
0118: }
0119: if (Debug.verboseOn()) {
0120: Debug.logVerbose("Fedex Response : " + response, module);
0121: }
0122:
0123: return response;
0124: }
0125:
0126: /*
0127: * Register a Fedex account for shipping by obtaining the meter number
0128: */
0129: public static Map fedexSubscriptionRequest(DispatchContext dctx,
0130: Map context) {
0131: GenericDelegator delegator = dctx.getDelegator();
0132: List errorList = new ArrayList();
0133:
0134: Boolean replaceMeterNumber = (Boolean) context
0135: .get("replaceMeterNumber");
0136:
0137: if (!replaceMeterNumber.booleanValue()) {
0138: String meterNumber = UtilProperties.getPropertyValue(
0139: shipmentPropertiesFile,
0140: "shipment.fedex.access.meterNumber");
0141: if (UtilValidate.isNotEmpty(meterNumber)) {
0142: return ServiceUtil
0143: .returnError("MeterNumber already exists: "
0144: + shipmentPropertiesFile
0145: + ":shipment.fedex.access.meterNumber="
0146: + meterNumber);
0147: }
0148: }
0149:
0150: String companyPartyId = (String) context.get("companyPartyId");
0151: String contactPartyName = (String) context
0152: .get("contactPartyName");
0153:
0154: Map result = new HashMap();
0155:
0156: String accountNumber = UtilProperties.getPropertyValue(
0157: shipmentPropertiesFile,
0158: "shipment.fedex.access.accountNbr");
0159: if (UtilValidate.isEmpty(accountNumber)) {
0160: return ServiceUtil
0161: .returnError("accountNbr not found for Fedex subscription request.");
0162: }
0163:
0164: if (UtilValidate.isEmpty(contactPartyName)) {
0165: return ServiceUtil
0166: .returnError("Contact name can't be empty.");
0167: }
0168:
0169: String companyName = null;
0170: GenericValue postalAddress = null;
0171: String phoneNumber = null;
0172: String faxNumber = null;
0173: String emailAddress = null;
0174: try {
0175:
0176: // Make sure the company exists
0177: GenericValue companyParty = delegator
0178: .findByPrimaryKeyCache("Party", UtilMisc.toMap(
0179: "partyId", companyPartyId));
0180: if (companyParty == null) {
0181: String errorMessage = "Party with partyId "
0182: + companyPartyId + " does not exist";
0183: Debug.logError(errorMessage, module);
0184: return ServiceUtil.returnError(errorMessage);
0185: }
0186:
0187: // Get the company name (required by Fedex)
0188: companyName = PartyHelper.getPartyName(companyParty);
0189: if (UtilValidate.isEmpty(companyName)) {
0190: String errorMessage = "Party with partyId "
0191: + companyPartyId + " has no name";
0192: Debug.logError(errorMessage, module);
0193: return ServiceUtil.returnError(errorMessage);
0194: }
0195:
0196: // Get the contact information for the company
0197: List partyContactDetails = delegator.findByAnd(
0198: "PartyContactDetailByPurpose", UtilMisc.toMap(
0199: "partyId", companyPartyId));
0200: partyContactDetails = EntityUtil
0201: .filterByDate(partyContactDetails);
0202: partyContactDetails = EntityUtil.filterByDate(
0203: partyContactDetails, UtilDateTime.nowTimestamp(),
0204: "purposeFromDate", "purposeThruDate", true);
0205:
0206: // Get the first valid postal address (address1, city, postalCode and countryGeoId are required by Fedex)
0207: List postalAddressConditions = new ArrayList();
0208: postalAddressConditions.add(new EntityExpr(
0209: "contactMechTypeId", EntityOperator.EQUALS,
0210: "POSTAL_ADDRESS"));
0211: postalAddressConditions.add(new EntityExpr("address1",
0212: EntityOperator.NOT_EQUAL, null));
0213: postalAddressConditions.add(new EntityExpr("address1",
0214: EntityOperator.NOT_EQUAL, ""));
0215: postalAddressConditions.add(new EntityExpr("city",
0216: EntityOperator.NOT_EQUAL, null));
0217: postalAddressConditions.add(new EntityExpr("city",
0218: EntityOperator.NOT_EQUAL, ""));
0219: postalAddressConditions.add(new EntityExpr("postalCode",
0220: EntityOperator.NOT_EQUAL, null));
0221: postalAddressConditions.add(new EntityExpr("postalCode",
0222: EntityOperator.NOT_EQUAL, ""));
0223: postalAddressConditions.add(new EntityExpr("countryGeoId",
0224: EntityOperator.NOT_EQUAL, null));
0225: postalAddressConditions.add(new EntityExpr("countryGeoId",
0226: EntityOperator.NOT_EQUAL, ""));
0227: List postalAddresses = EntityUtil.filterByCondition(
0228: partyContactDetails,
0229: new EntityConditionList(postalAddressConditions,
0230: EntityOperator.AND));
0231:
0232: // Fedex requires USA or Canada addresses to have a state/province ID, so filter out the ones without
0233: postalAddressConditions.clear();
0234: postalAddressConditions.add(new EntityExpr("countryGeoId",
0235: EntityOperator.IN, UtilMisc.toList("CAN", "USA")));
0236: postalAddressConditions.add(new EntityExpr(
0237: "stateProvinceGeoId", EntityOperator.EQUALS, null));
0238: postalAddresses = EntityUtil.filterOutByCondition(
0239: postalAddresses,
0240: new EntityConditionList(postalAddressConditions,
0241: EntityOperator.AND));
0242: postalAddressConditions.clear();
0243: postalAddressConditions.add(new EntityExpr("countryGeoId",
0244: EntityOperator.IN, UtilMisc.toList("CAN", "USA")));
0245: postalAddressConditions.add(new EntityExpr(
0246: "stateProvinceGeoId", EntityOperator.EQUALS, ""));
0247: postalAddresses = EntityUtil.filterOutByCondition(
0248: postalAddresses,
0249: new EntityConditionList(postalAddressConditions,
0250: EntityOperator.AND));
0251:
0252: postalAddress = EntityUtil.getFirst(postalAddresses);
0253: if (UtilValidate.isEmpty(postalAddress)) {
0254: String errorMessage = "Party with partyId "
0255: + companyPartyId
0256: + " does not have a current, fully populated postal address";
0257: Debug.logError(errorMessage, module);
0258: return ServiceUtil.returnError(errorMessage);
0259: }
0260: GenericValue countryGeo = delegator.findByPrimaryKeyCache(
0261: "Geo", UtilMisc.toMap("geoId", postalAddress
0262: .getString("countryGeoId")));
0263: String countryCode = countryGeo.getString("geoCode");
0264: String stateOrProvinceCode = null;
0265: // Only add the StateOrProvinceCode element if the address is in USA or Canada
0266: if (countryCode.equals("CA") || countryCode.equals("US")) {
0267: GenericValue stateProvinceGeo = delegator
0268: .findByPrimaryKeyCache(
0269: "Geo",
0270: UtilMisc
0271: .toMap(
0272: "geoId",
0273: postalAddress
0274: .getString("stateProvinceGeoId")));
0275: stateOrProvinceCode = stateProvinceGeo
0276: .getString("geoCode");
0277: }
0278:
0279: // Get the first valid primary phone number (required by Fedex)
0280: List phoneNumberConditions = new ArrayList();
0281: phoneNumberConditions.add(new EntityExpr(
0282: "contactMechTypeId", EntityOperator.EQUALS,
0283: "TELECOM_NUMBER"));
0284: phoneNumberConditions.add(new EntityExpr(
0285: "contactMechPurposeTypeId", EntityOperator.EQUALS,
0286: "PRIMARY_PHONE"));
0287: phoneNumberConditions.add(new EntityExpr("areaCode",
0288: EntityOperator.NOT_EQUAL, null));
0289: phoneNumberConditions.add(new EntityExpr("areaCode",
0290: EntityOperator.NOT_EQUAL, ""));
0291: phoneNumberConditions.add(new EntityExpr("contactNumber",
0292: EntityOperator.NOT_EQUAL, null));
0293: phoneNumberConditions.add(new EntityExpr("contactNumber",
0294: EntityOperator.NOT_EQUAL, ""));
0295: List phoneNumbers = EntityUtil.filterByCondition(
0296: partyContactDetails, new EntityConditionList(
0297: phoneNumberConditions, EntityOperator.AND));
0298: GenericValue phoneNumberValue = EntityUtil
0299: .getFirst(phoneNumbers);
0300: if (UtilValidate.isEmpty(phoneNumberValue)) {
0301: String errorMessage = "Party with partyId "
0302: + companyPartyId
0303: + " does not have a current, fully populated primary phone number";
0304: Debug.logError(errorMessage, module);
0305: return ServiceUtil.returnError(errorMessage);
0306: }
0307: phoneNumber = phoneNumberValue.getString("areaCode")
0308: + phoneNumberValue.getString("contactNumber");
0309: // Fedex doesn't want the North American country code
0310: if (UtilValidate.isNotEmpty(phoneNumberValue
0311: .getString("countryCode"))
0312: && !(countryCode.equals("CA") || countryCode
0313: .equals("US"))) {
0314: phoneNumber = phoneNumberValue.getString("countryCode")
0315: + phoneNumber;
0316: }
0317: phoneNumber = phoneNumber.replaceAll("[^+\\d]", "");
0318:
0319: // Get the first valid fax number
0320: List faxNumberConditions = new ArrayList();
0321: faxNumberConditions.add(new EntityExpr("contactMechTypeId",
0322: EntityOperator.EQUALS, "TELECOM_NUMBER"));
0323: faxNumberConditions.add(new EntityExpr(
0324: "contactMechPurposeTypeId", EntityOperator.EQUALS,
0325: "FAX_NUMBER"));
0326: faxNumberConditions.add(new EntityExpr("areaCode",
0327: EntityOperator.NOT_EQUAL, null));
0328: faxNumberConditions.add(new EntityExpr("areaCode",
0329: EntityOperator.NOT_EQUAL, ""));
0330: faxNumberConditions.add(new EntityExpr("contactNumber",
0331: EntityOperator.NOT_EQUAL, null));
0332: faxNumberConditions.add(new EntityExpr("contactNumber",
0333: EntityOperator.NOT_EQUAL, ""));
0334: List faxNumbers = EntityUtil.filterByCondition(
0335: partyContactDetails, new EntityConditionList(
0336: faxNumberConditions, EntityOperator.AND));
0337: GenericValue faxNumberValue = EntityUtil
0338: .getFirst(faxNumbers);
0339: if (!UtilValidate.isEmpty(faxNumberValue)) {
0340: faxNumber = faxNumberValue.getString("areaCode")
0341: + faxNumberValue.getString("contactNumber");
0342: // Fedex doesn't want the North American country code
0343: if (UtilValidate.isNotEmpty(faxNumberValue
0344: .getString("countryCode"))
0345: && !(countryCode.equals("CA") || countryCode
0346: .equals("US"))) {
0347: faxNumber = faxNumberValue.getString("countryCode")
0348: + faxNumber;
0349: }
0350: faxNumber = faxNumber.replaceAll("[^+\\d]", "");
0351: }
0352:
0353: // Get the first valid email address
0354: List emailConditions = new ArrayList();
0355: emailConditions.add(new EntityExpr("contactMechTypeId",
0356: EntityOperator.EQUALS, "EMAIL_ADDRESS"));
0357: emailConditions.add(new EntityExpr("infoString",
0358: EntityOperator.NOT_EQUAL, null));
0359: emailConditions.add(new EntityExpr("infoString",
0360: EntityOperator.NOT_EQUAL, ""));
0361: List emailAddresses = EntityUtil.filterByCondition(
0362: partyContactDetails, new EntityConditionList(
0363: emailConditions, EntityOperator.AND));
0364: GenericValue emailAddressValue = EntityUtil
0365: .getFirst(emailAddresses);
0366: if (!UtilValidate.isEmpty(emailAddressValue)) {
0367: emailAddress = emailAddressValue
0368: .getString("infoString");
0369: }
0370:
0371: // Get the location of the Freemarker (XML) template for the FDXSubscriptionRequest
0372: String templateLocation = UtilProperties.getPropertyValue(
0373: shipmentPropertiesFile,
0374: "shipment.template.fedex.subscription.location");
0375: if (UtilValidate.isEmpty(templateLocation)) {
0376: return ServiceUtil
0377: .returnError("Can't find location for FDXSubscriptionRequest template - should be in "
0378: + shipmentPropertiesFile
0379: + ":shipment.template.fedex.subscription.location");
0380: }
0381:
0382: // Populate the Freemarker context
0383: Map subscriptionRequestContext = new HashMap();
0384: subscriptionRequestContext.put("AccountNumber",
0385: accountNumber);
0386: subscriptionRequestContext.put("PersonName",
0387: contactPartyName);
0388: subscriptionRequestContext.put("CompanyName", companyName);
0389: subscriptionRequestContext.put("PhoneNumber", phoneNumber);
0390: if (UtilValidate.isNotEmpty(faxNumber)) {
0391: subscriptionRequestContext.put("FaxNumber", faxNumber);
0392: }
0393: if (UtilValidate.isNotEmpty(emailAddress)) {
0394: subscriptionRequestContext.put("EMailAddress",
0395: emailAddress);
0396: }
0397: subscriptionRequestContext.put("Line1", postalAddress
0398: .getString("address1"));
0399: if (UtilValidate.isNotEmpty(postalAddress
0400: .getString("address2"))) {
0401: subscriptionRequestContext.put("Line2", postalAddress
0402: .getString("address2"));
0403: }
0404: subscriptionRequestContext.put("City", postalAddress
0405: .getString("city"));
0406: if (UtilValidate.isNotEmpty(stateOrProvinceCode)) {
0407: subscriptionRequestContext.put("StateOrProvinceCode",
0408: stateOrProvinceCode);
0409: }
0410: subscriptionRequestContext.put("PostalCode", postalAddress
0411: .getString("postalCode"));
0412: subscriptionRequestContext.put("CountryCode", countryCode);
0413:
0414: StringWriter outWriter = new StringWriter();
0415: try {
0416: FreeMarkerWorker.renderTemplateAtLocation(
0417: templateLocation, subscriptionRequestContext,
0418: outWriter);
0419: } catch (Exception e) {
0420: String errorMessage = "Cannot send Fedex subscription request: Failed to render Fedex XML Subscription Request Template ["
0421: + templateLocation + "].";
0422: Debug.logError(e, errorMessage, module);
0423: return ServiceUtil.returnError(errorMessage + ": "
0424: + e.getMessage());
0425: }
0426: String fDXSubscriptionRequestString = outWriter.toString();
0427:
0428: // Send the request
0429: String fDXSubscriptionReplyString = null;
0430: try {
0431: fDXSubscriptionReplyString = sendFedexRequest(fDXSubscriptionRequestString);
0432: Debug.log("Fedex response for FDXSubscriptionRequest:"
0433: + fDXSubscriptionReplyString);
0434: } catch (FedexConnectException e) {
0435: String errorMessage = "Error sending Fedex request for FDXSubscriptionRequest: "
0436: + e.toString();
0437: Debug.logError(e, errorMessage, module);
0438: return ServiceUtil.returnError(errorMessage);
0439: }
0440:
0441: Document fDXSubscriptionReplyDocument = null;
0442: try {
0443: fDXSubscriptionReplyDocument = UtilXml.readXmlDocument(
0444: fDXSubscriptionReplyString, false);
0445: Debug.log("Fedex response for FDXSubscriptionRequest:"
0446: + fDXSubscriptionReplyString);
0447: } catch (SAXException se) {
0448: String errorMessage = "Error parsing the FDXSubscriptionRequest response: "
0449: + se.toString();
0450: Debug.logError(se, errorMessage, module);
0451: return ServiceUtil.returnError(errorMessage);
0452: } catch (ParserConfigurationException pce) {
0453: String errorMessage = "Error parsing the FDXSubscriptionRequest response: "
0454: + pce.toString();
0455: Debug.logError(pce, errorMessage, module);
0456: return ServiceUtil.returnError(errorMessage);
0457: } catch (IOException ioe) {
0458: String errorMessage = "Error parsing the FDXSubscriptionRequest response: "
0459: + ioe.toString();
0460: Debug.logError(ioe, errorMessage, module);
0461: return ServiceUtil.returnError(errorMessage);
0462: }
0463:
0464: Element fedexSubscriptionReplyElement = fDXSubscriptionReplyDocument
0465: .getDocumentElement();
0466: handleErrors(fedexSubscriptionReplyElement, errorList);
0467:
0468: if (UtilValidate.isNotEmpty(errorList)) {
0469: return ServiceUtil.returnError(errorList);
0470: }
0471:
0472: String meterNumber = UtilXml.childElementValue(
0473: fedexSubscriptionReplyElement, "MeterNumber");
0474:
0475: result.put("meterNumber", meterNumber);
0476:
0477: } catch (GenericEntityException e) {
0478: Debug.logError(e, module);
0479: return ServiceUtil.returnError(e.getMessage());
0480: }
0481:
0482: return result;
0483: }
0484:
0485: /**
0486: *
0487: * Send a FDXShipRequest via the Ship Manager Direct API
0488: */
0489: public static Map fedexShipRequest(DispatchContext dctx, Map context) {
0490: GenericDelegator delegator = dctx.getDelegator();
0491: LocalDispatcher dispatcher = dctx.getDispatcher();
0492: GenericValue userLogin = (GenericValue) context
0493: .get("userLogin");
0494: Locale locale = (Locale) context.get("locale");
0495: Map result = ServiceUtil.returnSuccess();
0496:
0497: String shipmentId = (String) context.get("shipmentId");
0498: String shipmentRouteSegmentId = (String) context
0499: .get("shipmentRouteSegmentId");
0500:
0501: // Get the location of the Freemarker (XML) template for the FDXShipRequest
0502: String templateLocation = UtilProperties.getPropertyValue(
0503: shipmentPropertiesFile,
0504: "shipment.template.fedex.ship.location");
0505: if (UtilValidate.isEmpty(templateLocation)) {
0506: return ServiceUtil
0507: .returnError("Can't find location for FDXShipRequest template - should be in "
0508: + shipmentPropertiesFile
0509: + ":shipment.template.fedex.ship.location");
0510: }
0511:
0512: // Get the Fedex account number
0513: String accountNumber = UtilProperties.getPropertyValue(
0514: shipmentPropertiesFile,
0515: "shipment.fedex.access.accountNbr");
0516: if (UtilValidate.isEmpty(accountNumber)) {
0517: return ServiceUtil
0518: .returnError("accountNbr not found for Fedex ship request.");
0519: }
0520:
0521: // Get the Fedex meter number
0522: String meterNumber = UtilProperties.getPropertyValue(
0523: shipmentPropertiesFile,
0524: "shipment.fedex.access.meterNumber");
0525: if (UtilValidate.isEmpty(meterNumber)) {
0526: return ServiceUtil
0527: .returnError("Meter number not found for Fedex ship request - should be in "
0528: + shipmentPropertiesFile
0529: + ":shipment.fedex.access.meterNumber (run the fedexSubscriptionRequest service).");
0530: }
0531:
0532: // Get the weight units to be used in the request
0533: String weightUomId = UtilProperties.getPropertyValue(
0534: shipmentPropertiesFile, "shipment.default.weight.uom");
0535: if (UtilValidate.isEmpty(weightUomId)) {
0536: return ServiceUtil
0537: .returnError("Default weightUomId not found for Fedex ship request - should be in "
0538: + shipmentPropertiesFile
0539: + ":shipment.default.weight.uom.");
0540: } else if (!("WT_lb".equals(weightUomId) || "WT_kg"
0541: .equals(weightUomId))) {
0542: return ServiceUtil
0543: .returnError("WeightUomId in "
0544: + shipmentPropertiesFile
0545: + ":shipment.default.weight.uom must be either WT_lb or WT_kg.");
0546: }
0547:
0548: // Get the dimension units to be used in the request
0549: String dimensionsUomId = UtilProperties.getPropertyValue(
0550: shipmentPropertiesFile,
0551: "shipment.default.dimension.uom");
0552: if (UtilValidate.isEmpty(dimensionsUomId)) {
0553: return ServiceUtil
0554: .returnError("Default dimensionUomId not found for Fedex ship request - should be in "
0555: + shipmentPropertiesFile
0556: + ":shipment.default.dimension.uom.");
0557: } else if (!("LEN_in".equals(dimensionsUomId) || "LEN_cm"
0558: .equals(dimensionsUomId))) {
0559: return ServiceUtil
0560: .returnError("WeightUomId in "
0561: + shipmentPropertiesFile
0562: + ":shipment.default.dimension.uom must be either LEN_in or LEN_cm.");
0563: }
0564:
0565: // Get the label image type to be returned
0566: String labelImageType = UtilProperties
0567: .getPropertyValue(shipmentPropertiesFile,
0568: "shipment.fedex.labelImageType");
0569: if (UtilValidate.isEmpty(labelImageType)) {
0570: return ServiceUtil
0571: .returnError("LabelImageType not found for Fedex ship request - should be in "
0572: + shipmentPropertiesFile
0573: + ":shipment.fedex.labelImageType.");
0574: } else if (!("PDF".equals(labelImageType) || "PNG"
0575: .equals(labelImageType))) {
0576: return ServiceUtil
0577: .returnError("LabelImageType in "
0578: + shipmentPropertiesFile
0579: + ":shipment.fedex.labelImageType must be either PDF or PNG.");
0580: }
0581:
0582: // Get the default dropoff type
0583: String dropoffType = UtilProperties.getPropertyValue(
0584: shipmentPropertiesFile,
0585: "shipment.fedex.default.dropoffType");
0586: if (UtilValidate.isEmpty(dropoffType)) {
0587: return ServiceUtil
0588: .returnError("Default dropoff type not found for Fedex ship request - should be in "
0589: + shipmentPropertiesFile
0590: + ":shipment.fedex.default.dropoffType.");
0591: }
0592:
0593: try {
0594:
0595: Map shipRequestContext = new HashMap();
0596:
0597: // Get the shipment and the shipmentRouteSegment
0598: GenericValue shipment = delegator.findByPrimaryKey(
0599: "Shipment", UtilMisc
0600: .toMap("shipmentId", shipmentId));
0601: if (UtilValidate.isEmpty(shipment)) {
0602: return ServiceUtil
0603: .returnError("Shipment not found with ID "
0604: + shipmentId);
0605: }
0606: GenericValue shipmentRouteSegment = delegator
0607: .findByPrimaryKey("ShipmentRouteSegment", UtilMisc
0608: .toMap("shipmentId", shipmentId,
0609: "shipmentRouteSegmentId",
0610: shipmentRouteSegmentId));
0611: if (UtilValidate.isEmpty(shipmentRouteSegment)) {
0612: return ServiceUtil
0613: .returnError("ShipmentRouteSegment not found with shipmentId "
0614: + shipmentId
0615: + " and shipmentRouteSegmentId "
0616: + shipmentRouteSegmentId);
0617: }
0618:
0619: // Determine the Fedex carrier
0620: String carrierPartyId = shipmentRouteSegment
0621: .getString("carrierPartyId");
0622: if (!"FEDEX".equals(carrierPartyId)) {
0623: return ServiceUtil
0624: .returnError("ERROR: The Carrier for ShipmentRouteSegment "
0625: + shipmentRouteSegmentId
0626: + " of Shipment "
0627: + shipmentId
0628: + ", is not Fedex.");
0629: }
0630:
0631: // Check the shipmentRouteSegment's carrier status
0632: if (UtilValidate.isNotEmpty(shipmentRouteSegment
0633: .getString("carrierServiceStatusId"))
0634: && !"SHRSCS_NOT_STARTED"
0635: .equals(shipmentRouteSegment
0636: .getString("carrierServiceStatusId"))) {
0637: return ServiceUtil
0638: .returnError("ERROR: The Carrier Service Status for ShipmentRouteSegment "
0639: + shipmentRouteSegmentId
0640: + " of Shipment "
0641: + shipmentId
0642: + ", is ["
0643: + shipmentRouteSegment
0644: .getString("carrierServiceStatusId")
0645: + "], but must be not-set or [SHRSCS_NOT_STARTED] to perform the Fedex Shipment Confirm operation.");
0646: }
0647:
0648: // Translate shipmentMethodTypeId to Fedex service code and carrier code
0649: String shipmentMethodTypeId = shipmentRouteSegment
0650: .getString("shipmentMethodTypeId");
0651: GenericValue carrierShipmentMethod = delegator
0652: .findByPrimaryKey("CarrierShipmentMethod", UtilMisc
0653: .toMap("shipmentMethodTypeId",
0654: shipmentMethodTypeId, "partyId",
0655: "FEDEX", "roleTypeId", "CARRIER"));
0656: if (UtilValidate.isEmpty(carrierShipmentMethod)) {
0657: return ServiceUtil
0658: .returnError("No CarrierShipmentMethod entry for carrier Fedex shipmentMethodTypeId "
0659: + shipmentMethodTypeId);
0660: }
0661: if (UtilValidate.isEmpty(carrierShipmentMethod
0662: .getString("carrierServiceCode"))) {
0663: return ServiceUtil
0664: .returnError("No Carrier service code for carrier Fedex shipmentMethodTypeId "
0665: + shipmentMethodTypeId);
0666: }
0667: String service = carrierShipmentMethod
0668: .getString("carrierServiceCode");
0669:
0670: // CarrierCode is FDXG only for FEDEXGROUND and GROUNDHOMEDELIVERY services.
0671: boolean isGroundService = service.equals("FEDEXGROUND")
0672: || service.equals("GROUNDHOMEDELIVERY");
0673: String carrierCode = isGroundService ? "FDXG" : "FDXE";
0674:
0675: // Determine the currency by trying the shipmentRouteSegment, then the Shipment, then the framework's default currency, and finally default to USD
0676: String currencyCode = null;
0677: if (UtilValidate.isNotEmpty(shipmentRouteSegment
0678: .getString("currencyUomId"))) {
0679: currencyCode = shipmentRouteSegment
0680: .getString("currencyUomId");
0681: } else if (UtilValidate.isNotEmpty(shipmentRouteSegment
0682: .getString("currencyUomId"))) {
0683: currencyCode = shipment.getString("currencyUomId");
0684: } else {
0685: currencyCode = UtilProperties.getPropertyValue(
0686: "general.properties",
0687: "currency.uom.id.default", "USD");
0688: }
0689:
0690: // Get and validate origin postal address
0691: GenericValue originPostalAddress = shipmentRouteSegment
0692: .getRelatedOne("OriginPostalAddress");
0693: if (UtilValidate.isEmpty(originPostalAddress)) {
0694: return ServiceUtil
0695: .returnError("OriginPostalAddress not found for ShipmentRouteSegment with shipmentId "
0696: + shipmentId
0697: + " and shipmentRouteSegmentId "
0698: + shipmentRouteSegmentId);
0699: } else if (UtilValidate.isEmpty(originPostalAddress
0700: .getString("address1"))
0701: || UtilValidate.isEmpty(originPostalAddress
0702: .getString("city"))
0703: || UtilValidate.isEmpty(originPostalAddress
0704: .getString("postalCode"))
0705: || UtilValidate.isEmpty(originPostalAddress
0706: .getString("countryGeoId"))) {
0707: return ServiceUtil
0708: .returnError("OriginPostalAddress not complete for ShipmentRouteSegment with shipmentId "
0709: + shipmentId
0710: + " and shipmentRouteSegmentId "
0711: + shipmentRouteSegmentId
0712: + " (missing address1, city, postalCode and/or countryGeoId).");
0713: }
0714: GenericValue originCountryGeo = originPostalAddress
0715: .getRelatedOne("CountryGeo");
0716: if (UtilValidate.isEmpty(originCountryGeo)) {
0717: return ServiceUtil
0718: .returnError("OriginCountryGeo not found for ShipmentRouteSegment with shipmentId "
0719: + shipmentId
0720: + " and shipmentRouteSegmentId "
0721: + shipmentRouteSegmentId);
0722: }
0723: String originAddressCountryCode = originCountryGeo
0724: .getString("geoCode");
0725: String originAddressStateOrProvinceCode = null;
0726:
0727: // Only add the StateOrProvinceCode element if the address is in USA or Canada
0728: if (originAddressCountryCode.equals("CA")
0729: || originAddressCountryCode.equals("US")) {
0730: if (UtilValidate.isEmpty(originPostalAddress
0731: .getString("stateProvinceGeoId"))) {
0732: return ServiceUtil
0733: .returnError("OriginStateProvinceGeoId required in contactMechId "
0734: + originPostalAddress
0735: .getString("contactMechId")
0736: + " for ShipmentRouteSegment with shipmentId "
0737: + shipmentId
0738: + " and shipmentRouteSegmentId "
0739: + shipmentRouteSegmentId);
0740: }
0741: GenericValue stateProvinceGeo = delegator
0742: .findByPrimaryKeyCache(
0743: "Geo",
0744: UtilMisc
0745: .toMap(
0746: "geoId",
0747: originPostalAddress
0748: .getString("stateProvinceGeoId")));
0749: originAddressStateOrProvinceCode = stateProvinceGeo
0750: .getString("geoCode");
0751: }
0752:
0753: // Get and validate origin telecom number
0754: GenericValue originTelecomNumber = shipmentRouteSegment
0755: .getRelatedOne("OriginTelecomNumber");
0756: if (UtilValidate.isEmpty(originTelecomNumber)) {
0757: return ServiceUtil
0758: .returnError("OriginTelecomNumber not found for ShipmentRouteSegment with shipmentId "
0759: + shipmentId
0760: + " and shipmentRouteSegmentId "
0761: + shipmentRouteSegmentId);
0762: }
0763: String originContactPhoneNumber = originTelecomNumber
0764: .getString("areaCode")
0765: + originTelecomNumber.getString("contactNumber");
0766:
0767: // Fedex doesn't want the North American country code
0768: if (UtilValidate.isNotEmpty(originTelecomNumber
0769: .getString("countryCode"))
0770: && !(originAddressCountryCode.equals("CA") || originAddressCountryCode
0771: .equals("US"))) {
0772: originContactPhoneNumber = originTelecomNumber
0773: .getString("countryCode")
0774: + originContactPhoneNumber;
0775: }
0776: originContactPhoneNumber = originContactPhoneNumber
0777: .replaceAll("[^+\\d]", "");
0778:
0779: // Get the origin contact name from the owner of the origin facility
0780: GenericValue partyFrom = null;
0781: GenericValue originFacility = shipment
0782: .getRelatedOne("OriginFacility");
0783: if (UtilValidate.isEmpty(originFacility)) {
0784: return ServiceUtil
0785: .returnError("Shipment.originFacilityId is required for Fedex shipments: shipmentId "
0786: + shipmentId
0787: + ", shipmentRouteSegmentId "
0788: + shipmentRouteSegmentId);
0789: } else {
0790: partyFrom = originFacility.getRelatedOne("OwnerParty");
0791: if (UtilValidate.isEmpty(partyFrom)) {
0792: return ServiceUtil
0793: .returnError("Facility.ownerPartyId is required for Fedex shipments: shipmentId "
0794: + shipmentId
0795: + ", shipmentRouteSegmentId "
0796: + shipmentRouteSegmentId
0797: + ", facilityId "
0798: + originFacility
0799: .getString("facilityId"));
0800: }
0801: }
0802:
0803: String originContactKey = "PERSON".equals(partyFrom
0804: .getString("partyTypeId")) ? "OriginContactPersonName"
0805: : "OriginContactCompanyName";
0806: String originContactName = PartyHelper.getPartyName(
0807: partyFrom, false);
0808: if (UtilValidate.isEmpty(originContactName)) {
0809: return ServiceUtil
0810: .returnError("partyIdFrom for shipmentId "
0811: + shipmentId
0812: + ", shipmentRouteSegmentId "
0813: + shipmentRouteSegmentId
0814: + " has no name (required for Fedex shipments)");
0815: }
0816:
0817: // Get and validate destination postal address
0818: GenericValue destinationPostalAddress = shipmentRouteSegment
0819: .getRelatedOne("DestPostalAddress");
0820: if (UtilValidate.isEmpty(destinationPostalAddress)) {
0821: return ServiceUtil
0822: .returnError("destinationPostalAddress not found for ShipmentRouteSegment with shipmentId "
0823: + shipmentId
0824: + " and shipmentRouteSegmentId "
0825: + shipmentRouteSegmentId);
0826: } else if (UtilValidate.isEmpty(destinationPostalAddress
0827: .getString("address1"))
0828: || UtilValidate.isEmpty(destinationPostalAddress
0829: .getString("city"))
0830: || UtilValidate.isEmpty(destinationPostalAddress
0831: .getString("postalCode"))
0832: || UtilValidate.isEmpty(destinationPostalAddress
0833: .getString("countryGeoId"))) {
0834: return ServiceUtil
0835: .returnError("destinationPostalAddress not complete for ShipmentRouteSegment with shipmentId "
0836: + shipmentId
0837: + " and shipmentRouteSegmentId "
0838: + shipmentRouteSegmentId
0839: + " (missing address1, city, postalCode and/or countryGeoId).");
0840: }
0841: GenericValue destinationCountryGeo = destinationPostalAddress
0842: .getRelatedOne("CountryGeo");
0843: if (UtilValidate.isEmpty(destinationCountryGeo)) {
0844: return ServiceUtil
0845: .returnError("destinationCountryGeo not found for ShipmentRouteSegment with shipmentId "
0846: + shipmentId
0847: + " and shipmentRouteSegmentId "
0848: + shipmentRouteSegmentId);
0849: }
0850: String destinationAddressCountryCode = destinationCountryGeo
0851: .getString("geoCode");
0852: String destinationAddressStateOrProvinceCode = null;
0853:
0854: // Only add the StateOrProvinceCode element if the address is in USA or Canada
0855: if (destinationAddressCountryCode.equals("CA")
0856: || destinationAddressCountryCode.equals("US")) {
0857: if (UtilValidate.isEmpty(destinationPostalAddress
0858: .getString("stateProvinceGeoId"))) {
0859: return ServiceUtil
0860: .returnError("destinationStateProvinceGeoId required in contactMechId "
0861: + destinationPostalAddress
0862: .getString("contactMechId")
0863: + " for ShipmentRouteSegment with shipmentId "
0864: + shipmentId
0865: + " and shipmentRouteSegmentId "
0866: + shipmentRouteSegmentId);
0867: }
0868: GenericValue stateProvinceGeo = delegator
0869: .findByPrimaryKeyCache(
0870: "Geo",
0871: UtilMisc
0872: .toMap(
0873: "geoId",
0874: destinationPostalAddress
0875: .getString("stateProvinceGeoId")));
0876: destinationAddressStateOrProvinceCode = stateProvinceGeo
0877: .getString("geoCode");
0878: }
0879:
0880: // Get and validate destination telecom number
0881: GenericValue destinationTelecomNumber = shipmentRouteSegment
0882: .getRelatedOne("DestTelecomNumber");
0883: if (UtilValidate.isEmpty(destinationTelecomNumber)) {
0884: return ServiceUtil
0885: .returnError("destinationTelecomNumber not found for ShipmentRouteSegment with shipmentId "
0886: + shipmentId
0887: + " and shipmentRouteSegmentId "
0888: + shipmentRouteSegmentId);
0889: }
0890: String destinationContactPhoneNumber = destinationTelecomNumber
0891: .getString("areaCode")
0892: + destinationTelecomNumber
0893: .getString("contactNumber");
0894:
0895: // Fedex doesn't want the North American country code
0896: if (UtilValidate.isNotEmpty(destinationTelecomNumber
0897: .getString("countryCode"))
0898: && !(destinationAddressCountryCode.equals("CA") || destinationAddressCountryCode
0899: .equals("US"))) {
0900: destinationContactPhoneNumber = destinationTelecomNumber
0901: .getString("countryCode")
0902: + destinationContactPhoneNumber;
0903: }
0904: destinationContactPhoneNumber = destinationContactPhoneNumber
0905: .replaceAll("[^+\\d]", "");
0906:
0907: // Get the destination contact name
0908: String destinationPartyId = shipment.getString("partyIdTo");
0909: if (UtilValidate.isEmpty(destinationPartyId)) {
0910: return ServiceUtil
0911: .returnError("Shipment.partyIdTo is required for Fedex shipments: shipmentId "
0912: + shipmentId
0913: + ", shipmentRouteSegmentId "
0914: + shipmentRouteSegmentId);
0915: }
0916: GenericValue partyTo = delegator.findByPrimaryKey("Party",
0917: UtilMisc.toMap("partyId", destinationPartyId));
0918: String destinationContactKey = "PERSON".equals(partyTo
0919: .getString("partyTypeId")) ? "DestinationContactPersonName"
0920: : "DestinationContactCompanyName";
0921: String destinationContactName = PartyHelper.getPartyName(
0922: partyTo, false);
0923: if (UtilValidate.isEmpty(destinationContactName)) {
0924: return ServiceUtil
0925: .returnError("partyTo for shipmentId "
0926: + shipmentId
0927: + ", shipmentRouteSegmentId "
0928: + shipmentRouteSegmentId
0929: + " has no name (required for Fedex shipments)");
0930: }
0931:
0932: String homeDeliveryType = null;
0933: Timestamp homeDeliveryDate = null;
0934: if ("GROUNDHOMEDELIVERY".equals(service)) {
0935:
0936: // Determine the home-delivery instructions
0937: homeDeliveryType = shipmentRouteSegment
0938: .getString("homeDeliveryType");
0939: if (UtilValidate.isNotEmpty(homeDeliveryType)) {
0940: if (!(homeDeliveryType.equals("DATECERTAIN")
0941: || homeDeliveryType.equals("EVENING") || homeDeliveryType
0942: .equals("APPOINTMENT"))) {
0943: return ServiceUtil
0944: .returnError("Invalid homeDeliveryType for ShipmentRouteSegment with shipmentId "
0945: + shipmentId
0946: + " and shipmentRouteSegmentId "
0947: + shipmentRouteSegmentId);
0948: }
0949: }
0950: homeDeliveryDate = shipmentRouteSegment
0951: .getTimestamp("homeDeliveryDate");
0952: if (UtilValidate.isEmpty(homeDeliveryDate)) {
0953: return ServiceUtil
0954: .returnError("homeDeliveryDate required for home deliveryType shipments - ShipmentRouteSegment with shipmentId "
0955: + shipmentId
0956: + " and shipmentRouteSegmentId "
0957: + shipmentRouteSegmentId);
0958: } else if (homeDeliveryDate.before(UtilDateTime
0959: .nowTimestamp())) {
0960: return ServiceUtil
0961: .returnError("homeDeliveryDate is before the current time for ShipmentRouteSegment with shipmentId "
0962: + shipmentId
0963: + " and shipmentRouteSegmentId "
0964: + shipmentRouteSegmentId);
0965: }
0966: }
0967:
0968: List shipmentPackageRouteSegs = shipmentRouteSegment
0969: .getRelated("ShipmentPackageRouteSeg", UtilMisc
0970: .toList("+shipmentPackageSeqId"));
0971: if (UtilValidate.isEmpty(shipmentPackageRouteSegs)) {
0972: return ServiceUtil
0973: .returnError("No ShipmentPackageRouteSegs (ie No Packages) found for ShipmentRouteSegment with shipmentId "
0974: + shipmentId
0975: + " and shipmentRouteSegmentId "
0976: + shipmentRouteSegmentId);
0977: }
0978: if (shipmentPackageRouteSegs.size() != 1) {
0979: return ServiceUtil
0980: .returnError("Cannot confirm shipment: fedexShipRequest service does not currently support more than one package per shipment.");
0981: }
0982:
0983: // TODO: Multi-piece shipments, including logic to cancel packages 1-n if FDXShipRequest n+1 fails
0984:
0985: // Populate the Freemarker context with the non-package-related information
0986: shipRequestContext.put("AccountNumber", accountNumber);
0987: shipRequestContext.put("MeterNumber", meterNumber);
0988: shipRequestContext.put("CarrierCode", carrierCode);
0989: shipRequestContext.put("ShipDate", UtilDateTime
0990: .nowTimestamp());
0991: shipRequestContext.put("ShipTime", UtilDateTime
0992: .nowTimestamp());
0993: shipRequestContext.put("DropoffType", dropoffType);
0994: shipRequestContext.put("Service", service);
0995: shipRequestContext.put("WeightUnits", weightUomId
0996: .equals("WT_kg") ? "KGS" : "LBS");
0997: shipRequestContext.put("CurrencyCode", currencyCode);
0998: shipRequestContext.put("PayorType", "SENDER");
0999: shipRequestContext.put(originContactKey, originContactName);
1000: shipRequestContext.put("OriginContactPhoneNumber",
1001: originContactPhoneNumber);
1002: shipRequestContext.put("OriginAddressLine1",
1003: originPostalAddress.getString("address1"));
1004: if (UtilValidate.isNotEmpty(originPostalAddress
1005: .getString("address2"))) {
1006: shipRequestContext.put("OriginAddressLine2",
1007: originPostalAddress.getString("address2"));
1008: }
1009: shipRequestContext.put("OriginAddressCity",
1010: originPostalAddress.getString("city"));
1011: if (UtilValidate
1012: .isNotEmpty(originAddressStateOrProvinceCode)) {
1013: shipRequestContext.put(
1014: "OriginAddressStateOrProvinceCode",
1015: originAddressStateOrProvinceCode);
1016: }
1017: shipRequestContext.put("OriginAddressPostalCode",
1018: originPostalAddress.getString("postalCode"));
1019: shipRequestContext.put("OriginAddressCountryCode",
1020: originAddressCountryCode);
1021: shipRequestContext.put(destinationContactKey,
1022: destinationContactName);
1023: shipRequestContext.put("DestinationContactPhoneNumber",
1024: destinationContactPhoneNumber);
1025: shipRequestContext.put("DestinationAddressLine1",
1026: destinationPostalAddress.getString("address1"));
1027: if (UtilValidate.isNotEmpty(destinationPostalAddress
1028: .getString("address2"))) {
1029: shipRequestContext.put("DestinationAddressLine2",
1030: destinationPostalAddress.getString("address2"));
1031: }
1032: shipRequestContext.put("DestinationAddressCity",
1033: destinationPostalAddress.getString("city"));
1034: if (UtilValidate
1035: .isNotEmpty(destinationAddressStateOrProvinceCode)) {
1036: shipRequestContext.put(
1037: "DestinationAddressStateOrProvinceCode",
1038: destinationAddressStateOrProvinceCode);
1039: }
1040: shipRequestContext.put("DestinationAddressPostalCode",
1041: destinationPostalAddress.getString("postalCode"));
1042: shipRequestContext.put("DestinationAddressCountryCode",
1043: destinationAddressCountryCode);
1044: shipRequestContext.put("LabelType", "2DCOMMON"); // Required type for FDXShipRequest. Not directly in the FTL because it shouldn't be changed.
1045: shipRequestContext.put("LabelImageType", labelImageType);
1046: if (UtilValidate.isNotEmpty(homeDeliveryType)) {
1047: shipRequestContext.put("HomeDeliveryType",
1048: homeDeliveryType);
1049: }
1050: if (homeDeliveryDate != null) {
1051: shipRequestContext.put("HomeDeliveryDate",
1052: homeDeliveryDate);
1053: }
1054:
1055: // Get the weight from the ShipmentRouteSegment first, which overrides all later weight computations
1056: boolean hasBillingWeight = false;
1057: Double billingWeight = shipmentRouteSegment
1058: .getDouble("billingWeight");
1059: String billingWeightUomId = shipmentRouteSegment
1060: .getString("billingWeightUomId");
1061: if ((billingWeight != null)
1062: && (billingWeight.doubleValue() > 0)) {
1063: hasBillingWeight = true;
1064: if (billingWeightUomId == null) {
1065: Debug
1066: .logWarning(
1067: "Shipment Route Segment missing billingWeightUomId in shipmentId "
1068: + shipmentId
1069: + ", assuming default shipment.fedex.weightUomId of "
1070: + weightUomId + " from "
1071: + shipmentPropertiesFile,
1072: module);
1073: billingWeightUomId = weightUomId;
1074: }
1075:
1076: // Convert the weight if necessary
1077: if (!billingWeightUomId.equals(weightUomId)) {
1078: Map results = dispatcher.runSync("convertUom",
1079: UtilMisc.toMap("uomId", billingWeightUomId,
1080: "uomIdTo", weightUomId,
1081: "originalValue", billingWeight));
1082: if (ServiceUtil.isError(results)
1083: || (results.get("convertedValue") == null)) {
1084: Debug.logWarning(
1085: "Unable to convert billing weights for shipmentId "
1086: + shipmentId, module);
1087:
1088: // Try getting the weight from package instead
1089: hasBillingWeight = false;
1090: } else {
1091: billingWeight = (Double) results
1092: .get("convertedValue");
1093: }
1094: }
1095: }
1096:
1097: // Loop through Shipment segments (NOTE: only one supported, loop is here for future refactoring reference)
1098: Iterator shipmentPackageRouteSegIter = shipmentPackageRouteSegs
1099: .iterator();
1100: while (shipmentPackageRouteSegIter.hasNext()) {
1101:
1102: GenericValue shipmentPackageRouteSeg = (GenericValue) shipmentPackageRouteSegIter
1103: .next();
1104: GenericValue shipmentPackage = shipmentPackageRouteSeg
1105: .getRelatedOne("ShipmentPackage");
1106: GenericValue shipmentBoxType = shipmentPackage
1107: .getRelatedOne("ShipmentBoxType");
1108:
1109: // FedEx requires the packaging type
1110: String packaging = null;
1111: if (UtilValidate.isEmpty(shipmentBoxType)) {
1112: Debug
1113: .logWarning(
1114: "Package "
1115: + shipmentPackage
1116: .getString("shipmentPackageSeqId")
1117: + " of shipment "
1118: + shipmentId
1119: + " has no packaging type set - defaulting to "
1120: + shipmentPropertiesFile
1121: + ":shipment.fedex.default.packagingType",
1122: module);
1123:
1124: // Try to get the default packaging type
1125: packaging = UtilProperties.getPropertyValue(
1126: shipmentPropertiesFile,
1127: "shipment.fedex.default.packagingType");
1128: if (UtilValidate.isEmpty(packaging)) {
1129: return ServiceUtil
1130: .returnError("Cannot confirm shipment: Package "
1131: + shipmentPackage
1132: .getString("shipmentPackageSeqId")
1133: + " of shipment "
1134: + shipmentId
1135: + " has no packaging type set, and "
1136: + shipmentPropertiesFile
1137: + ":shipment.fedex.default.packagingType is not configured");
1138: }
1139: } else {
1140: packaging = shipmentBoxType
1141: .getString("shipmentBoxTypeId");
1142: }
1143:
1144: // Make sure that the packaging type is valid for FedEx
1145: GenericValue carrierShipmentBoxType = delegator
1146: .findByPrimaryKey("CarrierShipmentBoxType",
1147: UtilMisc.toMap("partyId", "FEDEX",
1148: "shipmentBoxTypeId", packaging));
1149: if (UtilValidate.isEmpty(carrierShipmentBoxType)) {
1150: return ServiceUtil
1151: .returnError("Cannot confirm shipment: Package "
1152: + shipmentPackage
1153: .getString("shipmentPackageSeqId")
1154: + " of shipment "
1155: + shipmentId
1156: + " has an invalid packaging type for FedEx.");
1157: } else if (UtilValidate.isEmpty(carrierShipmentBoxType
1158: .getString("packagingTypeCode"))) {
1159: return ServiceUtil
1160: .returnError("Cannot confirm shipment: Package type for package "
1161: + shipmentPackage
1162: .getString("shipmentPackageSeqId")
1163: + " of shipment "
1164: + shipmentId
1165: + " is missing packagingTypeCode.");
1166: }
1167: packaging = carrierShipmentBoxType
1168: .getString("packagingTypeCode");
1169:
1170: // Determine the dimensions of the package
1171: Double dimensionsLength = null;
1172: Double dimensionsWidth = null;
1173: Double dimensionsHeight = null;
1174: if (shipmentBoxType != null) {
1175: dimensionsLength = shipmentBoxType
1176: .getDouble("boxLength");
1177: dimensionsWidth = shipmentBoxType
1178: .getDouble("boxWidth");
1179: dimensionsHeight = shipmentBoxType
1180: .getDouble("boxHeight");
1181:
1182: String boxDimensionsUomId = null;
1183: GenericValue boxDimensionsUom = shipmentBoxType
1184: .getRelatedOne("DimensionUom");
1185: if (!UtilValidate.isEmpty(boxDimensionsUom)) {
1186: boxDimensionsUomId = boxDimensionsUom
1187: .getString("uomId");
1188: } else {
1189: Debug
1190: .logWarning(
1191: "Packaging type for package "
1192: + shipmentPackage
1193: .getString("shipmentPackageSeqId")
1194: + " of shipmentRouteSegment "
1195: + shipmentRouteSegmentId
1196: + " of shipment "
1197: + shipmentId
1198: + " is missing dimensionUomId, assuming default shipment.default.dimension.uom of "
1199: + dimensionsUomId
1200: + " from "
1201: + shipmentPropertiesFile,
1202: module);
1203: boxDimensionsUomId = dimensionsUomId;
1204: }
1205: if (dimensionsLength != null
1206: && dimensionsLength.doubleValue() > 0) {
1207: if (!boxDimensionsUomId.equals(dimensionsUomId)) {
1208: Map results = dispatcher.runSync(
1209: "convertUom", UtilMisc.toMap(
1210: "uomId",
1211: boxDimensionsUomId,
1212: "uomIdTo", dimensionsUomId,
1213: "originalValue",
1214: dimensionsLength));
1215: if (ServiceUtil.isError(results)
1216: || (results.get("convertedValue") == null)) {
1217: Debug
1218: .logWarning(
1219: "Unable to convert length for package "
1220: + shipmentPackage
1221: .getString("shipmentPackageSeqId")
1222: + " of shipmentRouteSegment "
1223: + shipmentRouteSegmentId
1224: + " of shipment "
1225: + shipmentId,
1226: module);
1227: dimensionsLength = null;
1228: } else {
1229: dimensionsLength = (Double) results
1230: .get("convertedValue");
1231: }
1232: }
1233:
1234: }
1235: if (dimensionsWidth != null
1236: && dimensionsWidth.doubleValue() > 0) {
1237: if (!boxDimensionsUomId.equals(dimensionsUomId)) {
1238: Map results = dispatcher.runSync(
1239: "convertUom", UtilMisc.toMap(
1240: "uomId",
1241: boxDimensionsUomId,
1242: "uomIdTo", dimensionsUomId,
1243: "originalValue",
1244: dimensionsWidth));
1245: if (ServiceUtil.isError(results)
1246: || (results.get("convertedValue") == null)) {
1247: Debug
1248: .logWarning(
1249: "Unable to convert width for package "
1250: + shipmentPackage
1251: .getString("shipmentPackageSeqId")
1252: + " of shipmentRouteSegment "
1253: + shipmentRouteSegmentId
1254: + " of shipment "
1255: + shipmentId,
1256: module);
1257: dimensionsWidth = null;
1258: } else {
1259: dimensionsWidth = (Double) results
1260: .get("convertedValue");
1261: }
1262: }
1263:
1264: }
1265: if (dimensionsHeight != null
1266: && dimensionsHeight.doubleValue() > 0) {
1267: if (!boxDimensionsUomId.equals(dimensionsUomId)) {
1268: Map results = dispatcher.runSync(
1269: "convertUom", UtilMisc.toMap(
1270: "uomId",
1271: boxDimensionsUomId,
1272: "uomIdTo", dimensionsUomId,
1273: "originalValue",
1274: dimensionsHeight));
1275: if (ServiceUtil.isError(results)
1276: || (results.get("convertedValue") == null)) {
1277: Debug
1278: .logWarning(
1279: "Unable to convert height for package "
1280: + shipmentPackage
1281: .getString("shipmentPackageSeqId")
1282: + " of shipmentRouteSegment "
1283: + shipmentRouteSegmentId
1284: + " of shipment "
1285: + shipmentId,
1286: module);
1287: dimensionsHeight = null;
1288: } else {
1289: dimensionsHeight = (Double) results
1290: .get("convertedValue");
1291: }
1292: }
1293:
1294: }
1295: }
1296:
1297: // Determine the package weight (possibly overriden by route segment billing weight)
1298: Double packageWeight = null;
1299: if (!hasBillingWeight) {
1300: if (UtilValidate.isNotEmpty(shipmentPackage
1301: .getString("weight"))) {
1302: packageWeight = shipmentPackage
1303: .getDouble("weight");
1304: } else {
1305:
1306: // Use default weight if available
1307: try {
1308: packageWeight = Double
1309: .valueOf(UtilProperties
1310: .getPropertyValue(
1311: shipmentPropertiesFile,
1312: "shipment.default.weight.value"));
1313: } catch (NumberFormatException ne) {
1314: Debug
1315: .logWarning(
1316: "Default shippable weight not configured (shipment.default.weight.value), assuming 1.0"
1317: + weightUomId,
1318: module);
1319: packageWeight = new Double(1.0);
1320: }
1321: }
1322:
1323: // Convert weight if necessary
1324: String packageWeightUomId = shipmentPackage
1325: .getString("weightUomId");
1326: if (UtilValidate.isEmpty(packageWeightUomId)) {
1327: Debug
1328: .logWarning(
1329: "Shipment Route Segment missing weightUomId in shipmentId "
1330: + shipmentId
1331: + ", assuming shipment.default.weight.uom of "
1332: + weightUomId
1333: + " from "
1334: + shipmentPropertiesFile,
1335: module);
1336: packageWeightUomId = weightUomId;
1337: }
1338: if (!packageWeightUomId.equals(weightUomId)) {
1339: Map results = dispatcher.runSync("convertUom",
1340: UtilMisc.toMap("uomId",
1341: packageWeightUomId, "uomIdTo",
1342: weightUomId, "originalValue",
1343: packageWeight));
1344: if (ServiceUtil.isError(results)
1345: || (results.get("convertedValue") == null)) {
1346: ServiceUtil
1347: .returnError("Unable to convert weight for package "
1348: + shipmentPackage
1349: .getString("shipmentPackageSeqId")
1350: + " of shipmentRouteSegment "
1351: + shipmentRouteSegmentId
1352: + " of shipment "
1353: + shipmentId);
1354: } else {
1355: packageWeight = (Double) results
1356: .get("convertedValue");
1357: }
1358: }
1359: }
1360: Double weight = hasBillingWeight ? billingWeight
1361: : packageWeight;
1362: if (weight == null || weight.doubleValue() < 0) {
1363: ServiceUtil
1364: .returnError("Unable to determine weight for package "
1365: + shipmentPackage
1366: .getString("shipmentPackageSeqId")
1367: + " of shipmentRouteSegment "
1368: + shipmentRouteSegmentId
1369: + " of shipment " + shipmentId);
1370: }
1371:
1372: // Populate the Freemarker context with package-related information
1373: shipRequestContext.put("CustomerReference", shipmentId
1374: + ":"
1375: + shipmentRouteSegmentId
1376: + ":"
1377: + shipmentPackage
1378: .getString("shipmentPackageSeqId"));
1379: shipRequestContext.put("DropoffType", dropoffType);
1380: shipRequestContext.put("Packaging", packaging);
1381: if (UtilValidate.isNotEmpty(dimensionsUomId)
1382: && dimensionsLength != null
1383: && Math.round(dimensionsLength.doubleValue()) > 0
1384: && dimensionsWidth != null
1385: && Math.round(dimensionsWidth.doubleValue()) > 0
1386: && dimensionsHeight != null
1387: && Math.round(dimensionsHeight.doubleValue()) > 0) {
1388: shipRequestContext.put("DimensionsUnits",
1389: dimensionsUomId.equals("LEN_in") ? "IN"
1390: : "CM");
1391: shipRequestContext.put("DimensionsLength", ""
1392: + Math
1393: .round(dimensionsLength
1394: .doubleValue()));
1395: shipRequestContext
1396: .put("DimensionsWidth", ""
1397: + Math.round(dimensionsWidth
1398: .doubleValue()));
1399: shipRequestContext.put("DimensionsHeight", ""
1400: + Math
1401: .round(dimensionsHeight
1402: .doubleValue()));
1403: }
1404: shipRequestContext.put("Weight", new BigDecimal(weight
1405: .doubleValue())
1406: .setScale(1, BigDecimal.ROUND_UP).toString());
1407: }
1408:
1409: StringWriter outWriter = new StringWriter();
1410: try {
1411: FreeMarkerWorker
1412: .renderTemplateAtLocation(templateLocation,
1413: shipRequestContext, outWriter);
1414: } catch (Exception e) {
1415: String errorMessage = "Cannot confirm Fedex shipment: Failed to render Fedex XML Ship Request Template ["
1416: + templateLocation + "].";
1417: Debug.logError(e, errorMessage, module);
1418: return ServiceUtil.returnError(errorMessage + ": "
1419: + e.getMessage());
1420: }
1421:
1422: // Pass the request string to the sending method
1423: String fDXShipRequestString = outWriter.toString();
1424: String fDXShipReplyString = null;
1425: try {
1426: fDXShipReplyString = sendFedexRequest(fDXShipRequestString);
1427: if (Debug.verboseOn()) {
1428: Debug.logVerbose(fDXShipReplyString, module);
1429: }
1430: } catch (FedexConnectException e) {
1431: String errorMessage = "Error sending Fedex request for FDXShipRequest: ";
1432: Debug.logError(e, errorMessage, module);
1433: return ServiceUtil.returnError(errorMessage
1434: + e.toString());
1435: }
1436:
1437: // Pass the reply to the handler method
1438: return handleFedexShipReply(fDXShipReplyString,
1439: shipmentRouteSegment, shipmentPackageRouteSegs);
1440:
1441: } catch (GenericEntityException e) {
1442: Debug.logError(e, module);
1443: return ServiceUtil
1444: .returnError("Error in fedexShipRequest service: "
1445: + e.toString());
1446: } catch (GenericServiceException se) {
1447: Debug.logError(se, module);
1448: return ServiceUtil
1449: .returnError("Error in fedexShipRequest service: "
1450: + se.toString());
1451: }
1452: }
1453:
1454: /**
1455: * Extract the tracking number and shipping label from the FDXShipReply XML string
1456: * @param fDXShipReplyString
1457: * @param shipmentRouteSegment
1458: * @param shipmentPackageRouteSegs
1459: * @throws GenericEntityException
1460: */
1461: public static Map handleFedexShipReply(String fDXShipReplyString,
1462: GenericValue shipmentRouteSegment,
1463: List shipmentPackageRouteSegs)
1464: throws GenericEntityException {
1465: List errorList = new ArrayList();
1466: GenericValue shipmentPackageRouteSeg = (GenericValue) shipmentPackageRouteSegs
1467: .get(0);
1468:
1469: Document fdxShipReplyDocument = null;
1470: try {
1471: fdxShipReplyDocument = UtilXml.readXmlDocument(
1472: fDXShipReplyString, false);
1473: } catch (SAXException se) {
1474: String errorMessage = "Error parsing the FDXShipReply: "
1475: + se.toString();
1476: Debug.logError(se, errorMessage, module);
1477: // TODO: Cancel the package
1478: } catch (ParserConfigurationException pe) {
1479: String errorMessage = "Error parsing the FDXShipReply: "
1480: + pe.toString();
1481: Debug.logError(pe, errorMessage, module);
1482: // TODO Cancel the package
1483: } catch (IOException ioe) {
1484: String errorMessage = "Error parsing the FDXShipReply: "
1485: + ioe.toString();
1486: Debug.logError(ioe, errorMessage, module);
1487: // TODO Cancel the package
1488: }
1489:
1490: if (UtilValidate.isEmpty(fdxShipReplyDocument)) {
1491: return ServiceUtil
1492: .returnError("Error parsing the FDXShipReply.");
1493: }
1494:
1495: // Tracking number: Tracking/TrackingNumber
1496: Element rootElement = fdxShipReplyDocument.getDocumentElement();
1497:
1498: handleErrors(rootElement, errorList);
1499:
1500: if (UtilValidate.isNotEmpty(errorList)) {
1501: return ServiceUtil.returnError(errorList);
1502: }
1503:
1504: Element trackingElement = UtilXml.firstChildElement(
1505: rootElement, "Tracking");
1506: String trackingNumber = UtilXml.childElementValue(
1507: trackingElement, "TrackingNumber");
1508:
1509: // Label: Labels/OutboundLabel
1510: Element labelElement = UtilXml.firstChildElement(rootElement,
1511: "Labels");
1512: String encodedImageString = UtilXml.childElementValue(
1513: labelElement, "OutboundLabel");
1514: if (UtilValidate.isEmpty(encodedImageString)) {
1515: Debug.logError(
1516: "Cannot find FDXShipReply label. FDXShipReply document is: "
1517: + fDXShipReplyString, module);
1518: return ServiceUtil
1519: .returnError("Cannot get FDXShipReply label for shipment package route segment "
1520: + shipmentPackageRouteSeg
1521: + ". FedEx response is: "
1522: + fDXShipReplyString);
1523: }
1524:
1525: byte[] labelBytes = Base64.base64Decode(encodedImageString
1526: .getBytes());
1527:
1528: if (labelBytes != null) {
1529:
1530: // Store in db blob
1531: shipmentPackageRouteSeg.setBytes("labelImage", labelBytes);
1532: } else {
1533: Debug
1534: .log("Failed to either decode returned FedEx label or no data found in Labels/OutboundLabel.");
1535: // TODO: Cancel the package
1536: }
1537:
1538: shipmentPackageRouteSeg.set("trackingCode", trackingNumber);
1539: shipmentPackageRouteSeg.set("labelHtml", encodedImageString);
1540: shipmentPackageRouteSeg.store();
1541:
1542: shipmentRouteSegment.set("trackingIdNumber", trackingNumber);
1543: shipmentRouteSegment.put("carrierServiceStatusId",
1544: "SHRSCS_CONFIRMED");
1545: shipmentRouteSegment.store();
1546:
1547: return ServiceUtil.returnSuccess("FedEx Shipment Confirmed.");
1548:
1549: }
1550:
1551: public static void handleErrors(Element rootElement, List errorList) {
1552: Element errorElement = null;
1553: if ("Error".equalsIgnoreCase(rootElement.getNodeName())) {
1554: errorElement = rootElement;
1555: } else {
1556: errorElement = UtilXml.firstChildElement(rootElement,
1557: "Error");
1558: }
1559: if (errorElement != null) {
1560: Element errorCodeElement = UtilXml.firstChildElement(
1561: errorElement, "Code");
1562: Element errorMessageElement = UtilXml.firstChildElement(
1563: errorElement, "Message");
1564: if (errorCodeElement != null || errorMessageElement != null) {
1565: String errorCode = UtilXml.childElementValue(
1566: errorElement, "Code");
1567: String errorMessage = UtilXml.childElementValue(
1568: errorElement, "Message");
1569: if (UtilValidate.isNotEmpty(errorCode)
1570: || UtilValidate.isNotEmpty(errorMessage)) {
1571: String errMsg = "An error occurred [code: "
1572: + errorCode + " [Description: "
1573: + errorMessage + "].";
1574: errorList.add(errMsg);
1575: }
1576: }
1577: }
1578: }
1579:
1580: }
1581:
1582: class FedexConnectException extends GeneralException {
1583: FedexConnectException() {
1584: super ();
1585: }
1586:
1587: FedexConnectException(String msg) {
1588: super (msg);
1589: }
1590:
1591: FedexConnectException(Throwable t) {
1592: super (t);
1593: }
1594:
1595: FedexConnectException(String msg, Throwable t) {
1596: super(msg, t);
1597: }
1598: }
|