cancel
Showing results for 
Search instead for 
Did you mean: 

C# SDK 1.7.1: Error Handling Missing for ARB and Transaction Details API

I did not seen a post about this when I searched the community, so hopefully I'm not re-posting an already discussed issue.

 

For those who have worked with the C# SDK, the lack of error handling in the ARB and Transaction Details API can be rather frustrating.  It seems to me that handling an error via throwing an exception (which is how the SDK is currrently dealing with errors) is not so friendly.

 

When an error occurs using the supplied classes, you get an exception like this:

System.InvalidOperationException was caught
  HResult=-2146233079
  Message=Error processing request: E00017 - Start Date must not occur before the submission date.
  Source=AuthorizeNet
  StackTrace:
       at AuthorizeNet.HttpXmlUtility.CheckForErrors(ANetApiResponse response)
       at AuthorizeNet.HttpXmlUtility.Send(ANetApiRequest apiRequest)
       at AuthorizeNet.SubscriptionGateway.CreateSubscription(ISubscriptionRequest subscription)
       at MyLibrary.AuthorizeNetSubscriptionProcessor.SubmitProcessing() in c:\Dev\MyLibrary\AuthorizeNetSubscriptionProcessor.cs:line 947
       at Troppus.Services.BusinessLogic.Providers.PaymentProviders.AuthorizeNet.SubmitPayment() in c:\Dev\MyLibrary\AuthorizeNet.cs:line 378
  InnerException:

 

For a more elegant way to manage errors (and so I didn't have to include extra exception handling around each gateway call), I modified the supplied gateway class to allow easy checks for errors and to supply handy return processing.

 

To start with I created a common base class:

using AuthorizeNet;
using AuthorizeNet.APICore;

using System;
using System.Text;

namespace MyCustomAuthorizeNet
{
    /// <summary>
    /// This class contains common properties and methods for gateway
    /// classes that need error handling added.
    /// </summary>
    internal abstract class GatewayWithErrorHandlingBase
    {
        #region Declarations

        /// <summary>
        /// The failure return code default value.
        /// </summary>
        public const int FailureReturnCodeDefault = -1;

        /// <summary>
        /// The success return code default value.
        /// </summary>
        public const int SuccessReturnCodeDefault = 0;

        /// <summary>
        /// The warning return code default value.
        /// </summary>
        public const int WarningReturnCodeDefault = 1;

        #endregion Declarations

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="GatewayWithErrorHandlingBase"/> class.
        /// </summary>
        /// <param name="apiLogin">The API login.</param>
        /// <param name="transactionKey">The transaction key.</param>
        /// <param name="mode">The mode.</param>
        protected GatewayWithErrorHandlingBase(
            string apiLogin,
            string transactionKey,
            ServiceMode mode)
        {
            this.Log = LogManager.GetLogger(this.GetType());

            this.Gateway =
                        mode == ServiceMode.Live
                        ? new HttpXmlUtility(ServiceMode.Live, apiLogin, transactionKey)
                        : new HttpXmlUtility(ServiceMode.Test, apiLogin, transactionKey);

            this.FailureReturnCodeValue = FailureReturnCodeDefault;
            this.SuccessReturnCodeValue = SuccessReturnCodeDefault;
            this.WarningReturnCodeValue = WarningReturnCodeDefault;
        }

        #endregion Constructors

        #region Properties

        /// <summary>
        /// Gets the return value for a failed execution.
        /// </summary>
        public int FailureReturnCodeValue { get; private set; }

        /// <summary>
        /// Gets and sets the processing gateway object.
        /// </summary>
        protected HttpXmlUtility Gateway { get; private set; }

        /// <summary>
        /// Gets or sets the Subscription Reference id.
        /// </summary>
        public string RefId { get; set; }

        /// <summary>
        /// Gets or sets the response Messages.
        /// </summary>
        public messagesTypeMessage[] Messages { get; set; }

        #region RequestResponse

        /// <summary>
        /// Gets or sets the request response object.
        /// </summary>
        private ANetApiResponse _requestResponse;

        /// <summary>
        /// Gets or sets the request response object.
        /// </summary>
        public ANetApiResponse RequestResponse
        {
            get
            {
                return this._requestResponse;
            }

            set
            {
                this._requestResponse = value;

                if (this._requestResponse == null)
                {
                    this.RefId = null;
                    this.Messages = null;
                    this.ResultCode = messageTypeEnum.Ok;
                }
                else
                {
                    this.RefId = this._requestResponse.refId;

                    if (this._requestResponse.messages == null)
                    {
                        this.Messages = null;
                        this.ResultCode = messageTypeEnum.Ok;
                    }
                    else
                    {
                        this.ResultCode = this._requestResponse.messages.resultCode;
                        this.Messages = this._requestResponse.messages.message;
                    }
                }
            }
        }

        #endregion RequestResponse

        /// <summary>
        /// Gets or sets the response ResultCode.
        /// </summary>
        public messageTypeEnum ResultCode { get; set; }

        /// <summary>
        /// Gets the return value for successful execution.
        /// </summary>
        public int SuccessReturnCodeValue { get; private set; }

        /// <summary>
        /// Gets the return value for an execution that generated a warning.
        /// </summary>
        public int WarningReturnCodeValue { get; private set; }

        #endregion Properties

        /// <summary>
        /// Examine the current status to return information about it.
        /// This method would be called after the Send method is called.
        /// </summary>
        /// <param name="returnCode">
        /// The return Code.
        /// </param>
        /// <param name="reasonCode">
        /// The reason Code.
        /// </param>
        /// <param name="returnMessage">
        /// The return message.
        /// If no error occurred, this will be Null.
        /// </param>
        public void GetProcessingResults(
            out int returnCode,
            out int reasonCode,
            out string returnMessage)
        {
            returnMessage = null;
            reasonCode = this.SuccessReturnCodeValue;
            returnCode = this.ResultCode == messageTypeEnum.Ok
                                ? this.SuccessReturnCodeValue
                                : this.FailureReturnCodeValue;

            StringBuilder returnMessages = new StringBuilder();
            if (this.Messages != null)
            {
                foreach (messagesTypeMessage messageItem in this.Messages)
                {
                    if (messageItem == null)
                    {
                        continue;
                    }

                    if (messageItem.code.Left(1) == "E")
                    {
                        {
                            // strip the leading 'E', and convert to a number
                            string codeNumber = messageItem.code.Substring(1);
                            Int32.TryParse(codeNumber, out reasonCode);
                        }

                        returnCode = this.FailureReturnCodeValue;
                    }
                    else if (messageItem.code.Left(1) == "I")
                    {
                        // strip the leading 'I', and convert to a number
                        string codeNumber = messageItem.code.Substring(1);
                        Int32.TryParse(codeNumber, out reasonCode);

                        if (reasonCode == 1)
                        {
                            // The request was processed successfully.
                            reasonCode = this.SuccessReturnCodeValue;
                        }
                        else
                        {
                            if (returnCode != this.FailureReturnCodeValue)
                            {
                                returnCode = this.WarningReturnCodeValue;
                            }
                        }
                    }
                    else
                    {
                        reasonCode = -99;

                        if (returnCode != this.FailureReturnCodeValue)
                        {
                            returnCode = this.WarningReturnCodeValue;
                        }
                    }

                    if (reasonCode != this.SuccessReturnCodeValue)
                    {
                        if (returnMessages.Length > 0)
                        {
                            returnMessages.Append(", ");
                        }

                        returnMessages.AppendFormat("{0} ({1})", messageItem.text, messageItem.code);
                    }
                }
            }

            returnMessage = returnMessages.Length > 0 ? returnMessages.ToString() : null;
        }

        /// <summary>
        /// Replace the default return code values with custom values.
        /// </summary>
        /// <param name="successValue">
        /// The value used for successful execution.
        /// </param>
        /// <param name="failureValue">
        /// The value used for failed execution.
        /// </param>
        /// <param name="warningValue">
        /// The value used for execution with warnings.
        /// </param>
        public void OverrideReturnCodeValues(int successValue, int failureValue, int warningValue)
        {
            this.FailureReturnCodeValue = failureValue;
            this.SuccessReturnCodeValue = successValue;
            this.WarningReturnCodeValue = warningValue;
        }

        /// <summary>
        /// Sends the specified API request and catches any errors that occur.
        /// </summary>
        /// <param name="apiRequest">
        /// The API request.
        /// </param>
        /// <returns>
        /// An <see cref="ANetApiResponse"/> object.
        /// </returns>
        public ANetApiResponse Send(ANetApiRequest apiRequest)
        {
            this.Messages = null;
            this.RequestResponse = null;
            this.ResultCode = messageTypeEnum.Ok;

            try
            {
                return this.Gateway.Send(apiRequest);
            }
            catch (InvalidOperationException invalidExc)
            {
                const string ErrorProcessingRequest = "Error processing request:";

                if (invalidExc.Message.Left(ErrorProcessingRequest.Length) == ErrorProcessingRequest)
                {
                    string fullMessage =
                        invalidExc.Message.Substring(ErrorProcessingRequest.Length + 1).TrimNull();
                    string errorCode = fullMessage.Left(6).TrimNull();
                    string errorText = fullMessage.Substring(6).TrimNull();
                    if (errorText.Left(2) == "- ")
                    {
                        errorText = errorText.Substring(2).TrimNull();
                    }

                    this.Messages = new[] { new messagesTypeMessage { code = errorCode, text = errorText } };
                    this.ResultCode = messageTypeEnum.Error;

                    return new ANetApiResponse
                    {
                        messages = new messagesType
                        {
                            resultCode = messageTypeEnum.Error,
                            message = this.Messages
                        }
                    };
                }

                throw;
            }
        }
    }
}

 

Then, I took the supplied subscription gateway and updated it to use the new base class:

using AuthorizeNet;
using AuthorizeNet.APICore;

namespace MyCustomAuthorizeNet
{
	/// <summary>
	/// This class takes a copy of the SubscriptionGateway class supplied by 
	/// the Authorize.NET SDK and adds error handling.
	/// </summary>
	internal class SubscriptionGatewayWithErrorHandling 
		: GatewayWithErrorHandlingBase, ISubscriptionGateway
	{
		#region Constructors

		/// <summary>
		/// Initializes a new instance of the <see cref="SubscriptionGatewayWithErrorHandling"/> class.
		/// </summary>
		/// <param name="apiLogin">The API login.</param>
		/// <param name="transactionKey">The transaction key.</param>
		/// <param name="mode">The mode.</param>
		public SubscriptionGatewayWithErrorHandling(
			string apiLogin, 
			string transactionKey, 
			ServiceMode mode)
			: base(apiLogin, transactionKey, mode)
		{
		}

		#endregion Constructors

		/// <summary>
		/// Creates a new subscription
		/// </summary>
		/// <param name="subscription">
		/// The subscription to create - requires that you add a credit 
		/// card and billing first and last.
		/// </param>
		/// <returns>
		/// The <see cref="ISubscriptionRequest"/>.
		/// </returns>
		public ISubscriptionRequest CreateSubscription(ISubscriptionRequest subscription)
		{
			ARBSubscriptionType sub = subscription.ToAPI();
			ARBCreateSubscriptionRequest req = 
				new ARBCreateSubscriptionRequest { subscription = sub };

			this.RequestResponse = this.Send(req);
			if (this.RequestResponse is ARBCreateSubscriptionResponse)
			{
				subscription.SubscriptionID = 
					(this.RequestResponse as ARBCreateSubscriptionResponse).subscriptionId;
			}

			return subscription;
		}

		/// <summary>
		/// Updates the subscription.
		/// </summary>
		/// <param name="subscription">
		/// The subscription to update. Can't change billing intervals however.
		/// </param>
		/// <returns>
		/// The <see cref="bool"/>.
		/// </returns>
		public bool UpdateSubscription(ISubscriptionRequest subscription)
		{
			ARBSubscriptionType sub = subscription.ToUpdateableAPI();
			ARBUpdateSubscriptionRequest req = 
				new ARBUpdateSubscriptionRequest 
					{
						subscription = sub, 
						subscriptionId = subscription.SubscriptionID
					};

			this.RequestResponse = this.Send(req);

			return this.ResultCode == messageTypeEnum.Ok;
		}

		/// <summary>
		/// Cancels the subscription.
		/// </summary>
		/// <param name="subscriptionID">
		/// The subscription ID.
		/// </param>
		/// <returns>
		/// False indicates an error occurred.
		/// </returns>
		public bool CancelSubscription(string subscriptionID)
		{
			ARBCancelSubscriptionRequest req = 
				new ARBCancelSubscriptionRequest { subscriptionId = subscriptionID };

			// will throw if there are errors
			this.RequestResponse = this.Send(req);

			return this.ResultCode == messageTypeEnum.Ok;
		}

		/// <summary>
		/// Gets the subscription status.
		/// </summary>
		/// <param name="subscriptionID">
		/// The subscription ID.
		/// </param>
		/// <returns>
		/// A <see cref="ARBSubscriptionStatusEnum"/> value.
		/// </returns>
		public ARBSubscriptionStatusEnum GetSubscriptionStatus(string subscriptionID)
		{
			ARBGetSubscriptionStatusRequest req = 
				new ARBGetSubscriptionStatusRequest { subscriptionId = subscriptionID };

			this.RequestResponse = this.Send(req);
			if (this.RequestResponse is ARBGetSubscriptionStatusResponse)
			{
				return (this.RequestResponse as ARBGetSubscriptionStatusResponse).status;
			}

			return ARBSubscriptionStatusEnum.terminated;
		}
	}
}

 

I also updated the supplied Transaction Details gateway, like so:

using AuthorizeNet;
using AuthorizeNet.APICore;

using System;
using System.Collections.Generic;

namespace MyCustomAuthorizeNet
{
    /// <summary>
    /// This class takes a copy of the ReportingGateway class supplied by
    /// the Authorize.NET SDK and adds error handling.
    /// </summary>
    internal class ReportingGatewayWithErrorHandling : GatewayWithErrorHandlingBase, IReportingGateway
    {
        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="ReportingGatewayWithErrorHandling"/> class.
        /// </summary>
        /// <param name="apiLogin">The API login.</param>
        /// <param name="transactionKey">The transaction key.</param>
        /// <param name="mode">The mode.</param>
        public ReportingGatewayWithErrorHandling(
            string apiLogin,
            string transactionKey,
            ServiceMode mode)
            : base(apiLogin, transactionKey, mode)
        {
        }

        #endregion Constructors

        /// <summary>
        /// Returns charges and statistics for a given batch
        /// </summary>
        /// <param name="batchId">the batch id</param>
        /// <returns>
        /// A batch with statistics.
        /// If an error occurs, Null will be returned.
        /// </returns>
        public Batch GetBatchStatistics(string batchId)
        {
            getBatchStatisticsRequest req =
                new getBatchStatisticsRequest
                    {
                        batchId = batchId
                    };
            this.RequestResponse = this.Send(req);

            if (this.RequestResponse is getBatchStatisticsResponse)
            {
                return Batch.NewFromResponse(
                        this.RequestResponse as getBatchStatisticsResponse);
            }

            return null;
        }

        /// <summary>
        /// Returns all Settled Batches for the last 30 days
        /// </summary>
        /// <returns>
        /// A list of <see cref="Batch"/> objects.
        /// If an error occurs, Null will be returned.
        /// </returns>
        public List<Batch> GetSettledBatchList()
        {
            var from = DateTime.Today.AddDays(-30);
            var to = DateTime.Today;

            return this.GetSettledBatchList(from, to, false);
        }

        /// <summary>
        /// Returns all Settled Batches for the last 30 days
        /// </summary>
        /// <param name="includeStats">
        /// The include Stats.
        /// </param>
        /// <returns>
        /// A list of <see cref="Batch"/> objects.
        /// If an error occurs, Null will be returned.
        /// </returns>
        public List<Batch> GetSettledBatchList(bool includeStats)
        {
            var from = DateTime.Today.AddDays(-30);
            var to = DateTime.Today;

            return this.GetSettledBatchList(from, to, includeStats);
        }

        /// <summary>
        /// Returns batch settlements for the specified date range
        /// </summary>
        /// <param name="from">
        /// The From date.
        /// </param>
        /// <param name="to">
        /// The To date.
        /// </param>
        /// <returns>
        /// A list of <see cref="Batch"/> objects.
        /// If an error occurs, Null will be returned.
        /// </returns>
        public List<Batch> GetSettledBatchList(DateTime from, DateTime to)
        {
            return this.GetSettledBatchList(from, to, false);
        }

        /// <summary>
        /// Returns batch settlements for the specified date range
        /// </summary>
        /// <param name="from">
        /// The From date.
        /// </param>
        /// <param name="to">
        /// The To date.
        /// </param>
        /// <param name="includeStats">
        /// The include Stats.
        /// </param>
        /// <returns>
        /// A list of <see cref="Batch"/> objects.
        /// If an error occurs, Null will be returned.
        /// </returns>
        public List<Batch> GetSettledBatchList(DateTime from, DateTime to, bool includeStats)
        {
            getSettledBatchListRequest req = new getSettledBatchListRequest
                        {
                            firstSettlementDate = @from.ToUniversalTime(),
                            lastSettlementDate = to.ToUniversalTime(),
                            firstSettlementDateSpecified = true,
                            lastSettlementDateSpecified = true
                        };

            if (includeStats)
            {
                req.includeStatistics = true;
                req.includeStatisticsSpecified = true;
            }

            this.RequestResponse = this.Send(req);

            if (this.RequestResponse is getSettledBatchListResponse)
            {
                return Batch.NewFromResponse(
                        this.RequestResponse as getSettledBatchListResponse);
            }

            return null;
        }

        /// <summary>
        /// Returns Transaction details for a given transaction ID (transid)
        /// </summary>
        /// <param name="transactionID">
        /// The id of the transaction for which to gather data.
        /// </param>
        /// <returns>
        /// A <see cref="Transaction"/> object.
        /// If an error occurs, Null will be returned.
        /// </returns>
        public Transaction GetTransactionDetails(string transactionID)
        {
            getTransactionDetailsRequest req =
                new getTransactionDetailsRequest { transId = transactionID };

            this.RequestResponse = this.Send(req);

            if (this.RequestResponse is getTransactionDetailsResponse)
            {
                return Transaction.NewFromResponse(
                        (this.RequestResponse as getTransactionDetailsResponse).transaction);
            }

            return null;
        }

        /// <summary>
        /// Returns all transaction within a particular batch
        /// </summary>
        /// <param name="batchId">
        /// The batch Id.
        /// </param>
        /// <returns>
        /// A list of <see cref="Transaction"/> objects.
        /// If an error occurs, Null will be returned.
        /// </returns>
        public List<Transaction> GetTransactionList(string batchId)
        {
            getTransactionListRequest req = new getTransactionListRequest { batchId = batchId };

            this.RequestResponse = this.Send(req);

            if (this.RequestResponse is getTransactionListResponse)
            {
                return Transaction.NewListFromResponse(
                        (this.RequestResponse as getTransactionListResponse).transactions);
            }

            return null;
        }

        /// <summary>
        /// Returns all transactions for the last 30 days
        /// </summary>
        /// <returns>
        /// A list of <see cref="Transaction"/> objects.
        /// If an error occurs, Null will be returned.
        /// </returns>
        public List<Transaction> GetTransactionList()
        {
            return this.GetTransactionList(DateTime.Today.AddDays(-30), DateTime.Today);
        }

        /// <summary>
        /// Returns all transactions for a given time period. This can result in
        /// a number of calls to the API
        /// </summary>
        /// <param name="from">
        /// The From date.
        /// </param>
        /// <param name="to">
        /// The To date.
        /// </param>
        /// <returns>
        /// A list of <see cref="Transaction"/> objects.
        /// If an error occurs, Null will be returned.
        /// </returns>
        public List<Transaction> GetTransactionList(DateTime from, DateTime to)
        {
            List<Batch> batches = this.GetSettledBatchList(from, to);
            if (batches == null)
            {
                return null;
            }

            List<Transaction> result = new List<Transaction>();
            foreach (var batch in batches)
            {
                result.AddRange(this.GetTransactionList(batch.ID));
            }

            return result;
        }

        /// <summary>
        /// returns the most recent 1000 transactions that are unsettled
        /// </summary>
        /// <returns>
        /// A list of <see cref="Transaction"/> objects.
        /// If an error occurs, Null will be returned.
        /// </returns>
        public List<Transaction> GetUnsettledTransactionList()
        {
            this.RequestResponse = this.Send(new getUnsettledTransactionListRequest());

            if (this.RequestResponse is getUnsettledTransactionListResponse)
            {
                return Transaction.NewListFromResponse(
                        (this.RequestResponse as
                                getUnsettledTransactionListResponse).transactions);
            }

            return null;
        }
    }
}

 

Once that was done, calling the gateway and checking for errors became much easier:

// create the request
SubscriptionRequest subscriptionProcessingRequest = this.BuildCreateRequest(detailItem);

// call the gateway method
ISubscriptionRequest responseCall = gateway.CreateSubscription(subscriptionProcessingRequest);

// extract the gateway settings
string returnMessage;
int reasonCode;
int returnCode;
gateway.GetProcessingResults(out returnCode, out reasonCode, out returnMessage);

// result:
// returnCode = -1
// reasonCode = 17
// returnMessage = "Start Date must not occur before the submission date."

 

MLooper
Member
3 REPLIES 3

Just realized I didn't include some extension code that is used by the base class - here is a class with those extension methods:

using System;

namespace MyCustomAuthorizeNet
{
	/// <summary>
	/// Extensions on the standard string type.
	/// </summary>
	public static class StringExtensions
	{
		/// <summary>
		/// Determine if a string is Null or is an empty string.
		/// A string of blanks will be treated as an empty string.
		/// </summary>
		/// <param name="inString">The string to check.</param>
		/// <returns>
		/// True if the string is Null or an empty string; False otherwise.
		/// </returns>
		public static bool IsNullOrEmpty(this string inString)
		{
			return String.IsNullOrEmpty(inString);
		}

		/// <summary>
		/// Extract the left most characters of a string to the
		/// specified length.
		/// If the specified length is greater than the length of the string,
		/// a Null value will be returned.
		/// A Null or empty string, will return a Null.
		/// </summary>
		/// <param name="inString">The string to get the result from.</param>
		/// <param name="inLength">The number of characters to return.</param>
		/// <returns>
		/// the left-most characters of the source string for the supplied length.
		/// </returns>
		public static string Left(this string inString, int inLength)
		{
			// verify this is not an empty string or null
			if (String.IsNullOrWhiteSpace(inString))
			{
				return null;
			}

			// verify the string is long enough
			if (inString.Length < inLength)
			{
				return inString;
			}

			// get the left-most characters
			inString = inString.Substring(0, inLength);

			return inString;
		}

		/// <summary>
		/// Trim the string with null checking.
		/// </summary>
		/// <param name="inString">The string to get the result from.</param>
		/// <returns>
		/// The trimmed string.
		/// </returns>
		public static string TrimNull(this string inString)
		{
			// verify this is not an empty string or null
			return inString.IsNullOrEmpty() ? null : inString.Trim();
		}
	}
}

 

MLooper
Member

A simple not in c# Exception handling...c# exception handling

 

tomin

tomincrow
Member

Hello,

 

Our SDKs are now hosted on GitHub: http://github.com/AuthorizeNet.

 

Richard