/*******************************************************************************
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *******************************************************************************/

package org.apache.ofbiz.accounting.thirdparty.authorizedotnet;

import java.math.BigDecimal;
import java.sql.Timestamp;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;

import org.apache.ofbiz.accounting.payment.PaymentGatewayServices;
import org.apache.ofbiz.base.util.Debug;
import org.apache.ofbiz.base.util.HttpClient;
import org.apache.ofbiz.base.util.HttpClientException;
import org.apache.ofbiz.base.util.UtilDateTime;
import org.apache.ofbiz.base.util.UtilFormatOut;
import org.apache.ofbiz.base.util.UtilMisc;
import org.apache.ofbiz.base.util.UtilProperties;
import org.apache.ofbiz.base.util.UtilValidate;
import org.apache.ofbiz.entity.Delegator;
import org.apache.ofbiz.entity.GenericEntityException;
import org.apache.ofbiz.entity.GenericValue;
import org.apache.ofbiz.entity.util.EntityQuery;
import org.apache.ofbiz.entity.util.EntityUtilProperties;
import org.apache.ofbiz.service.DispatchContext;
import org.apache.ofbiz.service.ModelService;
import org.apache.ofbiz.service.ServiceUtil;

import com.ibm.icu.util.Calendar;

public class AIMPaymentServices {

    public static final String module = AIMPaymentServices.class.getName();
    public final static String resource = "AccountingUiLabels";

    // The list of refund failure response codes that would cause the ccRefund service
    // to attempt to void the refund's associated authorization transaction.  This list
    // contains the responses where the voiding does not need to be done within a certain
    // time limit
    private static final List<String> VOIDABLE_RESPONSES_NO_TIME_LIMIT = UtilMisc.toList("50");

    // A list of refund failure response codes that would cause the ccRefund service
    // to first check whether the refund's associated authorization transaction has occurred
    // within a certain time limit, and if so, cause it to void the transaction
    private static final List<String> VOIDABLE_RESPONSES_TIME_LIMIT = UtilMisc.toList("54");

    // The number of days in the time limit when one can safely consider an unsettled
    // transaction to be still valid
    private static final int TIME_LIMIT_VERIFICATION_DAYS = 120;

    private static Properties AIMProperties = null;

    // A routine to check whether a given refund failure response code will cause the
    // ccRefund service to attempt to void the refund's associated authorization transaction
    private static boolean isVoidableResponse(String responseCode) {
        return
            VOIDABLE_RESPONSES_NO_TIME_LIMIT.contains(responseCode) ||
            VOIDABLE_RESPONSES_TIME_LIMIT.contains(responseCode);
    }

    public static Map<String, Object> ccAuth(DispatchContext ctx, Map<String, Object> context) {
        Delegator delegator = ctx.getDelegator();
        Locale locale = (Locale) context.get("locale");
        Map<String, Object> results = ServiceUtil.returnSuccess();
        Map<String, Object> request = new HashMap<String, Object>();
        Properties props = buildAIMProperties(context, delegator);
        buildMerchantInfo(context, props, request);
        buildGatewayResponeConfig(context, props, request);
        buildCustomerBillingInfo(context, props, request);
        buildEmailSettings(context, props, request);
        buildInvoiceInfo(context, props, request);
        props.put("transType", "AUTH_ONLY");
        buildAuthTransaction(context, props, request);
        Map<String, Object> validateResults = validateRequest(context, props, request);
        String respMsg = (String)validateResults.get(ModelService.RESPONSE_MESSAGE);
        if (ModelService.RESPOND_ERROR.equals(respMsg)) {
            results.put(ModelService.ERROR_MESSAGE, UtilProperties.getMessage(resource, "AccountingValidationFailedInvalidValues", locale));
            return results;
        }
        Map<String, Object> reply = processCard(request, props, locale);
        //now we need to process the result
        processAuthTransResult(request, reply, results);
        return results;
    }

    public static Map<String, Object> ccCapture(DispatchContext ctx, Map<String, Object> context) {
        Locale locale = (Locale) context.get("locale");
        Delegator delegator = ctx.getDelegator();
        GenericValue orderPaymentPreference = (GenericValue) context.get("orderPaymentPreference");
        GenericValue creditCard = null;
        try {
            creditCard = delegator.getRelatedOne("CreditCard",orderPaymentPreference, false);
        } catch (GenericEntityException e) {
            Debug.logError(e, module);
            return ServiceUtil.returnError(UtilProperties.getMessage(resource, 
                    "AccountingPaymentUnableToGetCCInfo", locale));
        }
        GenericValue authTransaction = PaymentGatewayServices.getAuthTransaction(orderPaymentPreference);
        if (authTransaction == null) {
            return ServiceUtil.returnError(UtilProperties.getMessage(resource, 
                    "AccountingPaymentTransactionAuthorizationNotFoundCannotCapture", locale));
        }
        context.put("creditCard", creditCard);
        context.put("authTransaction", authTransaction);
        Map<String, Object> results = ServiceUtil.returnSuccess();
        Map<String, Object> request = new HashMap<String, Object>();
        Properties props = buildAIMProperties(context, delegator);
        buildMerchantInfo(context, props, request);
        buildGatewayResponeConfig(context, props, request);
        buildCustomerBillingInfo(context, props, request);
        buildEmailSettings(context, props, request);
        request.put("x_Invoice_Num","Order " + orderPaymentPreference.getString("orderId"));
        // PRIOR_AUTH_CAPTURE is the right one to use, since we already have an authorization from the authTransaction.
        // CAPTURE_ONLY is a "force" transaction to be used if there is no prior authorization
        props.put("transType", "PRIOR_AUTH_CAPTURE");
        props.put("cardtype", creditCard.get("cardType"));
        buildCaptureTransaction(context,props,request);
        Map<String, Object> validateResults = validateRequest(context, props, request);
        String respMsg = (String)validateResults.get(ModelService.RESPONSE_MESSAGE);
        if (ModelService.RESPOND_ERROR.equals(respMsg)) {
            results.put(ModelService.ERROR_MESSAGE, UtilProperties.getMessage(resource, "AccountingValidationFailedInvalidValues", locale));
            return results;
        }
        Map<String, Object> reply = processCard(request, props, locale);
        processCaptureTransResult(request, reply, results);
        // if there is no captureRefNum, then the capture failed
        if (results.get("captureRefNum") == null) {
             return ServiceUtil.returnError((String) results.get("captureMessage"));
        }
        return results;
    }

    public static Map<String, Object> ccRefund(DispatchContext ctx, Map<String, Object> context) {
        Locale locale = (Locale) context.get("locale");
        Delegator delegator = ctx.getDelegator();
        GenericValue orderPaymentPreference = (GenericValue) context.get("orderPaymentPreference");
        GenericValue creditCard = null;
        try {
            creditCard = delegator.getRelatedOne("CreditCard", orderPaymentPreference, false);
        } catch (GenericEntityException e) {
            Debug.logError(e, module);
            return ServiceUtil.returnError(UtilProperties.getMessage(resource, 
                    "AccountingPaymentUnableToGetCCInfo", locale));
        }
        GenericValue authTransaction = PaymentGatewayServices.getAuthTransaction(orderPaymentPreference);
        if (authTransaction == null) {
            return ServiceUtil.returnError(UtilProperties.getMessage(resource, 
                    "AccountingPaymentTransactionAuthorizationNotFoundCannotRefund", locale));
        }
        context.put("creditCard",creditCard);
        context.put("authTransaction",authTransaction);
        Map<String, Object> results = ServiceUtil.returnSuccess();
        Map<String, Object> request = new HashMap<String, Object>();
        Properties props = buildAIMProperties(context, delegator);
        buildMerchantInfo(context, props, request);
        buildGatewayResponeConfig(context, props, request);
        buildCustomerBillingInfo(context, props, request);
        buildEmailSettings(context, props, request);
        buildInvoiceInfo(context, props, request);
        props.put("transType", "CREDIT");
        props.put("cardtype", creditCard.get("cardType"));
        buildRefundTransaction(context, props, request);
        Map<String, Object> validateResults = validateRequest(context, props, request);
        String respMsg = (String)validateResults.get(ModelService.RESPONSE_MESSAGE);
        if (ModelService.RESPOND_ERROR.equals(respMsg)) {
            results.put(ModelService.ERROR_MESSAGE, UtilProperties.getMessage(resource, "AccountingValidationFailedInvalidValues", locale));
            return results;
        }
        Map<String, Object> reply = processCard(request, props, locale);
        results.putAll(processRefundTransResult(request, reply));
        boolean refundResult = ((Boolean)results.get("refundResult")).booleanValue();
        String refundFlag = (String)results.get("refundFlag");
        // Since the refund failed, we are going to void the previous authorization against
        // which ccRefunds attempted to issue the refund.  This happens because Authorize.NET requires
        // that settled transactions need to be voided the same day.  unfortunately they provide no method for
        // determining what transactions can be voided and what can be refunded, so we'll have to try it with timestamps
        if (!refundResult && isVoidableResponse(refundFlag)) {
            boolean canDoVoid = false;
            if (VOIDABLE_RESPONSES_TIME_LIMIT.contains(refundFlag)) {
                // We are calculating the timestamp that is at the beginning of a time limit,
                // since we can safely assume that, within this time limit, an unsettled transaction
                // can still be considered valid
                Calendar startCalendar = UtilDateTime.toCalendar(UtilDateTime.nowTimestamp());
                startCalendar.add(Calendar.DATE, -TIME_LIMIT_VERIFICATION_DAYS);
                Timestamp startTimestamp = new java.sql.Timestamp(startCalendar.getTime().getTime());
                Timestamp authTimestamp = authTransaction.getTimestamp("transactionDate");
                if (startTimestamp.before(authTimestamp)) {
                    canDoVoid = true;
                }
            } else {
                // Since there's no time limit to check, the voiding of the transaction will go
                // through as usual
                canDoVoid = true;
            }
            if (canDoVoid) {
                Debug.logWarning("Refund was unsuccessful; will now attempt a VOID transaction.", module);
                BigDecimal authAmountObj = authTransaction.getBigDecimal("amount");
                BigDecimal refundAmountObj = (BigDecimal)context.get("refundAmount");
                BigDecimal authAmount = authAmountObj != null ? authAmountObj : BigDecimal.ZERO;
                BigDecimal refundAmount = refundAmountObj != null ? refundAmountObj : BigDecimal.ZERO;
                if (authAmount.compareTo(refundAmount) == 0) {
                    reply = voidTransaction(authTransaction, context, delegator);
                    if (ServiceUtil.isError(reply)) {
                        return reply;
                    }
                    results = ServiceUtil.returnSuccess();
                    results.putAll(processRefundTransResult(request, reply));
                    return results;
                } else {
                    // TODO: Modify the code to (a) do a void of the whole transaction, and (b)
                    // create a new auth-capture of the difference.
                    return ServiceUtil.returnError(UtilProperties.getMessage(resource, 
                            "AccountingAuthorizeNetCannotPerformVoidTransaction", 
                            UtilMisc.toMap("authAmount", authAmount, "refundAmount", refundAmount), locale));
                }
            }
        }
        return results;
    }

    public static Map<String, Object> ccRelease(DispatchContext ctx, Map<String, Object> context) {
        Locale locale = (Locale) context.get("locale");
        Delegator delegator = ctx.getDelegator();
        GenericValue orderPaymentPreference = (GenericValue) context.get("orderPaymentPreference");
        GenericValue authTransaction = PaymentGatewayServices.getAuthTransaction(orderPaymentPreference);
        if (authTransaction == null) {
            return ServiceUtil.returnError(UtilProperties.getMessage(resource, 
                    "AccountingPaymentTransactionAuthorizationNotFoundCannotRelease", locale));
        }
        Map<String, Object> reply = voidTransaction(authTransaction, context, delegator);
        if (ServiceUtil.isError(reply)) {
            return reply;
        }
        Map<String, Object> results = ServiceUtil.returnSuccess();
        context.put("x_Amount", ((BigDecimal) context.get("releaseAmount")).toPlainString()); // hack for releaseAmount
        results.putAll(processReleaseTransResult(context, reply));
        return results;
    }

    private static Map<String, Object> voidTransaction(GenericValue authTransaction, Map<String, Object> context, Delegator delegator) {
        Locale locale = (Locale) context.get("locale");
        context.put("authTransaction", authTransaction);
        Map<String, Object> results = ServiceUtil.returnSuccess();
        Map<String, Object> request = new HashMap<String, Object>();
        Properties props = buildAIMProperties(context, delegator);
        buildMerchantInfo(context, props, request);
        buildGatewayResponeConfig(context, props, request);
        buildEmailSettings(context, props, request);
        props.put("transType", "VOID");
        buildVoidTransaction(context, props, request);
        Map<String, Object> validateResults = validateRequest(context, props, request);
        String respMsg = (String)validateResults.get(ModelService.RESPONSE_MESSAGE);
        if (ModelService.RESPOND_ERROR.equals(respMsg)) {
            results.put(ModelService.ERROR_MESSAGE, UtilProperties.getMessage(resource, "AccountingValidationFailedInvalidValues", locale));
            return results;
        }
        return processCard(request, props, locale);
    }

    public static Map<String, Object> ccCredit(DispatchContext ctx, Map<String, Object> context) {
    	Locale locale = (Locale) context.get("locale");
        Map<String, Object> results = new HashMap<String, Object>();
        results.put(ModelService.RESPONSE_MESSAGE, ModelService.RESPOND_ERROR);
        results.put(ModelService.ERROR_MESSAGE, UtilProperties.getMessage(resource, "AccountingAuthorizeNetccCreditUnsupported", locale));
        return results;
    }

    public static Map<String, Object> ccAuthCapture(DispatchContext ctx, Map<String, Object> context) {
        Delegator delegator = ctx.getDelegator();
        Locale locale = (Locale) context.get("locale");
        Map<String, Object> results = ServiceUtil.returnSuccess();
        Map<String, Object> request = new HashMap<String, Object>();
        Properties props = buildAIMProperties(context, delegator);
        buildMerchantInfo(context, props, request);
        buildGatewayResponeConfig(context, props, request);
        buildCustomerBillingInfo(context, props, request);
        buildEmailSettings(context, props, request);
        buildInvoiceInfo(context, props, request);
        props.put("transType", "AUTH_CAPTURE");
        buildAuthTransaction(context, props, request);
        Map<String, Object> validateResults = validateRequest(context, props, request);
        String respMsg = (String)validateResults.get(ModelService.RESPONSE_MESSAGE);
        if (ModelService.RESPOND_ERROR.equals(respMsg)) {
            results.put(ModelService.ERROR_MESSAGE, UtilProperties.getMessage(resource, "AccountingValidationFailedInvalidValues", locale));
            return results;
        }
        Map<String, Object> reply = processCard(request, props, locale);
        //now we need to process the result
        processAuthCaptureTransResult(request, reply, results);
        // if there is no captureRefNum, then the capture failed
        if (results.get("captureRefNum") == null) {
             return ServiceUtil.returnError((String) results.get("captureMessage"));
        }
        return results;
    }

    private static Map<String, Object> processCard(Map<String, Object> request, Properties props, Locale locale) {
        Map<String, Object> result = new HashMap<String, Object>();
        String url = props.getProperty("url");
        if (UtilValidate.isEmpty(url)) {
            return ServiceUtil.returnError(UtilProperties.getMessage(resource, 
                    "AccountingAuthorizeNetTransactionUrlNotFound", locale));
        }
        if (isTestMode()) {
            Debug.logInfo("TEST Authorize.net using url [" + url + "]", module);
            Debug.logInfo("TEST Authorize.net request string " + request.toString(),module);
            Debug.logInfo("TEST Authorize.net properties string " + props.toString(),module);
        }
        
        // card present has a different layout from standard AIM; this determines how to parse the response
        int apiType = UtilValidate.isEmpty(props.get("cpMarketType")) ? AuthorizeResponse.AIM_RESPONSE : AuthorizeResponse.CP_RESPONSE;
        
        try {
            HttpClient httpClient = new HttpClient(url, request);
            String certificateAlias = props.getProperty("certificateAlias");
            httpClient.setClientCertificateAlias(certificateAlias);
            String httpResponse = httpClient.post();
            Debug.logInfo("transaction response: " + httpResponse,module);
            AuthorizeResponse ar = new AuthorizeResponse(httpResponse, apiType);            
            if (ar.isApproved()) {            
                result.put("authResult", Boolean.TRUE);
            }
            //When the transaction is already expired in Authorize.net, then the response is an error message with reason code 16 (i.e. "The transaction cannot be found");
            // in this case we proceed without generating an error in order to void/cancel the transaction record in OFBiz as well.
            //This else if block takes care of the expired transaction.
            else if ("VOID".equals(props.get("transType")) && "16".equals(ar.getReasonCode())) {
                result.put("authResult", Boolean.TRUE);
            } else {
                result.put("authResult", Boolean.FALSE);
                if (Debug.infoOn()) {
                    Debug.logInfo("transactionId:  " + ar.getTransactionId(), module);
                    Debug.logInfo("responseCode:   " + ar.getResponseCode(), module);
                    Debug.logInfo("responseReason: " + ar.getReasonCode(), module);
                    Debug.logInfo("reasonText:     " + ar.getReasonText(), module);
                }
            }
            result.put("httpResponse", httpResponse);
            result.put("authorizeResponse", ar);
        } catch (HttpClientException e) {
            Debug.logInfo(e, "Could not complete Authorize.Net transaction: " + e.toString(),module);
        }
        result.put(ModelService.RESPONSE_MESSAGE, ModelService.RESPOND_SUCCESS);
        return result;
    }

    private static boolean isTestMode() {
        return "true".equalsIgnoreCase((String)AIMProperties.get("testReq"));
    }

    private static Properties buildAIMProperties(Map<String, Object> context, Delegator delegator) {
        String paymentGatewayConfigId = (String) context.get("paymentGatewayConfigId");
        String configStr = (String) context.get("paymentConfig");
        if (configStr == null) {
            configStr = "payment.properties";
        }
        GenericValue cc = (GenericValue)context.get("creditCard");
        String url = getPaymentGatewayConfigValue(delegator, paymentGatewayConfigId, "transactionUrl", configStr, "payment.authorizedotnet.url");
        String certificateAlias = getPaymentGatewayConfigValue(delegator, paymentGatewayConfigId, "certificateAlias", configStr, "payment.authorizedotnet.certificateAlias");
        String ver = getPaymentGatewayConfigValue(delegator, paymentGatewayConfigId, "apiVersion", configStr, "payment.authorizedotnet.version");
        String delimited = getPaymentGatewayConfigValue(delegator, paymentGatewayConfigId, "delimitedData", configStr, "payment.authorizedotnet.delimited");
        String delimiter = getPaymentGatewayConfigValue(delegator, paymentGatewayConfigId, "delimiterChar", configStr, "payment.authorizedotnet.delimiter");
        String cpVersion = getPaymentGatewayConfigValue(delegator, paymentGatewayConfigId, "cpVersion", configStr, "payment.authorizedotnet.cpVersion");
        String cpMarketType = getPaymentGatewayConfigValue(delegator, paymentGatewayConfigId, "cpMarketType", configStr, "payment.authorizedotnet.cpMarketType");
        String cpDeviceType = getPaymentGatewayConfigValue(delegator, paymentGatewayConfigId, "cpDeviceType", configStr, "payment.authorizedotnet.cpDeviceType");
        String method = getPaymentGatewayConfigValue(delegator, paymentGatewayConfigId, "method", configStr, "payment.authorizedotnet.method");
        String emailCustomer = getPaymentGatewayConfigValue(delegator, paymentGatewayConfigId, "emailCustomer", configStr, "payment.authorizedotnet.emailcustomer");
        String emailMerchant = getPaymentGatewayConfigValue(delegator, paymentGatewayConfigId, "emailMerchant", configStr, "payment.authorizedotnet.emailmerchant");
        String testReq = getPaymentGatewayConfigValue(delegator, paymentGatewayConfigId, "testMode", configStr, "payment.authorizedotnet.test");
        String relay = getPaymentGatewayConfigValue(delegator, paymentGatewayConfigId, "relayResponse", configStr, "payment.authorizedotnet.relay");
        String tranKey = getPaymentGatewayConfigValue(delegator, paymentGatewayConfigId, "tranKey", configStr, "payment.authorizedotnet.trankey");
        String login = getPaymentGatewayConfigValue(delegator, paymentGatewayConfigId, "userId", configStr, "payment.authorizedotnet.login");
        String password = getPaymentGatewayConfigValue(delegator, paymentGatewayConfigId, "pwd", configStr, "payment.authorizedotnet.password");
        String transDescription = getPaymentGatewayConfigValue(delegator, paymentGatewayConfigId, "transDescription", configStr, "payment.authorizedotnet.transdescription");
        String duplicateWindow = getPaymentGatewayConfigValue(delegator, paymentGatewayConfigId, "duplicateWindow", configStr, "payment.authorizedotnet.duplicateWindow");        
        if (UtilValidate.isEmpty(ver)) {
            ver = "3.0";
        }
        if (UtilValidate.isEmpty(login)) {
            Debug.logInfo("the login property in " + configStr + " is not configured.", module);
        }
        if (UtilValidate.isEmpty(password) && !("3.1".equals(ver))) {
            Debug.logInfo("The password property in " + configStr + " is not configured.", module);
        }
        if ("3.1".equals(ver)) {
            if (UtilValidate.isEmpty(tranKey)) {
                Debug.logInfo("Trankey property required for version 3.1 reverting to 3.0",module);
                ver = "3.0";
            }
        }
        if (UtilValidate.isNotEmpty(cpMarketType) && UtilValidate.isEmpty(cpVersion)) {
            cpVersion = "1.0";
        }
        
        Properties props = new Properties();
        props.put("url", url);
        props.put("certificateAlias", certificateAlias);
        props.put("ver", ver);
        props.put("delimited", delimited);
        props.put("delimiter", delimiter);
        props.put("method", method);
        props.put("cpVersion", cpVersion);
        props.put("cpMarketType", cpMarketType);
        props.put("cpDeviceType", cpDeviceType);
        props.put("emailCustomer", emailCustomer);
        props.put("emailMerchant", emailMerchant);
        props.put("testReq", testReq);
        props.put("relay", relay);
        props.put("transDescription", transDescription);
        props.put("login", login);
        props.put("password", password);
        props.put("trankey", tranKey);
        props.put("duplicateWindow", duplicateWindow);
        if (cc != null) {
            props.put("cardtype", cc.get("cardType"));
        }
        if (AIMProperties == null) {
            AIMProperties = props;
        }
        if (isTestMode()) {
            Debug.logInfo("Created Authorize.Net properties file: " + props.toString(), module);
        }
        return props;
    }

    private static void buildMerchantInfo(Map<String, Object> params, Properties props, Map<String, Object> AIMRequest) {
        AIMRequest.put("x_Login", props.getProperty("login"));
        String trankey = props.getProperty("trankey");
        if (UtilValidate.isNotEmpty(trankey)) {
            AIMRequest.put("x_Tran_Key", props.getProperty("trankey"));
        } else {
            // only send password if no tran key
            AIMRequest.put("x_Password",props.getProperty("password"));
        }
        
        // api version (non Card Present)
        String apiVersion = props.getProperty("ver");
        if (UtilValidate.isNotEmpty(apiVersion)) {
            AIMRequest.put("x_Version", props.getProperty("ver"));
        }
        
        // CP version
        String cpVersion = props.getProperty("cpver");
        if (UtilValidate.isNotEmpty(cpVersion)) {
            AIMRequest.put("x_cpversion", cpVersion);
        }

        // Check duplicateWindow time frame. If same transaction happens in the predefined time frame then return error. 
        String duplicateWindow = props.getProperty("duplicateWindow");
        if (UtilValidate.isNotEmpty(duplicateWindow)) {
            AIMRequest.put("x_duplicate_window", props.getProperty("duplicateWindow"));
        }
        
        // CP market type
        String cpMarketType = props.getProperty("cpMarketType");
        if (UtilValidate.isNotEmpty(cpMarketType)) {
            AIMRequest.put("x_market_type", cpMarketType);
            
            // CP test mode
            if ("true".equalsIgnoreCase(props.getProperty("testReq"))) {
                AIMRequest.put("x_test_request", props.getProperty("testReq"));                
            }
        }
        
        // CP device typ
        String cpDeviceType = props.getProperty("cpDeviceType");
        if (UtilValidate.isNotEmpty(cpDeviceType)) {
            AIMRequest.put("x_device_type", cpDeviceType);
        }                      
    }

    private static void buildGatewayResponeConfig(Map<String, Object> params, Properties props, Map<String, Object> AIMRequest) {
        if (AIMRequest.get("x_market_type") != null) {
            // card present transaction
            AIMRequest.put("x_response_format", "true".equalsIgnoreCase(props.getProperty("delimited")) ? "1" : "0");            
        } else {
            AIMRequest.put("x_Delim_Data", props.getProperty("delimited"));            
        }
        AIMRequest.put("x_Delim_Char", props.getProperty("delimiter"));
    }

    private static void buildCustomerBillingInfo(Map<String, Object> params, Properties props, Map<String, Object> AIMRequest) {
        try {
            // this would be used in the case of a capture, where one of the parameters is an OrderPaymentPreference
            if (params.get("orderPaymentPreference") != null) {
                GenericValue opp = (GenericValue) params.get("orderPaymentPreference");
                if ("CREDIT_CARD".equals(opp.getString("paymentMethodTypeId"))) {
                    // sometimes the ccAuthCapture interface is used, in which case the creditCard is passed directly
                    GenericValue creditCard = (GenericValue) params.get("creditCard");
                    if (creditCard == null || ! (opp.get("paymentMethodId").equals(creditCard.get("paymentMethodId")))) {
                        creditCard = opp.getRelatedOne("CreditCard", false);
                    }
                    AIMRequest.put("x_First_Name", UtilFormatOut.checkNull(creditCard.getString("firstNameOnCard")));
                    AIMRequest.put("x_Last_Name", UtilFormatOut.checkNull(creditCard.getString("lastNameOnCard")));
                    AIMRequest.put("x_Company", UtilFormatOut.checkNull(creditCard.getString("companyNameOnCard")));
                    if (UtilValidate.isNotEmpty(creditCard.getString("contactMechId"))) {
                        GenericValue address = creditCard.getRelatedOne("PostalAddress", false);
                        if (address != null) {
                            AIMRequest.put("x_Address", UtilFormatOut.checkNull(address.getString("address1")));
                            AIMRequest.put("x_City", UtilFormatOut.checkNull(address.getString("city")));
                            AIMRequest.put("x_State", UtilFormatOut.checkNull(address.getString("stateProvinceGeoId")));
                            AIMRequest.put("x_Zip", UtilFormatOut.checkNull(address.getString("postalCode")));
                            AIMRequest.put("x_Country", UtilFormatOut.checkNull(address.getString("countryGeoId")));
                        }
                    }                    
                } else {
                    Debug.logWarning("Payment preference " + opp + " is not a credit card", module);
                }
            } else {
                // this would be the case for an authorization
                GenericValue cp = (GenericValue)params.get("billToParty");
                GenericValue ba = (GenericValue)params.get("billingAddress");
                AIMRequest.put("x_First_Name", UtilFormatOut.checkNull(cp.getString("firstName")));
                AIMRequest.put("x_Last_Name", UtilFormatOut.checkNull(cp.getString("lastName")));
                AIMRequest.put("x_Address", UtilFormatOut.checkNull(ba.getString("address1")));
                AIMRequest.put("x_City", UtilFormatOut.checkNull(ba.getString("city")));
                AIMRequest.put("x_State", UtilFormatOut.checkNull(ba.getString("stateProvinceGeoId")));
                AIMRequest.put("x_Zip", UtilFormatOut.checkNull(ba.getString("postalCode")));
                AIMRequest.put("x_Country", UtilFormatOut.checkNull(ba.getString("countryGeoId")));
            }
            return;
        } catch (GenericEntityException ex) {
            Debug.logError("Cannot build customer information for " + params + " due to error: " + ex.getMessage(), module);
            return;
        }
    }

    private static void buildEmailSettings(Map<String, Object> params, Properties props, Map<String, Object> AIMRequest) {
        GenericValue ea = (GenericValue)params.get("billToEmail");
        AIMRequest.put("x_Email_Customer", props.getProperty("emailCustomer"));
        AIMRequest.put("x_Email_Merchant", props.getProperty("emailMerchant"));
        if (ea != null) {
            AIMRequest.put("x_Email", UtilFormatOut.checkNull(ea.getString("infoString")));
        }
    }

    private static void buildInvoiceInfo(Map<String, Object> params, Properties props, Map<String, Object> AIMRequest) {
        String description = UtilFormatOut.checkNull(props.getProperty("transDescription"));
        String orderId = UtilFormatOut.checkNull((String)params.get("orderId"));
        if (UtilValidate.isEmpty(orderId)) {
            GenericValue orderPaymentPreference = (GenericValue) params.get("orderPaymentPreference");
            if (orderPaymentPreference != null) {
                orderId = (String) orderPaymentPreference.get("orderId");
            }
        }
        AIMRequest.put("x_Invoice_Num", "Order " + orderId);
        AIMRequest.put("x_Description", description);
    }

    private static void buildAuthTransaction(Map<String, Object> params, Properties props, Map<String, Object> AIMRequest) {
        GenericValue cc = (GenericValue) params.get("creditCard");
        String currency = (String) params.get("currency");
        String amount = ((BigDecimal)params.get("processAmount")).toString();
        String number = UtilFormatOut.checkNull(cc.getString("cardNumber"));
        String expDate = UtilFormatOut.checkNull(cc.getString("expireDate"));
        String cardSecurityCode = (String) params.get("cardSecurityCode");
        AIMRequest.put("x_Amount", amount);
        AIMRequest.put("x_Currency_Code", currency);        
        AIMRequest.put("x_Method", props.getProperty("method"));
        AIMRequest.put("x_Type", props.getProperty("transType"));
        AIMRequest.put("x_Card_Num", number);
        AIMRequest.put("x_Exp_Date", expDate);
        if (UtilValidate.isNotEmpty(cardSecurityCode)) {
            AIMRequest.put("x_card_code", cardSecurityCode);
        }
        if (AIMRequest.get("x_market_type") != null) {
            AIMRequest.put("x_card_type", getCardType(UtilFormatOut.checkNull(cc.getString("cardType"))));
        }
    }

    private static void buildCaptureTransaction(Map<String, Object> params, Properties props, Map<String, Object> AIMRequest) {
        GenericValue at = (GenericValue) params.get("authTransaction");
        GenericValue cc = (GenericValue) params.get("creditCard");
        String currency = (String) params.get("currency");
        String amount = ((BigDecimal) params.get("captureAmount")).toString();
        String number = UtilFormatOut.checkNull(cc.getString("cardNumber"));
        String expDate = UtilFormatOut.checkNull(cc.getString("expireDate"));
        AIMRequest.put("x_Amount", amount);
        AIMRequest.put("x_Currency_Code", currency);
        AIMRequest.put("x_Method", props.getProperty("method"));
        AIMRequest.put("x_Type", props.getProperty("transType"));
        AIMRequest.put("x_Card_Num", number);
        AIMRequest.put("x_Exp_Date", expDate);
        AIMRequest.put("x_Trans_ID", at.get("referenceNum"));
        AIMRequest.put("x_ref_trans_id", at.get("referenceNum"));
        AIMRequest.put("x_Auth_Code", at.get("gatewayCode"));
        if (AIMRequest.get("x_market_type") != null) {
            AIMRequest.put("x_card_type", getCardType(UtilFormatOut.checkNull(cc.getString("cardType"))));
        }
    }

    private static void buildRefundTransaction(Map<String, Object> params, Properties props, Map<String, Object> AIMRequest) {
        GenericValue at = (GenericValue) params.get("authTransaction");
        GenericValue cc = (GenericValue) params.get("creditCard");
        String currency = (String) params.get("currency");
        String amount = ((BigDecimal) params.get("refundAmount")).toString();
        String number = UtilFormatOut.checkNull(cc.getString("cardNumber"));
        String expDate = UtilFormatOut.checkNull(cc.getString("expireDate"));
        AIMRequest.put("x_Amount", amount);
        AIMRequest.put("x_Currency_Code", currency);
        AIMRequest.put("x_Method", props.getProperty("method"));
        AIMRequest.put("x_Type", props.getProperty("transType"));
        AIMRequest.put("x_Card_Num", number);
        AIMRequest.put("x_Exp_Date", expDate);
        AIMRequest.put("x_Trans_ID", at.get("referenceNum"));
        AIMRequest.put("x_Auth_Code", at.get("gatewayCode"));
        AIMRequest.put("x_ref_trans_id", at.get("referenceNum"));
        if (AIMRequest.get("x_market_type") != null) {
            AIMRequest.put("x_card_type", getCardType(UtilFormatOut.checkNull(cc.getString("cardType"))));
        }
        Debug.logInfo("buildCaptureTransaction. " + at.toString(), module);
    }

    private static void buildVoidTransaction(Map<String, Object> params, Properties props, Map<String, Object> AIMRequest) {
        GenericValue at = (GenericValue) params.get("authTransaction");
        String currency = (String) params.get("currency");
        AIMRequest.put("x_Currency_Code", currency);
        AIMRequest.put("x_Method", props.getProperty("method"));
        AIMRequest.put("x_Type", props.getProperty("transType"));
        AIMRequest.put("x_ref_trans_id", at.get("referenceNum"));
        AIMRequest.put("x_Trans_ID", at.get("referenceNum"));
        AIMRequest.put("x_Auth_Code", at.get("gatewayCode"));
        Debug.logInfo("buildVoidTransaction. " + at.toString(), module);
    }

    private static Map<String, Object> validateRequest(Map<String, Object> params, Properties props, Map<String, Object> AIMRequest) {
        Map<String, Object> result = new HashMap<String, Object>();
        result.put(ModelService.RESPONSE_MESSAGE, ModelService.RESPOND_SUCCESS);
        return result;
    }

    private static void processAuthTransResult(Map<String, Object> request, Map<String, Object> reply, Map<String, Object> results) {
        AuthorizeResponse ar = (AuthorizeResponse) reply.get("authorizeResponse");
        try {
            Boolean authResult = (Boolean) reply.get("authResult");
            results.put("authResult", new Boolean(authResult.booleanValue()));
            results.put("authFlag", ar.getReasonCode());
            results.put("authMessage", ar.getReasonText());
            if (authResult.booleanValue()) { //passed
                results.put("authCode", ar.getAuthorizationCode());
                results.put("authRefNum", ar.getTransactionId());
                results.put("cvCode", ar.getCvResult());
                results.put("avsCode", ar.getAvsResult());
                if (BigDecimal.ZERO.compareTo(ar.getAmount()) == 0) {
                    results.put("processAmount", getXAmount(request));
                } else {
                    results.put("processAmount", ar.getAmount());
                }
            } else {
                results.put("authCode", ar.getResponseCode());
                results.put("processAmount", BigDecimal.ZERO);
                results.put("authRefNum", AuthorizeResponse.ERROR);
            }
        } catch (Exception ex) {
            Debug.logError(ex, module);
            results.put("authCode", ar.getResponseCode());
            results.put("processAmount", BigDecimal.ZERO);
            results.put("authRefNum", AuthorizeResponse.ERROR);
        }
        Debug.logInfo("processAuthTransResult: " + results.toString(),module);
    }

    private static void processCaptureTransResult(Map<String, Object> request, Map<String, Object> reply, Map<String, Object> results) {
        AuthorizeResponse ar = (AuthorizeResponse) reply.get("authorizeResponse");
        try {
            Boolean captureResult = (Boolean) reply.get("authResult");
            results.put("captureResult", new Boolean(captureResult.booleanValue()));
            results.put("captureFlag", ar.getReasonCode());
            results.put("captureMessage", ar.getReasonText());
            results.put("captureRefNum", ar.getTransactionId());
            if (captureResult.booleanValue()) { //passed
                results.put("captureCode", ar.getAuthorizationCode());
                if (BigDecimal.ZERO.compareTo(ar.getAmount()) == 0) {
                    results.put("captureAmount", getXAmount(request));
                } else {
                    results.put("captureAmount", ar.getAmount());
                }
            } else {
                results.put("captureAmount", BigDecimal.ZERO);
            }
        } catch (Exception ex) {
            Debug.logError(ex, module);
            results.put("captureAmount", BigDecimal.ZERO);
        }
        Debug.logInfo("captureRefNum: " + results.toString(),module);
    }

    private static Map<String, Object> processRefundTransResult(Map<String, Object> request, Map<String, Object> reply) {
        Map<String, Object> results = new HashMap<String, Object>();
        AuthorizeResponse ar = (AuthorizeResponse) reply.get("authorizeResponse");
        try {
            Boolean captureResult = (Boolean) reply.get("authResult");
            results.put("refundResult", new Boolean(captureResult.booleanValue()));
            results.put("refundFlag", ar.getReasonCode());
            results.put("refundMessage", ar.getReasonText());
            results.put("refundRefNum", ar.getTransactionId());
            if (captureResult.booleanValue()) { //passed
                results.put("refundCode", ar.getAuthorizationCode());
                if (BigDecimal.ZERO.compareTo(ar.getAmount()) == 0) {
                    results.put("refundAmount", getXAmount(request));
                } else {
                    results.put("refundAmount", ar.getAmount());
                }
            } else {
                results.put("refundAmount", BigDecimal.ZERO);
            }
        } catch (Exception ex) {
            Debug.logError(ex, module);
            results.put("refundAmount", BigDecimal.ZERO);
        }
        Debug.logInfo("processRefundTransResult: " + results.toString(),module);
        return results;
    }

    private static Map<String, Object> processReleaseTransResult(Map<String, Object> request, Map<String, Object> reply) {
        Map<String, Object> results = new HashMap<String, Object>();
        AuthorizeResponse ar = (AuthorizeResponse) reply.get("authorizeResponse");
        try {
            Boolean captureResult = (Boolean) reply.get("authResult");
            results.put("releaseResult", new Boolean(captureResult.booleanValue()));
            results.put("releaseFlag", ar.getReasonCode());
            results.put("releaseMessage", ar.getReasonText());
            results.put("releaseRefNum", ar.getTransactionId());
            if (captureResult.booleanValue()) { //passed
                results.put("releaseCode", ar.getAuthorizationCode());
                if (BigDecimal.ZERO.compareTo(ar.getAmount()) == 0) {
                    results.put("releaseAmount", getXAmount(request));
                } else {
                    results.put("releaseAmount", ar.getAmount());
                }
            } else {
                results.put("releaseAmount", BigDecimal.ZERO);
            }
        } catch (Exception ex) {
            Debug.logError(ex, module);
            results.put("releaseAmount", BigDecimal.ZERO);
        }
        Debug.logInfo("processReleaseTransResult: " + results.toString(),module);
        return results;
    }

    private static void processAuthCaptureTransResult(Map<String, Object> request, Map<String, Object> reply, Map<String, Object> results) {
        AuthorizeResponse ar = (AuthorizeResponse) reply.get("authorizeResponse");
        try {
        Boolean authResult = (Boolean) reply.get("authResult");
        results.put("authResult", new Boolean(authResult.booleanValue()));
        results.put("authFlag", ar.getReasonCode());
        results.put("authMessage", ar.getReasonText());
        results.put("captureResult", new Boolean(authResult.booleanValue()));
        results.put("captureFlag", ar.getReasonCode());
        results.put("captureMessage", ar.getReasonText());
        results.put("captureRefNum", ar.getTransactionId());
        if (authResult.booleanValue()) { //passed
            results.put("authCode", ar.getAuthorizationCode());
            results.put("authRefNum", ar.getTransactionId());
            results.put("cvCode", ar.getCvResult());
            results.put("avsCode", ar.getAvsResult());
            if (BigDecimal.ZERO.compareTo(ar.getAmount()) == 0) {
                results.put("processAmount", getXAmount(request));
            } else {
                results.put("processAmount", ar.getAmount());
            }
        } else {
            results.put("authCode", ar.getResponseCode());
            results.put("processAmount", BigDecimal.ZERO);
            results.put("authRefNum", AuthorizeResponse.ERROR);
        }
        } catch (Exception ex) {
            Debug.logError(ex, module);
            results.put("authCode", ar.getResponseCode());
            results.put("processAmount", BigDecimal.ZERO);
            results.put("authRefNum", AuthorizeResponse.ERROR);
        }
        Debug.logInfo("processAuthTransResult: " + results.toString(),module);
    }

    private static String getPaymentGatewayConfigValue(Delegator delegator, String paymentGatewayConfigId, String paymentGatewayConfigParameterName,
                                                       String resource, String parameterName) {
        String returnValue = "";
        if (UtilValidate.isNotEmpty(paymentGatewayConfigId)) {
            try {
                GenericValue payflowPro = EntityQuery.use(delegator).from("PaymentGatewayAuthorizeNet").where("paymentGatewayConfigId", paymentGatewayConfigId).queryOne();
                if (payflowPro != null) {
                    Object payflowProField = payflowPro.get(paymentGatewayConfigParameterName);
                    if (payflowProField != null) {
                        returnValue = payflowProField.toString().trim();
                    }
                }
            } catch (GenericEntityException e) {
                Debug.logError(e, module);
            }
        } else {
            String value = EntityUtilProperties.getPropertyValue(resource, parameterName, delegator);
            if (value != null) {
                returnValue = value.trim();
            }
        }
        return returnValue;
    }
    
    private static String getCardType(String cardType) {
        if (("CCT_VISA".equalsIgnoreCase(cardType))) return "V";
        if (("CCT_MASTERCARD".equalsIgnoreCase(cardType))) return "M";
        if ((("CCT_AMERICANEXPRESS".equalsIgnoreCase(cardType)) || ("CCT_AMEX".equalsIgnoreCase(cardType)))) return "A";
        if (("CCT_DISCOVER".equalsIgnoreCase(cardType))) return "D";
        if (("CCT_JCB".equalsIgnoreCase(cardType))) return "J";
        if ((("CCT_DINERSCLUB".equalsIgnoreCase(cardType)))) return "C";        
        return "";
    }
    
    private static BigDecimal getXAmount(Map<String, Object> request) {
        BigDecimal amt = BigDecimal.ZERO;
        if (request.get("x_Amount") != null) {
            try {
                BigDecimal amount = new BigDecimal((String) request.get("x_Amount"));
                amt = amount;
            } catch (NumberFormatException e) {
                Debug.logWarning(e, e.getMessage(), module);
            }
        }
        return amt;
    }
}
