Creating a Stripe Payments Plugin for Sitecore Commerce Engine

11.22.17   Prasanth Nittala

In this post, I will cover the general concepts of developing plugins for Sitecore Commerce Engine. To illustrate the process, I developed a plugin that integrates with Stripe as the payment processing system. Here are the steps for the development of a plugin:

  1. Create a VS .netcore project (in this example it is named Oshyn.Plugin.SC.Payments.Stripe).
  2. Since we are replacing the payment processing in Sitecore Commerce Engine, remove the reference to BrainTreePayments Plugin project and add a reference to this newly Created project in the Sitecore commerce engine project. Because we are making Stripe our payment processor, we are removing references to the BrainTreePayment plugin.
  3. Create a Stripe account using this page. Then find the PublishableKey, SecretKey from the dashboard.
  4. In the wwwroot/data/Environment/Plugin.Habitat.CommerceAuthoring-1.0.0.json file remove the BrainTreeClientPolicy
    StripeClientPolicy
    {
       "$type":"Oshyn.Plugin.SC.Payments.Stripe.StripeClientPolicy, Oshyn.Plugin.SC.Payments.Stripe",
       "Environment":"sandbox",
       "PublishableKey":"pk_test_abc133424242424242",//publishable key provided by Stripe
       "SecretKey":"sk_test_abc133424424424424442",//secret key provided by Stripe
       "ConnectTimeout": 120000,
       "PolicyId":"d30f5f4bb31041ca99006640f0309e51",//just generate random Guid and use it here
       "Models": {
          "$type":"System.Collections.Generic.List`1[[Sitecore.Commerce.Core.Model, Sitecore.Commerce.Core]], mscorlib",
          "$values": []
    }
  5. Create the object that reads this configuration as StripeClientPolicy as shown below:
    StripeClientPolicy
    namespaceOshyn.Plugin.SC.Payments.Stripe
    {
       usingSitecore.Commerce.Core;
     
       publicclassStripeClientPolicy : Policy
       {
          /// <summary>
          /// Initializes a new instance of the <see cref="StripeClientPolicy" /> class.
          /// </summary>
          publicStripeClientPolicy()
          {
             this.Environment =string.Empty;
             this.PublishableKey =string.Empty;
             this.SecretKey =string.Empty;
          }
     
          /// <summary>
          /// Gets or sets the environment.
          /// </summary>
          /// <value>
          /// The environment.
          /// </value>
          publicstringEnvironment {get;set; }
     
          /// <summary>
          /// Gets or sets the publishable key.
          /// </summary>
          /// <value>
          /// The publishable key.
          /// </value>
          publicstringPublishableKey {get;set; }
     
          /// <summary>
          /// Gets or sets the Secret key.
          /// </summary>
          /// <value>
          /// The Secret key.
          /// </value>
          publicstringSecretKey {get;set; }
       }
    }
  6. Create CreateFederatedPaymentBlock which actually does the integration to Stripe:
    CreateFederatedPaymentBlock
    namespaceOshyn.Plugin.SC.Payments.Stripe
    {
       usingSitecore.Framework.Pipelines;
       usingSystem.Threading.Tasks;
       usingSitecore.Commerce.Core;
       usingSitecore.Framework.Conditions;
       usingSitecore.Commerce.Plugin.Orders;
       usingSitecore.Commerce.Plugin.Payments;
       usingglobal::Stripe;
       usingSystem;
     
       publicclassCreateFederatedPaymentBlock : PipelineBlock<CartEmailArgument, CartEmailArgument, CommercePipelineExecutionContext>
       {
          publicoverrideTask<CartEmailArgument> Run(CartEmailArgument arg, CommercePipelineExecutionContext context)
          {
             Condition.Requires(arg).IsNotNull($"{this.Name}: The cart can not be null");
     
             var cart = arg.Cart;
             if(!cart.HasComponent<FederatedPaymentComponent>())
             {
                returnTask.FromResult(arg);
             }
     
             //get the payment component
             var payment = cart.GetComponent<FederatedPaymentComponent>();
     
             if(string.IsNullOrEmpty(payment.PaymentMethodNonce))
             {
                context.Abort(context.CommerceContext.AddMessage(
                   context.GetPolicy<KnownResultCodes>().Error,
                   "InvalidOrMissingPropertyValue",
                   newobject[] {"PaymentMethodNonce"},
                   $"Invalid or missing value for property 'PaymentMethodNonce'."), context);
     
                returnTask.FromResult(arg);
             }
     
             //get the stripeclient policy
             var stripeClientPolicy = context.GetPolicy<StripeClientPolicy>();
             if(string.IsNullOrEmpty(stripeClientPolicy?.Environment) ||
                string.IsNullOrEmpty(stripeClientPolicy?.PublishableKey) ||
                string.IsNullOrEmpty(stripeClientPolicy?.SecretKey))
             {
                context.CommerceContext.AddMessage(
                   context.GetPolicy<KnownResultCodes>().Error,
                   "InvalidClientPolicy",
                   newobject[] {"StripeClientPolicy"},
                   $"{this.Name}. Invalid StripeClientPolicy");
                returnTask.FromResult(arg);
             }
     
             StripeConfiguration.SetApiKey(stripeClientPolicy.SecretKey);
     
             var customers =newStripeCustomerService();
             var charges =newStripeChargeService();
     
             // this pagehttps://stripe.com/docs/testingprovides for
             // testing cards and testing tokens for example: tok_visa
             StripeCustomer customer =null;
     
             //Currently we are not storing customerId created, but you could store the customer.Id
             //in your user database as stripeCustomerId and can be used later using the customers.Get
             //to do recurrent charges.
             customer = customers.Create(newStripeCustomerCreateOptions
             {
                Email = arg.Email,
                SourceToken = payment.PaymentMethodNonce
             });
     
             StripeCharge charge = charges.Create(newStripeChargeCreateOptions
             {
                Amount = Convert.ToInt32(payment.Amount.Amount*100),
                Description ="Sample Payment Description",
                Currency = payment.Amount.CurrencyCode,
                CustomerId = customer.Id
             });
     
             //if the charge was successful
             if(charge !=null&&string.IsNullOrEmpty(charge.FailureCode) &&string.IsNullOrEmpty(charge.FailureMessage))
             {
                payment.TransactionId = charge.Id;
                payment.TransactionStatus = charge.Status;
     
                //use cardid or card fingerprint or last4 based on business needs
                payment.MaskedNumber = charge.Source.Card.Fingerprint;
                payment.ExpiresMonth = charge.Source.Card.ExpirationMonth;
                payment.ExpiresYear = charge.Source.Card.ExpirationYear;
             }
             else
             {
                context.Abort(context.CommerceContext.AddMessage(
                   context.GetPolicy<KnownResultCodes>().Error,
                   "CreatePaymentFailed",
                   newobject[] {"PaymentMethodNonce"},
                   $"{this.Name}. Create payment failed for Stripe:{charge.FailureMessage}"), context);
             }
     
          returnTask.FromResult(arg);
          }
       }
    }
  7. Register the CreateFederatedPaymentBlock. This assumes that the BrainTreePayment reference is removed from Commerce Engine, as you do not want to process payments with the BrainTreePayment integration. You can look at the latest log file in CommerceAuthoring\wwwroot\logs\NodeConfiguration_Deployment01_*.txt. This provides the list of pipelines and the blocks registered for each pipeline. You can append or override existing block registrations with your new registration as follows:
    CreateFederatedPaymentBlock
    namespaceOshyn.Plugin.SC.Payments.Stripe
    {
       usingMicrosoft.Extensions.DependencyInjection;
       usingSitecore.Commerce.Core;
       usingSitecore.Framework.Configuration;
       usingSystem.Reflection;
       usingSitecore.Framework.Pipelines.Definitions.Extensions;
       usingSitecore.Commerce.Plugin.Orders;
       usingSitecore.Commerce.Plugin.Payments;
     
       /// <summary>
       /// The carts configure sitecore class.
       /// </summary>
       publicclassConfigureSitecore : IConfigureSitecore
       {
          /// <summary>
          /// The configure services.
          /// </summary>
          /// <param name="services">
          /// The services.
          /// </param>
          publicvoidConfigureServices(IServiceCollection services)
          {
             var assembly = Assembly.GetExecutingAssembly();
             services.RegisterAllPipelineBlocks(assembly);
     
             //modifying the ICreateOrderPipeline and adding our Block
             //before the CreateOrderBlock
             services.Sitecore().Pipelines(config =>
             config.ConfigurePipeline<ICreateOrderPipeline>(d =>
             {
                d.Add<Oshyn.Plugin.SC.Payments.Stripe.CreateFederatedPaymentBlock>().Before<CreateOrderBlock>();
             }));
          }
       }
    }
  8. Debug it in place by running the Sitecore.Commerce.Engine and first running http://{server:port}/CommerceOps/Bootstrap(), which will load the updated StripeClientPolicy sections into the database.
  9. Import the Postman folder in Sitecore.Commerce.SDK.1.0.2301 into the Postman app. Assume your Visual Studio project is running at some address such as localhost:5005, you need to go to your postman collection and update the variables so that the "ServiceHost", "OpsApiHost", "AuthoringHost" are set to "localhost:5005" instead of the default "localhost:5000". Then you can use the postman API collection provided as follows:
    1. execute the "CartAPISamples\Get Car Cart01" API  - this creates an empty cart named "Cart 01"
    2. execute the "CartAPISamples\Add Cart Line with Variant" API - this adds a line item to the "Cart 01"
    3. execute the "CartAPISamples\Set Cart to Physical Fullfillment" - this fills the fulfillment component with an address
    4. execute the "CartAPISamples\Add Federated Payment" - in this request update  "PaymentMethodNonce": "tok_visa" as follows and then execute. This will add payment component data.
    5. Now go to and execute "OrdersAPISamples\Create order" - this will invoke the ICreateOrderPipeline and inherently the block we registered and creates a payment via Stripe.
  10. Now you can go to your Stripe Dashboard and see that it records a payment corresponding to the above transaction.

By following a similar approach, you can extend the relevant pipelines to process refunds using the Stripe service.

I hope this helps in not only showing how to integrate with the Stripe payment service, but by also illustrating the approach that can be used to write other plugins to extend the Sitecore.Commerce.Engine platform.