Java tutorial
/******************************************************************************* * 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(); } } }