/******************************************************************************* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. *******************************************************************************/ package org.apache.ofbiz.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.HashMap; import java.util.LinkedList; 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 org.apache.commons.lang.StringUtils; import org.apache.ofbiz.accounting.payment.PaymentGatewayServices; import org.apache.ofbiz.base.util.Debug; import org.apache.ofbiz.base.util.GeneralException; import org.apache.ofbiz.base.util.StringUtil; import org.apache.ofbiz.base.util.UtilDateTime; import org.apache.ofbiz.base.util.UtilHttp; import org.apache.ofbiz.base.util.UtilMisc; import org.apache.ofbiz.base.util.UtilProperties; import org.apache.ofbiz.base.util.UtilValidate; import org.apache.ofbiz.entity.Delegator; import org.apache.ofbiz.entity.GenericEntityException; import org.apache.ofbiz.entity.GenericValue; import org.apache.ofbiz.entity.condition.EntityComparisonOperator; import org.apache.ofbiz.entity.condition.EntityCondition; import org.apache.ofbiz.entity.condition.EntityFunction; import org.apache.ofbiz.entity.transaction.GenericTransactionException; import org.apache.ofbiz.entity.transaction.TransactionUtil; import org.apache.ofbiz.entity.util.EntityQuery; import org.apache.ofbiz.entity.util.EntityUtil; import org.apache.ofbiz.order.order.OrderReadHelper; import org.apache.ofbiz.order.shoppingcart.CartItemModifyException; import org.apache.ofbiz.order.shoppingcart.CheckOutHelper; import org.apache.ofbiz.order.shoppingcart.ShoppingCart; import org.apache.ofbiz.order.shoppingcart.ShoppingCartItem; import org.apache.ofbiz.order.shoppingcart.ShoppingCart.CartShipInfo; import org.apache.ofbiz.order.shoppingcart.shipping.ShippingEstimateWrapper; import org.apache.ofbiz.order.shoppingcart.shipping.ShippingEvents; import org.apache.ofbiz.product.store.ProductStoreWorker; import org.apache.ofbiz.service.DispatchContext; import org.apache.ofbiz.service.GenericServiceException; import org.apache.ofbiz.service.LocalDispatcher; import org.apache.ofbiz.service.ModelService; import org.apache.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(); public final static String resource = "AccountingErrorUiLabels"; // 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(resource, "AccountingPayPalShoppingCartIsEmpty", locale)); } GenericValue payPalConfig = getPaymentMethodGatewayPayPal(dctx, context, null); if (payPalConfig == null) { return ServiceUtil.returnError(UtilProperties.getMessage(resource, "AccountingPayPalPaymentGatewayConfigCannotFind", locale)); } 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 { encoder.add("CALLBACK", payPalConfig.getString("shippingCallbackUrl")); encoder.add("CALLBACKTIMEOUT", "6"); // Default to no String reqConfirmShipping = "Y".equals(payPalConfig.getString("requireConfirmedShipping")) ? "1" : "0"; encoder.add("REQCONFIRMSHIPPING", reqConfirmShipping); // Default shipment method encoder.add("L_SHIPPINGOPTIONISDEFAULT0", "true"); encoder.add("L_SHIPPINGOPTIONNAME0", "Calculated Offline"); encoder.add("L_SHIPPINGOPTIONAMOUNT0", "0.00"); } 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(UtilProperties.getMessage(resource, "AccountingPayPalErrorDuringRetrievingCartDetails", locale)); } 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 = new HashMap<String, Object>(); 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 = EntityQuery.use(delegator).from("UserLogin").where("userLoginId", "system").cache().queryOne(); 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.setAllShipmentMethodTypeId(shipMethod.getString("shipmentMethodTypeId")); cart.setAllCarrierPartyId(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 = EntityQuery.use(delegator).from("PostalAddress").where("contactMechId", contactMechId).queryOne(); postalAddress.remove(); GenericValue contactMech = EntityQuery.use(delegator).from("ContactMech").where("contactMechId", contactMechId).queryOne(); 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 = new HashMap<String, Object>(); 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++; } encoder.add("ITEMAMT", cart.getSubTotal().add(otherAdjustments).setScale(2).toPlainString()); encoder.add("SHIPPINGAMT", "0.00"); encoder.add("TAXAMT", "0.00"); encoder.add("AMT", cart.getSubTotal().add(otherAdjustments).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) { Locale locale = (Locale) context.get("locale"); 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(UtilProperties.getMessage(resource, "AccountingPayPalPaymentGatewayConfigCannotFind", locale)); } 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(UtilProperties.getMessage(resource, "AccountingPayPalTokenNotFound", locale)); } 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 = EntityQuery.use(delegator).from("UserLogin").where("userLoginId", "anonymous").queryOne(); 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()); } } 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 = EntityQuery.use(delegator).from("Party").where("partyId", partyId).queryOne(); } catch (GenericEntityException e) { Debug.logError(e, module); } if (party == null) { partyId = null; } } Map<String, Object> inMap = new HashMap<String, Object>(); 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)) )); try { GenericValue matchingEmail = EntityQuery.use(delegator).from("PartyAndContactMech").where(cond).orderBy("fromDate").filterByDate().queryFirst(); 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 long primaryEmails = EntityQuery.use(delegator) .from("PartyContactWithPurpose") .where("partyId", partyId, "contactMechTypeId", "EMAIL_ADDRESS", "contactMechPurposeTypeId", "PRIMARY_EMAIL") .filterByDate("contactFromDate", "contactThruDate", "purposeFromDate", "purposeThruDate") .queryCount(); if (primaryEmails > 0) emailContactPurposeTypeId = "BILLING_EMAIL"; } } catch (GenericEntityException e) { Debug.logError(e, 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); // 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); } } // 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 = new HashMap<String, Object>(); 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)), EntityCondition.makeCondition("partyId", partyId) )); try { GenericValue postalMatch = EntityQuery.use(delegator).from("PartyAndPostalAddress") .where(cond).orderBy("fromDate").filterByDate().queryFirst(); if (postalMatch != null) { postalContactId = postalMatch.getString("contactMechId"); List<GenericValue> postalPurposes = EntityQuery.use(delegator).from("PartyContactMechPurpose") .where("partyId", partyId, "contactMechId", postalContactId) .filterByDate().queryList(); 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); } } 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()); } } 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.logInfo(e.getMessage(), 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"); if ("Calculated Offline".equals(shipMethod)) { cart.setAllCarrierPartyId("_NA_"); cart.setAllShipmentMethodTypeId("NO_SHIPPING"); } else { String[] shipMethodSplit = shipMethod.split(" - "); cart.setAllCarrierPartyId(shipMethodSplit[0]); String shippingMethodTypeDesc = StringUtils.join(shipMethodSplit, " - ", 1, shipMethodSplit.length); try { GenericValue shipmentMethod = EntityQuery.use(delegator) .from("ProductStoreShipmentMethView") .where("productStoreId", cart.getProductStoreId(), "partyId", shipMethodSplit[0], "roleTypeId", "CARRIER", "description", shippingMethodTypeDesc) .queryFirst(); cart.setAllShipmentMethodTypeId(shipmentMethod.getString("shipmentMethodTypeId")); } catch (GenericEntityException e1) { Debug.logError(e1, 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.setAllShippingContactMechId(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)); } BigDecimal shippingTotal = (BigDecimal) result.get("shippingTotal"); if (shippingTotal == null) { shippingTotal = BigDecimal.ZERO; } cart.setItemShipGroupEstimate(shippingTotal, 0); CheckOutHelper cho = new CheckOutHelper(dispatcher, delegator, cart); try { cho.calcAndAddTax(); } catch (GeneralException e) { Debug.logError(e, module); return ServiceUtil.returnError(e.getMessage()); } // 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"); cart.clearPayments(); BigDecimal maxAmount = cart.getGrandTotal().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")); Locale locale = (Locale) context.get("locale"); GenericValue payPalPaymentSetting = getPaymentMethodGatewayPayPal(dctx, context, null); GenericValue payPalPaymentMethod = null; try { payPalPaymentMethod = paymentPref.getRelatedOne("PaymentMethod", false); payPalPaymentMethod = payPalPaymentMethod.getRelatedOne("PayPalPaymentMethod", false); } 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 = grandTotal.subtract(shippingTotal).subtract(taxTotal).setScale(2, BigDecimal.ROUND_HALF_UP); encoder.add("ITEMAMT", subTotal.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(UtilProperties.getMessage(resource, "AccountingPayPalUnknownError", locale)); } 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 = new HashMap<String, Object>(); 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); Locale locale = (Locale) context.get("locale"); 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(UtilProperties.getMessage(resource, "AccountingPayPalUnknownError", locale)); } 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"); Locale locale = (Locale) context.get("locale"); 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(UtilProperties.getMessage(resource, "AccountingPayPalUnknownError", locale)); } 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); Locale locale = (Locale) context.get("locale"); if (payPalConfig == null) { return ServiceUtil.returnError(UtilProperties.getMessage(resource, "AccountingPayPalPaymentGatewayConfigCannotFind", locale)); } 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(UtilProperties.getMessage(resource, "AccountingPayPalUnknownError", locale)); } 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) { Locale locale = (Locale) context.get("locale"); GenericValue payPalConfig = getPaymentMethodGatewayPayPal(dctx, context, null); if (payPalConfig == null) { return ServiceUtil.returnError(UtilProperties.getMessage(resource, "AccountingPayPalPaymentGatewayConfigCannotFind", locale)); } 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(UtilProperties.getMessage(resource, "AccountingPayPalUnknownError", locale)); } 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 = EntityQuery.use(delegator).from("PaymentGatewayPayPal").where("paymentGatewayConfigId", paymentGatewayConfigId).cache().queryOne(); } 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 { GenericValue countryGeo = EntityQuery.use(delegator).from("Geo") .where("geoTypeId", "COUNTRY", "geoCode", geoCode).cache().queryFirst(); 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 = new LinkedList<EntityCondition>(); 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 = EntityQuery.use(delegator).from("GeoAssocAndGeoTo").where(cond).cache().queryFirst(); } catch (GenericEntityException e) { Debug.logError(e, module); } if (geoAssocAndGeoTo != null) { return geoAssocAndGeoTo.getString("geoId"); } return null; } @SuppressWarnings("serial") 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(); } } }