/* jBilling - The Enterprise Open Source Billing System Copyright (C) 2003-2011 Enterprise jBilling Software Ltd. and Emiliano Conde This file is part of jbilling. jbilling is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. jbilling is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with jbilling. If not, see <http://www.gnu.org/licenses/>. */ package com.sapienter.jbilling.server.payment.tasks; import java.math.BigDecimal; import java.math.RoundingMode; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import org.apache.log4j.Logger; import com.paypal.sdk.exceptions.PayPalException; import com.sapienter.jbilling.common.CommonConstants; import com.sapienter.jbilling.common.Util; import com.sapienter.jbilling.server.payment.IExternalCreditCardStorage; import com.sapienter.jbilling.server.payment.PaymentAuthorizationBL; import com.sapienter.jbilling.server.payment.PaymentDTOEx; import com.sapienter.jbilling.server.payment.db.PaymentAuthorizationDTO; import com.sapienter.jbilling.server.payment.db.PaymentDAS; import com.sapienter.jbilling.server.payment.db.PaymentDTO; import com.sapienter.jbilling.server.payment.db.PaymentMethodDAS; import com.sapienter.jbilling.server.payment.db.PaymentResultDAS; import com.sapienter.jbilling.server.payment.tasks.paypal.PaypalApi; import com.sapienter.jbilling.server.payment.tasks.paypal.dto.CreditCard; import com.sapienter.jbilling.server.payment.tasks.paypal.dto.Payer; import com.sapienter.jbilling.server.payment.tasks.paypal.dto.Payment; import com.sapienter.jbilling.server.payment.tasks.paypal.dto.PaypalResult; import com.sapienter.jbilling.server.pluggableTask.PaymentTaskWithTimeout; import com.sapienter.jbilling.server.pluggableTask.admin.ParameterDescription; import com.sapienter.jbilling.server.pluggableTask.admin.PluggableTaskException; import com.sapienter.jbilling.server.user.ContactBL; import com.sapienter.jbilling.server.user.UserBL; import com.sapienter.jbilling.server.user.contact.db.ContactDTO; import com.sapienter.jbilling.server.user.db.CreditCardDTO; import com.sapienter.jbilling.server.user.db.AchDTO; import com.sapienter.jbilling.server.user.db.UserDTO; import com.sapienter.jbilling.server.util.Constants; import com.sapienter.jbilling.server.payment.tasks.paypal.dto.*; import com.sapienter.jbilling.server.payment.db.*; /** * Created by Roman Liberov, 03/02/2010 */ public class PaymentPaypalExternalTask extends PaymentTaskWithTimeout implements IExternalCreditCardStorage { private static final Logger LOG = Logger.getLogger(PaymentPaypalExternalTask.class); /* Plugin parameters */ public static final ParameterDescription PARAMETER_PAYPAL_USER_ID = new ParameterDescription("PaypalUserId", true, ParameterDescription.Type.STR); public static final ParameterDescription PARAMETER_PAYPAL_PASSWORD = new ParameterDescription("PaypalPassword", true, ParameterDescription.Type.STR); public static final ParameterDescription PARAMETER_PAYPAL_SIGNATURE = new ParameterDescription("PaypalSignature", true, ParameterDescription.Type.STR); public static final ParameterDescription PARAMETER_PAYPAL_ENVIRONMENT = new ParameterDescription("PaypalEnvironment", false, ParameterDescription.Type.STR); public static final ParameterDescription PARAMETER_PAYPAL_SUBJECT = new ParameterDescription("PaypalSubject", false, ParameterDescription.Type.STR); public String getUserId() throws PluggableTaskException { return ensureGetParameter(PARAMETER_PAYPAL_USER_ID.getName()); } public String getPassword() throws PluggableTaskException { return ensureGetParameter(PARAMETER_PAYPAL_PASSWORD.getName()); } public String getSignature() throws PluggableTaskException { return ensureGetParameter(PARAMETER_PAYPAL_SIGNATURE.getName()); } public String getEnvironment() throws PluggableTaskException { return getOptionalParameter(PARAMETER_PAYPAL_ENVIRONMENT.getName(), "Live"); } public String getSubject() { return getOptionalParameter(PARAMETER_PAYPAL_SUBJECT.getName(), ""); } //initializer for pluggable params { descriptions.add(PARAMETER_PAYPAL_USER_ID); descriptions.add(PARAMETER_PAYPAL_PASSWORD); descriptions.add(PARAMETER_PAYPAL_SIGNATURE); descriptions.add(PARAMETER_PAYPAL_ENVIRONMENT); descriptions.add(PARAMETER_PAYPAL_SUBJECT); } private PaypalApi getApi() throws PluggableTaskException, PayPalException { return new PaypalApi(getUserId(), getPassword(), getSignature(), getEnvironment(), getSubject(), getTimeoutSeconds() * 1000); } /** * Prepares a given payment to be processed using an external storage gateway key instead of * the raw credit card number. If the associated credit card has been obscured it will be * replaced with the users stored credit card from the database, which contains all the relevant * external storage data. * * New or un-obscured credit cards will be left as is. * * @param payment payment to prepare for processing from external storage */ public void prepareExternalPayment(PaymentDTOEx payment) { if (payment.getCreditCard().useGatewayKey()) { LOG.debug("credit card is obscured, retrieving from database to use external store."); payment.setCreditCard(new UserBL(payment.getUserId()).getCreditCard()); } else { LOG.debug("new credit card or previously un-obscured, using as is."); } } /** * Updates the gateway key of the credit card associated with this payment. PayPal * returns a TRANSACTIONID which can be used to start new transaction without specifying * payer info. * * @param payment successful payment containing the credit card to update. * */ public void updateGatewayKey(PaymentDTOEx payment) { PaymentAuthorizationDTO auth = payment.getAuthorization(); // update the gateway key with the returned PayPal TRANSACTIONID CreditCardDTO card = payment.getCreditCard(); card.setGatewayKey(auth.getTransactionId()); // obscure new credit card numbers if (!com.sapienter.jbilling.common.Constants.PAYMENT_METHOD_GATEWAY_KEY.equals(card.getCcType())) card.obscureNumber(); } /** * Utility method to format the given dollar float value to a two * digit number in compliance with the PayPal gateway API. * * @param amount dollar float value to format * @return formatted amount as a string */ private static String formatDollarAmount(BigDecimal amount) { amount = amount.abs().setScale(2, RoundingMode.HALF_EVEN); // gateway format, do not change! return amount.toPlainString(); } /** * Utility method to check if a given {@link PaymentDTOEx} payment can be processed * by this task. * * @param payment payment to check * @return true if payment can be processed with this task, false if not */ private static boolean isApplicable(PaymentDTOEx payment) { if (payment.getCreditCard() == null && payment.getAch() == null) { LOG.warn("Can't process without a credit card or ach"); return false; } return true; } /** * Returns the name of this payment processor. * @return payment processor name */ private String getProcessorName() { return "PayPal"; } private static boolean isRefund(PaymentDTOEx payment) { return BigDecimal.ZERO.compareTo(payment.getAmount()) > 0 || payment.getIsRefund() != 0; } private static boolean isCreditCardStored(PaymentDTOEx payment) { return payment.getCreditCard().useGatewayKey(); } private PaymentAuthorizationDTO buildPaymentAuthorization(PaypalResult paypalResult) { LOG.debug("Payment authorization result of " + getProcessorName() + " gateway parsing...."); PaymentAuthorizationDTO paymentAuthDTO = new PaymentAuthorizationDTO(); paymentAuthDTO.setProcessor(getProcessorName()); String txID = paypalResult.getTransactionId(); if (txID != null) { paymentAuthDTO.setTransactionId(txID); paymentAuthDTO.setCode1(txID); LOG.debug("transactionId/code1 [" + txID + "]"); } String errorMsg = paypalResult.getErrorCode(); if (errorMsg != null) { paymentAuthDTO.setResponseMessage(errorMsg); LOG.debug("errorMessage [" + errorMsg + "]"); } String avs = paypalResult.getAvs(); if(avs != null) { paymentAuthDTO.setAvs(avs); LOG.debug("avs [" + avs + "]"); } return paymentAuthDTO; } private static String convertCreditCardType(int ccType) { switch(ccType) { case 2: return CreditCardType.VISA.toString(); case 3: return CreditCardType.MASTER_CARD.toString(); case 4: return CreditCardType.AMEX.toString(); case 6: return CreditCardType.DISCOVER.toString(); } return ""; } private static String convertCreditCardExpiration(Date ccExpiry) { return new SimpleDateFormat("MMyyyy").format(ccExpiry); } private static CreditCard convertCreditCard(PaymentDTOEx payment) { return new CreditCard( convertCreditCardType(payment.getCreditCard().getCcType()), payment.getCreditCard().getCcNumberPlain(), convertCreditCardExpiration(payment.getCreditCard().getExpiry()), payment.getCreditCard().getSecurityCode()); } private static Payer convertPayer(PaymentDTOEx payment) { ContactBL contactBL = new ContactBL(); contactBL.set(payment.getUserId()); ContactDTO contact = contactBL.getEntity(); Payer payer = new Payer(); payer.setEmail(contact.getEmail()); payer.setFirstName(contact.getFirstName()); payer.setLastName(contact.getLastName()); payer.setStreet(contact.getAddress1()); payer.setCity(contact.getCity()); payer.setState(contact.getStateProvince()); payer.setCountryCode(contact.getCountryCode()); payer.setZip(contact.getPostalCode()); return payer; } private void storePaypalResult(PaypalResult result, PaymentDTOEx payment, PaymentAuthorizationDTO paymentAuthorization, boolean updateKey) { if(result.isSucceseeded()) { payment.setPaymentResult(new PaymentResultDAS().find(Constants.RESULT_OK)); new PaymentAuthorizationBL().create(paymentAuthorization, payment.getId()); payment.setAuthorization(paymentAuthorization); if(updateKey) { updateGatewayKey(payment); } } else { payment.setPaymentResult(new PaymentResultDAS().find(Constants.RESULT_FAIL)); } } private Result doRefund(PaymentDTOEx payment) throws PluggableTaskException { try { PaypalApi api = getApi(); PaypalResult result = api.refundTransaction( payment.getAuthorization().getTransactionId(), formatDollarAmount(payment.getAmount()), RefundType.FULL); PaymentAuthorizationDTO paymentAuthorization = buildPaymentAuthorization(result); storePaypalResult(result, payment, paymentAuthorization, false); return new Result(paymentAuthorization, false); } catch (PayPalException e) { LOG.error("Couldn't handle payment request due to error", e); payment.setPaymentResult(new PaymentResultDAS().find(Constants.RESULT_UNAVAILABLE)); return NOT_APPLICABLE; } } private Result doPaymentWithStoredCreditCard(PaymentDTOEx payment, PaymentAction paymentAction) throws PluggableTaskException { try { PaypalResult result = getApi().doReferenceTransaction( payment.getAuthorization().getTransactionId(), paymentAction, new Payment(formatDollarAmount(payment.getAmount()), "USD")); PaymentAuthorizationDTO paymentAuthorization = buildPaymentAuthorization(result); storePaypalResult(result, payment, paymentAuthorization, true); return new Result(paymentAuthorization, false); } catch (PayPalException e) { LOG.error("Couldn't handle payment request due to error", e); payment.setPaymentResult(new PaymentResultDAS().find(Constants.RESULT_UNAVAILABLE)); return NOT_APPLICABLE; } } private Result doPaymentWithoutStoredCreditCard(PaymentDTOEx payment, PaymentAction paymentAction, boolean updateKey) throws PluggableTaskException { try { PaypalResult result = getApi().doDirectPayment( paymentAction, convertPayer(payment), convertCreditCard(payment), new Payment(formatDollarAmount(payment.getAmount()), "USD")); PaymentAuthorizationDTO paymentAuthorization = buildPaymentAuthorization(result); storePaypalResult(result, payment, paymentAuthorization, updateKey); return new Result(paymentAuthorization, false); } catch (PayPalException e) { LOG.error("Couldn't handle payment request due to error", e); payment.setPaymentResult(new PaymentResultDAS().find(Constants.RESULT_UNAVAILABLE)); return NOT_APPLICABLE; } } private Result doCapture(PaymentDTOEx payment, PaymentAuthorizationDTO auth) throws PluggableTaskException { try { PaypalResult result = getApi().doCapture( auth.getTransactionId(), new Payment(formatDollarAmount(payment.getAmount()), "USD"), CompleteType.COMPLETE); PaymentAuthorizationDTO paymentAuthorization = buildPaymentAuthorization(result); storePaypalResult(result, payment, paymentAuthorization, true); return new Result(paymentAuthorization, false); } catch (PayPalException e) { LOG.error("Couldn't handle payment request due to error", e); payment.setPaymentResult(new PaymentResultDAS().find(Constants.RESULT_UNAVAILABLE)); return NOT_APPLICABLE; } } private boolean doProcess(PaymentDTOEx payment, PaymentAction paymentAction, boolean updateKey) throws PluggableTaskException { if(isRefund(payment)) { return doRefund(payment).shouldCallOtherProcessors(); } if(isCreditCardStored(payment)) { return doPaymentWithStoredCreditCard(payment, paymentAction) .shouldCallOtherProcessors(); } return doPaymentWithoutStoredCreditCard(payment, paymentAction, updateKey) .shouldCallOtherProcessors(); } private void doVoid(PaymentDTOEx payment) throws PluggableTaskException { try { getApi().doVoid(payment.getAuthorization().getTransactionId()); } catch (PayPalException e) { LOG.error("Couldn't void payment authorization due to error", e); throw new PluggableTaskException(e); } } public boolean process(PaymentDTOEx payment) throws PluggableTaskException { LOG.debug("Payment processing for " + getProcessorName() + " gateway"); if (payment.getPayoutId() != null) { return true; } if(!isApplicable(payment)) { return NOT_APPLICABLE.shouldCallOtherProcessors(); } prepareExternalPayment(payment); return doProcess(payment, PaymentAction.SALE, true /* updateKey */); } public void failure(Integer userId, Integer retry) { // do nothing } public boolean preAuth(PaymentDTOEx payment) throws PluggableTaskException { LOG.debug("Pre-authorization processing for " + getProcessorName() + " gateway"); prepareExternalPayment(payment); return doProcess(payment, PaymentAction.AUTHORIZATION, true /* updateKey */); } public boolean confirmPreAuth(PaymentAuthorizationDTO auth, PaymentDTOEx payment) throws PluggableTaskException { LOG.debug("Confirming pre-authorization for " + getProcessorName() + " gateway"); if (!getProcessorName().equals(auth.getProcessor())) { /* let the processor be called and fail, so the caller can do something about it: probably re-call this payment task as a new "process()" run */ LOG.warn("The processor of the pre-auth is not " + getProcessorName() + ", is " + auth.getProcessor()); } CreditCardDTO card = payment.getCreditCard(); if (card == null) { throw new PluggableTaskException("Credit card is required, capturing payment: " + payment.getId()); } if (!isApplicable(payment)) { LOG.error("This payment can not be captured: " + payment); return NOT_APPLICABLE.shouldCallOtherProcessors(); } // process prepareExternalPayment(payment); return doCapture(payment, auth).shouldCallOtherProcessors(); } public String storeCreditCard(ContactDTO contact, CreditCardDTO creditCard, AchDTO ach) { LOG.debug("Storing creadit card info within " + getProcessorName() + " gateway"); UserDTO user; if (contact != null) { UserBL bl = new UserBL(contact.getUserId()); user = bl.getEntity(); creditCard = bl.getCreditCard(); } else if (creditCard != null && !creditCard.getBaseUsers().isEmpty()) { user = creditCard.getBaseUsers().iterator().next(); } else { LOG.error("Could not determine user id for external credit card storage"); return null; } // new contact that has not had a credit card created yet if (creditCard == null) { LOG.warn("No credit card to store externally."); return null; } /* Note, don't use PaymentBL.findPaymentInstrument() as the given creditCard is still being processed at the time that this event is being handled, and will not be found. PaymentBL()#create() will cause a stack overflow as it will attempt to update the credit card, emitting another NewCreditCardEvent which is then handled by this method and repeated. */ PaymentDTO payment = new PaymentDTO(); payment.setBaseUser(user); payment.setCurrency(user.getCurrency()); payment.setAmount(CommonConstants.BIGDECIMAL_ONE_CENT); payment.setCreditCard(creditCard); payment.setPaymentMethod(new PaymentMethodDAS().find(Util.getPaymentMethod(creditCard.getNumber()))); payment.setIsRefund(0); payment.setIsPreauth(0); payment.setDeleted(0); payment.setAttempt(1); payment.setPaymentDate(new Date()); payment.setCreateDatetime(new Date()); PaymentDTOEx paymentEx = new PaymentDTOEx(new PaymentDAS().save(payment)); try { doProcess(paymentEx, PaymentAction.SALE, false /* updateKey */); doVoid(paymentEx); PaymentAuthorizationDTO auth = paymentEx.getAuthorization(); return auth.getTransactionId(); } catch (PluggableTaskException e) { LOG.error("Could not process external storage payment", e); return null; } } /** * */ public String deleteCreditCard(ContactDTO contact, CreditCardDTO creditCard, AchDTO ach) { //noop return null; } }