/*******************************************************************************
 * 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.ofbiz.accounting.thirdparty.paypal;

import java.io.IOException;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.lang.ref.WeakReference;
import java.math.BigDecimal;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.WeakHashMap;

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

import javolution.util.FastList;
import javolution.util.FastMap;

import org.apache.commons.lang.StringUtils;
import org.ofbiz.accounting.payment.PaymentGatewayServices;
import org.ofbiz.base.util.Debug;
import org.ofbiz.base.util.GeneralException;
import org.ofbiz.base.util.StringUtil;
import org.ofbiz.base.util.UtilDateTime;
import org.ofbiz.base.util.UtilHttp;
import org.ofbiz.base.util.UtilMisc;
import org.ofbiz.base.util.UtilProperties;
import org.ofbiz.base.util.UtilValidate;
import org.ofbiz.entity.Delegator;
import org.ofbiz.entity.GenericEntityException;
import org.ofbiz.entity.GenericValue;
import org.ofbiz.entity.condition.EntityComparisonOperator;
import org.ofbiz.entity.condition.EntityCondition;
import org.ofbiz.entity.condition.EntityFunction;
import org.ofbiz.entity.transaction.GenericTransactionException;
import org.ofbiz.entity.transaction.TransactionUtil;
import org.ofbiz.entity.util.EntityUtil;
import org.ofbiz.order.order.OrderReadHelper;
import org.ofbiz.order.shoppingcart.CartItemModifyException;
import org.ofbiz.order.shoppingcart.CheckOutHelper;
import org.ofbiz.order.shoppingcart.ShoppingCart;
import org.ofbiz.order.shoppingcart.ShoppingCartItem;
import org.ofbiz.order.shoppingcart.ShoppingCart.CartShipInfo;
import org.ofbiz.order.shoppingcart.shipping.ShippingEstimateWrapper;
import org.ofbiz.order.shoppingcart.shipping.ShippingEvents;
import org.ofbiz.product.store.ProductStoreWorker;
import org.ofbiz.service.DispatchContext;
import org.ofbiz.service.GenericServiceException;
import org.ofbiz.service.LocalDispatcher;
import org.ofbiz.service.ModelService;
import org.ofbiz.service.ServiceUtil;

import com.paypal.sdk.core.nvp.NVPDecoder;
import com.paypal.sdk.core.nvp.NVPEncoder;
import com.paypal.sdk.exceptions.PayPalException;
import com.paypal.sdk.profiles.APIProfile;
import com.paypal.sdk.profiles.ProfileFactory;
import com.paypal.sdk.services.NVPCallerServices;

/**
 * PayPalServices for NVP API communication
 */
public class PayPalServices {

    public static final String module = PayPalServices.class.getName();
    // Used to maintain a weak reference to the ShoppingCart for customers who have gone to PayPal to checkout
    // so that we can quickly grab the cart, perform shipment estimates and send the info back to PayPal.
    // The weak key is a simple wrapper for the checkout token String and is stored as a cart attribute. The value
    // is a weak reference to the ShoppingCart itself.  Entries will be removed as carts are removed from the
    // session (i.e. on cart clear or successful checkout) or when the session is destroyed
    private static Map<TokenWrapper, WeakReference<ShoppingCart>> tokenCartMap = new WeakHashMap<TokenWrapper, WeakReference<ShoppingCart>>();

    public static Map<String, Object> setExpressCheckout(DispatchContext dctx, Map<String, ? extends Object> context) {
        ShoppingCart cart = (ShoppingCart) context.get("cart");
        Locale locale = cart.getLocale();
        if (cart == null || cart.items().size() <= 0) {
            return ServiceUtil.returnError(UtilProperties.getMessage("AccountingErrorUiLabels", "AccountingPayPalShoppingCartIsEmpty", locale));
        }

        GenericValue payPalConfig = getPaymentMethodGatewayPayPal(dctx, context, null);
        if (payPalConfig == null) {
            return ServiceUtil.returnError("Couldn't retrieve a PaymentGatewayConfigPayPal record for Express Checkout, cannot continue.");
        }


        NVPEncoder encoder = new NVPEncoder();

        // Set Express Checkout Request Parameters
        encoder.add("METHOD", "SetExpressCheckout");
        String token = (String) cart.getAttribute("payPalCheckoutToken");
        if (UtilValidate.isNotEmpty(token)) {
            encoder.add("TOKEN", token);
        }
        encoder.add("RETURNURL", payPalConfig.getString("returnUrl"));
        encoder.add("CANCELURL", payPalConfig.getString("cancelReturnUrl"));
        if (!cart.shippingApplies()) {
            encoder.add("NOSHIPPING", "1");
        } else {
            // Default to no
            String reqConfirmShipping = "Y".equals(payPalConfig.getString("requireConfirmedShipping")) ? "1" : "0";
            encoder.add("REQCONFIRMSHIPPING", reqConfirmShipping);
            // Default shipment method
            //These are used in conjunction with the Shipping Call back URL.
//            encoder.add("CALLBACK", payPalConfig.getString("shippingCallbackUrl"));
//            encoder.add("CALLBACKTIMEOUT", "6");
//            encoder.add("L_SHIPPINGOPTIONISDEFAULT0", "true");
//            encoder.add("L_SHIPPINGOPTIONNAME0", "Calculated Offline");
//            encoder.add("L_SHIPPINGOPTIONAMOUNT0", "0.00");

            // shipping address
            GenericValue shippingAddress = cart.getShippingAddress();
            if (UtilValidate.isNotEmpty(shippingAddress))
            {
                encoder.add("NOSHIPPING", "1");
                encoder.add("PAYMENTREQUEST_0_SHIPTOSTREET", shippingAddress.getString("address1"));
                encoder.add("PAYMENTREQUEST_0_SHIPTOSTREET2", shippingAddress.getString("address2"));
                encoder.add("PAYMENTREQUEST_0_SHIPTOCITY", shippingAddress.getString("city"));
                encoder.add("PAYMENTREQUEST_0_SHIPTOSTATE", shippingAddress.getString("stateProvinceGeoId"));
                encoder.add("PAYMENTREQUEST_0_SHIPTOZIP", shippingAddress.getString("postalCode"));
                encoder.add("PAYMENTREQUEST_0_SHIPTOCOUNTRY", shippingAddress.getString("countryGeoId"));
            }
        }
        encoder.add("ALLOWNOTE", "1");
        encoder.add("INSURANCEOPTIONOFFERED", "false");
        if (UtilValidate.isNotEmpty(payPalConfig.getString("imageUrl")));
        encoder.add("PAYMENTACTION", "Order");

        // Cart information
        try {
            addCartDetails(encoder, cart);
        } catch (GenericEntityException e) {
            Debug.logError(e, module);
            return ServiceUtil.returnError("An error occurred while retreiving cart details");
        }

        NVPDecoder decoder;
        try {
            decoder = sendNVPRequest(payPalConfig, encoder);
        } catch (PayPalException e) {
            Debug.logError(e, module);
            return ServiceUtil.returnError(e.getMessage());
        }

        Map<String, String> errorMessages = getErrorMessageMap(decoder);
        if (UtilValidate.isNotEmpty(errorMessages)) {
            if (errorMessages.containsKey("10411")) {
                // Token has expired, get a new one
                cart.setAttribute("payPalCheckoutToken", null);
                return PayPalServices.setExpressCheckout(dctx, context);
            }
            return ServiceUtil.returnError(UtilMisc.toList(errorMessages.values()));
        }

        token = decoder.get("TOKEN");
        cart.setAttribute("payPalCheckoutToken", token);
        TokenWrapper tokenWrapper = new TokenWrapper(token);
        cart.setAttribute("payPalCheckoutTokenObj", tokenWrapper);
        PayPalServices.tokenCartMap.put(tokenWrapper, new WeakReference<ShoppingCart>(cart));
        return ServiceUtil.returnSuccess();
    }

    public static Map<String, Object> payPalCheckoutUpdate(DispatchContext dctx, Map<String, Object> context) {
        LocalDispatcher dispatcher = dctx.getDispatcher();
        Delegator delegator = dctx.getDelegator();
        HttpServletRequest request = (HttpServletRequest) context.get("request");
        HttpServletResponse response = (HttpServletResponse) context.get("response");

        Map<String, Object> paramMap = UtilHttp.getParameterMap(request);

        String token = (String)paramMap.get("TOKEN");
        WeakReference<ShoppingCart> weakCart = tokenCartMap.get(new TokenWrapper(token));
        ShoppingCart cart = null;
        if (weakCart != null) {
            cart = weakCart.get();
        }
        if (cart == null) {
            Debug.logError("Could locate the ShoppingCart for token " + token, module);
            return ServiceUtil.returnSuccess();
        }
        // Since most if not all of the shipping estimate codes requires a persisted contactMechId we'll create one and
        // then delete once we're done, now is not the time to worry about updating everything
        String contactMechId = null;
        Map<String, Object> inMap = FastMap.newInstance();
        inMap.put("address1", paramMap.get("SHIPTOSTREET"));
        inMap.put("address2", paramMap.get("SHIPTOSTREET2"));
        inMap.put("city", paramMap.get("SHIPTOCITY"));
        String countryGeoCode = (String) paramMap.get("SHIPTOCOUNTRY");
        String countryGeoId = PayPalServices.getCountryGeoIdFromGeoCode(countryGeoCode, delegator);
        if (countryGeoId == null) {
            return ServiceUtil.returnSuccess();
        }
        inMap.put("countryGeoId", countryGeoId);
        inMap.put("stateProvinceGeoId", parseStateProvinceGeoId((String)paramMap.get("SHIPTOSTATE"), countryGeoId, delegator));
        inMap.put("postalCode", paramMap.get("SHIPTOZIP"));

        try {
            GenericValue userLogin = delegator.findOne("UserLogin", true, UtilMisc.toMap("userLoginId", "system"));
            inMap.put("userLogin", userLogin);
        } catch (GenericEntityException e) {
            Debug.logError(e, module);
        }
        boolean beganTransaction = false;
        Transaction parentTransaction = null;
        try {
            parentTransaction = TransactionUtil.suspend();
            beganTransaction = TransactionUtil.begin();
        } catch (GenericTransactionException e1) {
            Debug.logError(e1, module);
        }
        try {
            Map<String, Object> outMap = dispatcher.runSync("createPostalAddress", inMap);
            contactMechId = (String) outMap.get("contactMechId");
        } catch (GenericServiceException e) {
            Debug.logError(e.getMessage(), module);
            return ServiceUtil.returnSuccess();
        }
        try {
            TransactionUtil.commit(beganTransaction);
            if (parentTransaction != null) TransactionUtil.resume(parentTransaction);
        } catch (GenericTransactionException e) {
            Debug.logError(e, module);
        }
        // clone the cart so we can modify it temporarily
        CheckOutHelper coh = new CheckOutHelper(dispatcher, delegator, cart);
        String oldShipAddress = cart.getShippingContactMechId();
        coh.setCheckOutShippingAddress(contactMechId);
        ShippingEstimateWrapper estWrapper = new ShippingEstimateWrapper(dispatcher, cart, 0);
        int line = 0;
        NVPEncoder encoder = new NVPEncoder();
        encoder.add("METHOD", "CallbackResponse");

        for (GenericValue shipMethod : estWrapper.getShippingMethods()) {
            BigDecimal estimate = estWrapper.getShippingEstimate(shipMethod);
            //Check that we have a valid estimate (allowing zero value estimates for now)
            if (estimate == null || estimate.compareTo(BigDecimal.ZERO) < 0) {
                continue;
            }
            cart.setShipmentMethodTypeId(shipMethod.getString("shipmentMethodTypeId"));
            cart.setCarrierPartyId(shipMethod.getString("partyId"));
            try {
                coh.calcAndAddTax();
            } catch (GeneralException e) {
                Debug.logError(e, module);
                continue;
            }
            String estimateLabel = shipMethod.getString("partyId") + " - " + shipMethod.getString("description");
            encoder.add("L_SHIPINGPOPTIONLABEL" + line, estimateLabel);
            encoder.add("L_SHIPPINGOPTIONAMOUNT" + line, estimate.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString());
            // Just make this first one default for now
            encoder.add("L_SHIPPINGOPTIONISDEFAULT" + line, line == 0 ? "true" : "false");
            encoder.add("L_TAXAMT" + line, cart.getTotalSalesTax().setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString());
            line++;
        }
        String responseMsg = null;
        try {
            responseMsg = encoder.encode();
        } catch (PayPalException e) {
            Debug.logError(e, module);
        }
        if (responseMsg != null) {
            try {
                response.setContentLength(responseMsg.getBytes("UTF-8").length);
            } catch (UnsupportedEncodingException e) {
                Debug.logError(e, module);
            }

            try {
                Writer writer = response.getWriter();
                writer.write(responseMsg);
                writer.close();
            } catch (IOException e) {
                Debug.logError(e, module);
            }
        }

        // Remove the temporary ship address
        try {
            GenericValue postalAddress = delegator.findOne("PostalAddress", false, UtilMisc.toMap("contactMechId", contactMechId));
            postalAddress.remove();
            GenericValue contactMech = delegator.findOne("ContactMech", false, UtilMisc.toMap("contactMechId", contactMechId));
            contactMech.remove();
        } catch (GenericEntityException e) {
            Debug.logError(e, module);
        }
        coh.setCheckOutShippingAddress(oldShipAddress);
        return ServiceUtil.returnSuccess();
    }

    private static Map<String, String> getErrorMessageMap(NVPDecoder decoder) {
        String ack = decoder.get("ACK");
        Map<String, String> result = null;
        if (!"Success".equals(ack)) {
            result = FastMap.newInstance();
            int i = 0;
            while (UtilValidate.isNotEmpty(decoder.get("L_ERRORCODE" + i))) {
                String errorCode = decoder.get("L_ERRORCODE" + i);
                String longMsg = decoder.get("L_LONGMESSAGE" + i);
                result.put(errorCode, "PayPal Response Error: [" + errorCode + "]" + longMsg);
                i++;
            }
        }
        return result;
    }

    private static void addCartDetails(NVPEncoder encoder, ShoppingCart cart) throws GenericEntityException {
        encoder.add("CURRENCYCODE", cart.getCurrency());
        int line = 0;
        for (ShoppingCartItem item : cart.items()) {
            encoder.add("L_NUMBER" + line, item.getProductId());
            encoder.add("L_NAME" + line, item.getName());
            encoder.add("L_AMT" + line, item.getBasePrice().setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString());
            encoder.add("L_QTY" + line, item.getQuantity().toBigInteger().toString());
            line++;
            BigDecimal otherAdjustments = item.getOtherAdjustments();
            if (otherAdjustments.compareTo(BigDecimal.ZERO) != 0) {
                encoder.add("L_NUMBER" + line, item.getProductId());
                encoder.add("L_NAME" + line, item.getName() + " Adjustments");
                encoder.add("L_AMT" + line, otherAdjustments.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString());
                encoder.add("L_QTY" + line, "1");
                line++;
            }
        }
        BigDecimal otherAdjustments = cart.getOrderOtherAdjustmentTotal();
        if (otherAdjustments.compareTo(BigDecimal.ZERO) != 0) {
            encoder.add("L_NUMBER" + line, "N/A");
            encoder.add("L_NAME" + line, "Order Adjustments");
            encoder.add("L_AMT" + line, otherAdjustments.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString());
            encoder.add("L_QTY" + line, "1");
            line++;
        }
        if (cart.getPaymentTotal().compareTo(BigDecimal.ZERO) != 0) {
            encoder.add("L_NUMBER" + line, "N/A");
            encoder.add("L_NAME" + line, "Applied Payment");
            encoder.add("L_AMT" + line, cart.getPaymentTotal().negate().setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString());
            encoder.add("L_QTY" + line, "1");
            line++;
        }
        encoder.add("ITEMAMT", cart.getSubTotal().add(otherAdjustments).subtract(cart.getPaymentTotal()).setScale(2).toPlainString());
        encoder.add("SHIPPINGAMT", cart.getTotalShipping().setScale(2).toPlainString());
        encoder.add("TAXAMT", cart.getTotalSalesTax().setScale(2).toPlainString());
        encoder.add("AMT", cart.getGrandTotal().subtract(cart.getPaymentTotal()).setScale(2).toPlainString());
        //NOTE: The docs say this is optional but then won't work without it
//        encoder.add("MAXAMT", cart.getSubTotal().add(otherAdjustments).setScale(2).toPlainString());
    }

    public static Map<String, Object> getExpressCheckout(DispatchContext dctx, Map<String, Object> context) {
        LocalDispatcher dispatcher = dctx.getDispatcher();
        Delegator delegator = dctx.getDelegator();

        ShoppingCart cart = (ShoppingCart) context.get("cart");
        GenericValue payPalConfig = getPaymentMethodGatewayPayPal(dctx, context, null);
        if (payPalConfig == null) {
            return ServiceUtil.returnError("Couldn't retrieve a PaymentGatewayConfigPayPal record for Express Checkout, cannot continue.");
        }

        NVPEncoder encoder = new NVPEncoder();
        encoder.add("METHOD", "GetExpressCheckoutDetails");
        String token = (String) cart.getAttribute("payPalCheckoutToken");
        if (UtilValidate.isNotEmpty(token)) {
            encoder.add("TOKEN", token);
        } else {
            return ServiceUtil.returnError("Express Checkout token not present in cart, cannot get checkout details.");
        }

        NVPDecoder decoder;
        try {
            decoder = sendNVPRequest(payPalConfig, encoder);
        } catch (PayPalException e) {
            Debug.logError(e, module);
            return ServiceUtil.returnError(e.getMessage());
        }

        if (UtilValidate.isNotEmpty(decoder.get("NOTE"))) {
            cart.addOrderNote(decoder.get("NOTE"));
        }

        if (cart.getUserLogin() == null) {
            try {
                GenericValue userLogin = delegator.findOne("UserLogin", false, "userLoginId", "anonymous");
                try {
                    cart.setUserLogin(userLogin, dispatcher);
                } catch (CartItemModifyException e) {
                    Debug.logError(e, module);
                    return ServiceUtil.returnError(e.getMessage());
                }
            } catch (GenericEntityException e) {
                Debug.logError(e, module);
                return ServiceUtil.returnError(e.getMessage());
            }
        }
        Debug.log("gotUser", module);
        
        boolean anon = "anonymous".equals(cart.getUserLogin().getString("userLoginId"));
        // Even if anon, a party could already have been created
        String partyId = cart.getOrderPartyId();
        if (partyId == null && anon) {
            // Check nothing has been set on the anon userLogin either
            partyId = cart.getUserLogin() != null ? cart.getUserLogin().getString("partyId") : null;
            cart.setOrderPartyId(partyId);
        }
        if (partyId != null) {
            GenericValue party = null;
            try {
                party = delegator.findOne("Party", false, "partyId", partyId);
            } catch (GenericEntityException e) {
                Debug.logError(e, module);
            }
            if (party == null) {
                partyId = null;
            }
        }
        Debug.log("gotParty" + partyId, module);

        Map<String, Object> inMap = FastMap.newInstance();
        Map<String, Object> outMap = null;
        // Create the person if necessary
        boolean newParty = false;
        if (partyId == null) {
            newParty = true;
            inMap.put("userLogin", cart.getUserLogin());
            inMap.put("personalTitle", decoder.get("SALUTATION"));
            inMap.put("firstName", decoder.get("FIRSTNAME"));
            inMap.put("middleName", decoder.get("MIDDLENAME"));
            inMap.put("lastName", decoder.get("LASTNAME"));
            inMap.put("suffix", decoder.get("SUFFIX"));
            try {
                outMap = dispatcher.runSync("createPerson", inMap);
                partyId = (String) outMap.get("partyId");
                cart.setOrderPartyId(partyId);
                cart.getUserLogin().setString("partyId", partyId);
                inMap.clear();
                inMap.put("userLogin", cart.getUserLogin());
                inMap.put("partyId", partyId);
                inMap.put("roleTypeId", "CUSTOMER");
                dispatcher.runSync("createPartyRole", inMap);
            } catch (GenericServiceException e) {
                Debug.logError(e, module);
                return ServiceUtil.returnError(e.getMessage());
            }
        }
        // Create a new email address if necessary
        String emailContactMechId = null;
        String emailContactPurposeTypeId = "PRIMARY_EMAIL";
        String emailAddress = decoder.get("EMAIL");
        if (!newParty) {
            EntityCondition cond = EntityCondition.makeCondition(UtilMisc.toList(
                    EntityCondition.makeCondition(UtilMisc.toMap("partyId", partyId, "contactMechTypeId", "EMAIL_ADDRESS")),
                    EntityCondition.makeCondition(EntityFunction.UPPER_FIELD("infoString"), EntityComparisonOperator.EQUALS, EntityFunction.UPPER(emailAddress)),
                    EntityUtil.getFilterByDateExpr()

           ));
            try {
                GenericValue matchingEmail = EntityUtil.getFirst(delegator.findList("PartyAndContactMech", cond, null, UtilMisc.toList("fromDate"), null, false));
                if (matchingEmail != null) {
                    emailContactMechId = matchingEmail.getString("contactMechId");
                } else {
                    // No email found so we'll need to create one but first check if it should be PRIMARY or just BILLING
                    cond = EntityCondition.makeCondition(UtilMisc.toList(
                            EntityCondition.makeCondition(UtilMisc.toMap("partyId", partyId, "contactMechTypeId", "EMAIL_ADDRESS", "contactMechPurposeTypeId", "PRIMARY_EMAIL")),
                            EntityCondition.makeConditionDate("contactFromDate", "contactThruDate"),
                            EntityCondition.makeConditionDate("purposeFromDate", "purposeThruDate")));
                    List<GenericValue> primaryEmails = delegator.findList("PartyContactWithPurpose", cond, null, null, null, false);
                    if (UtilValidate.isNotEmpty(primaryEmails)) emailContactPurposeTypeId = "BILLING_EMAIL";
                }
            } catch (GenericEntityException e) {
                Debug.logError(e, module);
            }
        }
        Debug.log("gotEmail" + emailAddress, module);
        
        if (emailContactMechId == null) {
            inMap.clear();
            inMap.put("userLogin", cart.getUserLogin());
            inMap.put("contactMechPurposeTypeId", emailContactPurposeTypeId);
            inMap.put("emailAddress", emailAddress);
            inMap.put("partyId", partyId);
            //inMap.put("roleTypeId", "CUSTOMER");
            inMap.put("verified", "Y");  // Going to assume PayPal has taken care of this for us
            inMap.put("fromDate", UtilDateTime.nowTimestamp());
            try {
                outMap = dispatcher.runSync("createPartyEmailAddress", inMap);
                emailContactMechId = (String) outMap.get("contactMechId");
            } catch (GenericServiceException e) {
                Debug.logError(e, module);
                return ServiceUtil.returnError(e.getMessage());
            }
        }
        cart.addContactMech("ORDER_EMAIL", emailContactMechId);
        Debug.log("gotEmailContactMech" + emailContactMechId, module);
        // Phone number
        String phoneNumber = decoder.get("PHONENUM");
        String phoneContactId = null;
        if (phoneNumber != null) {
            inMap.clear();
            if (phoneNumber.startsWith("+")) {
                // International, format is +XXX XXXXXXXX which we'll split into countryCode + contactNumber
                String[] phoneNumbers = phoneNumber.split(" ");
                inMap.put("countryCode", StringUtil.removeNonNumeric(phoneNumbers[0]));
                inMap.put("contactNumber", phoneNumbers[1]);
            } else {
                // U.S., format is XXX-XXX-XXXX which we'll split into areaCode + contactNumber
                inMap.put("countryCode", "1");
                String[] phoneNumbers = phoneNumber.split("-");
                inMap.put("areaCode", phoneNumbers[0]);
                inMap.put("contactNumber", phoneNumbers[1] + phoneNumbers[2]);
            }
            inMap.put("userLogin", cart.getUserLogin());
            inMap.put("partyId", partyId);
            try {
                outMap = dispatcher.runSync("createUpdatePartyTelecomNumber", inMap);
                phoneContactId = (String) outMap.get("contactMechId");
                cart.addContactMech("PHONE_BILLING", phoneContactId);
            } catch (GenericServiceException e) {
                Debug.logError(e, module);
            }
        }
        Debug.log("gotPhone" + phoneNumber, module);

        //We do not need to create the paypal address.
        /*
        // Create a new Postal Address if necessary
        String postalContactId = null;
        boolean needsShippingPurpose = true;
        // if the cart for some reason already has a billing address, we'll leave it be
        boolean needsBillingPurpose = (cart.getContactMech("BILLING_LOCATION") == null);
        Map<String, Object> postalMap = FastMap.newInstance();
        postalMap.put("toName", decoder.get("SHIPTONAME"));
        postalMap.put("address1", decoder.get("SHIPTOSTREET"));
        postalMap.put("address2", decoder.get("SHIPTOSTREET2"));
        postalMap.put("city", decoder.get("SHIPTOCITY"));
        String countryGeoId = PayPalServices.getCountryGeoIdFromGeoCode(decoder.get("SHIPTOCOUNTRYCODE"), delegator);
        postalMap.put("countryGeoId", countryGeoId);
        postalMap.put("stateProvinceGeoId", parseStateProvinceGeoId(decoder.get("SHIPTOSTATE"), countryGeoId, delegator));
        postalMap.put("postalCode", decoder.get("SHIPTOZIP"));
        if (!newParty) {
            // We want an exact match only
            EntityCondition cond = EntityCondition.makeCondition(UtilMisc.toList(
                    EntityCondition.makeCondition(postalMap),
                    EntityCondition.makeCondition(UtilMisc.toMap("attnName", null, "directions", null, "postalCodeExt", null,"postalCodeGeoId", null)),
                    EntityUtil.getFilterByDateExpr(),
                    EntityCondition.makeCondition("partyId", partyId)
           ));
            try {
                GenericValue postalMatch = EntityUtil.getFirst(delegator.findList("PartyAndPostalAddress", cond, null, UtilMisc.toList("fromDate"), null, false));
                if (postalMatch != null) {
                    postalContactId = postalMatch.getString("contactMechId");
                    EntityCondition purposeCond = EntityCondition.makeCondition(UtilMisc.toList(
                            EntityCondition.makeCondition(UtilMisc.toMap("partyId", partyId, "contactMechId", postalContactId)),
                            EntityUtil.getFilterByDateExpr()
                   ));
                    List<GenericValue> postalPurposes = delegator.findList("PartyContactMechPurpose", purposeCond, null, null, null, false);
                    List<Object> purposeStrings = EntityUtil.getFieldListFromEntityList(postalPurposes, "contactMechPurposeTypeId", false);
                    if (UtilValidate.isNotEmpty(purposeStrings) && purposeStrings.contains("SHIPPING_LOCATION")) {
                        needsShippingPurpose = false;
                    }
                    if (needsBillingPurpose && UtilValidate.isNotEmpty(purposeStrings) && purposeStrings.contains("BILLING_LOCATION")) {
                        needsBillingPurpose = false;
                    }
                }
            } catch (GenericEntityException e) {
                Debug.logError(e, module);
            }
        }
        Debug.log("gotBillingAddr" + postalContactId, module);
        
        if (postalContactId == null) {
            postalMap.put("userLogin", cart.getUserLogin());
            postalMap.put("fromDate", UtilDateTime.nowTimestamp());
            try {
                outMap = dispatcher.runSync("createPartyPostalAddress", postalMap);
                postalContactId = (String) outMap.get("contactMechId");
            } catch (GenericServiceException e) {
                Debug.logError(e, module);
                return ServiceUtil.returnError(e.getMessage());
            }
        }
        Debug.log("createAddr", module);
        if (needsShippingPurpose || needsBillingPurpose) {
            inMap.clear();
            inMap.put("userLogin", cart.getUserLogin());
            inMap.put("contactMechId", postalContactId);
            inMap.put("partyId", partyId);
            try {
                if (needsShippingPurpose) {
                    inMap.put("contactMechPurposeTypeId", "SHIPPING_LOCATION");
                    dispatcher.runSync("createPartyContactMechPurpose", inMap);
                }
                if (needsBillingPurpose) {
                    inMap.put("contactMechPurposeTypeId", "BILLING_LOCATION");
                    dispatcher.runSync("createPartyContactMechPurpose", inMap);
                }
            } catch (GenericServiceException e) {
                // Not the end of the world, we'll carry on
                Debug.log(e.getMessage());
            }
        }
        Debug.log("createPurpose", module);
        
        if (postalContactId == null) {
            postalMap.put("userLogin", cart.getUserLogin());
            try {
                outMap = dispatcher.runSync("createPostalAddress", postalMap);
                postalContactId = (String) outMap.get("contactMechId");
            } catch (GenericServiceException e) {
                Debug.logError(e, module);
                return ServiceUtil.returnError(e.getMessage());
            }
        }
        Debug.log("createAddr", module);

        // Load the selected shipping method - thanks to PayPal's less than sane API all we've to work with is the shipping option label
        // that was shown to the customer
        String shipMethod = decoder.get("SHIPPINGOPTIONNAME");
        Debug.log("gotPayPalShipMethod" + shipMethod, module);
        if (UtilValidate.isNotEmpty(shipMethod))
        {
            if ("Calculated Offline".equals(shipMethod)) {
                cart.setCarrierPartyId("_NA_");
                cart.setShipmentMethodTypeId("NO_SHIPPING");
            } else {
                String[] shipMethodSplit = shipMethod.split(" - ");
                cart.setCarrierPartyId(shipMethodSplit[0]);
                Debug.log("gotCarrier" + shipMethodSplit[0], module);
                String shippingMethodTypeDesc = StringUtils.join(shipMethodSplit, " - ", 1, shipMethodSplit.length);
                Debug.log("gotShipDesc" + shipMethodSplit, module);
                try {
                    EntityCondition cond = EntityCondition.makeCondition(
                            UtilMisc.toMap("productStoreId", cart.getProductStoreId(), "partyId", shipMethodSplit[0], "roleTypeId", "CARRIER", "description", shippingMethodTypeDesc)
                   );
                    GenericValue shipmentMethod = EntityUtil.getFirst(delegator.findList("ProductStoreShipmentMethView", cond, null, null, null, false));
                    Debug.log("gotshipmentMethodTypeID" + shipmentMethod.getString("shipmentMethodTypeId"), module);
                    cart.setShipmentMethodTypeId(shipmentMethod.getString("shipmentMethodTypeId"));
                } catch (GenericEntityException e1) {
                    Debug.logError(e1, module);
                }
            }
            Debug.log("gotShipMethod" + shipMethod, module);
        	
        }
        
        //Get rid of any excess ship groups
        List<CartShipInfo> shipGroups = cart.getShipGroups();
        for (int i = 1; i < shipGroups.size(); i++) {
            Map<ShoppingCartItem, BigDecimal> items = cart.getShipGroupItems(i);
            for (Map.Entry<ShoppingCartItem, BigDecimal> entry : items.entrySet()) {
                cart.positionItemToGroup(entry.getKey(), entry.getValue(), i, 0, false);
            }
        }
        cart.cleanUpShipGroups();
        cart.setShippingContactMechId(postalContactId);
        Map<String, Object> result = ShippingEvents.getShipGroupEstimate(dispatcher, delegator, cart, 0);
        if (result.get(ModelService.RESPONSE_MESSAGE).equals(ModelService.RESPOND_ERROR)) {
            return ServiceUtil.returnError((String) result.get(ModelService.ERROR_MESSAGE));
        }

        Debug.log("gotShipEst", module);
        BigDecimal shippingTotal = (BigDecimal) result.get("shippingTotal");
        if (shippingTotal == null) {
            shippingTotal = BigDecimal.ZERO;
        }
        cart.setItemShipGroupEstimate(shippingTotal, 0);
        Debug.log("gotShipTotalEst" + shippingTotal, module);
        CheckOutHelper cho = new CheckOutHelper(dispatcher, delegator, cart);
        try {
            cho.calcAndAddTax();
        } catch (GeneralException e) {
            Debug.logError(e, module);
            return ServiceUtil.returnError(e.getMessage());
        }
        Debug.log("gotTax", module);*/

        // Create the PayPal payment method
        inMap.clear();
        inMap.put("userLogin", cart.getUserLogin());
        inMap.put("partyId", partyId);
        //inMap.put("contactMechId", postalContactId);
        inMap.put("fromDate", UtilDateTime.nowTimestamp());
        inMap.put("payerId", decoder.get("PAYERID"));
        inMap.put("expressCheckoutToken", token);
        inMap.put("payerStatus", decoder.get("PAYERSTATUS"));

        try {
            outMap = dispatcher.runSync("createPayPalPaymentMethod", inMap);
        } catch (GenericServiceException e) {
            Debug.logError(e, module);
            return ServiceUtil.returnError(e.getMessage());
        }
        String paymentMethodId = (String) outMap.get("paymentMethodId");
        Debug.log("gotPayPalPayment" + paymentMethodId, module);

        //cart.clearPayments(); for support the multiple payment
        BigDecimal maxAmount = cart.getGrandTotal().subtract(cart.getPaymentTotal()).setScale(2, BigDecimal.ROUND_HALF_UP);
        cart.addPaymentAmount(paymentMethodId, maxAmount, true);

        return ServiceUtil.returnSuccess();

    }

    // Note we're not doing a lot of error checking here as this method is really only used
    // to confirm the order with PayPal, the subsequent authorizations will handle any errors
    // that may occur.
    public static Map<String, Object> doExpressCheckout(DispatchContext dctx, Map<String, Object> context) {
        LocalDispatcher dispatcher = dctx.getDispatcher();
        Delegator delegator = dctx.getDelegator();
        GenericValue userLogin = (GenericValue) context.get("userLogin");
        GenericValue paymentPref = (GenericValue) context.get("orderPaymentPreference");
        OrderReadHelper orh = new OrderReadHelper(delegator, paymentPref.getString("orderId"));

        GenericValue payPalPaymentSetting = getPaymentMethodGatewayPayPal(dctx, context, null);
        GenericValue payPalPaymentMethod = null;
        try {
            payPalPaymentMethod = paymentPref.getRelatedOne("PaymentMethod");
            payPalPaymentMethod = payPalPaymentMethod.getRelatedOne("PayPalPaymentMethod");
        } catch (GenericEntityException e) {
            Debug.logError(e, module);
            return ServiceUtil.returnError(e.getMessage());
        }
        BigDecimal processAmount = paymentPref.getBigDecimal("maxAmount");

        NVPEncoder encoder = new NVPEncoder();
        encoder.add("METHOD", "DoExpressCheckoutPayment");
        encoder.add("TOKEN", payPalPaymentMethod.getString("expressCheckoutToken"));
        encoder.add("PAYMENTACTION", "Order");
        encoder.add("PAYERID", payPalPaymentMethod.getString("payerId"));
        // set the amount
        encoder.add("AMT", processAmount.setScale(2).toPlainString());
        encoder.add("CURRENCYCODE", orh.getCurrency());
        BigDecimal grandTotal = orh.getOrderGrandTotal();
        BigDecimal shippingTotal = orh.getShippingTotal().setScale(2, BigDecimal.ROUND_HALF_UP);
        BigDecimal taxTotal = orh.getTaxTotal().setScale(2, BigDecimal.ROUND_HALF_UP);
        BigDecimal subTotal = processAmount.subtract(shippingTotal).subtract(taxTotal).setScale(2, BigDecimal.ROUND_HALF_UP);
        encoder.add("ITEMAMT", subTotal.setScale(2).toPlainString());
        encoder.add("SHIPPINGAMT", shippingTotal.toPlainString());
        encoder.add("TAXAMT", taxTotal.toPlainString());

        NVPDecoder decoder = null;
        try {
            decoder = sendNVPRequest(payPalPaymentSetting, encoder);
        } catch (PayPalException e) {
            Debug.logError(e, module);
            return ServiceUtil.returnError(e.getMessage());
        }
        if (decoder == null) {
            return ServiceUtil.returnError("An error occurred while communicating with PayPal");
        }

        Map<String, String> errorMessages = getErrorMessageMap(decoder);
        if (UtilValidate.isNotEmpty(errorMessages)) {
            if (errorMessages.containsKey("10417")) {
                // "The transaction cannot complete successfully,  Instruct the customer to use an alternative payment method"
                // I've only encountered this once and there's no indication of the cause so the temporary solution is to try again
                boolean retry = context.get("_RETRY_") == null || (Boolean) context.get("_RETRY_");
                if (retry) {
                    context.put("_RETRY_", false);
                    return PayPalServices.doExpressCheckout(dctx, context);
                }
            }
            return ServiceUtil.returnError(UtilMisc.toList(errorMessages.values()));
        }

        Map<String, Object> inMap = FastMap.newInstance();
        inMap.put("userLogin", userLogin);
        inMap.put("paymentMethodId", payPalPaymentMethod.get("paymentMethodId"));
        inMap.put("transactionId", decoder.get("TRANSACTIONID"));

        Map<String, Object> outMap = null;
        try {
            outMap = dispatcher.runSync("updatePayPalPaymentMethod", inMap);
        } catch (GenericServiceException e) {
            Debug.logError(e, module);
            return ServiceUtil.returnError(e.getMessage());
        }
        if (ServiceUtil.isError(outMap)) {
            Debug.logError(ServiceUtil.getErrorMessage(outMap), module);
            return outMap;
        }
        return ServiceUtil.returnSuccess();
    }

    public static Map<String, Object> doAuthorization(DispatchContext dctx, Map<String, Object> context) {
        Delegator delegator = dctx.getDelegator();
        String orderId = (String) context.get("orderId");
        BigDecimal processAmount = (BigDecimal) context.get("processAmount");
        GenericValue payPalPaymentMethod = (GenericValue) context.get("payPalPaymentMethod");
        OrderReadHelper orh = new OrderReadHelper(delegator, orderId);
        GenericValue payPalConfig = getPaymentMethodGatewayPayPal(dctx, context, PaymentGatewayServices.AUTH_SERVICE_TYPE);

        NVPEncoder encoder = new NVPEncoder();
        encoder.add("METHOD", "DoAuthorization");
        encoder.add("TRANSACTIONID", payPalPaymentMethod.getString("transactionId"));
        encoder.add("AMT", processAmount.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString());
        encoder.add("TRANSACTIONENTITY", "Order");
        String currency = (String) context.get("currency");
        if (currency == null) {
            currency = orh.getCurrency();
        }
        encoder.add("CURRENCYCODE", currency);

        NVPDecoder decoder = null;
        try {
            decoder = sendNVPRequest(payPalConfig, encoder);
        } catch (PayPalException e) {
            Debug.logError(e, module);
            return ServiceUtil.returnError(e.getMessage());
        }

        if (decoder == null) {
            return ServiceUtil.returnError("An unknown error occurred while contacting PayPal");
        }

        Map<String, Object> result = ServiceUtil.returnSuccess();
        Map<String, String> errors = getErrorMessageMap(decoder);
        if (UtilValidate.isNotEmpty(errors)) {
            result.put("authResult", false);
            result.put("authRefNum", "N/A");
            result.put("processAmount", BigDecimal.ZERO);
            if (errors.size() == 1) {
                Map.Entry<String, String> error = errors.entrySet().iterator().next();
                result.put("authCode", error.getKey());
                result.put("authMessage", error.getValue());
            } else {
                result.put("authMessage", "Multiple errors occurred, please refer to the gateway response messages");
                result.put("internalRespMsgs", errors);
            }
        } else {
            result.put("authResult", true);
            result.put("processAmount", new BigDecimal(decoder.get("AMT")));
            result.put("authRefNum", decoder.get("TRANSACTIONID"));
        }
        //TODO: Look into possible PAYMENTSTATUS and PENDINGREASON return codes, it is unclear what should be checked for this type of transaction
        return result;
    }

    public static Map<String, Object> doCapture(DispatchContext dctx, Map<String, Object> context) {
        GenericValue paymentPref = (GenericValue) context.get("orderPaymentPreference");
        BigDecimal captureAmount = (BigDecimal) context.get("captureAmount");
        GenericValue payPalConfig = getPaymentMethodGatewayPayPal(dctx, context, PaymentGatewayServices.AUTH_SERVICE_TYPE);
        GenericValue authTrans = (GenericValue) context.get("authTrans");
        if (authTrans == null) {
            authTrans = PaymentGatewayServices.getAuthTransaction(paymentPref);
        }

        NVPEncoder encoder = new NVPEncoder();
        encoder.add("METHOD", "DoCapture");
        encoder.add("AUTHORIZATIONID", authTrans.getString("referenceNum"));
        encoder.add("AMT", captureAmount.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString());
        encoder.add("CURRENCYCODE", authTrans.getString("currencyUomId"));
        encoder.add("COMPLETETYPE", "NotComplete");

        NVPDecoder decoder = null;
        try {
            decoder = sendNVPRequest(payPalConfig, encoder);
        } catch (PayPalException e) {
            Debug.logError(e, module);
            return ServiceUtil.returnError(e.getMessage());
        }

        if (decoder == null) {
            return ServiceUtil.returnError("An unknown error occurred while contacting PayPal");
        }

        Map<String, Object> result = ServiceUtil.returnSuccess();
        Map<String, String> errors = getErrorMessageMap(decoder);
        if (UtilValidate.isNotEmpty(errors)) {
            result.put("captureResult", false);
            result.put("captureRefNum", "N/A");
            result.put("captureAmount", BigDecimal.ZERO);
            if (errors.size() == 1) {
                Map.Entry<String, String> error = errors.entrySet().iterator().next();
                result.put("captureCode", error.getKey());
                result.put("captureMessage", error.getValue());
            } else {
                result.put("captureMessage", "Multiple errors occurred, please refer to the gateway response messages");
                result.put("internalRespMsgs", errors);
            }
        } else {
            result.put("captureResult", true);
            result.put("captureAmount", new BigDecimal(decoder.get("AMT")));
            result.put("captureRefNum", decoder.get("TRANSACTIONID"));
        }
        //TODO: Look into possible PAYMENTSTATUS and PENDINGREASON return codes, it is unclear what should be checked for this type of transaction
        return result;
    }

    public static Map<String, Object> doVoid(DispatchContext dctx, Map<String, Object> context) {
        GenericValue payPalConfig = getPaymentMethodGatewayPayPal(dctx, context, null);
        if (payPalConfig == null) {
            return ServiceUtil.returnError("Couldn't retrieve a PaymentGatewayConfigPayPal record for Express Checkout, cannot continue.");
        }
        GenericValue orderPaymentPreference = (GenericValue) context.get("orderPaymentPreference");
        GenericValue authTrans = PaymentGatewayServices.getAuthTransaction(orderPaymentPreference);
        NVPEncoder encoder = new NVPEncoder();
        encoder.add("METHOD", "DoVoid");
        encoder.add("AUTHORIZATIONID", authTrans.getString("referenceNum"));
        NVPDecoder decoder = null;
        try {
            decoder = sendNVPRequest(payPalConfig, encoder);
        } catch (PayPalException e) {
            Debug.logError(e, module);
            return ServiceUtil.returnError(e.getMessage());
        }

        if (decoder == null) {
            return ServiceUtil.returnError("An unknown error occurred while contacting PayPal");
        }

        Map<String, Object> result = ServiceUtil.returnSuccess();
        Map<String, String> errors = getErrorMessageMap(decoder);
        if (UtilValidate.isNotEmpty(errors)) {
            result.put("releaseResult", false);
            result.put("releaseRefNum", authTrans.getString("referenceNum"));
            result.put("releaseAmount", BigDecimal.ZERO);
            if (errors.size() == 1) {
                Map.Entry<String, String> error = errors.entrySet().iterator().next();
                result.put("releaseCode", error.getKey());
                result.put("releaseMessage", error.getValue());
            } else {
                result.put("releaseMessage", "Multiple errors occurred, please refer to the gateway response messages");
                result.put("internalRespMsgs", errors);
            }
        } else {
            result.put("releaseResult", true);
            // PayPal voids the entire order amount minus any captures, that's a little difficult to figure out here
            // so until further testing proves we should do otherwise I'm just going to return requested void amount
            result.put("releaseAmount", context.get("releaseAmount"));
            result.put("releaseRefNum", decoder.get("AUTHORIZATIONID"));
        }
        return result;
    }

    public static Map<String, Object> doRefund (DispatchContext dctx, Map<String, Object> context) {
        GenericValue payPalConfig = getPaymentMethodGatewayPayPal(dctx, context, null);
        if (payPalConfig == null) {
            return ServiceUtil.returnError("Couldn't retrieve a PaymentGatewayConfigPayPal record for Express Checkout, cannot continue.");
        }
        GenericValue orderPaymentPreference = (GenericValue) context.get("orderPaymentPreference");
        GenericValue captureTrans = PaymentGatewayServices.getCaptureTransaction(orderPaymentPreference);
        BigDecimal refundAmount = (BigDecimal) context.get("refundAmount");
        NVPEncoder encoder = new NVPEncoder();
        encoder.add("METHOD", "RefundTransaction");
        encoder.add("TRANSACTIONID", captureTrans.getString("referenceNum"));
        encoder.add("REFUNDTYPE", "Partial");
        encoder.add("CURRENCYCODE", captureTrans.getString("currencyUomId"));
        encoder.add("AMT", refundAmount.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString());
        encoder.add("NOTE", "Order #" + orderPaymentPreference.getString("orderId"));
        NVPDecoder decoder = null;
        try {
            decoder = sendNVPRequest(payPalConfig, encoder);
        } catch (PayPalException e) {
            Debug.logError(e, module);
            return ServiceUtil.returnError(e.getMessage());
        }

        if (decoder == null) {
            return ServiceUtil.returnError("An unknown error occurred while contacting PayPal");
        }

        Map<String, Object> result = ServiceUtil.returnSuccess();
        Map<String, String> errors = getErrorMessageMap(decoder);
        if (UtilValidate.isNotEmpty(errors)) {
            result.put("refundResult", false);
            result.put("refundRefNum", captureTrans.getString("referenceNum"));
            result.put("refundAmount", BigDecimal.ZERO);
            if (errors.size() == 1) {
                Map.Entry<String, String> error = errors.entrySet().iterator().next();
                result.put("refundCode", error.getKey());
                result.put("refundMessage", error.getValue());
            } else {
                result.put("refundMessage", "Multiple errors occurred, please refer to the gateway response messages");
                result.put("internalRespMsgs", errors);
            }
        } else {
            result.put("refundResult", true);
            result.put("refundAmount", new BigDecimal(decoder.get("GROSSREFUNDAMT")));
            result.put("refundRefNum", decoder.get("REFUNDTRANSACTIONID"));
        }
        return result;
    }

    private static GenericValue getPaymentMethodGatewayPayPal(DispatchContext dctx, Map<String, ? extends Object> context, String paymentServiceTypeEnumId) {
        Delegator delegator = dctx.getDelegator();
        String paymentGatewayConfigId = (String) context.get("paymentGatewayConfigId");
        GenericValue payPalGatewayConfig = null;

        if (paymentGatewayConfigId == null) {
            String productStoreId = null;
            GenericValue orderPaymentPreference = (GenericValue) context.get("orderPaymentPreference");
            if (orderPaymentPreference != null) {
                OrderReadHelper orh = new OrderReadHelper(delegator, orderPaymentPreference.getString("orderId"));
                productStoreId = orh.getProductStoreId();
            } else {
                ShoppingCart cart = (ShoppingCart) context.get("cart");
                if (cart != null) {
                    productStoreId = cart.getProductStoreId();
                }
            }
            if (productStoreId != null) {
                GenericValue payPalPaymentSetting = ProductStoreWorker.getProductStorePaymentSetting(delegator, productStoreId, "EXT_PAYPAL", paymentServiceTypeEnumId, true);
                if (payPalPaymentSetting != null) {
                    paymentGatewayConfigId = payPalPaymentSetting.getString("paymentGatewayConfigId");
                }
            }
        }
        if (paymentGatewayConfigId != null) {
            try {
                payPalGatewayConfig = delegator.findOne("PaymentGatewayPayPal", true, "paymentGatewayConfigId", paymentGatewayConfigId);
            } catch (GenericEntityException e) {
                Debug.logError(e, module);
            }
        }
        return payPalGatewayConfig;
    }

    private static NVPDecoder sendNVPRequest(GenericValue payPalConfig, NVPEncoder encoder) throws PayPalException {
        NVPCallerServices caller = new NVPCallerServices();
        try {
            APIProfile profile = ProfileFactory.createSignatureAPIProfile();
            profile.setAPIUsername(payPalConfig.getString("apiUserName"));
            profile.setAPIPassword(payPalConfig.getString("apiPassword"));
            profile.setSignature(payPalConfig.getString("apiSignature"));
            profile.setEnvironment(payPalConfig.getString("apiEnvironment"));
            caller.setAPIProfile(profile);
        } catch (PayPalException e) {
            Debug.logError(e.getMessage(), module);
        }

        String requestMessage = encoder.encode();
        String responseMessage = caller.call(requestMessage);

        NVPDecoder decoder = new NVPDecoder();
        decoder.decode(responseMessage);
        if (!"Success".equals(decoder.get("ACK"))) {
            Debug.logError("A response other than success was received from PayPal: " + responseMessage, module);
        }

        return decoder;
    }

    private static String getCountryGeoIdFromGeoCode(String geoCode, Delegator delegator) {
        String geoId = null;
        try {
            EntityCondition cond =EntityCondition.makeCondition(UtilMisc.toMap("geoTypeId", "COUNTRY", "geoCode", geoCode));
            GenericValue countryGeo = EntityUtil.getFirst(delegator.findList("Geo", cond, null, null, null, true));
            if (countryGeo != null) {
                geoId = countryGeo.getString("geoId");
            }
        } catch (GenericEntityException e) {
            Debug.logError(e, module);
        }
        return geoId;
    }

    private static String parseStateProvinceGeoId(String payPalShipToState, String countryGeoId, Delegator delegator) {
        String lookupField = "geoName";
        List<EntityCondition> conditionList = FastList.newInstance();
        conditionList.add(EntityCondition.makeCondition("geoAssocTypeId", "REGIONS"));
        if ("USA".equals(countryGeoId) || "CAN".equals(countryGeoId)) {
            // PayPal returns two letter code for US and Canadian States/Provinces
            String geoTypeId = "USA".equals(countryGeoId) ? "STATE" : "PROVINCE";
            conditionList.add(EntityCondition.makeCondition("geoTypeId", geoTypeId));
            lookupField = "geoCode";
        }
        conditionList.add(EntityCondition.makeCondition("geoIdFrom", countryGeoId));
        conditionList.add(EntityCondition.makeCondition(lookupField, payPalShipToState));
        EntityCondition cond = EntityCondition.makeCondition(conditionList);
        GenericValue geoAssocAndGeoTo = null;
        try {
            geoAssocAndGeoTo = EntityUtil.getFirst(delegator.findList("GeoAssocAndGeoTo", cond, null, null, null, true));
        } catch (GenericEntityException e) {
            Debug.logError(e, module);
        }
        if (geoAssocAndGeoTo != null) {
            return geoAssocAndGeoTo.getString("geoId");
        }
        return null;
    }

    public static class TokenWrapper implements Serializable {
        String theString;
        public TokenWrapper(String theString) {
            this.theString = theString;
        }

        @Override
        public boolean equals(Object o) {
            if (o == null) return false;
            if (!(o instanceof TokenWrapper)) return false;
            TokenWrapper other = (TokenWrapper) o;
            return theString.equals(other.theString);
        }
        @Override
        public int hashCode() {
            return theString.hashCode();
        }
    }
}
