RW

Initial and Delayed Charges Using Stripe in C#

Published: 2015-01-04 | By: Ryan Williams
Background
 
I recently built a site that has an intiial (trial) charge of $9.95 and then, if the product is not returned 30 days after shipment, a final $59.95 charge. This is an article about how I set up the initial and delayed charges in C# using Stripe, a payment service provider.
 
 
Getting Setup with Stripe
 
There are many companies out there that can help you process credit cards. The reason I decided to use Stripe was the simplicity of a single account that had an SDK in C# (although not officially supported). You could use a gateway processor and merchant service provider with a merchant account, it could save you money at the cost of time and complexity. Stripe charges 30¢ per successful charge and then 2.9% on top of that, see Stripe's pricing for more details.
 
To get started with Stripe, you first make an account; fortunately, you can use your account and their test API right away without business details. They have many test card numbers to emulate responses. To go live, you submit business information such as the bank account that funds will be transferred to (they can transfer funds every 2 days), the statement descriptor (what the buyer sees on their credit card statement), an address for the business and enable fraud filters (CVC and zip code verification aren't required but are a best practice). From there you can use a live mode API key to start accepting payment. They have an excellent dashboard to display account activity.
 
 
Payment Workflow 
 
Stripe has an easy client side option, Stripe Checkout, which pops a credit card form (good for designers with simple requirements), server side SDKs (for more complicated scenarios) as well as other options for both deeper client and server side integration. Stripe's API does have the concept of subscriptions which make rebilling easy but in the case of a small initial and larger delayed charge, it doesn't make a lot of sense to use that. Although there are some companies that specialize in more complicated workflows using Stripe (like BillForward), I decided to do it myself to save money.
 
The basic workflow I implemented is this: 
  1. Authorize the initial + delayed amount of $69.90
  2. If success, add their card details to Stripe as a customer and store the customer ID, setting the order to "not shipped" and "authorized"
  3. Every day, run a batch process which gets all orders that are "shipped" in "authorized" status or have been "shipped" with a "trial" status that has expired (was shipped over 30 days ago)
    • For "shipped," "authorized" orders: check to see that there is no record of the $9.95 captured from the $69.90 auth in Stripe and then capture it, if needed, from the authorization charge ID (note: there are only 7 days to capture all or part of an auth), then mark the order as in "trial" status
    • For "shipped," expired "trial" orders (not "returned"): check that the $59.95 amount has not been charged in Stripe, if it hasn't been charged, charge the remaining $59.95 due and set the order status as "payment complete"
       
I decided to use the auth, capture model simply because I wanted to know that the customer could at one time pay the full amount. Although it's still possible the 2nd charge could fail, the likelihood is lower. The orders are processed daily using a cron job, in this case Quartz.NET.

Every day, the Quartz job looks for shipped orders that were authorized (capturing the $9.95) and orders in trial status that were shipped over 30 days ago (charging $59.95). In Stripe, once you store a customer's payment details, you can charge the customer by using the customer's ID. This enables workflows with multiple, potentially smaller, payments. Again, you cannot know if any payment after the authorization expires will succeed. 
 
The problem with this workflow is that some customers will look at their statement and see their bank listing the authorization of $69.90 as a charge. In this case they may contact customer service and complain. It's important to assure the buyer that they must wait 7 days until the remaining auth is reversed, as Stripe says regarding auth/ capture
 
 
Some Code Examples
 
I wrote the application in C# using ASP.NET for the front end and an Azure Worker Role for the batch tasks; as such, I used the Stripe.net NuGet package, the source code for which is on GitHub. Below are some snippets of code I used to accomplish the workflow.
 
Initializing Stripe.net in the application:
        protected void Application_Start()
        {
            XmlConfigurator.Configure();

            Log.Info("Application Started");

            AreaRegistration.RegisterAllAreas();
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            StripeConfiguration.SetApiKey(_appKey);
        }
 

Authorizing full amount:
        public static bool AuthorizeFullAmount(
                        UsOrderModel model,
                        Item item,
                        out object stripeChargeResult,
                        out string responseMessage)
        {
            var myCharge = new StripeChargeCreateOptions
            {
                Amount = StripeHelper.ConvertToStripeAmount(item.InitialPrice + item.DelayedPrice),
                Currency = EnumHelper.GetDescription(item.CurrencyCode),
                Description = item.ItemName,
                CardNumber = model.CardNumber,
                CardExpirationYear = model.ExpirationYear,
                CardExpirationMonth = model.ExpirationMonth,
                CardAddressCountry = EnumHelper.GetDescription(model.Country),
                CardAddressLine1 = model.BillingAddress,
                CardAddressState = model.BillingState,
                CardAddressZip = model.BillingZipCode,
                CardCvc = model.Cvv,
                Capture = false
            };

            var chargeService = new StripeChargeService();

            try
            {
                Log.Info(LogCodes.AttemptingIntitialSale);
                stripeChargeResult = chargeService.Create(myCharge);
                responseMessage = "success";
                return StripeHelper.IsPaid(stripeChargeResult);
            }
            catch (StripeException stripeException)
            {
                Log.Info(LogCodes.StripeChargeFailure, stripeException);
                stripeChargeResult = stripeException;
                responseMessage = stripeException.Message;
                return false;
            }
            catch (Exception ex)
            {
                Log.Fatal(LogCodes.Unknown, ex);
                stripeChargeResult = ex;
                responseMessage = "fail";
                return false;
            }
        }

 

Creating a Stripe customer (a Stripe customer ID is used to charge them later):
        public static StripeCustomer CreateStripeCustomer(UsOrderModel model)
        {
            var myCustomer = new StripeCustomerCreateOptions
            {
                Email = model.Email,
                CardNumber = model.CardNumber,
                CardExpirationYear = model.ExpirationYear,
                CardExpirationMonth = model.ExpirationMonth,
                CardAddressCountry = model.Country.ToString(),
                CardAddressLine1 = model.BillingAddress,
                CardAddressCity = model.BillingCity,
                CardAddressState = model.BillingState,
                CardAddressZip = model.BillingZipCode,
                CardCvc = model.Cvv
            };

            StripeCustomer customer;
            var customerService = new StripeCustomerService();

            try
            {
                customer = customerService.Create(myCustomer);
            }
            catch (Exception ex)
            {
                Log.Fatal(LogCodes.FailedToAddCustomer, ex);
                throw;
            }

            return customer;
        }

 

Check that the amount (trial or delayed) is due:
        private static bool IsAmountDue(Order orderToProcess, decimal amount, CurrencyCode currencyCode)
        {
            var alreadyPaidAmountDue = false;
            var chargeService = new StripeChargeService();
            var response = chargeService.List(new StripeChargeListOptions()
            {
                CustomerId = orderToProcess.ExternalId
            });
            var stripeCharges = response.ToList();

            foreach (var charge in stripeCharges)
            {
                alreadyPaidAmountDue = HasAlreadyPaidAmount(amount, currencyCode, charge);

                if (alreadyPaidAmountDue)
                {
                    Log.Info(
                        string.Format("Customer '{0}' was alread charged '{1}' '{2}'",
                            charge.CustomerId,
                            charge.Amount,
                            charge.Currency));
                    break;
                }
            }
            return alreadyPaidAmountDue;
        }

 

Try to capture part of the authorized amount:
        private bool IsCaptureSuccessful(Order order, decimal amount, CurrencyCode currency, out object stripeChargeResult)
        {
            var chargeId = GetStripeChargeId(order, out stripeChargeResult);
            if (chargeId == null) return false;

            var chargeService = new StripeChargeService();

            try
            {
                Log.Info(
                    string.Format("Attempting capture charge of customer id: '{0}', for amount: '{1}', currency: '{2}'", 
                        order.ExternalId,
                        amount,
                        currency));

                stripeChargeResult = chargeService.Capture(chargeId, StripeHelper.ConvertToStripeAmount(amount));

                if (StripeHelper.IsPaid(stripeChargeResult)) return true;
            }
            catch (StripeException stripeException)
            {
                stripeChargeResult = stripeException;
                Log.Info(LogCodes.StripeChargeFailure, stripeException);
            }
            catch (Exception ex)
            {
                stripeChargeResult = ex;             
                Log.Fatal(LogCodes.Unknown, ex);
            }

            return false;
        }

 

Charge the final amount to the happy customer:
        private bool IsChargeSuccessful(string customerId, decimal amount, CurrencyCode currency, out object stripeChargeResult)
        {
            var myCharge = new StripeChargeCreateOptions
            {
                Amount = StripeHelper.ConvertToStripeAmount(amount),
                Currency = EnumHelper.GetDescription(currency),
                CustomerId = customerId,
                Capture = true
            };

            var chargeService = new StripeChargeService();

            try
            {
                Log.Info(
                    string.Format("Attempting charge customer id: '{0}', for amount: '{1}', currency: '{2}'",
                        customerId,
                        myCharge.Amount,
                        myCharge.Currency));

                stripeChargeResult = chargeService.Create(myCharge);

                if (StripeHelper.IsPaid(stripeChargeResult)) return true;
            }
            catch (StripeException stripeException)
            {
                stripeChargeResult = stripeException;
                Log.Info(LogCodes.StripeChargeFailure, stripeException);
            }
            catch (Exception ex)
            {
                stripeChargeResult = ex;
                Log.Fatal(LogCodes.Unknown, ex);
            }

            return false;
        }

 

 
Summary
 
This article showed the problem and a solution for an initial and delayed amount for a single product processed with Stripe in C# using the Stripe.net NuGet package. Regarding the product, there have been very few returns. In the case of a return, any remaining authorized amount expires. I also added the ability to do refunds on captured charges in the backend portal.
 
In the process of building this site, I learned about initial/ delayed charges. I think this is an important workflow to think about for American e-commerce sites. As for the product, you may not know you snore; in such a case, you can record your snoring using an app such as SnoreLab. The app measures how loud you snore and allows you to include factors and remedies that contribute to the result, from which you can track your progress. One of my co-worker tried the SleepTight Mouthpiece, which uses the initial and delayed changes, while using the app and his results indicated significant reduction in snoring (from a 50 to a 4 out of 100).


No Comments... Yet


Comment On

Prove you are human 13 + 4 =