blob: 8da912b869adb7dd4613d328f51dccb1c8ef98b6 [file] [log] [blame]
/*******************************************************************************
* 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();
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 = 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++;
}
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 = 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());
}
}
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;
}
}
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);
}
}
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 = 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);
}
}
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.setCarrierPartyId("_NA_");
cart.setShipmentMethodTypeId("NO_SHIPPING");
} else {
String[] shipMethodSplit = shipMethod.split(" - ");
cart.setCarrierPartyId(shipMethodSplit[0]);
String shippingMethodTypeDesc = StringUtils.join(shipMethodSplit, " - ", 1, shipMethodSplit.length);
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));
cart.setShipmentMethodTypeId(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.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));
}
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");
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 = 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 = 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);
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 = 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;
}
@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();
}
}
}