com.brienwheeler.svc.authorize_net.impl.CIMClientService.java Source code

Java tutorial

Introduction

Here is the source code for com.brienwheeler.svc.authorize_net.impl.CIMClientService.java

Source

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2013 Brien L. Wheeler (brienwheeler@yahoo.com)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.brienwheeler.svc.authorize_net.impl;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;

import com.brienwheeler.lib.monitor.work.MonitoredWork;
import com.brienwheeler.lib.svc.GracefulShutdown;
import net.authorize.Environment;
import net.authorize.Merchant;
import net.authorize.ResponseCode;
import net.authorize.ResponseField;
import net.authorize.ResponseReasonCode;
import net.authorize.cim.Result;
import net.authorize.cim.Transaction;
import net.authorize.cim.TransactionType;
import net.authorize.cim.ValidationModeType;
import net.authorize.data.Order;
import net.authorize.data.cim.CustomerProfile;
import net.authorize.data.cim.DirectResponse;
import net.authorize.data.cim.PaymentProfile;
import net.authorize.data.cim.PaymentTransaction;
import net.authorize.data.xml.Payment;
import net.authorize.util.BasicXmlDocument;
import net.authorize.xml.Message;

import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.entity.EntityBuilder;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.brienwheeler.lib.db.DbValidationUtils;
import com.brienwheeler.lib.db.TransactionWrapper;
import com.brienwheeler.lib.db.domain.DbId;
import com.brienwheeler.lib.util.OperationDisallowedException;
import com.brienwheeler.svc.authorize_net.AmountTooLargeException;
import com.brienwheeler.svc.authorize_net.AuthorizeNetException;
import com.brienwheeler.svc.authorize_net.CardExpiredException;
import com.brienwheeler.svc.authorize_net.ICIMClientService;
import com.brienwheeler.svc.authorize_net.InvalidCardAddressException;
import com.brienwheeler.svc.authorize_net.InvalidCardCodeException;
import com.brienwheeler.svc.authorize_net.InvalidCardNumberException;
import com.brienwheeler.svc.authorize_net.PaymentDeclinedException;
import com.brienwheeler.svc.authorize_net.PaymentMethod;
import com.brienwheeler.svc.users.IUserAttributeService;
import com.brienwheeler.svc.users.IUserService;
import com.brienwheeler.svc.users.domain.User;

public class CIMClientService extends AuthorizeNetClientBase implements ICIMClientService {
    private static final String PRODUCTION_URL = "https://api.authorize.net/xml/v1/request.api";
    private static final String TEST_URL = "https://apitest.authorize.net/xml/v1/request.api";

    private static final String ELEMENT_TOKEN_OPEN = "<token>";
    private static final String ELEMENT_TOKEN_CLOSE = "</token>";

    private static final Map<ResponseReasonCode, Class<? extends AuthorizeNetException>> exceptionMap = new HashMap<ResponseReasonCode, Class<? extends AuthorizeNetException>>();

    private IUserAttributeService userAttributeService;
    private IUserService userService;
    private TransactionWrapper transactionWrapper;

    private Environment environment;
    private Merchant merchant;
    private String apiLoginID;
    private String transactionKey;
    private ValidationModeType validationMode = ValidationModeType.NONE;

    static {
        exceptionMap.put(ResponseReasonCode.RRC_2_27, InvalidCardAddressException.class);
        exceptionMap.put(ResponseReasonCode.RRC_2_37, InvalidCardNumberException.class);
        exceptionMap.put(ResponseReasonCode.RRC_2_44, InvalidCardCodeException.class);
        exceptionMap.put(ResponseReasonCode.RRC_2_65, InvalidCardCodeException.class);
        exceptionMap.put(ResponseReasonCode.RRC_2_127, InvalidCardAddressException.class);
        exceptionMap.put(ResponseReasonCode.RRC_2_315, InvalidCardNumberException.class);
        exceptionMap.put(ResponseReasonCode.RRC_2_317, CardExpiredException.class);
        exceptionMap.put(ResponseReasonCode.RRC_3_6, InvalidCardNumberException.class);
        exceptionMap.put(ResponseReasonCode.RRC_3_8, CardExpiredException.class);
        exceptionMap.put(ResponseReasonCode.RRC_3_49, AmountTooLargeException.class);
        exceptionMap.put(ResponseReasonCode.RRC_3_78, InvalidCardCodeException.class);
    }

    @Override
    protected void onStart() throws InterruptedException {
        super.onStart();
        merchant = Merchant.createMerchant(environment, apiLoginID, transactionKey);
    }

    @Override
    public boolean isProduction() {
        return !merchant.isSandboxEnvironment();
    }

    @Override
    @MonitoredWork
    @GracefulShutdown
    // the write logic inside this function is in its own new transaction, so the interceptor can
    // treat this as a readOnly transaction
    @Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
    public String createCustomerProfile(DbId<User> userId) {
        String existingCustomerProfileId = userAttributeService.getAttribute(userId, ATTR_PROFILE_ID);
        if (existingCustomerProfileId != null)
            return existingCustomerProfileId;

        final User user = userService.findById(userId);
        DbValidationUtils.assertPersisted(user);

        CustomerProfile customerProfile = CustomerProfile.createCustomerProfile();
        customerProfile.setMerchantCustomerId(Long.toString(userId.getId()));

        Transaction transaction = createTransaction(TransactionType.CREATE_CUSTOMER_PROFILE);
        transaction.setCustomerProfile(customerProfile);

        Result<Transaction> result = executeTransaction("create profile", userId, transaction);
        final String createdCustomerProfileId = result.getCustomerProfileId();
        log.info("created Authorize.Net customer profile " + createdCustomerProfileId + " for " + user);

        // we want to commit our own transaction to prevent the record from being created at Authorize.Net
        // and then an exception in a calling function that might have a transaction open preventing
        // the save of the UserAttribute recording the customer profile ID
        return transactionWrapper.doInNewWriteTransaction(new Callable<String>() {
            @Override
            public String call() throws Exception {
                try {
                    userAttributeService.setAttribute(user, ATTR_PROFILE_ID, createdCustomerProfileId);
                } catch (RuntimeException e) {
                    cleanupProfileId(createdCustomerProfileId);
                    throw e;
                } catch (Error e) {
                    cleanupProfileId(createdCustomerProfileId);
                    throw e;
                }
                return createdCustomerProfileId;
            }
        });
    }

    @Override
    @MonitoredWork
    @GracefulShutdown
    @Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
    public List<PaymentMethod> getPaymentMethods(DbId<User> userId) {
        String customerProfileId = userAttributeService.getAttribute(userId, ATTR_PROFILE_ID);
        if (customerProfileId == null)
            return new ArrayList<PaymentMethod>();
        return getPaymentMethods(userId, customerProfileId);
    }

    private List<PaymentMethod> getPaymentMethods(DbId<User> userId, String customerProfileId) {
        Transaction transaction = createTransaction(TransactionType.GET_CUSTOMER_PROFILE);
        transaction.setCustomerProfileId(customerProfileId);

        Result<Transaction> result = executeTransaction("get payment methods", userId, transaction);

        List<PaymentMethod> paymentMethods = new ArrayList<PaymentMethod>();
        for (PaymentProfile paymentProfile : result.getCustomerPaymentProfileList()) {
            for (Payment payment : paymentProfile.getPaymentList()) {
                if (payment.getCreditCard() != null) {
                    paymentMethods.add(new PaymentMethod(paymentProfile.getCustomerPaymentProfileId(),
                            payment.getCreditCard().getCreditCardNumber()));
                }
            }
        }

        return paymentMethods;
    }

    @Override
    @MonitoredWork
    @GracefulShutdown
    @Transactional //(readOnly=true, propagation=Propagation.SUPPORTS)
    public String getHostedProfilePageToken(DbId<User> userId, String returnUrl) {
        // More than two years later this still isn't in their Java SDK.  Oh well, let's just do it
        // the stupid way...

        String customerProfileId = userAttributeService.getAttribute(userId, ATTR_PROFILE_ID);
        if (customerProfileId == null)
            customerProfileId = createCustomerProfile(userId);

        StringBuffer buffer = new StringBuffer(4096);
        buffer.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
        buffer.append("<getHostedProfilePageRequest xmlns=\"AnetApi/xml/v1/schema/AnetApiSchema.xsd\">\n");
        buffer.append("  <merchantAuthentication>\n");
        buffer.append("    <name>" + apiLoginID + "</name>");
        buffer.append("    <transactionKey>" + transactionKey + "</transactionKey>\n");
        buffer.append("  </merchantAuthentication>\n");
        buffer.append("  <customerProfileId>" + customerProfileId + "</customerProfileId> \n");
        buffer.append("  <hostedProfileSettings>\n");
        buffer.append("    <setting>\n");
        buffer.append("      <settingName>hostedProfileReturnUrl</settingName>\n");
        buffer.append("      <settingValue>" + returnUrl + "</settingValue>\n");
        buffer.append("    </setting>\n");
        buffer.append("  </hostedProfileSettings>\n");
        buffer.append("</getHostedProfilePageRequest>\n");

        CloseableHttpClient httpClient = HttpClients.createDefault();
        HttpPost httpPost = new HttpPost(merchant.isSandboxEnvironment() ? TEST_URL : PRODUCTION_URL);
        EntityBuilder entityBuilder = EntityBuilder.create();
        entityBuilder.setContentType(ContentType.TEXT_XML);
        entityBuilder.setContentEncoding("utf-8");
        entityBuilder.setText(buffer.toString());
        httpPost.setEntity(entityBuilder.build());

        try {
            CloseableHttpResponse httpResponse = httpClient.execute(httpPost);
            String response = EntityUtils.toString(httpResponse.getEntity());
            int start = response.indexOf(ELEMENT_TOKEN_OPEN);
            if (start == -1)
                throw new AuthorizeNetException(
                        "error fetching hosted profile page token for " + userId + ", response: " + response);
            int end = response.indexOf(ELEMENT_TOKEN_CLOSE);
            if (end == -1)
                throw new AuthorizeNetException(
                        "error fetching hosted profile page token for " + userId + ", response: " + response);
            return response.substring(start + ELEMENT_TOKEN_OPEN.length(), end);

        } catch (ClientProtocolException e) {
            throw new AuthorizeNetException(e.getMessage(), e);
        } catch (IOException e) {
            throw new AuthorizeNetException(e.getMessage(), e);
        }
    }

    @Override
    public String authorizePayment(DbId<User> userId, String paymentProfileId, BigDecimal amountToAuthorize) {
        return authorizePayment(userId, paymentProfileId, null, amountToAuthorize);
    }

    @Override
    @MonitoredWork
    @GracefulShutdown
    public String authorizePayment(DbId<User> userId, String paymentProfileId, String cardCode,
            BigDecimal amountToAuthorize) {
        String customerProfileId = userAttributeService.getAttribute(userId, ATTR_PROFILE_ID);
        if (customerProfileId == null)
            throw new OperationDisallowedException(userId + " has no Authorize.Net customer profile");

        List<PaymentMethod> paymentMethods = getPaymentMethods(userId, customerProfileId);
        PaymentMethod paymentMethodToUse = null;
        for (PaymentMethod paymentMethod : paymentMethods)
            if (paymentMethod.getPaymentProfileId().equals(paymentProfileId)) {
                paymentMethodToUse = paymentMethod;
                break;
            }
        if (paymentMethodToUse == null)
            throw new OperationDisallowedException(userId + " does not have paymentProfileId " + paymentProfileId);

        Order order = Order.createOrder();
        order.setTotalAmount(amountToAuthorize);

        PaymentTransaction paymentTransaction = PaymentTransaction.createPaymentTransaction();
        paymentTransaction.setTransactionType(net.authorize.TransactionType.AUTH_ONLY);
        paymentTransaction.setCustomerPaymentProfileId(paymentProfileId);
        paymentTransaction.setOrder(order);
        if (cardCode != null)
            paymentTransaction.setCardCode(cardCode);

        Transaction transaction = createTransaction(TransactionType.CREATE_CUSTOMER_PROFILE_TRANSACTION);
        transaction.setPaymentTransaction(paymentTransaction);
        transaction.setCustomerProfileId(customerProfileId);

        Result<Transaction> result = executeTransaction("authorize", userId, amountToAuthorize, transaction);

        Map<ResponseField, String> responseMap = result.getDirectResponseList().get(0).getDirectResponseMap();
        ResponseReasonCode responseReasonCode = ResponseReasonCode
                .findByReasonCode(responseMap.get(ResponseField.RESPONSE_REASON_CODE));
        switch (responseReasonCode) {
        case RRC_1_1:
            log.info("successfully authorized payment of " + amountToAuthorize + " for " + userId + ": "
                    + responseReasonCode);
            return responseMap.get(ResponseField.TRANSACTION_ID);
        case RRC_4_253:
            log.info("successfully authorized (but held for review) payment of " + amountToAuthorize + " for "
                    + userId + ": " + responseReasonCode);
            return responseMap.get(ResponseField.TRANSACTION_ID);
        default:
            log.info("authorization failed in amount of " + amountToAuthorize + " for " + userId + ": "
                    + responseReasonCode);
            throw new AuthorizeNetException(responseReasonCode.getReasonText());
        }
    }

    @Override
    @MonitoredWork
    @GracefulShutdown
    public String settlePayment(DbId<User> userId, String transactionId, BigDecimal amountToSettle) {
        String customerProfileId = userAttributeService.getAttribute(userId, ATTR_PROFILE_ID);
        if (customerProfileId == null)
            throw new OperationDisallowedException(userId + " has no Authorize.Net customer profile");

        Order order = Order.createOrder();
        order.setTotalAmount(amountToSettle);

        PaymentTransaction paymentTransaction = PaymentTransaction.createPaymentTransaction();
        paymentTransaction.setTransactionType(net.authorize.TransactionType.PRIOR_AUTH_CAPTURE);
        paymentTransaction.setTransactionId(transactionId);
        paymentTransaction.setOrder(order);

        Transaction transaction = createTransaction(TransactionType.CREATE_CUSTOMER_PROFILE_TRANSACTION);
        transaction.setPaymentTransaction(paymentTransaction);
        transaction.setCustomerProfileId(customerProfileId);

        Result<Transaction> result = executeTransaction("settle", userId, amountToSettle, transaction);

        Map<ResponseField, String> responseMap = result.getDirectResponseList().get(0).getDirectResponseMap();
        ResponseReasonCode responseReasonCode = ResponseReasonCode
                .findByReasonCode(responseMap.get(ResponseField.RESPONSE_REASON_CODE));
        if (responseReasonCode == ResponseReasonCode.RRC_1_1) {
            log.info("successfully settled payment of " + amountToSettle + " for " + userId);
            return responseMap.get(ResponseField.TRANSACTION_ID);
        } else
            throw new AuthorizeNetException(responseReasonCode.getReasonText());
    }

    private void cleanupProfileId(String customerProfileId) {
        Transaction transaction = createTransaction(TransactionType.DELETE_CUSTOMER_PROFILE);
        transaction.setCustomerProfileId(customerProfileId);
        BasicXmlDocument response = net.authorize.util.HttpClient.executeXML(environment, transaction);
        Result<Transaction> result = Result.createResult(transaction, response);
        if (!result.isOk()) {
            recordInterventionRequest("failed to clean up Authorize.Net customer profile id " + customerProfileId
                    + " " + createErrorMessage(result));
        }
    }

    private Transaction createTransaction(TransactionType transactionType) {
        Transaction transaction = merchant.createCIMTransaction(transactionType);
        transaction.setValidationMode(validationMode);
        return transaction;
    }

    private Result<Transaction> executeTransaction(String logOperation, DbId<User> userId,
            Transaction transaction) {
        return executeTransaction(logOperation, userId, new BigDecimal(0), transaction);
    }

    private Result<Transaction> executeTransaction(String logOperation, DbId<User> userId, BigDecimal amount,
            Transaction transaction) {
        BasicXmlDocument response = net.authorize.util.HttpClient.executeXML(environment, transaction);

        if (log.isInfoEnabled()) {
            BasicXmlDocument request = transaction.getCurrentRequest();
            if (request != null) {
                log.info(request.dump());
                log.info(response.dump());
            }
        }

        Result<Transaction> result = Result.createResult(transaction, response);

        List<DirectResponse> directResponses = result.getDirectResponseList();
        // check ResponseReasonCode to see if it means we should throw an exception
        if (directResponses != null && directResponses.size() > 0) {
            Map<ResponseField, String> directResponseMap = directResponses.get(0).getDirectResponseMap();
            ResponseCode responseCode = ResponseCode
                    .findByResponseCode(directResponseMap.get(ResponseField.RESPONSE_CODE));
            ResponseReasonCode responseReasonCode = ResponseReasonCode
                    .findByReasonCode(directResponseMap.get(ResponseField.RESPONSE_REASON_CODE));

            // check exceptionMap to see if we should throw specific exception
            Class<? extends AuthorizeNetException> exceptionClass = exceptionMap.get(responseReasonCode);
            if (exceptionClass != null) {
                log.info(logOperation + " failed in amount of " + amount + " for " + userId + ": "
                        + responseReasonCode);
                try {
                    Constructor<? extends AuthorizeNetException> constructor = exceptionClass
                            .getConstructor(String.class);
                    throw constructor.newInstance(responseReasonCode.getReasonText());
                } catch (NoSuchMethodException e) {
                    /* fall through */ } catch (InvocationTargetException e) {
                    /* fall through */ } catch (IllegalAccessException e) {
                    /* fall through */ } catch (InstantiationException e) {
                    /* fall through */ }
                log.warn("Exception class " + exceptionClass.getSimpleName() + " failed reflection instantiation");
                // we know we want an exception but failed to instantiate it.  Throw ANE as default
                throw new AuthorizeNetException(responseReasonCode.getReasonText());
            }

            // umbrella processing for DECLINED
            if (responseCode == ResponseCode.DECLINED) {
                log.info(logOperation + " failed in amount of " + amount + " for " + userId + ": "
                        + responseReasonCode);
                throw new PaymentDeclinedException(responseReasonCode.getReasonText());
            }
        }

        // map all other failures (where no DirectResponseMap or RRC may not be in exceptionMap) into ANE
        if (!result.isOk()) {
            String message = createErrorMessage(result);
            log.warn(logOperation + " failed for " + userId + ": " + message);
            throw new AuthorizeNetException(message);
        }

        return result;
    }

    private String createErrorMessage(Result<Transaction> result) {
        StringBuffer buffer = new StringBuffer();
        buffer.append(result.getResultCode());
        for (Message message : result.getMessages()) {
            buffer.append(" (");
            buffer.append(message.getCode());
            buffer.append(":");
            buffer.append(message.getText());
            buffer.append(")");
        }
        return buffer.toString();
    }

    @Required
    public void setUserService(IUserService userService) {
        this.userService = userService;
    }

    @Required
    public void setUserAttributeService(IUserAttributeService userAttributeService) {
        this.userAttributeService = userAttributeService;
    }

    @Required
    public void setEnvironment(String environment) {
        this.environment = Environment.valueOf(environment);
    }

    @Required
    public void setApiLoginID(String apiLoginID) {
        this.apiLoginID = apiLoginID;
    }

    @Required
    public void setTransactionKey(String transactionKey) {
        this.transactionKey = transactionKey;
    }

    public void setValidationMode(String validationMode) {
        this.validationMode = ValidationModeType.valueOf(validationMode);
    }

    @Required
    public void setTransactionWrapper(TransactionWrapper transactionWrapper) {
        this.transactionWrapper = transactionWrapper;
    }

}